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
+7 -3
View File
@@ -3,20 +3,24 @@ WORKDIR /app
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
FROM base AS deps FROM base AS deps
COPY package.json pnpm-lock.yml ./ COPY package.json pnpm-lock.yaml ./
RUN npm config set registry https://hub.megan.ir/npm
RUN npm i -g pnpm RUN npm i -g pnpm
RUN pnpm install --frozen-lockfile RUN pnpm install --frozen-lockfile
FROM base AS builder FROM base AS builder
COPY --from=deps /app/node_modules ./node_modules COPY --from=deps /app/node_modules ./node_modules
COPY . . COPY . .
RUN npm config set registry https://hub.megan.ir/npm
RUN npm install -g pnpm
RUN pnpm build RUN pnpm build
FROM node:20-alpine AS runner FROM node:20-alpine AS runner
WORKDIR /app WORKDIR /app
ARG PORT=5000
ENV NODE_ENV=production ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1 ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=5000 ENV PORT=$PORT
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
@@ -27,5 +31,5 @@ COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
RUN touch /app/contactMessages.json && chown nextjs:nodejs /app/contactMessages.json RUN touch /app/contactMessages.json && chown nextjs:nodejs /app/contactMessages.json
USER nextjs USER nextjs
EXPOSE 5000 EXPOSE ${PORT}
CMD ["node", "server.js"] CMD ["node", "server.js"]
+3 -2
View File
@@ -3,14 +3,15 @@ services:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
args:
PORT: ${PORT:-5000}
image: pasargad-bricks:latest image: pasargad-bricks:latest
container_name: pasargad-bricks container_name: pasargad-bricks
restart: unless-stopped restart: unless-stopped
ports: ports:
- "5000:5000" - "${PORT:-3000}:${PORT:-3000}"
environment: environment:
NODE_ENV: production NODE_ENV: production
PORT: 5000
env_file: env_file:
- .env - .env
volumes: volumes:
+4
View File
@@ -108,6 +108,10 @@ declare const messages: {
"pages_notFound_description": "You can either head back to the Homepage or try a search.", "pages_notFound_description": "You can either head back to the Homepage or try a search.",
"pages_notFound_button": "Go To Home", "pages_notFound_button": "Go To Home",
"com_about_certifications_title": "Our Certifications",
"com_about_certifications_title_bold": "& Standards",
"com_about_certifications_description": "We maintain the highest quality standards through internationally recognized certifications.",
"page_about_metadata_title": "About Us", "page_about_metadata_title": "About Us",
"page_about_metadata_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.", "page_about_metadata_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.",
"page_about_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.", "page_about_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.",
+4
View File
@@ -105,6 +105,10 @@
"pages_notFound_description": "You can either head back to the Homepage or try a search.", "pages_notFound_description": "You can either head back to the Homepage or try a search.",
"pages_notFound_button": "Go To Home", "pages_notFound_button": "Go To Home",
"com_about_certifications_title": "Our Certifications",
"com_about_certifications_title_bold": "& Standards",
"com_about_certifications_description": "We maintain the highest quality standards through internationally recognized certifications.",
"page_about_metadata_title": "About Us", "page_about_metadata_title": "About Us",
"page_about_metadata_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.", "page_about_metadata_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.",
"page_about_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.", "page_about_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.",
+4
View File
@@ -105,6 +105,10 @@
"pages_notFound_description": "صفحه‌ای که به دنبال آن هستید وجود ندارد یا منتقل شده است. لطفاً آدرس را بررسی کنید یا به صفحه اصلی بازگردید.", "pages_notFound_description": "صفحه‌ای که به دنبال آن هستید وجود ندارد یا منتقل شده است. لطفاً آدرس را بررسی کنید یا به صفحه اصلی بازگردید.",
"pages_notFound_button": "بازگشت به صفحه اصلی", "pages_notFound_button": "بازگشت به صفحه اصلی",
"com_about_certifications_title": "گواهینامه‌ها",
"com_about_certifications_title_bold": "و استانداردهای ما",
"com_about_certifications_description": "ما بالاترین استانداردهای کیفیت را از طریق گواهینامه‌های معتبر بین‌المللی حفظ می‌کنیم.",
"page_about_metadata_title": "درباره ما", "page_about_metadata_title": "درباره ما",
"page_about_metadata_description": "با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته.", "page_about_metadata_description": "با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته.",
"page_about_description": "با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته.", "page_about_description": "با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته.",
+1 -1
View File
@@ -1,5 +1,5 @@
module.exports = { module.exports = {
siteUrl: process.env.SITE_URL || 'http://localhost:3000', siteUrl: process.env.SITE_URL || 'http://localhost:4000',
generateRobotsTxt: true, generateRobotsTxt: true,
changefreq: 'weekly', changefreq: 'weekly',
priority: 0.7, priority: 0.7,
+46
View File
@@ -25,6 +25,7 @@
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"dotenv-cli": "^11.0.0",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.3", "eslint-config-next": "15.3.3",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
@@ -2588,6 +2589,51 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/dotenv-cli": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-11.0.0.tgz",
"integrity": "sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.6",
"dotenv": "^17.1.0",
"dotenv-expand": "^12.0.0",
"minimist": "^1.2.6"
},
"bin": {
"dotenv": "cli.js"
}
},
"node_modules/dotenv-cli/node_modules/dotenv": {
"version": "17.4.2",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz",
"integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dotenv-expand": {
"version": "12.0.3",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz",
"integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"dotenv": "^16.4.5"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+12 -12
View File
@@ -1,15 +1,4 @@
{ {
"name": "pasargad_bricks",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint",
"sitemap": "ts-node scripts/generate-sitemap.ts",
"postbuild": "npm run sitemap"
},
"dependencies": { "dependencies": {
"axios": "^1.9.0", "axios": "^1.9.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
@@ -28,6 +17,7 @@
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"dotenv-cli": "^11.0.0",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.3.3", "eslint-config-next": "15.3.3",
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
@@ -40,5 +30,15 @@
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"typescript": "^5.8.3" "typescript": "^5.8.3"
} },
"name": "pasargad_bricks",
"private": true,
"scripts": {
"build": "next build",
"dev": "dotenv -- next dev --turbopack",
"lint": "next lint",
"sitemap": "ts-node scripts/generate-sitemap.ts",
"start": "dotenv -- next start"
},
"version": "0.1.0"
} }
Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+2 -2
View File
@@ -3,7 +3,7 @@ User-agent: *
Allow: / Allow: /
# Host # Host
Host: http://localhost:3000 Host: http://localhost:4000
# Sitemaps # Sitemaps
Sitemap: http://localhost:3000/sitemap.xml Sitemap: http://localhost:4000/sitemap.xml
+3 -2
View File
@@ -1,4 +1,5 @@
import '@/app/globals.css'; import '@/app/globals.css';
import { isRtl } from '@/config';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { NextIntlClientProvider } from 'next-intl'; import { NextIntlClientProvider } from 'next-intl';
import type { IPageParams } from '@/models/layout.d'; import type { IPageParams } from '@/models/layout.d';
@@ -17,7 +18,7 @@ export default async function ErrorLayout({
}>) { }>) {
const paramsData = await params; const paramsData = await params;
const locale = paramsData.locale; const locale = paramsData.locale;
const fontClass = locale === 'fa' ? 'font-fa' : 'font-en'; const fontClass = isRtl(locale) ? 'font-fa' : 'font-en';
let messages; let messages;
try { try {
messages = (await import(`../../../../messages/${locale}.json`)).default; messages = (await import(`../../../../messages/${locale}.json`)).default;
@@ -26,7 +27,7 @@ export default async function ErrorLayout({
} }
return ( 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}`}> <body className={`min-h-svh w-full overflow-x-hidden ${fontClass}`}>
<NextIntlClientProvider locale={locale} messages={messages}> <NextIntlClientProvider locale={locale} messages={messages}>
{children} {children}
+2
View File
@@ -1,6 +1,7 @@
import images from '@/assets/images/images'; import images from '@/assets/images/images';
import InnerPageBanner from '@/components/layout/innerPages/banner'; import InnerPageBanner from '@/components/layout/innerPages/banner';
import AboutDescription from '@/components/pages/about/AboutDescription'; import AboutDescription from '@/components/pages/about/AboutDescription';
import CertificationsSection from '@/components/pages/about/CertificationsSection';
import AboutTopInfo from '@/components/pages/about/TopInfo'; import AboutTopInfo from '@/components/pages/about/TopInfo';
import HomeStorySection from '@/components/pages/home/HomeStorySection'; import HomeStorySection from '@/components/pages/home/HomeStorySection';
import { IPageParams } from '@/models/layout'; import { IPageParams } from '@/models/layout';
@@ -36,6 +37,7 @@ export default async function AboutUs({
<AboutTopInfo /> <AboutTopInfo />
<AboutDescription className="mb-8 md:mb-14" locale={locale} /> <AboutDescription className="mb-8 md:mb-14" locale={locale} />
<HomeStorySection /> <HomeStorySection />
<CertificationsSection locale={locale} />
</main> </main>
); );
} }
+3 -3
View File
@@ -6,7 +6,7 @@ import { NextIntlClientProvider } from 'next-intl';
import type { IPageParams } from '@/models/layout.d'; import type { IPageParams } from '@/models/layout.d';
import Head from 'next/head'; import Head from 'next/head';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { host } from '@/config'; import { host, isRtl } from '@/config';
export async function generateMetadata({ params }: { params: IPageParams }) { export async function generateMetadata({ params }: { params: IPageParams }) {
const { locale } = await params; const { locale } = await params;
@@ -41,7 +41,7 @@ export default async function RootLayout({
const paramsData = await params; const paramsData = await params;
const locale = paramsData.locale; const locale = paramsData.locale;
const fontClass = locale === 'fa' ? 'font-fa' : 'font-en'; const fontClass = isRtl(locale) ? 'font-fa' : 'font-en';
let messages; let messages;
try { try {
@@ -51,7 +51,7 @@ export default async function RootLayout({
} }
return ( return (
<html lang={locale} dir={locale === 'fa' ? 'rtl' : 'ltr'}> <html lang={locale} dir={isRtl(locale) ? 'rtl' : 'ltr'}>
<Head> <Head>
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
{/* <link rel="icon" href="/favicon.ico" /> */} {/* <link rel="icon" href="/favicon.ico" /> */}
+16 -15
View File
@@ -1,28 +1,29 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { posts } from '../data'; 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) { type RouteContext = {
const params = await context.params; params: {
const postId = params.id; id: string;
};
};
const cookieStore = await cookies(); export async function GET(request: Request, context: RouteContext) {
const lang = cookieStore.get('locale')?.value || 'en'; const postId = context.params.id;
const locale = await getLocaleFromCookies();
let post = posts.find((post) => post.id === postId); let post = posts.find((post) => post.id === postId);
if (!post) { if (!post) {
return NextResponse.json({ error: 'Not found' }, { status: 404 }); return notFound();
} }
if (lang === 'fa') {
post = { post = {
...post, ...post,
title: post.fa_title || post.title, title: localizeText(locale, post.title, post.fa_title),
content: post.fa_content || post.content, content: localizeContent(locale, post.content, post.fa_content),
}; };
}
return NextResponse.json({ return ok(post);
data: post,
});
} }
+42
View File
@@ -64,6 +64,27 @@ export const posts: IPostData[] = [
'Advantages of customized manufacturing solutions', '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: [ 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 { generatePosts } from './data';
import { cookies } from 'next/headers'; import { getLocaleFromCookies } from '@/lib/locale';
import { generatePosts, IPostData } from './data'; import { localizeText } from '@/lib/domain';
import { buildPagination, getPaginationParams } from '@/lib/pagination';
import { okPaginated } from '@/lib/api-response';
export async function GET(request: Request) { export async function GET(request: Request) {
const { searchParams } = new URL(request.url); const locale = await getLocaleFromCookies();
const lang = (await cookies()).get('lang')?.value || 'fa'; const { page, perPage, start, end } = getPaginationParams(request, 10);
const page = parseInt(searchParams.get('page') || '1', 10);
const perPage = 10;
const posts = generatePosts(); const posts = generatePosts();
const start = (page - 1) * perPage;
const end = start + perPage;
const paginatedPosts = posts.slice(start, end); const paginatedPosts = posts.slice(start, end);
const data = paginatedPosts.map((post) => { const data = paginatedPosts.map((post) => {
return { return {
id: post.id, id: post.id,
imageSrc: post.imageSrc, imageSrc: post.imageSrc,
title: post.fa_title || post.title, title: localizeText(locale, post.title, post.fa_title),
}; };
}); });
return NextResponse.json({ return okPaginated(data, buildPagination(page, perPage, posts.length));
data,
pagination: {
page,
perPage,
total: posts.length,
totalPages: Math.ceil(posts.length / perPage),
},
});
} }
+2 -4
View File
@@ -1,6 +1,7 @@
import { NextRequest, NextResponse } from 'next/server'; import { NextRequest, NextResponse } from 'next/server';
import { mkdir, writeFile, readFile } from 'fs/promises'; import { mkdir, writeFile, readFile } from 'fs/promises';
import path from 'path'; import path from 'path';
import { serverError } from '@/lib/api-response';
const filePath = path.resolve(process.cwd(), 'data', 'contactMessages.json'); 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.', message: 'Your message has been received.',
}); });
} catch (error) { } catch (error) {
return NextResponse.json( return serverError('مشکلی پیش آمده.');
{ error, message: 'مشکلی پیش آمده.' },
{ status: 400 },
);
} }
} }
@@ -1,35 +1,29 @@
import { NextResponse, NextRequest } from 'next/server'; import { NextRequest } from 'next/server';
import { cookies } from 'next/headers';
import { generateProductsForCategory } from './data'; import { generateProductsForCategory } from './data';
import { productCategoriesData } 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) { type RouteContext = {
const cookieStore = await cookies(); params: {
const lang = cookieStore.get('locale')?.value || 'en'; 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); const category = productCategoriesData.find((cat) => cat.id === params.id);
if (!category) { if (!category) {
return NextResponse.json({ error: 'Not found' }, { status: 404 }); return notFound();
} }
const { searchParams } = new URL(request.url); const { page, perPage, start, end } = getPaginationParams(request, 5);
const page = parseInt(searchParams.get('page') || '1', 10);
const perPage = 5;
const allProducts = generateProductsForCategory(category.id); const allProducts = generateProductsForCategory(category.id);
const start = (page - 1) * perPage;
const end = start + perPage;
const paginatedProducts = allProducts.slice(start, end); const paginatedProducts = allProducts.slice(start, end);
return NextResponse.json({ return okPaginated(
data: paginatedProducts, paginatedProducts,
pagination: { buildPagination(page, perPage, allProducts.length),
page, );
perPage,
total: allProducts.length,
totalPages: Math.ceil(allProducts.length / perPage),
},
});
} }
+20 -15
View File
@@ -1,24 +1,26 @@
'use server'; import { NextRequest } from 'next/server';
import { cookies } from 'next/headers';
import { NextRequest, NextResponse } from 'next/server';
import { productCategoriesData } from '../data'; 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) { type RouteContext = {
const cookieStore = await cookies(); params: {
const lang = cookieStore.get('locale')?.value || 'en'; id: string;
};
};
const params = (await context).params; export async function GET(request: NextRequest, context: RouteContext) {
const locale = await getLocaleFromCookies();
const { id } = await params; const { id } = context.params;
let category = productCategoriesData.find((cat) => cat.id === id); let category = productCategoriesData.find((cat) => cat.id === id);
if (!category) { if (!category) {
return NextResponse.json({ error: 'Not found' }, { status: 404 }); return notFound();
} }
if (lang === 'fa') { if (locale === 'fa') {
category = { category = {
...category, ...category,
title: category.fa_title || category.title, title: category.fa_title || category.title,
@@ -28,7 +30,10 @@ export async function GET(request: NextRequest, context: any) {
}; };
} }
return NextResponse.json({ category = {
data: 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 { APIProductCategoryEntity } from '@/models/api';
import { productCategoriesData } from './data'; import { productCategoriesData } from './data';
import { productsRelatedByCategory } from './[id]/products/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) => const getTypesCount = (categoryId: string) =>
// @ts-ignore // @ts-ignore
productsRelatedByCategory[categoryId]?.length || 0; productsRelatedByCategory[categoryId]?.length || 0;
const setRelatedLocaleData = (productCategory: APIProductCategoryEntity) => ({ const setRelatedLocaleData = (productCategory: APIProductCategoryEntity) => ({
title: productCategory.fa_title || productCategory.title, title: localizeText(
'fa',
productCategory.title,
productCategory.fa_title,
),
priceTypeTitle: priceTypeTitle:
productCategory.fa_priceTypeTitle || productCategory.priceTypeTitle, productCategory.fa_priceTypeTitle || productCategory.priceTypeTitle,
bestForTitle: productCategory.fa_bestForTitle || productCategory.bestForTitle, bestForTitle: productCategory.fa_bestForTitle || productCategory.bestForTitle,
@@ -20,21 +25,25 @@ export async function GET(request: Request) {
try { try {
let data = productCategoriesData as APIProductCategoryEntity[]; let data = productCategoriesData as APIProductCategoryEntity[];
const cookieStore = await cookies(); const locale = await getLocaleFromCookies();
const lang = (await cookieStore).get('locale')?.value || 'en';
data = productCategoriesData.map((productCategory) => ({ data = productCategoriesData.map((productCategory) => ({
...productCategory, ...productCategory,
...(lang === 'fa' ? setRelatedLocaleData(productCategory) : {}), ...(locale === 'fa'
? setRelatedLocaleData(productCategory)
: {
title: localizeText(
locale,
productCategory.title,
productCategory.fa_title,
),
}),
typesCount: getTypesCount(productCategory.id), typesCount: getTypesCount(productCategory.id),
})); }));
return NextResponse.json({ data }); return ok(data);
} catch (error) { } catch (error) {
console.error('Error fetching product categories:', error); console.error('Error fetching product categories:', error);
return NextResponse.json( return serverError('Failed to fetch product categories');
{ error: 'Failed to fetch product categories' },
{ status: 500 },
);
} }
} }
+16 -15
View File
@@ -1,28 +1,29 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { projects } from '../data'; 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) { type RouteContext = {
const params = await context.params; params: {
const projectId = params.id; id: string;
};
};
const cookieStore = await cookies(); export async function GET(request: Request, context: RouteContext) {
const lang = cookieStore.get('locale')?.value || 'en'; const projectId = context.params.id;
const locale = await getLocaleFromCookies();
let project = projects.find((project) => project.id === projectId); let project = projects.find((project) => project.id === projectId);
if (!project) { if (!project) {
return NextResponse.json({ error: 'Not found' }, { status: 404 }); return notFound();
} }
if (lang === 'fa') {
project = { project = {
...project, ...project,
title: project.fa_title || project.title, title: localizeText(locale, project.title, project.fa_title),
content: project.fa_content || project.content, content: localizeContent(locale, project.content, project.fa_content),
}; };
}
return NextResponse.json({ return ok(project);
data: project,
});
} }
+9 -26
View File
@@ -1,47 +1,30 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { import {
generateProjects, generateProjects,
IProjectData, IProjectData,
IProjectThumbResponseData, IProjectThumbResponseData,
} from './data'; } 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) { export async function GET(request: Request) {
let projects = generateProjects(); let projects = generateProjects();
let data = projects as IProjectThumbResponseData[]; let data = projects as IProjectThumbResponseData[];
const cookieStore = await cookies(); const locale = await getLocaleFromCookies();
const lang = (await cookieStore).get('lang')?.value || 'fa'; const { page, perPage, start, end } = getPaginationParams(request, 6);
// 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 paginatedProjects = projects.slice(start, end); const paginatedProjects = projects.slice(start, end);
const setRelatedLocaleData = (project: IProjectData) => ({ const setRelatedLocaleData = (project: IProjectData) => ({
title: project.fa_title || project.title, title: localizeText(locale, project.title, project.fa_title),
}); });
data = paginatedProjects.map((project) => ({ data = paginatedProjects.map((project) => ({
id: project.id, id: project.id,
imageSrc: project.imageSrc, imageSrc: project.imageSrc,
...(lang === 'fa' ...setRelatedLocaleData(project),
? setRelatedLocaleData(project)
: { title: project.title }),
})); }));
return NextResponse.json({ return okPaginated(data, buildPagination(page, perPage, projects.length));
data,
pagination: {
page,
perPage,
total: projects.length,
totalPages: Math.ceil(projects.length / perPage),
},
});
} }
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 langFa from './lang_fa.png';
import en from './en_square.png'; import en from './en_square.png';
import fa from './ir_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 { export default {
homeBanner, homeBanner,
logo, logo,
@@ -32,4 +36,8 @@ export default {
fa,en, fa,en,
langEn, langEn,
langFa, 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: [ 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 { import {
IContentCTA,
IContentData, IContentData,
IContentDataLink, IContentDataLink,
IContentImage, IContentImage,
IContentQuote,
IDynamicContent, IDynamicContent,
} from '@/models/dynamicContent'; } from '@/models/dynamicContent';
function ImageContent(data: IContentImage) { function imageContent(data: IContentImage) {
return ( return (
<div className="flex flex-col items-center gap-3"> <div className="flex flex-col items-center gap-3">
<img <img
@@ -26,12 +28,23 @@ function titleContent(data: string) {
return <h2 className="text-2xl font-bold text-gray-500">{data}</h2>; return <h2 className="text-2xl font-bold text-gray-500">{data}</h2>;
} }
function fullContent(data: IContentData) { function subtitleContent(data: string) {
const splittedData = data.data?.split('\n') || []; return <h3 className="text-xl font-semibold text-gray-500">{data}</h3>;
return splittedData.map((e) => { }
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 ( return (
<p className="indent-5 text-base text-gray-400"> <p key={index} className="indent-5 text-base text-gray-400">
{generateLinkedContent(e, data.links)} {generateLinkedContent(e, normalizedData.links)}
</p> </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({ export default function ContentGenerator({
data, data,
className, className,
@@ -89,17 +130,27 @@ export default function ContentGenerator({
}) { }) {
return ( return (
<div className={`flex flex-col gap-3 md:gap-6 ${className}`}> <div className={`flex flex-col gap-3 md:gap-6 ${className}`}>
{data.map((e) => { {data.map((e, index) => {
switch (e.type) { switch (e.type) {
case 'title': 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': case 'content':
return <div>{fullContent(e.data as IContentData)}</div>; return <div key={index}>{fullContent(e.data)}</div>;
case 'image': case 'image':
return ImageContent(e.data as IContentImage); return <div key={index}>{imageContent(e.data)}</div>;
case 'bullet': case 'bullet':
case 'numberList': 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> </div>
+26 -9
View File
@@ -1,16 +1,18 @@
'use client'; 'use client';
import images from '@/assets/images/images'; import { appConstants } from '@/assets/constants';
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 { getQuickLinks } from '@/assets/constants/footerMenu/quickLinks.const'; import { getQuickLinks } from '@/assets/constants/footerMenu/quickLinks.const';
import { getWhoAreWeLinks } from '@/assets/constants/footerMenu/whoAreWe.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 { 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() { export default function Footer() {
const t = useTranslations(); const t = useTranslations();
@@ -41,7 +43,7 @@ export default function Footer() {
</h5> </h5>
</div> </div>
<div className="flex items-start justify-between gap-8 border-y border-gray-300/50 py-14 max-lg:flex-col-reverse"> <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"> <div className="grid w-full grid-cols-2 gap-5 md:grid-cols-3 md:gap-10 lg:max-w-3/5">
<FooterMenuBuilder <FooterMenuBuilder
title={t('footer_links_quick_links')} title={t('footer_links_quick_links')}
@@ -74,6 +76,21 @@ export default function Footer() {
{t('footer_links_address')} {t('footer_links_address')}
</span> </span>
</div> </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> </FooterMenuBuilderWrapper>
</div> </div>
</div> </div>
+5 -15
View File
@@ -1,11 +1,10 @@
'use client'; 'use client';
import { appConstants } from '@/assets/constants';
import images from '@/assets/images/images'; 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 Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { useTranslations } from 'next-intl';
import LanguageSwitch from '../languageSwitch'; import LanguageSwitch from '../languageSwitch';
export default function FooterInfo({ export default function FooterInfo({
@@ -32,18 +31,9 @@ export default function FooterInfo({
)} )}
<p className="text-sm tracking-normal">{t('footer_slogan')}</p> <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) => ( {withLogo && <CertificateGallery />}
<Link {!withLogo && <LanguageSwitch />}
href={social.link}
key={social.title}
className="text-white lg:flex-1"
>
<Icon name={social.icon as IconName} className="text-primary" />
</Link>
))}
</div>
<LanguageSwitch />
</div> </div>
); );
} }
@@ -1,14 +1,14 @@
'use client'; 'use client';
import Link from 'next/link'; import routeFactory from '@/assets/constants/routeFactory';
import Image from 'next/image';
import images from '@/assets/images/images'; import images from '@/assets/images/images';
import { usePathname, useParams } from 'next/navigation';
import { Icon } from '@/components/uikit/icons'; import { Icon } from '@/components/uikit/icons';
import { useTranslations } from 'next-intl'; 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 { useEffect, useRef } from 'react';
import FooterInfo from '../footer/FooterInfo'; import FooterInfo from '../footer/FooterInfo';
import routeFactory from '@/assets/constants/routeFactory';
interface Props { interface Props {
isOpen: boolean; isOpen: boolean;
@@ -66,7 +66,9 @@ export default function HamburgerMenu({ isOpen, onClose }: Props) {
if (!isOpen) return null; if (!isOpen) return null;
return ( 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 className="absolute inset-0 bg-black/60" />
<div <div
@@ -24,11 +24,17 @@ export default function ModalLanguageSelector() {
const t = useTranslations(); const t = useTranslations();
useEffect(() => { useEffect(() => {
const selected = localStorage.getItem('selectedLocale'); 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) => { const changeLocale = (locale: TLocales) => {
localStorage.setItem('selectedLocale', locale); localStorage.setItem('selectedLocale', locale);
document.cookie = `locale=${locale}; path=/; max-age=31536000; samesite=lax`;
const path = window.location.pathname.replace(/^\/(en|fa)/, ''); const path = window.location.pathname.replace(/^\/(en|fa)/, '');
window.location.pathname = `/${locale}${path}`; window.location.pathname = `/${locale}${path}`;
}; };
+16 -30
View File
@@ -2,28 +2,22 @@
import images from '@/assets/images/images'; import images from '@/assets/images/images';
import { TLocales } from '@/models/layout'; import { TLocales } from '@/models/layout';
import Image from 'next/image'; import Select from '@/components/uikit/select';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
const languages = [
{ value: 'en', label: 'English', image: images.en },
{ value: 'fa', label: 'فارسی', image: images.fa },
];
export default function LanguageSwitch() { export default function LanguageSwitch() {
const pathname = usePathname(); const pathname = usePathname();
const currentLocale = pathname.split('/')[1] as TLocales;
const languages = [ const changeLocale = (locale: string) => {
{
title: 'EN',
image: images.langEn,
locale: 'en',
},
{
title: 'FA',
image: images.langFa,
locale: 'fa',
},
];
const changeLocale = (locale: TLocales) => {
const currentLocale = pathname.split('/')[1];
if (currentLocale === locale) return; if (currentLocale === locale) return;
document.cookie = `locale=${locale}; path=/; max-age=31536000; samesite=lax`;
let newPath = pathname.replace(/^\/(en|fa)/, ''); let newPath = pathname.replace(/^\/(en|fa)/, '');
if (newPath === '/') newPath = ''; if (newPath === '/') newPath = '';
@@ -31,19 +25,11 @@ export default function LanguageSwitch() {
}; };
return ( return (
<div className="flex items-center justify-center gap-3"> <Select
{languages.map((language) => ( options={languages}
<div value={currentLocale}
className="flex cursor-pointer flex-col items-center justify-center gap-2 px-1" onChange={changeLocale}
onClick={() => changeLocale(language.locale as TLocales)} size="small"
key={language.title} />
>
<Image src={language.image} alt={language.title} />
<span className="text-xs font-normal text-white">
{language.title}
</span>
</div>
))}
</div>
); );
} }
+10 -4
View File
@@ -1,14 +1,15 @@
'use client'; 'use client';
import routeFactory from '@/assets/constants/routeFactory';
import images from '@/assets/images/images'; import images from '@/assets/images/images';
import { Icon } from '@/components/uikit/icons'; import { Icon } from '@/components/uikit/icons';
import { useTranslations } from 'next-intl';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname, useParams } from 'next/navigation'; import { useParams, usePathname } from 'next/navigation';
import { useState } from 'react'; import { useState } from 'react';
import HamburgerMenu from '../hamburgerMenu'; import HamburgerMenu from '../hamburgerMenu';
import { useTranslations } from 'next-intl'; import LanguageSwitch from '../languageSwitch';
import routeFactory from '@/assets/constants/routeFactory';
export default function Navbar() { export default function Navbar() {
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
@@ -50,7 +51,7 @@ export default function Navbar() {
/> />
</Link> </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 }) => ( {links.map(({ route, title }) => (
<li key={route()}> <li key={route()}>
<Link <Link
@@ -61,12 +62,17 @@ export default function Navbar() {
</Link> </Link>
</li> </li>
))} ))}
<div className="h-4 w-px bg-white opacity-35"></div>
<li> <li>
<a className="flex items-center gap-2" href="tel:+098123456789"> <a className="flex items-center gap-2" href="tel:+098123456789">
<Icon name="phone" className="text-primary" /> <Icon name="phone" className="text-primary" />
<p>+098 123456789</p> <p>+098 123456789</p>
</a> </a>
</li> </li>
<li>
<LanguageSwitch />
</li>
</ul> </ul>
{!open && ( {!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) => { const handleSubmit = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!validate()) return;
setLoading(true); setLoading(true);
setContact(formData).finally(() => { setContact(formData).finally(() => {
setLoading(false); setLoading(false);
@@ -1,3 +1,7 @@
'use client';
import { useEffect } from 'react';
import { useState } from 'react';
import type { IProductCardProps } from './productCard.d'; import type { IProductCardProps } from './productCard.d';
export default function ProductCard({ export default function ProductCard({
@@ -5,8 +9,27 @@ export default function ProductCard({
imageSrc, imageSrc,
size, size,
}: IProductCardProps) { }: 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 ( return (
<div className="overflow-hidden rounded-xl bg-gray-200 p-4"> <>
<div
className="cursor-pointer overflow-hidden rounded-xl bg-gray-200 p-4"
onClick={() => setIsPreviewOpen(true)}
>
<div className="aspect-square w-full"> <div className="aspect-square w-full">
<img <img
src={imageSrc} src={imageSrc}
@@ -17,5 +40,29 @@ export default function ProductCard({
<h3 className="mt-4 mb-10 text-xl font-bold text-gray-500">{model}</h3> <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> <span className="text-base font-normal text-gray-300">{size}</span>
</div> </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 export const host = process.env.VERCEL_PROJECT_PRODUCTION_URL
? `${process.env.VERCEL_PROJECT_PRODUCTION_URL}` ? `${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
: `http://localhost:${port}`; : `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'; import axios from 'axios';
const api = axios.create({ 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: { headers: {
'Content-Type': 'application/json', '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}`, (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; let response: NextResponse;
@@ -26,7 +30,7 @@ export function middleware(request: NextRequest) {
const pathnameIsMissingLocale = !matchedLocale; const pathnameIsMissingLocale = !matchedLocale;
if (pathnameIsMissingLocale) { if (pathnameIsMissingLocale) {
response = NextResponse.redirect( response = NextResponse.redirect(
new URL(`/${defaultLocale}${pathname}`, request.url), new URL(`/${preferredLocale}${pathname}`, request.url),
); );
} else { } else {
response = NextResponse.next(); response = NextResponse.next();
+68 -9
View File
@@ -1,14 +1,62 @@
export interface IDynamicContent { export type DynamicContentType =
type: keyof IDynamicContentItem; | 'title'
data: string[] | string | IContentImage | IContentData; | '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 { export interface ISubtitleContentBlock {
bullet?: string[]; type: 'subtitle';
numberList?: string[]; data: string;
image?: IContentImage; }
title?: string;
content?: IContentData; 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 { export interface IContentImage {
@@ -28,3 +76,14 @@ export interface IContentDataLink {
link: string; link: string;
label: string; label: string;
} }
export interface IContentQuote {
text: string;
author?: string;
}
export interface IContentCTA {
label: string;
link: string;
target?: '_self' | '_blank';
}
+5 -24
View File
@@ -1,37 +1,18 @@
'use server'; 'use server';
import { IPostResponseData, IPostThumbResponseData } from '@/app/api/blog/data'; 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 { IResponse } from '@/models/response';
import { cookies } from 'next/headers';
export async function getPosts( export async function getPosts(
page = 1, page = 1,
): Promise<IResponse<IPostThumbResponseData[]>> { ): Promise<IResponse<IPostThumbResponseData[]>> {
const cookiesStore = await cookies(); return serverGet<IResponse<IPostThumbResponseData[]>>('/blog', {
return api
.get('/blog', {
headers: {
Cookie: cookiesStore.toString(),
},
params: { page }, params: { page },
}) });
.then(({ data }) => {
return Promise.resolve(data);
})
.catch((error) => Promise.reject(error));
} }
export async function getPost(postId: string): Promise<IPostResponseData> { export async function getPost(postId: string): Promise<IPostResponseData> {
const cookiesStore = await cookies(); const data = await serverGet<{ data: IPostResponseData }>(`/blog/${postId}`);
return api return data.data;
.get(`/blog/${postId}`, {
headers: {
Cookie: cookiesStore.toString(),
},
})
.then(({ data }) => {
return Promise.resolve(data.data);
})
.catch((error) => Promise.reject(error));
} }
+9 -14
View File
@@ -1,18 +1,13 @@
import api from "@/lib/axios"; import api from '@/lib/axios';
interface payload { interface Payload {
first_name: string, first_name: string;
last_name: string, last_name: string;
email: string, email: string;
phone: string, phone: string;
message: string, message: string;
} }
export async function setContact(payload:payload) { export async function setContact(payload: Payload) {
return api await api.post('/contact', payload);
.post('/contact', payload)
.then(({ data }) => {
return Promise.resolve();
})
.catch((error) => Promise.reject(error));
} }
+14 -38
View File
@@ -2,57 +2,33 @@
import type { IProductCategoryCardProps } from '@/components/pages/productCategories/productCategoryCard.d'; import type { IProductCategoryCardProps } from '@/components/pages/productCategories/productCategoryCard.d';
import type { IProductCardProps } from '@/components/pages/products/productCard/productCard.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 { IResponse } from '@/models/response';
import { cookies } from 'next/headers';
export async function getProductCategories() { export async function getProductCategories() {
const cookiesStore = await cookies(); const data = await serverGet<{ data: IProductCategoryCardProps[] }>(
return api '/product_categories',
.get('/product_categories', { );
headers: { return data.data;
Cookie: cookiesStore.toString(),
},
})
.then(({ data }) => {
return Promise.resolve(data.data);
})
.catch((error) => Promise.reject(error));
} }
export async function getProductCategoryById( export async function getProductCategoryById(
categoryId: string, categoryId: string,
): Promise<IProductCategoryCardProps> { ): Promise<IProductCategoryCardProps> {
const cookiesStore = await cookies(); const data = await serverGet<{ data: IProductCategoryCardProps }>(
return api `/product_categories/${categoryId}`,
.get(`/product_categories/${categoryId}`, { );
headers: { return data.data;
Cookie: cookiesStore.toString(),
},
})
.then(({ data }) => {
return Promise.resolve(data.data);
})
.catch((error) => Promise.reject(error));
} }
export async function getProducts( export async function getProducts(
categoryId: string, categoryId: string,
page = 1, page = 1,
): Promise<IResponse<IProductCardProps[]>> { ): Promise<IResponse<IProductCardProps[]>> {
const cookiesStore = await cookies(); return serverGet<IResponse<IProductCardProps[]>>(
console.log('request'); `/product_categories/${categoryId}/products`,
console.log(page); {
return api
.get(`/product_categories/${categoryId}/products`, {
headers: {
Cookie: cookiesStore.toString(),
},
params: { page }, params: { page },
}) },
.then(({ data }) => { );
return Promise.resolve(data);
})
.catch((error) => Promise.reject(error));
} }
+7 -23
View File
@@ -1,35 +1,19 @@
'use server'; 'use server';
import { IProjectResponseData } from '@/app/api/projects/data'; import { IProjectResponseData } from '@/app/api/projects/data';
import api from '@/lib/axios'; import { serverGet } from '@/lib/server-api';
import { cookies } from 'next/headers';
export async function getProjects(page = 1 as number) { export async function getProjects(page = 1 as number) {
const cookiesStore = await cookies(); return serverGet('/projects', {
return api
.get('/projects', {
headers: {
Cookie: cookiesStore.toString(),
},
params: { page }, params: { page },
}) });
.then(({ data }) => Promise.resolve(data))
.catch((error) => Promise.reject(error));
} }
export async function getProject( export async function getProject(
projectId: string, projectId: string,
): Promise<IProjectResponseData> { ): Promise<IProjectResponseData> {
const cookiesStore = await cookies(); const data = await serverGet<{ data: IProjectResponseData }>(
return api `/projects/${projectId}`,
.get(`/projects/${projectId}`, { );
headers: { return data.data;
Cookie: cookiesStore.toString(),
},
})
.then(({ data }) => {
return Promise.resolve(data.data);
})
.catch((error) => Promise.reject(error));
} }
+679 -1046
View File
File diff suppressed because it is too large Load Diff