update project api and page, set pagination in blog and create content generator component, create blog single page, ui fix

This commit is contained in:
2025-06-19 15:48:47 +03:30
parent 38df8ec335
commit e69e2bf7e1
34 changed files with 984 additions and 137 deletions
@@ -0,0 +1,107 @@
import {
IContentData,
IContentDataLink,
IContentImage,
IDynamicContent,
} from '@/models/dynamicContent';
function ImageContent(data: IContentImage) {
return (
<div className="flex flex-col items-center gap-3">
<img
src={data.src}
alt={data.alt}
className="mx-auto max-h-[60vh] max-w-[90vw] overflow-hidden rounded-2xl object-cover"
/>
{data.thumbTitle ? (
<h5 className="text-center text-sm text-gray-400">{data.thumbTitle}</h5>
) : (
<></>
)}
</div>
);
}
function titleContent(data: string) {
return <h2 className="text-2xl font-bold text-gray-500">{data}</h2>;
}
function fullContent(data: IContentData) {
const splittedData = data.data?.split('\n') || [];
return splittedData.map((e) => {
return (
<p className="indent-5 text-base text-gray-400">
{generateLinkedContent(e, data.links)}
</p>
);
});
}
function generateLinkedContent(data: string, links?: IContentDataLink[]) {
const splitted = data.split(' ');
const mappedLinks = {} as Record<string, IContentDataLink>;
links?.forEach((link) => {
mappedLinks[link.key] = link;
});
return (
<p>
{splitted.map((data) => {
return mappedLinks[data] ? (
<strong>
<a
href={mappedLinks[data].link}
className="text-primary hover:underline"
>
{mappedLinks[data].label}
</a>{' '}
</strong>
) : (
<>{data} </>
);
})}
</p>
);
}
function listContent(data: string[], type: 'bullet' | 'numberList') {
return (
<ul>
{data.map((item, index) => (
<li
key={index}
className={`list-inside ${type === 'bullet' ? 'list-disc' : 'list-decimal'} text-sm text-gray-400`}
>
<h4 className="inline text-sm">{item}</h4>
</li>
))}
</ul>
);
}
export default function ContentGenerator({
data,
className,
}: {
data: IDynamicContent[];
className?: string;
}) {
return (
<div className={`flex flex-col gap-3 md:gap-6 ${className}`}>
{data.map((e) => {
switch (e.type) {
case 'title':
return titleContent(e.data as string);
case 'content':
return <div>{fullContent(e.data as IContentData)}</div>;
case 'image':
return ImageContent(e.data as IContentImage);
case 'bullet':
case 'numberList':
return listContent(e.data as string[], e.type);
}
})}
</div>
);
}
+2 -2
View File
@@ -50,7 +50,7 @@ export default function Navbar() {
/>
</Link>
<ul className="hidden space-x-6 text-sm md:flex lg:text-base">
<ul className="hidden space-x-6 text-sm lg:flex lg:text-base">
{links.map(({ route, title }) => (
<li key={route()}>
<Link
@@ -71,7 +71,7 @@ export default function Navbar() {
{!open && (
<button
className="text-white md:hidden"
className="text-white lg:hidden"
onClick={() => setOpen(true)}
>
<Icon name="menu" className="h-6 w-6" />
@@ -0,0 +1,21 @@
import { TLocales } from '@/models/layout';
import { useTranslations } from 'next-intl';
import { getTranslations } from 'next-intl/server';
export default async function AboutDescription({
locale,
className = '',
}: {
locale: TLocales;
className?: string;
}) {
const t = await getTranslations({ locale });
return (
<div className={`bg-primary w-full ${className}`}>
<div className="container-fluid mx-auto py-20">
<p className="text-xl text-white">{t('page_about_description')}</p>
</div>
</div>
);
}
+33 -18
View File
@@ -6,22 +6,28 @@ import Pagination from '@/components/layout/pagination';
import Image from 'next/image';
import Link from 'next/link';
import { getPosts } from '@/services/blog';
import { IPostCardProps } from '@/app/api/blog/data';
import { IPostData } from '@/app/api/blog/data';
import { IPagination } from '@/models/response';
import routeFactory from '@/assets/constants/routeFactory';
import { useParams } from 'next/navigation';
import { TLocales } from '@/models/layout';
import { useTranslations } from 'next-intl';
import Button from '@/components/uikit/button';
export interface IPostsGridProps {
initialPosts: IPostCardProps[];
initialPagination: {
currentPage: number;
totalPages: number;
totalItems: number;
limit: number;
};
initialPosts: IPostData[];
initialPagination: IPagination;
}
export default function PostsGrid({
initialPosts,
initialPagination,
}: IPostsGridProps) {
const t = useTranslations();
const { locale } = useParams();
const routes = routeFactory(t, locale as TLocales);
const [posts, setPosts] = useState(initialPosts);
const [pagination, setPagination] = useState(initialPagination);
@@ -40,7 +46,10 @@ export default function PostsGrid({
<div className="container mx-auto px-4 py-14 md:py-28">
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{posts.map((post) => (
<div key={post.id} className="overflow-hidden bg-white">
<div
key={post.id}
className="group flex flex-col gap-4 overflow-hidden bg-white md:gap-6"
>
<div className="relative aspect-[1.09] w-full overflow-hidden rounded-4xl">
<Image
src={post.imageSrc}
@@ -48,16 +57,22 @@ export default function PostsGrid({
fill
className="h-auto w-full object-cover"
/>
<div className="invisible absolute inset-0 flex items-center justify-center bg-black/20 opacity-0 backdrop-blur-xs transition-all group-hover:visible group-hover:opacity-100">
<Button
link={routes.singlePost.route(post.id)}
endIcon="showMore"
>
{t('page_blog_details_CTA')}
</Button>
</div>
</div>
<div className="py-7">
<h2 className="mb-5 text-xl font-semibold">{post.title}</h2>
<a
href={post.link}
className="text-primary text-base hover:underline"
>
Read More
</a>
</div>
<Link
href={routes.singlePost.route(post.id)}
className="group-hover:text-primary text-center text-gray-500"
>
<h2 className="text-xl font-semibold">{post.title}</h2>
</Link>
</div>
))}
</div>
@@ -4,10 +4,9 @@ import { useState } from 'react';
import Button from '@/components/uikit/button';
import FormField from '@/components/uikit/inputs';
import { useTranslations } from 'next-intl';
import api from '@/lib/axios';
import { setContact } from '@/services/contact';
const ContactForm = () => {
const ContactForm = ({ className = '' }) => {
const t = useTranslations();
const [formData, setFormData] = useState({
@@ -57,7 +56,6 @@ const ContactForm = () => {
};
const handleSubmit = (e: React.FormEvent) => {
console.log(e);
e.preventDefault();
setLoading(true);
setContact(formData).finally(() => {
@@ -68,7 +66,7 @@ const ContactForm = () => {
return (
<form
onSubmit={handleSubmit}
className="z-10 w-full rounded-4xl bg-white p-4 md:w-1/2"
className={`z-10 w-full rounded-4xl bg-white ${className}`}
>
<h2 className="mt-8 mb-10 text-5xl text-gray-800">
{t('com_contact_title')}{' '}
+2 -2
View File
@@ -6,13 +6,13 @@ import { TLocales } from '@/models/layout';
import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
const TouchUs = () => {
const TouchUs = ({ className = '' }) => {
const t = useTranslations();
const params = useParams();
const locale = params.locale as TLocales;
return (
<div className="w-full p-8 md:w-1/2">
<div className={`w-full ${className}`}>
<SectionTitle
title={t('pages_contact_title')}
description={t('com_contact_touch_title')}
+1 -5
View File
@@ -2,7 +2,6 @@ import SectionTitle from '@/components/layout/sectionTitle';
import { getTranslations } from 'next-intl/server';
import { TLocales } from '@/models/layout';
import { getProjects } from '@/services/projects';
import { IProjectCardProps } from '../project/project';
import ProjectsGrid from '../project/ProjectGrid';
import Button from '@/components/uikit/button';
import routeFactory from '@/assets/constants/routeFactory';
@@ -12,14 +11,11 @@ interface Props {
export default async function HomePageProjects({ locale }: Props) {
const t = await getTranslations({ locale });
const projects = await getProjects();
console.log(projects);
const routes = routeFactory(t, locale);
return (
<div className="w-full">
<div
className={`relative container mx-auto w-full bg-white bg-cover bg-fixed bg-center bg-no-repeat pt-28 max-lg:pt-6`}
>
<div className="relative container mx-auto w-full bg-white">
<div className="relative z-20 grid grid-cols-1 lg:grid-cols-2 lg:gap-28">
<SectionTitle
title={t('pages_projects_title')}
+5 -2
View File
@@ -1,5 +1,8 @@
export interface IProductCardProps {
imageSrc: string;
title: string;
id: string;
model: string;
color_code: string;
size: string;
categoryId: string;
imageSrc: string;
}
+5 -5
View File
@@ -17,11 +17,11 @@ export default function ProjectsGrid({
return (
<div
className={`grid grid-cols-1 gap-8 md:grid-cols-2 md:gap-12 xl:grid-cols-3 xl:gap-18 ${className}`}
className={`grid grid-cols-1 gap-6 md:grid-cols-2 md:gap-8 xl:grid-cols-3 xl:gap-10 ${className}`}
>
{projects.map((project, index) => (
<div className="group flex flex-col gap-4 overflow-hidden bg-white md:gap-6">
<div className="relative aspect-[1.4] w-full overflow-hidden rounded-4xl">
<div className="group flex flex-col gap-3 overflow-hidden bg-white md:gap-5">
<div className="relative aspect-[1.4] w-full overflow-hidden rounded-2xl">
<Image
src={project.imageSrc}
alt={project.title}
@@ -38,9 +38,9 @@ export default function ProjectsGrid({
<Link
href={'/'}
key={index}
className="group-hover:text-primary text-center text-gray-500"
className="group-hover:text-primary mb-4 text-center text-gray-500"
>
<h2 className="text-xl font-semibold">{project.title}</h2>
<h2 className="text-base font-medium">{project.title}</h2>
</Link>
</div>
))}
@@ -14,8 +14,6 @@ export default function ProjectsGridView({
const [pagination, setPagination] = useState(initialPagination);
const fetchPage = (page: number) => {
console.log(page);
getProjects(page)
.then((res) => {
setProjects(res.data);
+3
View File
@@ -13,8 +13,11 @@ export interface IBaseButtonProps
export interface ITextButtonProps extends IBaseButtonProps {
endIcon?: IconName;
variant?: TButtonVariant;
}
export interface IIconButtonProps extends IBaseButtonProps {
color?: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
}
export type TButtonVariant = 'text' | 'solid';
+22 -9
View File
@@ -1,6 +1,6 @@
'use client';
import React from 'react';
import { IIconButtonProps, ITextButtonProps } from './button';
import { IIconButtonProps, ITextButtonProps, TButtonVariant } from './button';
import { Icon } from '../icons';
import Link from 'next/link';
@@ -11,14 +11,20 @@ const Button: React.FC<ITextButtonProps> = ({
endIcon,
className,
link,
variant = 'solid',
onClick,
...props
}) => {
const wrapperClass = `group bg-primary relative h-12 cursor-pointer rounded-[0.5625rem] p-[0.125rem] flex w-fit ${className || ''}`;
const wrapperClass = `group relative h-12 cursor-pointer rounded-[0.5625rem] p-[0.125rem] flex w-fit ${className || ''} ${variant === 'solid' ? 'bg-primary' : ''}`;
return link ? (
<Link href={link} className={wrapperClass} {...props}>
<ButtonBody {...props} loading={loading} endIcon={endIcon}>
<ButtonBody
{...props}
loading={loading}
endIcon={endIcon}
variant={variant}
>
{children}
</ButtonBody>
</Link>
@@ -29,17 +35,28 @@ const Button: React.FC<ITextButtonProps> = ({
onClick={onClick}
className={wrapperClass}
>
<ButtonBody {...props} loading={loading} endIcon={endIcon}>
<ButtonBody
{...props}
loading={loading}
endIcon={endIcon}
variant={variant}
>
{children}
</ButtonBody>
</button>
);
};
const variantClassNames = (variant: TButtonVariant) =>
variant === 'solid'
? 'flex items-center justify-center rounded-lg bg-white text-gray-500'
: 'text-primary text-base hover:underline';
const ButtonBody: React.FC<ITextButtonProps> = ({
children,
loading,
endIcon,
variant = 'solid',
}) => (
<div className="flex">
{loading ? (
@@ -65,11 +82,7 @@ const ButtonBody: React.FC<ITextButtonProps> = ({
</svg>
) : null}
<div
className={`flex items-center justify-center rounded-lg bg-white px-3 text-gray-500`}
>
{children}
</div>
<div className={`px-3 ${variantClassNames(variant)}`}>{children}</div>
{endIcon && !loading ? (
<span className="flex items-center justify-center p-3">