feat: add certificate gallery and certifications section components

- Implemented CertificateGallery component to display certification images in a gallery format.
- Created CertificationsSection component to showcase certifications with titles and descriptions.
- Enhanced Gallery component to support animations and keyboard navigation.
- Introduced Select component for dropdown selection with images.
- Added utility functions for API responses and pagination handling.
- Implemented localization functions for text and content.
- Created a custom hook for detecting clicks outside of a component.
This commit is contained in:
2026-07-19 07:59:16 +03:30
parent 478b550c51
commit 730e2d8ca8
60 changed files with 1859 additions and 1403 deletions
+3 -2
View File
@@ -1,4 +1,5 @@
import '@/app/globals.css';
import { isRtl } from '@/config';
import { notFound } from 'next/navigation';
import { NextIntlClientProvider } from 'next-intl';
import type { IPageParams } from '@/models/layout.d';
@@ -17,7 +18,7 @@ export default async function ErrorLayout({
}>) {
const paramsData = await params;
const locale = paramsData.locale;
const fontClass = locale === 'fa' ? 'font-fa' : 'font-en';
const fontClass = isRtl(locale) ? 'font-fa' : 'font-en';
let messages;
try {
messages = (await import(`../../../../messages/${locale}.json`)).default;
@@ -26,7 +27,7 @@ export default async function ErrorLayout({
}
return (
<html lang={locale} dir={locale === 'fa' ? 'rtl' : 'ltr'}>
<html lang={locale} dir={isRtl(locale) ? 'rtl' : 'ltr'}>
<body className={`min-h-svh w-full overflow-x-hidden ${fontClass}`}>
<NextIntlClientProvider locale={locale} messages={messages}>
{children}
+2
View File
@@ -1,6 +1,7 @@
import images from '@/assets/images/images';
import InnerPageBanner from '@/components/layout/innerPages/banner';
import AboutDescription from '@/components/pages/about/AboutDescription';
import CertificationsSection from '@/components/pages/about/CertificationsSection';
import AboutTopInfo from '@/components/pages/about/TopInfo';
import HomeStorySection from '@/components/pages/home/HomeStorySection';
import { IPageParams } from '@/models/layout';
@@ -36,6 +37,7 @@ export default async function AboutUs({
<AboutTopInfo />
<AboutDescription className="mb-8 md:mb-14" locale={locale} />
<HomeStorySection />
<CertificationsSection locale={locale} />
</main>
);
}
+3 -3
View File
@@ -6,7 +6,7 @@ import { NextIntlClientProvider } from 'next-intl';
import type { IPageParams } from '@/models/layout.d';
import Head from 'next/head';
import { getTranslations } from 'next-intl/server';
import { host } from '@/config';
import { host, isRtl } from '@/config';
export async function generateMetadata({ params }: { params: IPageParams }) {
const { locale } = await params;
@@ -41,7 +41,7 @@ export default async function RootLayout({
const paramsData = await params;
const locale = paramsData.locale;
const fontClass = locale === 'fa' ? 'font-fa' : 'font-en';
const fontClass = isRtl(locale) ? 'font-fa' : 'font-en';
let messages;
try {
@@ -51,7 +51,7 @@ export default async function RootLayout({
}
return (
<html lang={locale} dir={locale === 'fa' ? 'rtl' : 'ltr'}>
<html lang={locale} dir={isRtl(locale) ? 'rtl' : 'ltr'}>
<Head>
<meta name="viewport" content="width=device-width, initial-scale=1" />
{/* <link rel="icon" href="/favicon.ico" /> */}
+19 -18
View File
@@ -1,28 +1,29 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { posts } from '../data';
import { getLocaleFromCookies } from '@/lib/locale';
import { localizeContent, localizeText } from '@/lib/domain';
import { notFound, ok } from '@/lib/api-response';
export async function GET(request: Request, context: any) {
const params = await context.params;
const postId = params.id;
type RouteContext = {
params: {
id: string;
};
};
const cookieStore = await cookies();
const lang = cookieStore.get('locale')?.value || 'en';
export async function GET(request: Request, context: RouteContext) {
const postId = context.params.id;
const locale = await getLocaleFromCookies();
let post = posts.find((post) => post.id === postId);
if (!post) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
return notFound();
}
if (lang === 'fa') {
post = {
...post,
title: post.fa_title || post.title,
content: post.fa_content || post.content,
};
}
post = {
...post,
title: localizeText(locale, post.title, post.fa_title),
content: localizeContent(locale, post.content, post.fa_content),
};
return NextResponse.json({
data: post,
});
return ok(post);
}
+42
View File
@@ -64,6 +64,27 @@ export const posts: IPostData[] = [
'Advantages of customized manufacturing solutions',
],
},
{
type: 'quote',
data: {
text: 'Sustainability starts with measurable, small operational improvements.',
author: 'Pasargad Bricks Team',
},
},
{
type: 'divider',
},
{
type: 'subtitle',
data: 'Explore related projects',
},
{
type: 'cta',
data: {
label: 'View Projects',
link: '/projects',
},
},
],
fa_content: [
{
@@ -118,6 +139,27 @@ export const posts: IPostData[] = [
'مزایای راه‌حل‌های سفارشی‌سازی در تولید',
],
},
{
type: 'quote',
data: {
text: 'پایداری با بهبودهای کوچک اما مداوم در عملیات آغاز می‌شود.',
author: 'تیم آجر پاسارگاد',
},
},
{
type: 'divider',
},
{
type: 'subtitle',
data: 'مشاهده پروژه‌های مرتبط',
},
{
type: 'cta',
data: {
label: 'مشاهده پروژه‌ها',
link: '/projects',
},
},
],
},
{
+9 -19
View File
@@ -1,33 +1,23 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { generatePosts, IPostData } from './data';
import { generatePosts } from './data';
import { getLocaleFromCookies } from '@/lib/locale';
import { localizeText } from '@/lib/domain';
import { buildPagination, getPaginationParams } from '@/lib/pagination';
import { okPaginated } from '@/lib/api-response';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const lang = (await cookies()).get('lang')?.value || 'fa';
const page = parseInt(searchParams.get('page') || '1', 10);
const perPage = 10;
const locale = await getLocaleFromCookies();
const { page, perPage, start, end } = getPaginationParams(request, 10);
const posts = generatePosts();
const start = (page - 1) * perPage;
const end = start + perPage;
const paginatedPosts = posts.slice(start, end);
const data = paginatedPosts.map((post) => {
return {
id: post.id,
imageSrc: post.imageSrc,
title: post.fa_title || post.title,
title: localizeText(locale, post.title, post.fa_title),
};
});
return NextResponse.json({
data,
pagination: {
page,
perPage,
total: posts.length,
totalPages: Math.ceil(posts.length / perPage),
},
});
return okPaginated(data, buildPagination(page, perPage, posts.length));
}
+2 -4
View File
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server';
import { mkdir, writeFile, readFile } from 'fs/promises';
import path from 'path';
import { serverError } from '@/lib/api-response';
const filePath = path.resolve(process.cwd(), 'data', 'contactMessages.json');
@@ -45,9 +46,6 @@ export async function POST(request: NextRequest) {
message: 'Your message has been received.',
});
} catch (error) {
return NextResponse.json(
{ error, message: 'مشکلی پیش آمده.' },
{ status: 400 },
);
return serverError('مشکلی پیش آمده.');
}
}
@@ -1,35 +1,29 @@
import { NextResponse, NextRequest } from 'next/server';
import { cookies } from 'next/headers';
import { NextRequest } from 'next/server';
import { generateProductsForCategory } from './data';
import { productCategoriesData } from '../../data';
import { buildPagination, getPaginationParams } from '@/lib/pagination';
import { notFound, okPaginated } from '@/lib/api-response';
export async function GET(request: NextRequest, context: any) {
const cookieStore = await cookies();
const lang = cookieStore.get('locale')?.value || 'en';
type RouteContext = {
params: {
id: string;
};
};
const params = await context.params;
export async function GET(request: NextRequest, context: RouteContext) {
const params = context.params;
const category = productCategoriesData.find((cat) => cat.id === params.id);
if (!category) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
return notFound();
}
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1', 10);
const perPage = 5;
const { page, perPage, start, end } = getPaginationParams(request, 5);
const allProducts = generateProductsForCategory(category.id);
const start = (page - 1) * perPage;
const end = start + perPage;
const paginatedProducts = allProducts.slice(start, end);
return NextResponse.json({
data: paginatedProducts,
pagination: {
page,
perPage,
total: allProducts.length,
totalPages: Math.ceil(allProducts.length / perPage),
},
});
return okPaginated(
paginatedProducts,
buildPagination(page, perPage, allProducts.length),
);
}
+20 -15
View File
@@ -1,24 +1,26 @@
'use server';
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { NextRequest } from 'next/server';
import { productCategoriesData } from '../data';
import { getLocaleFromCookies } from '@/lib/locale';
import { localizeText } from '@/lib/domain';
import { notFound, ok } from '@/lib/api-response';
export async function GET(request: NextRequest, context: any) {
const cookieStore = await cookies();
const lang = cookieStore.get('locale')?.value || 'en';
type RouteContext = {
params: {
id: string;
};
};
const params = (await context).params;
const { id } = await params;
export async function GET(request: NextRequest, context: RouteContext) {
const locale = await getLocaleFromCookies();
const { id } = context.params;
let category = productCategoriesData.find((cat) => cat.id === id);
if (!category) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
return notFound();
}
if (lang === 'fa') {
if (locale === 'fa') {
category = {
...category,
title: category.fa_title || category.title,
@@ -28,7 +30,10 @@ export async function GET(request: NextRequest, context: any) {
};
}
return NextResponse.json({
data: category,
});
category = {
...category,
title: localizeText(locale, category.title, category.fa_title),
};
return ok(category);
}
+20 -11
View File
@@ -1,15 +1,20 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { APIProductCategoryEntity } from '@/models/api';
import { productCategoriesData } from './data';
import { productsRelatedByCategory } from './[id]/products/data';
import { getLocaleFromCookies } from '@/lib/locale';
import { localizeText } from '@/lib/domain';
import { ok, serverError } from '@/lib/api-response';
const getTypesCount = (categoryId: string) =>
// @ts-ignore
productsRelatedByCategory[categoryId]?.length || 0;
const setRelatedLocaleData = (productCategory: APIProductCategoryEntity) => ({
title: productCategory.fa_title || productCategory.title,
title: localizeText(
'fa',
productCategory.title,
productCategory.fa_title,
),
priceTypeTitle:
productCategory.fa_priceTypeTitle || productCategory.priceTypeTitle,
bestForTitle: productCategory.fa_bestForTitle || productCategory.bestForTitle,
@@ -20,21 +25,25 @@ export async function GET(request: Request) {
try {
let data = productCategoriesData as APIProductCategoryEntity[];
const cookieStore = await cookies();
const lang = (await cookieStore).get('locale')?.value || 'en';
const locale = await getLocaleFromCookies();
data = productCategoriesData.map((productCategory) => ({
...productCategory,
...(lang === 'fa' ? setRelatedLocaleData(productCategory) : {}),
...(locale === 'fa'
? setRelatedLocaleData(productCategory)
: {
title: localizeText(
locale,
productCategory.title,
productCategory.fa_title,
),
}),
typesCount: getTypesCount(productCategory.id),
}));
return NextResponse.json({ data });
return ok(data);
} catch (error) {
console.error('Error fetching product categories:', error);
return NextResponse.json(
{ error: 'Failed to fetch product categories' },
{ status: 500 },
);
return serverError('Failed to fetch product categories');
}
}
+19 -18
View File
@@ -1,28 +1,29 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { projects } from '../data';
import { getLocaleFromCookies } from '@/lib/locale';
import { localizeContent, localizeText } from '@/lib/domain';
import { notFound, ok } from '@/lib/api-response';
export async function GET(request: Request, context: any) {
const params = await context.params;
const projectId = params.id;
type RouteContext = {
params: {
id: string;
};
};
const cookieStore = await cookies();
const lang = cookieStore.get('locale')?.value || 'en';
export async function GET(request: Request, context: RouteContext) {
const projectId = context.params.id;
const locale = await getLocaleFromCookies();
let project = projects.find((project) => project.id === projectId);
if (!project) {
return NextResponse.json({ error: 'Not found' }, { status: 404 });
return notFound();
}
if (lang === 'fa') {
project = {
...project,
title: project.fa_title || project.title,
content: project.fa_content || project.content,
};
}
project = {
...project,
title: localizeText(locale, project.title, project.fa_title),
content: localizeContent(locale, project.content, project.fa_content),
};
return NextResponse.json({
data: project,
});
return ok(project);
}
+9 -26
View File
@@ -1,47 +1,30 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import {
generateProjects,
IProjectData,
IProjectThumbResponseData,
} from './data';
import { getLocaleFromCookies } from '@/lib/locale';
import { localizeText } from '@/lib/domain';
import { buildPagination, getPaginationParams } from '@/lib/pagination';
import { okPaginated } from '@/lib/api-response';
export async function GET(request: Request) {
let projects = generateProjects();
let data = projects as IProjectThumbResponseData[];
const cookieStore = await cookies();
const lang = (await cookieStore).get('lang')?.value || 'fa';
// Pagination logic
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1', 10);
const perPage = 6;
const start = (page - 1) * perPage;
const end = start + perPage;
const locale = await getLocaleFromCookies();
const { page, perPage, start, end } = getPaginationParams(request, 6);
const paginatedProjects = projects.slice(start, end);
const setRelatedLocaleData = (project: IProjectData) => ({
title: project.fa_title || project.title,
title: localizeText(locale, project.title, project.fa_title),
});
data = paginatedProjects.map((project) => ({
id: project.id,
imageSrc: project.imageSrc,
...(lang === 'fa'
? setRelatedLocaleData(project)
: { title: project.title }),
...setRelatedLocaleData(project),
}));
return NextResponse.json({
data,
pagination: {
page,
perPage,
total: projects.length,
totalPages: Math.ceil(projects.length / perPage),
},
});
return okPaginated(data, buildPagination(page, perPage, projects.length));
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

+8
View File
@@ -15,6 +15,10 @@ import langEn from './lang_en.png';
import langFa from './lang_fa.png';
import en from './en_square.png';
import fa from './ir_square.png';
import certificateIso9001 from './certificates/iso9001.jpg';
import certificateIso14001 from './certificates/iso14001.jpg';
import certificateStandard from './certificates/standard.jpg';
import certificateLia from './certificates/lia.jpg';
export default {
homeBanner,
logo,
@@ -32,4 +36,8 @@ export default {
fa,en,
langEn,
langFa,
certificateIso9001,
certificateIso14001,
certificateStandard,
certificateLia,
};
+42
View File
@@ -39,6 +39,27 @@ export const tempContent = {
'مزایای راه‌حل‌های سفارشی‌سازی در تولید',
],
},
{
type: 'quote',
data: {
text: 'پایداری با بهینه‌سازی مداوم فرآیندها ممکن می‌شود.',
author: 'تیم پاسارگاد',
},
},
{
type: 'divider',
},
{
type: 'subtitle',
data: 'پروژه‌های بیشتر',
},
{
type: 'cta',
data: {
label: 'رفتن به پروژه‌ها',
link: '/projects',
},
},
],
fa_content: [
{
@@ -93,5 +114,26 @@ export const tempContent = {
'مزایای راه‌حل‌های سفارشی‌سازی در تولید',
],
},
{
type: 'quote',
data: {
text: 'پایداری با بهینه‌سازی مداوم فرآیندها ممکن می‌شود.',
author: 'تیم پاسارگاد',
},
},
{
type: 'divider',
},
{
type: 'subtitle',
data: 'پروژه‌های بیشتر',
},
{
type: 'cta',
data: {
label: 'رفتن به پروژه‌ها',
link: '/projects',
},
},
],
};
@@ -0,0 +1,53 @@
'use client';
import images from '@/assets/images/images';
import Gallery from '@/components/uikit/gallery';
import { GalleryItem } from '@/components/uikit/gallery/gallery';
import { isRtl } from '@/config';
import { TLocales } from '@/models/layout';
import Image from 'next/image';
import { useParams } from 'next/navigation';
import { useState } from 'react';
const certificates: GalleryItem[] = [
{ src: images.certificateIso9001, alt: 'ISO 9001' },
{ src: images.certificateIso14001, alt: 'ISO 14001' },
{ src: images.certificateStandard, alt: 'Standard' },
{ src: images.certificateLia, alt: 'LIA' },
];
export default function CertificateGallery() {
const params = useParams();
const locale = params.locale as TLocales;
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
return (
<>
<div className="flex flex-wrap items-center justify-center gap-3">
{certificates.map((cert, i) => (
<button
key={cert.alt}
onClick={() => setSelectedIndex(i)}
className="group relative h-16 w-16 cursor-pointer overflow-hidden rounded-md border border-white/20 transition-transform hover:scale-105"
>
<Image
src={cert.src}
alt={cert.alt}
fill
className="object-contain p-1"
/>
</button>
))}
</div>
{selectedIndex !== null && (
<Gallery
items={certificates}
initialIndex={selectedIndex}
onClose={() => setSelectedIndex(null)}
rtl={isRtl(locale)}
/>
)}
</>
);
}
@@ -1,11 +1,13 @@
import {
IContentCTA,
IContentData,
IContentDataLink,
IContentImage,
IContentQuote,
IDynamicContent,
} from '@/models/dynamicContent';
function ImageContent(data: IContentImage) {
function imageContent(data: IContentImage) {
return (
<div className="flex flex-col items-center gap-3">
<img
@@ -26,12 +28,23 @@ 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) => {
function subtitleContent(data: string) {
return <h3 className="text-xl font-semibold text-gray-500">{data}</h3>;
}
function fullContent(data: IContentData | string) {
const normalizedData =
typeof data === 'string'
? ({
data,
} as IContentData)
: data;
const splittedData = normalizedData.data?.split('\n') || [];
return splittedData.map((e, index) => {
return (
<p className="indent-5 text-base text-gray-400">
{generateLinkedContent(e, data.links)}
<p key={index} className="indent-5 text-base text-gray-400">
{generateLinkedContent(e, normalizedData.links)}
</p>
);
});
@@ -80,6 +93,34 @@ function listContent(data: string[], type: 'bullet' | 'numberList') {
);
}
function quoteContent(data: IContentQuote) {
return (
<blockquote className="rounded-xl border-l-4 border-primary bg-gray-100 px-4 py-3 text-gray-500">
<p className="text-base italic">{data.text}</p>
{data.author ? (
<cite className="mt-2 block text-sm text-gray-400"> {data.author}</cite>
) : null}
</blockquote>
);
}
function ctaContent(data: IContentCTA) {
return (
<a
href={data.link}
target={data.target || '_self'}
rel={data.target === '_blank' ? 'noopener noreferrer' : undefined}
className="bg-primary inline-flex w-fit rounded-lg px-4 py-2 text-sm text-white"
>
{data.label}
</a>
);
}
function dividerContent() {
return <hr className="my-2 border-gray-200" />;
}
export default function ContentGenerator({
data,
className,
@@ -89,17 +130,27 @@ export default function ContentGenerator({
}) {
return (
<div className={`flex flex-col gap-3 md:gap-6 ${className}`}>
{data.map((e) => {
{data.map((e, index) => {
switch (e.type) {
case 'title':
return titleContent(e.data as string);
return <div key={index}>{titleContent(e.data)}</div>;
case 'subtitle':
return <div key={index}>{subtitleContent(e.data)}</div>;
case 'content':
return <div>{fullContent(e.data as IContentData)}</div>;
return <div key={index}>{fullContent(e.data)}</div>;
case 'image':
return ImageContent(e.data as IContentImage);
return <div key={index}>{imageContent(e.data)}</div>;
case 'bullet':
case 'numberList':
return listContent(e.data as string[], e.type);
return <div key={index}>{listContent(e.data, e.type)}</div>;
case 'quote':
return <div key={index}>{quoteContent(e.data)}</div>;
case 'cta':
return <div key={index}>{ctaContent(e.data)}</div>;
case 'divider':
return <div key={index}>{dividerContent()}</div>;
default:
return null;
}
})}
</div>
+26 -9
View File
@@ -1,16 +1,18 @@
'use client';
import images from '@/assets/images/images';
import Image from 'next/image';
import Link from 'next/link';
import FooterMenuBuilder from './FooterMenuBuilder';
import FooterMenuBuilderWrapper from './FooterMenuBuilderWrapper';
import FooterInfo from './FooterInfo';
import { useTranslations } from 'next-intl';
import { useParams } from 'next/navigation';
import { appConstants } from '@/assets/constants';
import { getQuickLinks } from '@/assets/constants/footerMenu/quickLinks.const';
import { getWhoAreWeLinks } from '@/assets/constants/footerMenu/whoAreWe.const';
import images from '@/assets/images/images';
import { Icon, IconName } from '@/components/uikit/icons';
import { TLocales } from '@/models/layout';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import Link from 'next/link';
import { useParams } from 'next/navigation';
import FooterInfo from './FooterInfo';
import FooterMenuBuilder from './FooterMenuBuilder';
import FooterMenuBuilderWrapper from './FooterMenuBuilderWrapper';
export default function Footer() {
const t = useTranslations();
@@ -41,7 +43,7 @@ export default function Footer() {
</h5>
</div>
<div className="flex items-start justify-between gap-8 border-y border-gray-300/50 py-14 max-lg:flex-col-reverse">
<FooterInfo withLogo className="lg:max-w-[200px]" />
<FooterInfo withLogo className="max-lg:mt-3 lg:max-w-[200px]" />
<div className="grid w-full grid-cols-2 gap-5 md:grid-cols-3 md:gap-10 lg:max-w-3/5">
<FooterMenuBuilder
title={t('footer_links_quick_links')}
@@ -74,6 +76,21 @@ export default function Footer() {
{t('footer_links_address')}
</span>
</div>
<div className="flex items-center gap-4 max-lg:justify-center">
{appConstants.socials.map((social) => (
<Link
href={social.link}
key={social.title}
className="text-white lg:flex-1"
>
<Icon
name={social.icon as IconName}
className="text-primary"
/>
</Link>
))}
</div>
</FooterMenuBuilderWrapper>
</div>
</div>
+5 -15
View File
@@ -1,11 +1,10 @@
'use client';
import { appConstants } from '@/assets/constants';
import images from '@/assets/images/images';
import { Icon, IconName } from '@/components/uikit/icons';
import CertificateGallery from '@/components/layout/certificateGallery';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import Link from 'next/link';
import { useTranslations } from 'next-intl';
import LanguageSwitch from '../languageSwitch';
export default function FooterInfo({
@@ -32,18 +31,9 @@ export default function FooterInfo({
)}
<p className="text-sm tracking-normal">{t('footer_slogan')}</p>
<div className="flex items-center gap-4 max-lg:justify-center">
{appConstants.socials.map((social) => (
<Link
href={social.link}
key={social.title}
className="text-white lg:flex-1"
>
<Icon name={social.icon as IconName} className="text-primary" />
</Link>
))}
</div>
<LanguageSwitch />
{withLogo && <CertificateGallery />}
{!withLogo && <LanguageSwitch />}
</div>
);
}
@@ -1,14 +1,14 @@
'use client';
import Link from 'next/link';
import Image from 'next/image';
import routeFactory from '@/assets/constants/routeFactory';
import images from '@/assets/images/images';
import { usePathname, useParams } from 'next/navigation';
import { Icon } from '@/components/uikit/icons';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import Link from 'next/link';
import { useParams, usePathname } from 'next/navigation';
import { useEffect, useRef } from 'react';
import FooterInfo from '../footer/FooterInfo';
import routeFactory from '@/assets/constants/routeFactory';
interface Props {
isOpen: boolean;
@@ -66,7 +66,9 @@ export default function HamburgerMenu({ isOpen, onClose }: Props) {
if (!isOpen) return null;
return (
<div className="fixed inset-x-0 top-0 z-50 flex h-screen">
<div
className={`fixed inset-x-0 start-0 top-0 z-50 flex h-screen justify-end`}
>
<div className="absolute inset-0 bg-black/60" />
<div
@@ -24,11 +24,17 @@ export default function ModalLanguageSelector() {
const t = useTranslations();
useEffect(() => {
const selected = localStorage.getItem('selectedLocale');
if (!selected) setShow(true);
const cookieLocale = document.cookie
.split('; ')
.find((item) => item.startsWith('locale='))
?.split('=')[1];
if (!selected && !cookieLocale) setShow(true);
}, []);
const changeLocale = (locale: TLocales) => {
localStorage.setItem('selectedLocale', locale);
document.cookie = `locale=${locale}; path=/; max-age=31536000; samesite=lax`;
const path = window.location.pathname.replace(/^\/(en|fa)/, '');
window.location.pathname = `/${locale}${path}`;
};
+16 -30
View File
@@ -2,28 +2,22 @@
import images from '@/assets/images/images';
import { TLocales } from '@/models/layout';
import Image from 'next/image';
import Select from '@/components/uikit/select';
import { usePathname } from 'next/navigation';
const languages = [
{ value: 'en', label: 'English', image: images.en },
{ value: 'fa', label: 'فارسی', image: images.fa },
];
export default function LanguageSwitch() {
const pathname = usePathname();
const currentLocale = pathname.split('/')[1] as TLocales;
const languages = [
{
title: 'EN',
image: images.langEn,
locale: 'en',
},
{
title: 'FA',
image: images.langFa,
locale: 'fa',
},
];
const changeLocale = (locale: TLocales) => {
const currentLocale = pathname.split('/')[1];
const changeLocale = (locale: string) => {
if (currentLocale === locale) return;
document.cookie = `locale=${locale}; path=/; max-age=31536000; samesite=lax`;
let newPath = pathname.replace(/^\/(en|fa)/, '');
if (newPath === '/') newPath = '';
@@ -31,19 +25,11 @@ export default function LanguageSwitch() {
};
return (
<div className="flex items-center justify-center gap-3">
{languages.map((language) => (
<div
className="flex cursor-pointer flex-col items-center justify-center gap-2 px-1"
onClick={() => changeLocale(language.locale as TLocales)}
key={language.title}
>
<Image src={language.image} alt={language.title} />
<span className="text-xs font-normal text-white">
{language.title}
</span>
</div>
))}
</div>
<Select
options={languages}
value={currentLocale}
onChange={changeLocale}
size="small"
/>
);
}
+10 -4
View File
@@ -1,14 +1,15 @@
'use client';
import routeFactory from '@/assets/constants/routeFactory';
import images from '@/assets/images/images';
import { Icon } from '@/components/uikit/icons';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import Link from 'next/link';
import { usePathname, useParams } from 'next/navigation';
import { useParams, usePathname } from 'next/navigation';
import { useState } from 'react';
import HamburgerMenu from '../hamburgerMenu';
import { useTranslations } from 'next-intl';
import routeFactory from '@/assets/constants/routeFactory';
import LanguageSwitch from '../languageSwitch';
export default function Navbar() {
const [open, setOpen] = useState<boolean>(false);
@@ -50,7 +51,7 @@ export default function Navbar() {
/>
</Link>
<ul className="hidden space-x-6 text-sm lg:flex lg:text-base">
<ul className="hidden items-center space-x-6 text-sm lg:flex lg:text-base">
{links.map(({ route, title }) => (
<li key={route()}>
<Link
@@ -61,12 +62,17 @@ export default function Navbar() {
</Link>
</li>
))}
<div className="h-4 w-px bg-white opacity-35"></div>
<li>
<a className="flex items-center gap-2" href="tel:+098123456789">
<Icon name="phone" className="text-primary" />
<p>+098 123456789</p>
</a>
</li>
<li>
<LanguageSwitch />
</li>
</ul>
{!open && (
@@ -0,0 +1,78 @@
'use client';
import images from '@/assets/images/images';
import SectionTitle from '@/components/layout/sectionTitle';
import Gallery from '@/components/uikit/gallery';
import { GalleryItem } from '@/components/uikit/gallery/gallery';
import { isRtl } from '@/config';
import { TLocales } from '@/models/layout';
import { useTranslations } from 'next-intl';
import Image from 'next/image';
import { useState } from 'react';
const certifications: GalleryItem[] = [
{ src: images.certificateIso9001, alt: 'ISO 9001', title: 'ISO 9001' },
{ src: images.certificateIso14001, alt: 'ISO 14001', title: 'ISO 14001' },
{ src: images.certificateStandard, alt: 'Standard', title: 'Standard' },
{ src: images.certificateLia, alt: 'LIA', title: 'LIA' },
];
export default function CertificationsSection({
locale,
className = '',
}: {
locale: TLocales;
className?: string;
}) {
const t = useTranslations();
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
return (
<div className={`w-full py-20 ${className}`}>
<div className="container mx-auto">
<SectionTitle
title={t('com_about_certifications_title')}
description={t('com_about_certifications_title_bold')}
locale={locale}
/>
<p className="mt-4 max-w-2xl text-base font-light text-gray-300">
{t('com_about_certifications_description')}
</p>
<div className="mt-10 grid grid-cols-2 gap-6 sm:grid-cols-4">
{certifications.map((cert, i) => (
<button
key={cert.alt}
onClick={() => setSelectedIndex(i)}
className="group cursor-pointer"
>
<div className="relative aspect-[3/4] overflow-hidden rounded-xl border border-gray-100 bg-white transition-all group-hover:shadow-lg">
<Image
src={cert.src}
alt={cert.alt}
fill
className="object-contain p-4"
/>
</div>
{cert.title && (
<p className="mt-3 text-center text-sm font-medium text-gray-500">
{cert.title}
</p>
)}
</button>
))}
</div>
</div>
{selectedIndex !== null && (
<Gallery
items={certifications}
initialIndex={selectedIndex}
onClose={() => setSelectedIndex(null)}
rtl={isRtl(locale)}
/>
)}
</div>
);
}
@@ -57,6 +57,8 @@ const ContactForm = ({ className = '' }) => {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!validate()) return;
setLoading(true);
setContact(formData).finally(() => {
setLoading(false);
@@ -1,3 +1,7 @@
'use client';
import { useEffect } from 'react';
import { useState } from 'react';
import type { IProductCardProps } from './productCard.d';
export default function ProductCard({
@@ -5,17 +9,60 @@ export default function ProductCard({
imageSrc,
size,
}: IProductCardProps) {
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
useEffect(() => {
if (!isPreviewOpen) return;
const handleEsc = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsPreviewOpen(false);
}
};
window.addEventListener('keydown', handleEsc);
return () => window.removeEventListener('keydown', handleEsc);
}, [isPreviewOpen]);
return (
<div className="overflow-hidden rounded-xl bg-gray-200 p-4">
<div className="aspect-square w-full">
<img
src={imageSrc}
alt={model}
className="h-full w-full object-cover"
/>
<>
<div
className="cursor-pointer overflow-hidden rounded-xl bg-gray-200 p-4"
onClick={() => setIsPreviewOpen(true)}
>
<div className="aspect-square w-full">
<img
src={imageSrc}
alt={model}
className="h-full w-full object-cover"
/>
</div>
<h3 className="mt-4 mb-10 text-xl font-bold text-gray-500">{model}</h3>
<span className="text-base font-normal text-gray-300">{size}</span>
</div>
<h3 className="mt-4 mb-10 text-xl font-bold text-gray-500">{model}</h3>
<span className="text-base font-normal text-gray-300">{size}</span>
</div>
{isPreviewOpen ? (
<div
className="fixed inset-0 z-100 flex items-center justify-center bg-black/70 p-4 transition-opacity duration-200"
onClick={() => setIsPreviewOpen(false)}
>
<div
className="relative max-h-[90vh] w-full max-w-3xl scale-100 rounded-2xl bg-white p-4 opacity-100 transition-all duration-200 ease-out"
onClick={(event) => event.stopPropagation()}
>
<button
className="absolute top-3 right-3 cursor-pointer rounded-full bg-black/70 px-3 py-1 text-sm text-white"
onClick={() => setIsPreviewOpen(false)}
>
</button>
<img
src={imageSrc}
alt={model}
className="max-h-[80vh] w-full rounded-xl object-contain"
/>
</div>
</div>
) : null}
</>
);
}
+14
View File
@@ -0,0 +1,14 @@
import { StaticImageData } from 'next/image';
export interface GalleryItem {
src: string | StaticImageData;
alt: string;
title?: string;
}
export interface GalleryProps {
items: GalleryItem[];
initialIndex?: number;
onClose: () => void;
rtl?: boolean;
}
+168
View File
@@ -0,0 +1,168 @@
'use client';
import Image from 'next/image';
import { useCallback, useEffect, useRef, useState } from 'react';
import { GalleryProps } from './gallery';
export default function Gallery({
items,
initialIndex = 0,
onClose,
rtl = false,
}: GalleryProps) {
const [currentIndex, setCurrentIndex] = useState(initialIndex);
const [direction, setDirection] = useState<'next' | 'prev'>('next');
const [isAnimating, setIsAnimating] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout>(null);
const animate = useCallback(
(dir: 'next' | 'prev', newIndex: number) => {
if (isAnimating) return;
setDirection(dir);
setIsAnimating(true);
setCurrentIndex(newIndex);
timeoutRef.current = setTimeout(() => setIsAnimating(false), 300);
},
[isAnimating],
);
const goNext = useCallback(() => {
animate('next', (currentIndex + 1) % items.length);
}, [currentIndex, items.length, animate]);
const goPrev = useCallback(() => {
animate('prev', (currentIndex - 1 + items.length) % items.length);
}, [currentIndex, items.length, animate]);
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);
useEffect(() => {
const handleKey = (e: KeyboardEvent) => {
if (rtl) {
if (e.key === 'ArrowLeft') goNext();
else if (e.key === 'ArrowRight') goPrev();
} else {
if (e.key === 'ArrowRight') goNext();
else if (e.key === 'ArrowLeft') goPrev();
}
if (e.key === 'Escape') onClose();
};
document.addEventListener('keydown', handleKey);
document.body.style.overflow = 'hidden';
return () => {
document.removeEventListener('keydown', handleKey);
document.body.style.overflow = '';
};
}, [goNext, goPrev, onClose, rtl]);
const current = items[currentIndex];
const slideInClass =
direction === 'next'
? 'animate-[slideInRight_300ms_ease-out]'
: 'animate-[slideInLeft_300ms_ease-out]';
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4"
onClick={onClose}
>
<div
className="relative flex items-center gap-4"
onClick={(e) => e.stopPropagation()}
>
<button
onClick={goPrev}
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-white/20 text-white transition-colors hover:bg-white/40"
>
<svg
className={`h-6 w-6 ${rtl ? 'scale-x-[-1]' : ''}`}
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
clipRule="evenodd"
/>
</svg>
</button>
<div className="relative max-h-[90vh] max-w-[90vw] overflow-hidden">
<button
onClick={onClose}
className="absolute top-0 right-0 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-white text-gray-800 shadow-lg transition-colors hover:bg-gray-200"
>
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
{current.title && (
<div className="absolute bottom-4 left-4 z-10 rounded-md bg-black/60 px-3 py-1.5 text-sm text-white">
{current.title}
</div>
)}
<div key={currentIndex} className={slideInClass}>
<Image
src={current.src}
alt={current.alt}
className="max-h-[85vh] rounded-lg object-contain"
/>
</div>
</div>
<button
onClick={goNext}
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-white/20 text-white transition-colors hover:bg-white/40"
>
<svg
className={`h-6 w-6 ${rtl ? 'scale-x-[-1]' : ''}`}
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
<style jsx global>{`
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideInLeft {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
`}</style>
</div>
);
}
+135
View File
@@ -0,0 +1,135 @@
'use client';
import useClickOutside from '@/hooks/useClickOutside';
import Image from 'next/image';
import React, { useCallback, useRef, useState } from 'react';
import { SelectProps, SelectSize } from './select';
const sizeClasses: Record<
SelectSize,
{ trigger: string; option: string; icon: number }
> = {
small: {
trigger: 'h-8 px-2 text-xs gap-1.5',
option: 'px-2 py-1.5 text-xs gap-1.5',
icon: 16,
},
medium: {
trigger: 'h-10 px-3 text-sm gap-2',
option: 'px-3 py-2 text-sm gap-2',
icon: 20,
},
large: {
trigger: 'h-12 px-4 text-base gap-2.5',
option: 'px-4 py-2.5 text-base gap-2.5',
icon: 24,
},
};
const DROPDOWN_MAX_HEIGHT = 240;
const Select: React.FC<SelectProps> = ({
options,
value,
onChange,
size = 'medium',
placeholder = 'Select...',
className = '',
}) => {
const [open, setOpen] = useState(false);
const [dropUp, setDropUp] = useState(false);
const ref = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLButtonElement>(null);
useClickOutside(ref, () => setOpen(false));
const selected = options.find((opt) => opt.value === value);
const classes = sizeClasses[size];
const toggle = useCallback(() => {
setOpen((prev) => {
if (!prev && triggerRef.current) {
const rect = triggerRef.current.getBoundingClientRect();
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
setDropUp(
spaceBelow < DROPDOWN_MAX_HEIGHT && spaceAbove > spaceBelow,
);
}
return !prev;
});
}, []);
return (
<div ref={ref} className={`relative ${className}`}>
<button
ref={triggerRef}
type="button"
onClick={toggle}
className={`flex w-full items-center justify-between rounded-lg border border-white/20 bg-transparent text-white transition-colors hover:border-white/40 ${classes.trigger}`}
>
<span className="flex items-center gap-1 truncate">
{selected ? (
<>
{selected.image && (
<Image
src={selected.image}
alt={selected.label}
width={classes.icon}
height={classes.icon}
className="rounded-sm object-cover"
/>
)}
<span>{selected.label}</span>
</>
) : (
<span className="text-white/50">{placeholder}</span>
)}
</span>
<svg
className={`h-4 w-4 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`}
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
clipRule="evenodd"
/>
</svg>
</button>
{open && (
<ul
className={`absolute z-50 w-full overflow-y-auto rounded-lg border border-white/20 bg-gray-800 shadow-lg ${dropUp ? 'bottom-full mb-1' : 'top-full mt-1'}`}
style={{ maxHeight: DROPDOWN_MAX_HEIGHT }}
>
{options.map((opt) => (
<li
key={opt.value}
onClick={() => {
onChange(opt.value);
setOpen(false);
}}
className={`flex cursor-pointer items-center transition-colors hover:bg-white/10 ${classes.option} ${opt.value === value ? 'bg-white/5' : ''}`}
>
{opt.image && (
<Image
src={opt.image}
alt={opt.label}
width={classes.icon}
height={classes.icon}
className="rounded-sm object-cover"
/>
)}
<span>{opt.label}</span>
</li>
))}
</ul>
)}
</div>
);
};
export default Select;
+18
View File
@@ -0,0 +1,18 @@
import { StaticImageData } from 'next/image';
export interface SelectOption {
value: string;
label: string;
image?: string | StaticImageData;
}
export interface SelectProps {
options: SelectOption[];
value: string;
onChange: (value: string) => void;
size?: 'small' | 'medium' | 'large';
placeholder?: string;
className?: string;
}
export type SelectSize = 'small' | 'medium' | 'large';
+4 -1
View File
@@ -1,4 +1,7 @@
export const port = process.env.PORT || 3000;
export const port = process.env.PORT || 4000;
export const host = process.env.VERCEL_PROJECT_PRODUCTION_URL
? `${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
: `http://localhost:${port}`;
export const rtlLocales = ['fa'];
export const isRtl = (locale: string) => rtlLocales.includes(locale);
+21
View File
@@ -0,0 +1,21 @@
import { RefObject, useEffect } from 'react';
export default function useClickOutside<T extends HTMLElement>(
ref: RefObject<T | null>,
handler: () => void,
) {
useEffect(() => {
const listener = (e: MouseEvent | TouchEvent) => {
if (!ref.current || ref.current.contains(e.target as Node)) return;
handler();
};
document.addEventListener('mousedown', listener);
document.addEventListener('touchstart', listener);
return () => {
document.removeEventListener('mousedown', listener);
document.removeEventListener('touchstart', listener);
};
}, [ref, handler]);
}
+18
View File
@@ -0,0 +1,18 @@
import { NextResponse } from 'next/server';
import type { IPagination } from '@/models/response';
export function ok<T>(data: T) {
return NextResponse.json({ data });
}
export function okPaginated<T>(data: T, pagination: IPagination) {
return NextResponse.json({ data, pagination });
}
export function notFound(message = 'Not found') {
return NextResponse.json({ error: message }, { status: 404 });
}
export function serverError(message = 'Internal server error') {
return NextResponse.json({ error: message }, { status: 500 });
}
+1 -1
View File
@@ -1,7 +1,7 @@
import axios from 'axios';
const api = axios.create({
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000/api',
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:4000/api',
headers: {
'Content-Type': 'application/json',
},
+13
View File
@@ -0,0 +1,13 @@
export function localizeText(
locale: string,
defaultValue: string,
faValue?: string,
) {
if (locale === 'fa') return faValue || defaultValue;
return defaultValue;
}
export function localizeContent<T>(locale: string, defaultValue: T, faValue?: T) {
if (locale === 'fa') return faValue || defaultValue;
return defaultValue;
}
+9
View File
@@ -0,0 +1,9 @@
import { cookies } from 'next/headers';
import { routing } from '@/i18n/routing';
export async function getLocaleFromCookies() {
const locale = (await cookies()).get('locale')?.value;
return locale && routing.locales.includes(locale as 'en' | 'fa')
? locale
: routing.defaultLocale;
}
+24
View File
@@ -0,0 +1,24 @@
import type { IPagination } from '@/models/response';
export function getPaginationParams(request: Request, defaultPerPage: number) {
const { searchParams } = new URL(request.url);
const page = Math.max(1, parseInt(searchParams.get('page') || '1', 10) || 1);
const perPage = defaultPerPage;
const start = (page - 1) * perPage;
const end = start + perPage;
return { page, perPage, start, end };
}
export function buildPagination(
page: number,
perPage: number,
total: number,
): IPagination {
return {
page,
perPage,
total,
totalPages: Math.ceil(total / perPage),
};
}
+24
View File
@@ -0,0 +1,24 @@
'use server';
import { cookies } from 'next/headers';
import api from '@/lib/axios';
type RequestConfig = {
params?: Record<string, string | number | boolean | undefined>;
};
async function withCookieHeader(config?: RequestConfig) {
const cookieStore = await cookies();
return {
...config,
headers: {
Cookie: cookieStore.toString(),
},
};
}
export async function serverGet<T>(url: string, config?: RequestConfig) {
const { data } = await api.get<T>(url, await withCookieHeader(config));
return data;
}
+6 -2
View File
@@ -18,7 +18,11 @@ export function middleware(request: NextRequest) {
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
);
const locale = matchedLocale || defaultLocale;
const cookieLocale = request.cookies.get('locale')?.value;
const preferredLocale = locales.includes(cookieLocale || '')
? cookieLocale
: defaultLocale;
const locale = matchedLocale || preferredLocale;
let response: NextResponse;
@@ -26,7 +30,7 @@ export function middleware(request: NextRequest) {
const pathnameIsMissingLocale = !matchedLocale;
if (pathnameIsMissingLocale) {
response = NextResponse.redirect(
new URL(`/${defaultLocale}${pathname}`, request.url),
new URL(`/${preferredLocale}${pathname}`, request.url),
);
} else {
response = NextResponse.next();
+68 -9
View File
@@ -1,14 +1,62 @@
export interface IDynamicContent {
type: keyof IDynamicContentItem;
data: string[] | string | IContentImage | IContentData;
export type DynamicContentType =
| 'title'
| 'subtitle'
| 'content'
| 'image'
| 'bullet'
| 'numberList'
| 'quote'
| 'cta'
| 'divider';
export type IDynamicContent =
| ITitleContentBlock
| ISubtitleContentBlock
| IParagraphContentBlock
| IImageContentBlock
| IListContentBlock
| IQuoteContentBlock
| ICTAContentBlock
| IDividerContentBlock;
export interface ITitleContentBlock {
type: 'title';
data: string;
}
export interface IDynamicContentItem {
bullet?: string[];
numberList?: string[];
image?: IContentImage;
title?: string;
content?: IContentData;
export interface ISubtitleContentBlock {
type: 'subtitle';
data: string;
}
export interface IParagraphContentBlock {
type: 'content';
data: IContentData | string;
}
export interface IImageContentBlock {
type: 'image';
data: IContentImage;
}
export interface IListContentBlock {
type: 'bullet' | 'numberList';
data: string[];
}
export interface IQuoteContentBlock {
type: 'quote';
data: IContentQuote;
}
export interface ICTAContentBlock {
type: 'cta';
data: IContentCTA;
}
export interface IDividerContentBlock {
type: 'divider';
data?: null;
}
export interface IContentImage {
@@ -28,3 +76,14 @@ export interface IContentDataLink {
link: string;
label: string;
}
export interface IContentQuote {
text: string;
author?: string;
}
export interface IContentCTA {
label: string;
link: string;
target?: '_self' | '_blank';
}
+6 -25
View File
@@ -1,37 +1,18 @@
'use server';
import { IPostResponseData, IPostThumbResponseData } from '@/app/api/blog/data';
import api from '@/lib/axios';
import { serverGet } from '@/lib/server-api';
import { IResponse } from '@/models/response';
import { cookies } from 'next/headers';
export async function getPosts(
page = 1,
): Promise<IResponse<IPostThumbResponseData[]>> {
const cookiesStore = await cookies();
return api
.get('/blog', {
headers: {
Cookie: cookiesStore.toString(),
},
params: { page },
})
.then(({ data }) => {
return Promise.resolve(data);
})
.catch((error) => Promise.reject(error));
return serverGet<IResponse<IPostThumbResponseData[]>>('/blog', {
params: { page },
});
}
export async function getPost(postId: string): Promise<IPostResponseData> {
const cookiesStore = await cookies();
return api
.get(`/blog/${postId}`, {
headers: {
Cookie: cookiesStore.toString(),
},
})
.then(({ data }) => {
return Promise.resolve(data.data);
})
.catch((error) => Promise.reject(error));
const data = await serverGet<{ data: IPostResponseData }>(`/blog/${postId}`);
return data.data;
}
+9 -14
View File
@@ -1,18 +1,13 @@
import api from "@/lib/axios";
import api from '@/lib/axios';
interface payload {
first_name: string,
last_name: string,
email: string,
phone: string,
message: string,
interface Payload {
first_name: string;
last_name: string;
email: string;
phone: string;
message: string;
}
export async function setContact(payload:payload) {
return api
.post('/contact', payload)
.then(({ data }) => {
return Promise.resolve();
})
.catch((error) => Promise.reject(error));
export async function setContact(payload: Payload) {
await api.post('/contact', payload);
}
+14 -38
View File
@@ -2,57 +2,33 @@
import type { IProductCategoryCardProps } from '@/components/pages/productCategories/productCategoryCard.d';
import type { IProductCardProps } from '@/components/pages/products/productCard/productCard.d';
import api from '@/lib/axios';
import { serverGet } from '@/lib/server-api';
import { IResponse } from '@/models/response';
import { cookies } from 'next/headers';
export async function getProductCategories() {
const cookiesStore = await cookies();
return api
.get('/product_categories', {
headers: {
Cookie: cookiesStore.toString(),
},
})
.then(({ data }) => {
return Promise.resolve(data.data);
})
.catch((error) => Promise.reject(error));
const data = await serverGet<{ data: IProductCategoryCardProps[] }>(
'/product_categories',
);
return data.data;
}
export async function getProductCategoryById(
categoryId: string,
): Promise<IProductCategoryCardProps> {
const cookiesStore = await cookies();
return api
.get(`/product_categories/${categoryId}`, {
headers: {
Cookie: cookiesStore.toString(),
},
})
.then(({ data }) => {
return Promise.resolve(data.data);
})
.catch((error) => Promise.reject(error));
const data = await serverGet<{ data: IProductCategoryCardProps }>(
`/product_categories/${categoryId}`,
);
return data.data;
}
export async function getProducts(
categoryId: string,
page = 1,
): Promise<IResponse<IProductCardProps[]>> {
const cookiesStore = await cookies();
console.log('request');
console.log(page);
return api
.get(`/product_categories/${categoryId}/products`, {
headers: {
Cookie: cookiesStore.toString(),
},
return serverGet<IResponse<IProductCardProps[]>>(
`/product_categories/${categoryId}/products`,
{
params: { page },
})
.then(({ data }) => {
return Promise.resolve(data);
})
.catch((error) => Promise.reject(error));
},
);
}
+8 -24
View File
@@ -1,35 +1,19 @@
'use server';
import { IProjectResponseData } from '@/app/api/projects/data';
import api from '@/lib/axios';
import { cookies } from 'next/headers';
import { serverGet } from '@/lib/server-api';
export async function getProjects(page = 1 as number) {
const cookiesStore = await cookies();
return api
.get('/projects', {
headers: {
Cookie: cookiesStore.toString(),
},
params: { page },
})
.then(({ data }) => Promise.resolve(data))
.catch((error) => Promise.reject(error));
return serverGet('/projects', {
params: { page },
});
}
export async function getProject(
projectId: string,
): Promise<IProjectResponseData> {
const cookiesStore = await cookies();
return api
.get(`/projects/${projectId}`, {
headers: {
Cookie: cookiesStore.toString(),
},
})
.then(({ data }) => {
return Promise.resolve(data.data);
})
.catch((error) => Promise.reject(error));
const data = await serverGet<{ data: IProjectResponseData }>(
`/projects/${projectId}`,
);
return data.data;
}