started setup to rendering fetched data

This commit is contained in:
2024-07-25 15:14:16 +05:00
parent 07e13f3c6e
commit dfb9ef6a7f
29 changed files with 327 additions and 81 deletions
+1 -1
View File
@@ -1 +1 @@
REVIEW_FORM_API=https://graff.estate/api
NEXT_PUBLIC_API=https://graff.estate/api
+1
View File
@@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"date-fns": "^3.6.0",
"framer-motion": "^11.3.9",
"ky": "^1.4.0",
"next": "14.2.5",
-5
View File
@@ -1,5 +0,0 @@
import ky from 'ky';
export const contactsFormApi = ky.extend({
prefixUrl: process.env.REVIEW_FORM_API,
});
+5
View File
@@ -0,0 +1,5 @@
import ky from 'ky';
export const api = ky.extend({
prefixUrl: process.env.NEXT_PUBLIC_API,
});
+20 -51
View File
@@ -1,29 +1,32 @@
import PostCard from '@/components/Blog/PostCard';
import { ArrowDownIcon } from '@/components/icons/ArrowDown';
import { CategoryFilterItem } from '@/components/CategoryFilterItem';
import { YearFilterItem } from '@/components/YearFilterItem';
import { Posts } from '@/consts/Posts';
import { Title } from '@/components/Title';
export default function BlogPage() {
return (
<div className="lg:pt-20 pt-14 lg:px-10 sm:px-6 px-4 lg:pb-6">
<div className="sm:pb-5 pb-4 flex flex-col justify-between border-b border-[#3D425C]">
<h1 className="font-medium h1 lg:mb-14 sm:mb-8 mb-4">Блог</h1>
<div className="lg:pt-20 pt-14">
<div className="sm:pb-5 pb-4 flex flex-col justify-between border-b lg:px-10 sm:px-6 px-4 border-[#3D425C]">
<Title className="lg:mb-14 sm:mb-8 mb-4">Блог</Title>
<div className="flex lg:items-center max-lg:flex-col gap-y-4 justify-between">
<div className="flex lg:gap-3 sm:gap-2 gap-1 flex-wrap">
<Category text="Все" count="99" chosen />
<Category text="Недвижимость" count="19" />
<Category text="Награды" count="19" />
<Category text="Выстаки" count="19" />
<div className="flex sm:gap-2 gap-1 flex-wrap">
<CategoryFilterItem text="Все" count="99" chosen />
<CategoryFilterItem text="Недвижимость" count="19" />
<CategoryFilterItem text="Награды" count="19" />
<CategoryFilterItem text="Выстаки" count="19" />
</div>
<div className="flex gap-x-4 h-fit max-sm:hidden">
<YearItem text="2024" />
<YearItem text="2023" />
<YearItem text="2022" />
<YearItem text="2021" />
<YearItem text="2020" />
<YearItem text="2019" />
<YearItem text="2018" />
<YearFilterItem text="2024" />
<YearFilterItem text="2023" />
<YearFilterItem text="2022" />
<YearFilterItem text="2021" />
<YearFilterItem text="2020" />
<YearFilterItem text="2019" />
<YearFilterItem text="2018" />
<div className="w-px bg-[#737AA1]" />
<YearItem text="Все время" />
<YearFilterItem text="Все время" />
</div>
<div className="sm:hidden flex justify-between w-full border border-[#3D425C] px-6 py-4">
Все время
@@ -31,7 +34,7 @@ export default function BlogPage() {
</div>
</div>
</div>
<div>
<div className="lg:px-10 sm:px-6 px-4 lg:pb-6">
{Posts.map(post => (
<PostCard key={post.id} {...post} />
))}
@@ -39,37 +42,3 @@ export default function BlogPage() {
</div>
);
}
function Category({
text,
count,
chosen = false,
}: {
text: string;
count: string;
chosen?: boolean;
}) {
return (
<button
className={
'flex gap-x-1 btn-text font-semibold rounded-3xl px-3 sm:py-3 py-2 ' +
(chosen ? 'bg-[#798FFF]' : 'border border-[#3D425C]')
}
>
{text}
<span
className={
'l-caption font-medium' + (chosen ? ' text-white' : ' text-[#737AA1]')
}
>
({count})
</span>
</button>
);
}
function YearItem({ text }: { text: string }) {
return (
<button className="text-[#737AA1] btn-text font-semibold">{text}</button>
);
}
+41 -1
View File
@@ -1,3 +1,43 @@
import { CategoryFilterItem } from '@/components/CategoryFilterItem';
import { ProjectsList } from '@/components/Projects/ProjectsList';
import { Title } from '@/components/Title';
import { YearFilterItem } from '@/components/YearFilterItem';
import { Vertical } from '@/ui/Vertical';
export default function ProjectsPage() {
return <div>Проекты</div>;
return (
<div className="lg:pt-20 pt-14">
<div className="sm:pb-5 pb-4 flex flex-col justify-between border-b lg:px-10 sm:px-6 px-4 border-[#3D425C]">
<Title className="lg:mb-14 sm:mb-8 mb-4">Проекты</Title>
<div className="flex justify-between items-center">
<div className="flex items-center gap-x-2">
<CategoryFilterItem text="Все" chosen />
<Vertical />
<CategoryFilterItem text="Prime" />
<CategoryFilterItem text="Web" />
<CategoryFilterItem text="View" />
</div>
<div className="flex items-center lg:gap-x-4">
<YearFilterItem text="2024" />
<YearFilterItem text="2023" />
<YearFilterItem text="2022" />
<YearFilterItem text="2021" />
<YearFilterItem text="2020" />
<YearFilterItem text="2019" />
<YearFilterItem text="2018" />
<YearFilterItem text="2017" />
<YearFilterItem text="2016" />
<YearFilterItem text="2015" />
<YearFilterItem text="2014" />
<YearFilterItem text="2013" />
<YearFilterItem text="2013" />
<YearFilterItem text="2012" />
<Vertical />
<YearFilterItem text="Все время" />
</div>
</div>
</div>
<ProjectsList />
</div>
);
}
+20 -12
View File
@@ -12,12 +12,20 @@ export default function PostCard({
id,
}: IBlogPost) {
const [date, month, year] = createdAt.split(' ');
return (
<Link
href={'/blog/' + id}
className="border-[#3D425C] border-b py-6 flex justify-stretch gap-x-4"
className="border-[#3D425C] border-b py-6 flex justify-stretch items-center gap-x-4 gap-y-3 max-sm:flex-col"
>
<div className="self-end flex-1 min-w-[105px] max-lg:hidden">
<Image
src={image}
alt={title}
fill
objectFit="cover"
className="sm:hidden !static"
/>
<div className="self-end min-w-[105px] flex-1 max-lg:hidden">
<h2 className="h2 font-medium mb-2">{date}</h2>
<p className="descriptor font-medium">
{month} {year}
@@ -26,28 +34,28 @@ export default function PostCard({
<Image
src={image}
alt={title}
width={496}
height={388}
className="max-lg:hidden object-cover"
fill
objectFit="cover"
className="max-lg:hidden !static max-w-[496px]"
/>
<Image
src={image}
alt={title}
width={264}
height={240}
className="hidden sm:max-lg:block object-cover"
fill
objectFit="cover"
className="hidden sm:max-lg:block !static max-w-[264px] min-h-60"
/>
<div className="flex justify-between flex-col lg:max-w-[39vw] sm-max-lg:flex-1">
<div className="sm:max-lg:mb-[66px]">
<div className="flex justify-between flex-col lg:max-w-[39vw] sm-max-lg:flex-1 self-stretch max-sm:gap-y-3">
<div className="">
<div className="flex gap-2 mb-4">
{tags.map(tag => (
<PostTag text={tag} key={tag} />
))}
</div>
<p className="accent font-medium mb-4">{title}</p>
<p className="accent font-medium mb-5">{title}</p>
<p className="m-text">{desc}</p>
</div>
<div className="flex sm:max-lg:justify-between lg:justify-end items-end">
<div className="flex max-lg:justify-between lg:justify-end items-end max-sm:mt-2">
<p className="descriptor lg:hidden font-medium opacity-70">
{date} {month} {year}
</p>
+3
View File
@@ -0,0 +1,3 @@
export function PostsFilters() {
return <div>PostsFilters</div>;
}
+9
View File
@@ -0,0 +1,9 @@
import { Posts } from '@/consts/Posts';
import { useEffect, useState } from 'react';
export function PostsList() {
const [posts, setPosts] = useState(Posts);
useEffect(() => {}, []);
return <div>PostsList</div>;
}
+30
View File
@@ -0,0 +1,30 @@
export function CategoryFilterItem({
text,
count,
chosen = false,
}: {
text: string;
count?: string;
chosen?: boolean;
}) {
return (
<button
className={
'flex gap-x-1 btn-text font-semibold rounded-3xl px-3 sm:py-3 py-2 ' +
(chosen ? 'bg-[#798FFF]' : 'border border-[#3D425C]')
}
>
{text}
{count && (
<span
className={
'l-caption font-medium' +
(chosen ? ' text-white' : ' text-[#737AA1]')
}
>
({count})
</span>
)}
</button>
);
}
+2 -2
View File
@@ -2,7 +2,7 @@
import InputMask from 'react-input-mask';
import { ChangeEvent, FormEvent, useState } from 'react';
import { contactsFormApi } from '@/api/contactsFormApi';
import { api } from '@/api';
import { AsteriskIcon } from '../icons/AsteriskIcon';
import { Button } from '@/ui/Button';
import { LoaderIcon } from '../icons/LoaderIcon';
@@ -27,7 +27,7 @@ export function ContactsForm({ inModal = false }: { inModal?: boolean }) {
setIsLoading(true);
try {
await contactsFormApi
await api
.post('mail', {
json: {
fullname: name,
+2 -1
View File
@@ -16,7 +16,8 @@ export function Feedback() {
<br />с нами
</h1>
<p className="2xl:text-xl lg:text-lg font-semibold leading-tight">
Хотите использовать интерактивные тренажеры в обучении? <br />
Хотите увеличить конверсию?
<br />
Давайте обсудим детали!
</p>
</div>
+1 -1
View File
@@ -36,7 +36,7 @@ export function Header() {
<LogoWithTextIcon className="max-lg:hidden" />
</Link>
<div className="flex border-x border-[#3D425C] mx-auto max-xl:hidden">
<div className="btn-text font-semibold flex gap-x-[135px] items-center px-10 py-[30px] hover:bg-[#3D425C] border-[#3D425C] border-r">
<div className="btn-text font-semibold flex gap-x-[135px] items-center px-10 py-6 hover:bg-[#3D425C] border-[#3D425C] border-r">
Продукты
<ArrowDownIcon />
</div>
+4 -3
View File
@@ -1,15 +1,16 @@
import SkolkovoIcon from '../icons/SkolkovoIcon';
import { SkolkovoIcon } from '../icons/SkolkovoIcon';
import { Title } from '../Title';
export function Motivation() {
return (
<div className="lg:py-20 lg:px-6 flex justify-between">
<div>
<h1 className="h1 font-medium mb-10">
<Title className="mb-10">
Интерактивный инструмент
<br />
продаж
<span className="text-gradient"> для застройщиков</span>
</h1>
</Title>
<h3 className="h3 font-semibold">
Помогаем девелоперам эффективно демонстрировать свой объект.
<br />
-3
View File
@@ -1,3 +0,0 @@
export function Video() {
return <div>Video</div>;
}
@@ -0,0 +1,38 @@
import { Vertical } from '@/ui/Vertical';
import { YearFilterItem } from '../YearFilterItem';
import { CategoryFilterItem } from '../CategoryFilterItem';
import { useEffect } from 'react';
export function ProjectsFilters() {
useEffect(() => {}, []);
return (
<div className="flex justify-between items-center">
<div className="flex items-center gap-x-2">
<CategoryFilterItem text="Все" chosen />
<Vertical />
<CategoryFilterItem text="Prime" />
<CategoryFilterItem text="Web" />
<CategoryFilterItem text="View" />
</div>
<div className="flex items-center lg:gap-x-4">
<YearFilterItem text="2024" />
<YearFilterItem text="2023" />
<YearFilterItem text="2022" />
<YearFilterItem text="2021" />
<YearFilterItem text="2020" />
<YearFilterItem text="2019" />
<YearFilterItem text="2018" />
<YearFilterItem text="2017" />
<YearFilterItem text="2016" />
<YearFilterItem text="2015" />
<YearFilterItem text="2014" />
<YearFilterItem text="2013" />
<YearFilterItem text="2013" />
<YearFilterItem text="2012" />
<Vertical />
<YearFilterItem text="Все время" />
</div>
</div>
);
}
+44
View File
@@ -0,0 +1,44 @@
'use client';
import { api } from '@/api';
import { IProject } from '@/types/IProject';
import { getSortedProjects } from '@/utils/getSortedProjects';
import { getTime } from 'date-fns';
import { useEffect, useState } from 'react';
export function ProjectsList() {
const [projects, setProjects] = useState<IProject[]>([]);
const [projectLabels, setProjectLabels] = useState<(string | number)[]>([]);
const [sortedProjects, setSortedProjects] =
useState<Map<string | number, IProject[]>>();
async function getProjects() {
try {
const _projects: IProject[] = await api.get('projects').json();
const _sortedProjects = getSortedProjects(_projects);
const _projectLabels = Array.from(_sortedProjects.keys()).sort(
(first, second) => getTime(second) - getTime(first),
);
setProjects(_projects);
setProjectLabels(_projectLabels);
setSortedProjects(_sortedProjects);
} catch (error) {
if (error instanceof Error) {
alert(`Error: ${error.message}`);
}
}
}
useEffect(() => {
getProjects();
}, []);
useEffect(() => {
console.log('projects: ', projects);
console.log('sortedProjects: ', sortedProjects);
console.log('projectLabels: ', projectLabels);
}, [sortedProjects, projectLabels, projects]);
return <div>ProjectsList</div>;
}
+8
View File
@@ -0,0 +1,8 @@
import { PropsWithChildren } from 'react';
export function Title({
className = '',
children,
}: PropsWithChildren<{ className?: string }>) {
return <h1 className={'font-medium h1 ' + className}>{children}</h1>;
}
+5
View File
@@ -0,0 +1,5 @@
export function YearFilterItem({ text }: { text: string }) {
return (
<button className="text-[#737AA1] btn-text font-semibold">{text}</button>
);
}
+1 -1
View File
@@ -1,4 +1,4 @@
export default function Skolkovo({
export function SkolkovoIcon({
className = '',
type = 'standart',
}: {
+11
View File
@@ -0,0 +1,11 @@
import { create } from 'zustand';
interface IPostsTagsState {
tags: string[];
setTags: (tags: string[]) => void;
}
export const usePostsTagsStore = create<IPostsTagsState>()(set => ({
tags: [],
setTags: tags => set({ tags }),
}));
+11
View File
@@ -0,0 +1,11 @@
import { create } from 'zustand';
interface IPostsYearsState {
years: string[];
setYears: (years: string[]) => void;
}
export const usePostsYearsStore = create<IPostsYearsState>()(set => ({
years: [],
setYears: years => set({ years }),
}));
+15
View File
@@ -0,0 +1,15 @@
import { create } from 'zustand';
interface IProjectsFilters {
devices: string[];
years: string[];
setDevices: (devices: string[]) => void;
setYears: (years: string[]) => void;
}
export const useProjectsFiltersStore = create<IProjectsFilters>()(set => ({
devices: ['Все'],
years: ['Все время'],
setDevices: devices => set({ devices }),
setYears: years => set({ years }),
}));
+12
View File
@@ -0,0 +1,12 @@
type Device = 'stream' | 'touch' | 'mobile' | 'vr';
export interface IProject {
id?: string;
name: string;
company: string;
city: string;
image: string;
stage?: number;
releaseDate: string;
devices: Device[];
}
+3
View File
@@ -0,0 +1,3 @@
export function Vertical() {
return <div className="w-px h-5 bg-[#3D425C]" />;
}
+6
View File
@@ -0,0 +1,6 @@
// import { IProject } from '@/types/IProject';
// export function getProjectsDevices(projects: IProject[]) {
// const projectsDevices: string[] = [];
// }
+13
View File
@@ -0,0 +1,13 @@
import { IBlogPost } from '@/types/IBlogPost';
export function getPostsTags(posts: IBlogPost[]) {
const tags: string[] = [];
posts.forEach(post => {
post.tags.forEach(tag => {
if (!tags.includes(tag)) {
tags.push(tag);
}
});
});
}
+16
View File
@@ -0,0 +1,16 @@
import { getTime, getYear } from 'date-fns';
import { IProject } from '../types/IProject';
export function getSortedProjects(projects: IProject[]) {
const sortedProjects = new Map<string | number, IProject[]>();
const sorted = projects.toSorted(
(first, second) => getTime(second.releaseDate) - getTime(first.releaseDate),
);
for (const project of sorted) {
const key = project.stage !== 6 ? 'В работе' : getYear(project.releaseDate);
sortedProjects.set(key, [...(sortedProjects.get(key) ?? []), project]);
}
return sortedProjects;
}
+5
View File
@@ -686,6 +686,11 @@ data-view-byte-offset@^1.0.0:
es-errors "^1.3.0"
is-data-view "^1.0.1"
date-fns@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-3.6.0.tgz#f20ca4fe94f8b754951b24240676e8618c0206bf"
integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==
debug@^3.2.7:
version "3.2.7"
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"