diff --git a/Dockerfile b/Dockerfile index 11b8396..779b760 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,20 +3,24 @@ WORKDIR /app ENV NEXT_TELEMETRY_DISABLED=1 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 pnpm install --frozen-lockfile FROM base AS builder COPY --from=deps /app/node_modules ./node_modules COPY . . +RUN npm config set registry https://hub.megan.ir/npm +RUN npm install -g pnpm RUN pnpm build FROM node:20-alpine AS runner WORKDIR /app +ARG PORT=5000 ENV NODE_ENV=production ENV NEXT_TELEMETRY_DISABLED=1 -ENV PORT=5000 +ENV PORT=$PORT 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 USER nextjs -EXPOSE 5000 +EXPOSE ${PORT} CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml index 0901c3b..dfd2c17 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,14 +3,15 @@ services: build: context: . dockerfile: Dockerfile + args: + PORT: ${PORT:-5000} image: pasargad-bricks:latest container_name: pasargad-bricks restart: unless-stopped ports: - - "5000:5000" + - "${PORT:-3000}:${PORT:-3000}" environment: NODE_ENV: production - PORT: 5000 env_file: - .env volumes: diff --git a/messages/en.d.json.ts b/messages/en.d.json.ts index 950a8e3..e89e301 100644 --- a/messages/en.d.json.ts +++ b/messages/en.d.json.ts @@ -108,6 +108,10 @@ declare const messages: { "pages_notFound_description": "You can either head back to the Homepage or try a search.", "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_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.", diff --git a/messages/en.json b/messages/en.json index b187862..271cd73 100644 --- a/messages/en.json +++ b/messages/en.json @@ -105,6 +105,10 @@ "pages_notFound_description": "You can either head back to the Homepage or try a search.", "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_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.", diff --git a/messages/fa.json b/messages/fa.json index f67e281..2f23689 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -105,6 +105,10 @@ "pages_notFound_description": "صفحه‌ای که به دنبال آن هستید وجود ندارد یا منتقل شده است. لطفاً آدرس را بررسی کنید یا به صفحه اصلی بازگردید.", "pages_notFound_button": "بازگشت به صفحه اصلی", + "com_about_certifications_title": "گواهینامه‌ها", + "com_about_certifications_title_bold": "و استانداردهای ما", + "com_about_certifications_description": "ما بالاترین استانداردهای کیفیت را از طریق گواهینامه‌های معتبر بین‌المللی حفظ می‌کنیم.", + "page_about_metadata_title": "درباره ما", "page_about_metadata_description": "با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته.", "page_about_description": "با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته.", diff --git a/next-sitemap.config.js b/next-sitemap.config.js index 1015340..886f5f5 100644 --- a/next-sitemap.config.js +++ b/next-sitemap.config.js @@ -1,5 +1,5 @@ module.exports = { - siteUrl: process.env.SITE_URL || 'http://localhost:3000', + siteUrl: process.env.SITE_URL || 'http://localhost:4000', generateRobotsTxt: true, changefreq: 'weekly', priority: 0.7, diff --git a/package-lock.json b/package-lock.json index f757f4b..40fea65 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "dotenv": "^16.5.0", + "dotenv-cli": "^11.0.0", "eslint": "^9", "eslint-config-next": "15.3.3", "eslint-config-prettier": "^10.1.5", @@ -2588,6 +2589,51 @@ "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": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index ac15eb1..37d0341 100644 --- a/package.json +++ b/package.json @@ -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": { "axios": "^1.9.0", "leaflet": "^1.9.4", @@ -28,6 +17,7 @@ "@types/react": "^19", "@types/react-dom": "^19", "dotenv": "^16.5.0", + "dotenv-cli": "^11.0.0", "eslint": "^9", "eslint-config-next": "15.3.3", "eslint-config-prettier": "^10.1.5", @@ -40,5 +30,15 @@ "ts-node": "^10.9.2", "tsconfig-paths": "^4.2.0", "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" } diff --git a/public/images/product1.png b/public/images/product1.png index 5377f24..021a482 100644 Binary files a/public/images/product1.png and b/public/images/product1.png differ diff --git a/public/images/product2.png b/public/images/product2.png index dc9d261..c9421fd 100644 Binary files a/public/images/product2.png and b/public/images/product2.png differ diff --git a/public/images/product3.png b/public/images/product3.png index 0f02dbc..8795ff1 100644 Binary files a/public/images/product3.png and b/public/images/product3.png differ diff --git a/public/robots.txt b/public/robots.txt index 3a72385..3d74463 100644 --- a/public/robots.txt +++ b/public/robots.txt @@ -3,7 +3,7 @@ User-agent: * Allow: / # Host -Host: http://localhost:3000 +Host: http://localhost:4000 # Sitemaps -Sitemap: http://localhost:3000/sitemap.xml +Sitemap: http://localhost:4000/sitemap.xml diff --git a/src/app/[locale]/[...not_found]/layout.tsx b/src/app/[locale]/[...not_found]/layout.tsx index f8a6818..8d87e98 100644 --- a/src/app/[locale]/[...not_found]/layout.tsx +++ b/src/app/[locale]/[...not_found]/layout.tsx @@ -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 ( - + {children} diff --git a/src/app/[locale]/about-us/page.tsx b/src/app/[locale]/about-us/page.tsx index 946bb89..3745a08 100644 --- a/src/app/[locale]/about-us/page.tsx +++ b/src/app/[locale]/about-us/page.tsx @@ -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({ + ); } diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 6f329dc..4f8166b 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -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 ( - + {/* */} diff --git a/src/app/api/blog/[id]/route.ts b/src/app/api/blog/[id]/route.ts index 70b9c4a..be2f28b 100644 --- a/src/app/api/blog/[id]/route.ts +++ b/src/app/api/blog/[id]/route.ts @@ -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); } diff --git a/src/app/api/blog/data.ts b/src/app/api/blog/data.ts index 9842206..57e54f4 100644 --- a/src/app/api/blog/data.ts +++ b/src/app/api/blog/data.ts @@ -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', + }, + }, ], }, { diff --git a/src/app/api/blog/route.ts b/src/app/api/blog/route.ts index 7f27ad5..e683b8d 100644 --- a/src/app/api/blog/route.ts +++ b/src/app/api/blog/route.ts @@ -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)); } diff --git a/src/app/api/contact/route.ts b/src/app/api/contact/route.ts index a0555bf..0d25ca1 100644 --- a/src/app/api/contact/route.ts +++ b/src/app/api/contact/route.ts @@ -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('مشکلی پیش آمده.'); } } diff --git a/src/app/api/product_categories/[id]/products/route.ts b/src/app/api/product_categories/[id]/products/route.ts index 3a6932e..9e781a3 100644 --- a/src/app/api/product_categories/[id]/products/route.ts +++ b/src/app/api/product_categories/[id]/products/route.ts @@ -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), + ); } diff --git a/src/app/api/product_categories/[id]/route.ts b/src/app/api/product_categories/[id]/route.ts index 100f3cd..bc5448f 100644 --- a/src/app/api/product_categories/[id]/route.ts +++ b/src/app/api/product_categories/[id]/route.ts @@ -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); } diff --git a/src/app/api/product_categories/route.ts b/src/app/api/product_categories/route.ts index 7c29a13..1e920d6 100644 --- a/src/app/api/product_categories/route.ts +++ b/src/app/api/product_categories/route.ts @@ -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'); } } diff --git a/src/app/api/projects/[id]/route.ts b/src/app/api/projects/[id]/route.ts index c64e945..dde9370 100644 --- a/src/app/api/projects/[id]/route.ts +++ b/src/app/api/projects/[id]/route.ts @@ -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); } diff --git a/src/app/api/projects/route.ts b/src/app/api/projects/route.ts index 5db0284..2dc6488 100644 --- a/src/app/api/projects/route.ts +++ b/src/app/api/projects/route.ts @@ -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)); } diff --git a/src/assets/images/certificates/iso14001.jpg b/src/assets/images/certificates/iso14001.jpg new file mode 100644 index 0000000..91a70d2 Binary files /dev/null and b/src/assets/images/certificates/iso14001.jpg differ diff --git a/src/assets/images/certificates/iso9001.jpg b/src/assets/images/certificates/iso9001.jpg new file mode 100644 index 0000000..e92b474 Binary files /dev/null and b/src/assets/images/certificates/iso9001.jpg differ diff --git a/src/assets/images/certificates/lia.jpg b/src/assets/images/certificates/lia.jpg new file mode 100644 index 0000000..4780965 Binary files /dev/null and b/src/assets/images/certificates/lia.jpg differ diff --git a/src/assets/images/certificates/standard.jpg b/src/assets/images/certificates/standard.jpg new file mode 100644 index 0000000..826d4ca Binary files /dev/null and b/src/assets/images/certificates/standard.jpg differ diff --git a/src/assets/images/images.ts b/src/assets/images/images.ts index 1807385..312d267 100644 --- a/src/assets/images/images.ts +++ b/src/assets/images/images.ts @@ -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, }; diff --git a/src/assets/temp/tempContent.ts b/src/assets/temp/tempContent.ts index dc4885a..f337b6b 100644 --- a/src/assets/temp/tempContent.ts +++ b/src/assets/temp/tempContent.ts @@ -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', + }, + }, ], }; diff --git a/src/components/layout/certificateGallery/index.tsx b/src/components/layout/certificateGallery/index.tsx new file mode 100644 index 0000000..7a63503 --- /dev/null +++ b/src/components/layout/certificateGallery/index.tsx @@ -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(null); + + return ( + <> +
+ {certificates.map((cert, i) => ( + + ))} +
+ + {selectedIndex !== null && ( + setSelectedIndex(null)} + rtl={isRtl(locale)} + /> + )} + + ); +} diff --git a/src/components/layout/contentGenerator/index.tsx b/src/components/layout/contentGenerator/index.tsx index a863161..f5cad36 100644 --- a/src/components/layout/contentGenerator/index.tsx +++ b/src/components/layout/contentGenerator/index.tsx @@ -1,11 +1,13 @@ import { + IContentCTA, IContentData, IContentDataLink, IContentImage, + IContentQuote, IDynamicContent, } from '@/models/dynamicContent'; -function ImageContent(data: IContentImage) { +function imageContent(data: IContentImage) { return (
{data}; } -function fullContent(data: IContentData) { - const splittedData = data.data?.split('\n') || []; - return splittedData.map((e) => { +function subtitleContent(data: string) { + return

{data}

; +} + +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 ( -

- {generateLinkedContent(e, data.links)} +

+ {generateLinkedContent(e, normalizedData.links)}

); }); @@ -80,6 +93,34 @@ function listContent(data: string[], type: 'bullet' | 'numberList') { ); } +function quoteContent(data: IContentQuote) { + return ( +
+

{data.text}

+ {data.author ? ( + — {data.author} + ) : null} +
+ ); +} + +function ctaContent(data: IContentCTA) { + return ( + + {data.label} + + ); +} + +function dividerContent() { + return
; +} + export default function ContentGenerator({ data, className, @@ -89,17 +130,27 @@ export default function ContentGenerator({ }) { return (
- {data.map((e) => { + {data.map((e, index) => { switch (e.type) { case 'title': - return titleContent(e.data as string); + return
{titleContent(e.data)}
; + case 'subtitle': + return
{subtitleContent(e.data)}
; case 'content': - return
{fullContent(e.data as IContentData)}
; + return
{fullContent(e.data)}
; case 'image': - return ImageContent(e.data as IContentImage); + return
{imageContent(e.data)}
; case 'bullet': case 'numberList': - return listContent(e.data as string[], e.type); + return
{listContent(e.data, e.type)}
; + case 'quote': + return
{quoteContent(e.data)}
; + case 'cta': + return
{ctaContent(e.data)}
; + case 'divider': + return
{dividerContent()}
; + default: + return null; } })}
diff --git a/src/components/layout/footer/Footer.tsx b/src/components/layout/footer/Footer.tsx index 7bfa8a8..94c68aa 100644 --- a/src/components/layout/footer/Footer.tsx +++ b/src/components/layout/footer/Footer.tsx @@ -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() {
- +
+ +
+ {appConstants.socials.map((social) => ( + + + + ))} +
diff --git a/src/components/layout/footer/FooterInfo.tsx b/src/components/layout/footer/FooterInfo.tsx index dca7faf..d6463ac 100644 --- a/src/components/layout/footer/FooterInfo.tsx +++ b/src/components/layout/footer/FooterInfo.tsx @@ -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({ )}

{t('footer_slogan')}

-
- {appConstants.socials.map((social) => ( - - - - ))} -
- + + {withLogo && } + {!withLogo && } ); } diff --git a/src/components/layout/hamburgerMenu/index.tsx b/src/components/layout/hamburgerMenu/index.tsx index f309ec9..ed4736a 100644 --- a/src/components/layout/hamburgerMenu/index.tsx +++ b/src/components/layout/hamburgerMenu/index.tsx @@ -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 ( -
+
{ 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}`; }; diff --git a/src/components/layout/languageSwitch/index.tsx b/src/components/layout/languageSwitch/index.tsx index cb6be6a..2117fba 100644 --- a/src/components/layout/languageSwitch/index.tsx +++ b/src/components/layout/languageSwitch/index.tsx @@ -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 ( -
- {languages.map((language) => ( -
changeLocale(language.locale as TLocales)} - key={language.title} - > - {language.title} - - {language.title} - -
- ))} -
+