feat: add product generation and pagination components

- Implemented product generation for categories with a helper function to create 50 products per category.
- Added image assets for English and Persian language support.
- Created a reusable Pagination component for navigating through product lists.
- Developed ProductList component to display products with pagination support.
- Defined TypeScript interfaces for product list props and API response structure.
This commit is contained in:
2025-06-08 15:52:05 +03:30
parent 929960b26f
commit 7073310a22
31 changed files with 424 additions and 205 deletions
+67
View File
@@ -0,0 +1,67 @@
// This file is auto-generated by next-intl, do not edit directly.
// See: https://next-intl.dev/docs/workflows/typescript#messages-arguments
declare const messages: {
"pages_home_title": "Home",
"pages_home_description": "Welcome to the home page",
"pages_about_title": "About Us",
"pages_about_description": "Learn more about us on this page",
"pages_contact_title": "Contact Us",
"pages_product_categories_title": "Products",
"pages_products_title": "Products",
"pages_blog_title": "Blog",
"pages_projects_title": "Projects",
"pages_contact_description": "Get in touch with us through this page",
"com_home_banner_title": "Excellence innovating",
"com_home_banner_title_bold": "industry for today",
"com_home_banner_description": "At the heart of our operations is a commitment to delivering superior products through cutting- <br/> edge technology and innovative processes.",
"com_home_banner_button": " Explore More",
"com_home_Banner_link1": " Advanced Manufacturing Solutions",
"com_home_Banner_link2": "Quality Assurance Systems",
"com_home_Banner_link3": "State-of-the-Art Technology",
"com_about_info_title": "Building quality through",
"com_about_info_title_bold": "industrial innovation",
"com_about_info_description": "At the heart of our operations is a commitment to delivering superior products through cutting-edge technology and innovative processes.",
"com_about_info_years_experience": "years of experience",
"com_about_info_achievements_1": "Sustainable Manufacturing",
"com_about_info_achievements_2": "Advanced Automation",
"com_about_info_achievements_3": "Efficient Production Processes",
"com_about_info_achievements_4": "Reliable Delivery Services",
"com_home_products_title": "The Qualified bricks for Your Projects",
"com_home_products_description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.",
"footer_title": "Industrial Solutions for a Modern World",
"footer_description": "We are committed to providing personalized industrial solutions that meet the unique needs of our clients. Our team of experts is dedicated to delivering high-quality products and services that drive success in todays competitive market.",
"footer_slogan": "We are committed to providing personalized industrial solutions.",
"footer_links_quick_links": "Quick Links",
"footer_links_who_we_are": "Who We Are",
"footer_links_contact": "Contact",
"footer_copyright": "Copyright © 2025 All Rights Reserved.",
"footer_links_address": "123 Industrial Ave, Suite 456, City, Country",
"com_home_story_title": "Our Story",
"com_home_story_slogan": "Building quality through",
"com_home_story_slogan_bold": "industrial innovation",
"com_home_story_description": "We are a leading provider of industrial solutions, dedicated to delivering high-quality products and services that meet the unique needs of our clients. Our team of experts is committed to driving success in todays competitive market through innovation and excellence.",
"com_home_story_projects_count": "Completed Projects",
"com_home_story_customer_count": "Happy Customers",
"com_home_story_experience": "Years of Experience",
"com_products_category_Ghazaghi_title": "Ghazaghi",
"com_products_category_Ghermez_title": "Ghermez",
"com_products_category_Semirom_title": "Semirom",
"com_products_category_Zard_title": "Zard",
"com_products_category_types_count": "count Types",
"com_products_category_show_all_CTA": "Show Products",
"pages_products_content_title": "Brick Classification: Understanding the Variety and Applications",
"pages_products_content_description": "Bricks, as one of humanity's oldest and most widely used building materials, have evolved significantly throughout history. Today, they are produced and supplied in an incredibly diverse range, allowing engineers and architects to choose the most suitable brick for any project and need, from traditional and historic structures to modern and advanced buildings. To better understand this diversity, we can classify bricks based on several key criteria, each with its own specific characteristics and applications. \n 1. Classification by Manufacturing Method: From Tradition to Advanced IndustryThe method by which bricks are produced directly impacts their appearance, texture, and properties. Hand-moulded/Stock Bricks: These bricks are produced by manually pressing clay into molds. The result is a brick with a relatively rough texture, uneven edges, and a more traditional appearance, where each piece feels unique. This type of brick is often used for restoration projects or for constructing buildings with a rustic or antique style. Machine-made Bricks: These bricks are produced using industrial machinery, resulting in much higher precision in dimensions and uniformity in appearance. Machine-made bricks themselves are divided into two main categories: Perforated/Hollow Bricks: These bricks have holes, offering several advantages: they reduce building weight, improve thermal and acoustic insulation, and enhance mortar adhesion. Solid Bricks: These bricks are without holes and possess very high compressive strength. They are ideal for load-bearing structures and walls requiring maximum strength. 2. Classification by Raw Material Type and Composition: From Clay to Concrete The material from which a brick is made determines its physical and chemical properties.",
"com_products_products_category_title": "Products of {title} Category",
"pages_notFound_title": "404 Not Found Page",
"pages_notFound_description": "The page you are looking for does not exist or has been moved. Please check the URL or return to the home page.",
"pages_notFound_button": "Go to Home Page"
};
export default messages;
+5 -1
View File
@@ -55,5 +55,9 @@
"pages_products_content_title": "Brick Classification: Understanding the Variety and Applications", "pages_products_content_title": "Brick Classification: Understanding the Variety and Applications",
"pages_products_content_description": "Bricks, as one of humanity's oldest and most widely used building materials, have evolved significantly throughout history. Today, they are produced and supplied in an incredibly diverse range, allowing engineers and architects to choose the most suitable brick for any project and need, from traditional and historic structures to modern and advanced buildings. To better understand this diversity, we can classify bricks based on several key criteria, each with its own specific characteristics and applications. \n 1. Classification by Manufacturing Method: From Tradition to Advanced IndustryThe method by which bricks are produced directly impacts their appearance, texture, and properties. Hand-moulded/Stock Bricks: These bricks are produced by manually pressing clay into molds. The result is a brick with a relatively rough texture, uneven edges, and a more traditional appearance, where each piece feels unique. This type of brick is often used for restoration projects or for constructing buildings with a rustic or antique style. Machine-made Bricks: These bricks are produced using industrial machinery, resulting in much higher precision in dimensions and uniformity in appearance. Machine-made bricks themselves are divided into two main categories: Perforated/Hollow Bricks: These bricks have holes, offering several advantages: they reduce building weight, improve thermal and acoustic insulation, and enhance mortar adhesion. Solid Bricks: These bricks are without holes and possess very high compressive strength. They are ideal for load-bearing structures and walls requiring maximum strength. 2. Classification by Raw Material Type and Composition: From Clay to Concrete The material from which a brick is made determines its physical and chemical properties.", "pages_products_content_description": "Bricks, as one of humanity's oldest and most widely used building materials, have evolved significantly throughout history. Today, they are produced and supplied in an incredibly diverse range, allowing engineers and architects to choose the most suitable brick for any project and need, from traditional and historic structures to modern and advanced buildings. To better understand this diversity, we can classify bricks based on several key criteria, each with its own specific characteristics and applications. \n 1. Classification by Manufacturing Method: From Tradition to Advanced IndustryThe method by which bricks are produced directly impacts their appearance, texture, and properties. Hand-moulded/Stock Bricks: These bricks are produced by manually pressing clay into molds. The result is a brick with a relatively rough texture, uneven edges, and a more traditional appearance, where each piece feels unique. This type of brick is often used for restoration projects or for constructing buildings with a rustic or antique style. Machine-made Bricks: These bricks are produced using industrial machinery, resulting in much higher precision in dimensions and uniformity in appearance. Machine-made bricks themselves are divided into two main categories: Perforated/Hollow Bricks: These bricks have holes, offering several advantages: they reduce building weight, improve thermal and acoustic insulation, and enhance mortar adhesion. Solid Bricks: These bricks are without holes and possess very high compressive strength. They are ideal for load-bearing structures and walls requiring maximum strength. 2. Classification by Raw Material Type and Composition: From Clay to Concrete The material from which a brick is made determines its physical and chemical properties.",
"com_products_products_category_title": "Products of {title} Category" "com_products_products_category_title": "Products of {title} Category",
"pages_notFound_title": "404 Not Found Page",
"pages_notFound_description": "The page you are looking for does not exist or has been moved. Please check the URL or return to the home page.",
"pages_notFound_button": "Go to Home Page"
} }
+5 -1
View File
@@ -55,5 +55,9 @@
"pages_products_content_title": "طبقه‌بندی آجر: درک تنوع و کاربردها", "pages_products_content_title": "طبقه‌بندی آجر: درک تنوع و کاربردها",
"pages_products_content_description": "آجرها به عنوان یکی از قدیمی‌ترین و پرکاربردترین مصالح ساختمانی بشر، در طول تاریخ دچار تحول زیادی شده‌اند. امروزه این مصالح در طیف بسیار متنوعی تولید و عرضه می‌شوند و به مهندسان و معماران این امکان را می‌دهند تا مناسب‌ترین آجر را برای هر پروژه و نیازی، از سازه‌های سنتی و تاریخی تا ساختمان‌های مدرن و پیشرفته، انتخاب کنند. برای درک بهتر این تنوع، می‌توان آجرها را بر اساس چند معیار کلیدی طبقه‌بندی کرد که هر یک ویژگی‌ها و کاربردهای خاص خود را دارند.\n۱. طبقه‌بندی بر اساس روش تولید: از سنت تا صنعت پیشرفته\nروش تولید آجرها مستقیماً بر ظاهر، بافت و ویژگی‌های آن‌ها تأثیر می‌گذارد.\nآجرهای دستی/قالبی: این آجرها با فشار دادن دستی خاک رس در قالب تولید می‌شوند. نتیجه، آجری با بافت نسبتاً زبر، لبه‌های نامنظم و ظاهری سنتی‌تر است که هر قطعه حس منحصر به فردی دارد. این نوع آجر اغلب برای پروژه‌های مرمت یا ساخت ساختمان‌هایی با سبک روستایی یا قدیمی استفاده می‌شود.\nآجرهای ماشینی: این آجرها با استفاده از ماشین‌آلات صنعتی تولید می‌شوند که دقت ابعادی و یکنواختی ظاهری بسیار بالاتری دارند. آجرهای ماشینی خود به دو دسته اصلی تقسیم می‌شوند:\nآجرهای سوراخ‌دار/توخالی: این آجرها دارای سوراخ‌هایی هستند که چندین مزیت دارند: کاهش وزن ساختمان، بهبود عایق حرارتی و صوتی و افزایش چسبندگی ملات.\nآجرهای توپر: این آجرها بدون سوراخ بوده و مقاومت فشاری بسیار بالایی دارند. آن‌ها برای سازه‌های باربر و دیوارهایی که به حداکثر استحکام نیاز دارند، ایده‌آل هستند.\n۲. طبقه‌بندی بر اساس نوع و ترکیب مواد اولیه: از خاک رس تا بتن\nجنس آجر تعیین‌کننده ویژگی‌های فیزیکی و شیمیایی آن است.", "pages_products_content_description": "آجرها به عنوان یکی از قدیمی‌ترین و پرکاربردترین مصالح ساختمانی بشر، در طول تاریخ دچار تحول زیادی شده‌اند. امروزه این مصالح در طیف بسیار متنوعی تولید و عرضه می‌شوند و به مهندسان و معماران این امکان را می‌دهند تا مناسب‌ترین آجر را برای هر پروژه و نیازی، از سازه‌های سنتی و تاریخی تا ساختمان‌های مدرن و پیشرفته، انتخاب کنند. برای درک بهتر این تنوع، می‌توان آجرها را بر اساس چند معیار کلیدی طبقه‌بندی کرد که هر یک ویژگی‌ها و کاربردهای خاص خود را دارند.\n۱. طبقه‌بندی بر اساس روش تولید: از سنت تا صنعت پیشرفته\nروش تولید آجرها مستقیماً بر ظاهر، بافت و ویژگی‌های آن‌ها تأثیر می‌گذارد.\nآجرهای دستی/قالبی: این آجرها با فشار دادن دستی خاک رس در قالب تولید می‌شوند. نتیجه، آجری با بافت نسبتاً زبر، لبه‌های نامنظم و ظاهری سنتی‌تر است که هر قطعه حس منحصر به فردی دارد. این نوع آجر اغلب برای پروژه‌های مرمت یا ساخت ساختمان‌هایی با سبک روستایی یا قدیمی استفاده می‌شود.\nآجرهای ماشینی: این آجرها با استفاده از ماشین‌آلات صنعتی تولید می‌شوند که دقت ابعادی و یکنواختی ظاهری بسیار بالاتری دارند. آجرهای ماشینی خود به دو دسته اصلی تقسیم می‌شوند:\nآجرهای سوراخ‌دار/توخالی: این آجرها دارای سوراخ‌هایی هستند که چندین مزیت دارند: کاهش وزن ساختمان، بهبود عایق حرارتی و صوتی و افزایش چسبندگی ملات.\nآجرهای توپر: این آجرها بدون سوراخ بوده و مقاومت فشاری بسیار بالایی دارند. آن‌ها برای سازه‌های باربر و دیوارهایی که به حداکثر استحکام نیاز دارند، ایده‌آل هستند.\n۲. طبقه‌بندی بر اساس نوع و ترکیب مواد اولیه: از خاک رس تا بتن\nجنس آجر تعیین‌کننده ویژگی‌های فیزیکی و شیمیایی آن است.",
"com_products_products_category_title": "محصولات دسته {title}" "com_products_products_category_title": "محصولات دسته {title}",
"pages_notFound_title": "صفحه ۴۰۴ پیدا نشد",
"pages_notFound_description": "صفحه‌ای که به دنبال آن هستید وجود ندارد یا منتقل شده است. لطفاً آدرس را بررسی کنید یا به صفحه اصلی بازگردید.",
"pages_notFound_button": "بازگشت به صفحه اصلی"
} }
-43
View File
@@ -1,43 +0,0 @@
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const PUBLIC_FILE = /\.(.*)$/;
const locales = ['en', 'fa'];
const defaultLocale = 'en';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Ignore public files and API routes
if (
pathname.startsWith('/api') ||
PUBLIC_FILE.test(pathname)
) {
return NextResponse.next();
}
// Check if the pathname already includes a locale
const pathnameIsMissingLocale = locales.every(
(locale) => !pathname.startsWith(`/${locale}/`) && pathname !== `/${locale}`
);
if (pathnameIsMissingLocale) {
// Redirect to default locale
return NextResponse.redirect(
new URL(`/${defaultLocale}${pathname}`, request.url)
);
}
return NextResponse.next();
}
export const config = {
matcher: [
/*
Match all request paths except for:
- static files (/_next/static/)
- API routes (/api/)
*/
'/((?!_next/static|_next/image|favicon.ico|api).*)',
],
};

Before

Width:  |  Height:  |  Size: 360 KiB

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 578 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

+1 -1
View File
@@ -15,7 +15,7 @@ export default async function RootLayout({
children: React.ReactNode; children: React.ReactNode;
params: ILayoutLocaleParams; params: ILayoutLocaleParams;
}>) { }>) {
const locale = params.locale; const locale = await params.locale;
let messages; let messages;
try { try {
messages = (await import(`../../../messages/${locale}.json`)).default; messages = (await import(`../../../messages/${locale}.json`)).default;
+8 -6
View File
@@ -2,13 +2,10 @@ import Footer from '@/components/layout/footer/Footer';
import '@/app/globals.css'; import '@/app/globals.css';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
// import { cookies } from 'next/headers';
import { NextIntlClientProvider } from 'next-intl'; import { NextIntlClientProvider } from 'next-intl';
import type { ILayoutLocaleParams } from '@/models/layout.d'; import type { ILayoutLocaleParams } from '@/models/layout.d';
import { cookies } from 'next/headers';
// export const metadata: Metadata = {
// title: 'Create Next App',
// description: 'Generated by create next app',
// };
export default async function RootLayout({ export default async function RootLayout({
children, children,
@@ -17,7 +14,12 @@ export default async function RootLayout({
children: React.ReactNode; children: React.ReactNode;
params: ILayoutLocaleParams; params: ILayoutLocaleParams;
}>) { }>) {
const locale = params.locale; const paramsData = await params;
const locale = paramsData.locale;
// const cookieStore = await cookies();
// // Set locale cookie on the server
// cookieStore.set('locale', locale, { path: '/', httpOnly: false });
const fontClass = locale === 'fa' ? 'font-fa' : 'font-en'; const fontClass = locale === 'fa' ? 'font-fa' : 'font-en';
@@ -3,8 +3,10 @@ import images from '@/assets/images/images';
import Breadcrumb from '@/components/layout/breadcrumb'; import Breadcrumb from '@/components/layout/breadcrumb';
import { IBreadcrumbItem } from '@/components/layout/breadcrumb/breadcrumb'; import { IBreadcrumbItem } from '@/components/layout/breadcrumb/breadcrumb';
import InnerPageBanner from '@/components/layout/innerPages/banner'; import InnerPageBanner from '@/components/layout/innerPages/banner';
import Pagination from '@/components/layout/pagination';
import ProductCategoryCardInProducts from '@/components/pages/productCategories/ProductCategoryCardInProducts'; import ProductCategoryCardInProducts from '@/components/pages/productCategories/ProductCategoryCardInProducts';
import ProductCard from '@/components/pages/products/ProductCard'; import ProductCard from '@/components/pages/products/ProductCard';
import ProductList from '@/components/pages/products/productList';
import { ILayoutLocaleParams } from '@/models/layout'; import { ILayoutLocaleParams } from '@/models/layout';
import { getProductCategoryById, getProducts } from '@/services/products'; import { getProductCategoryById, getProducts } from '@/services/products';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
@@ -33,6 +35,8 @@ export default async function ProductPage({
{ title: productCategory.title }, { title: productCategory.title },
] as IBreadcrumbItem[]; ] as IBreadcrumbItem[];
const refetch = () => {};
return ( return (
<main className=""> <main className="">
<InnerPageBanner <InnerPageBanner
@@ -49,11 +53,11 @@ export default async function ProductPage({
<h2 className="mb-6 text-3xl font-semibold text-gray-500"> <h2 className="mb-6 text-3xl font-semibold text-gray-500">
{productCategory.title} Products {productCategory.title} Products
</h2> </h2>
<div className="grid gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5"> <ProductList
{products.map((product, index) => ( initialProducts={products.data}
<ProductCard {...product} key={index} /> initialPagination={products.pagination}
))} categoryId={productCategoryId}
</div> />
</div> </div>
</div> </div>
</main> </main>
@@ -0,0 +1,25 @@
import { productCategories } from '../../data';
import type { IProductCardProps } from '@/components/pages/products/productCard.d';
// Helper to generate 50 products for a category
export function generateProductsForCategory(
categoryId: string,
categoryTitle: string,
): IProductCardProps[] {
return Array.from({ length: 50 }, (_, i) => ({
id: `${categoryId}-${i + 1}`,
title: `${categoryTitle} Product ${i + 1}`,
size: `W ${10 + i} * L${20 + i}`,
imageSrc: `/images/product${(i % 4) + 1}.png`, // Example image path
// Add other fields from IProductCardProps if needed
}));
}
// Map of categoryId to products
export const productsByCategory: Record<string, IProductCardProps[]> =
Object.fromEntries(
productCategories.map((cat) => [
cat.id,
generateProductsForCategory(cat.id, cat.title),
]),
);
@@ -1,29 +1,36 @@
import { NextResponse, NextRequest } from 'next/server'; import { NextResponse, NextRequest } from 'next/server';
// import type { NextApiRequestContext } from 'next';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { productCategories } from '../../data'; import { productCategories } from '../../data';
import images from '@/assets/images/images'; import { generateProductsForCategory } from './data';
export async function GET(request: NextRequest, context: any) { export async function GET(request: NextRequest, context: any) {
const cookieStore = await cookies(); const cookieStore = await cookies();
const lang = cookieStore.get('lang')?.value || 'en'; const lang = cookieStore.get('locale')?.value || 'en';
const category = productCategories.find( const params = context.params;
(cat) => cat.id === context.params.id, const category = productCategories.find((cat) => cat.id === params.id);
);
if (!category) { if (!category) {
return NextResponse.json({ error: 'Not found' }, { status: 404 }); return NextResponse.json({ error: 'Not found' }, { status: 404 });
} }
const product = {
title: 'Product title', // Pagination logic
size: 'W 18 * L10', const { searchParams } = new URL(request.url);
imageSrc: '/product1.png', const page = parseInt(searchParams.get('page') || '1', 10);
}; const perPage = 20;
const products = Array.from({ length: 10 }, () => product);
const allProducts = generateProductsForCategory(category.id, category.title);
const start = (page - 1) * perPage;
const end = start + perPage;
const paginatedProducts = allProducts.slice(start, end);
return NextResponse.json({ return NextResponse.json({
data: products, data: paginatedProducts,
pagination: {
page,
perPage,
total: allProducts.length,
totalPages: Math.ceil(allProducts.length / perPage),
},
}); });
} }
+22 -30
View File
@@ -1,45 +1,37 @@
import { NextResponse, NextRequest } from 'next/server'; 'use server';
// import type { NextApiRequestContext } from 'next';
import { NextResponse, NextRequest } from 'next/server';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { productCategories } from '../data'; import { productCategories } from '../data';
export async function GET(request: NextRequest, context: any) { export async function GET(
request: NextRequest,
context: { params: { id: string } },
) {
const cookieStore = await cookies(); const cookieStore = await cookies();
const lang = cookieStore.get('lang')?.value || 'en'; const lang = cookieStore.get('locale')?.value || 'en';
const category = productCategories.find( const params = await context.params;
(cat) => cat.id === context.params.id,
); const { id } = params;
let category = productCategories.find((cat) => cat.id === id);
if (!category) { if (!category) {
return NextResponse.json({ error: 'Not found' }, { status: 404 }); return NextResponse.json({ error: 'Not found' }, { status: 404 });
} }
const titleData = if (lang === 'fa') {
lang === 'fa' && category.fa_title ? category.fa_title : category.title; category = {
const priceTypeTitleData = ...category,
lang === 'fa' && category.fa_priceTypeTitle title: category.fa_title || category.title,
? category.fa_priceTypeTitle priceTypeTitle: category.fa_priceTypeTitle || category.priceTypeTitle,
: category.priceTypeTitle; bestForTitle: category.fa_bestForTitle || category.bestForTitle,
const bestForTitleData = description: category.fa_description || category.description,
lang === 'fa' && category.fa_bestForTitle };
? category.fa_bestForTitle }
: category.bestForTitle;
const descriptionData =
lang === 'fa' && category.fa_description
? category.fa_description
: category.description;
return NextResponse.json({ return NextResponse.json({
data: { data: category,
id: category.id,
icon: category.icon,
typesCount: category.typesCount,
title: titleData,
priceTypeTitle: priceTypeTitleData,
bestForTitle: bestForTitleData,
description: descriptionData,
},
}); });
} }
+15 -13
View File
@@ -23,23 +23,25 @@ export async function GET(request: Request) {
description, description,
fa_description, fa_description,
}) => { }) => {
const titleData = lang === 'fa' && fa_title ? fa_title : title; if (lang === 'fa') {
const priceTypeTitleData = return {
lang === 'fa' && fa_priceTypeTitle id,
? fa_priceTypeTitle icon,
: priceTypeTitle; typesCount,
const bestForTitleData = title: fa_title || title,
lang === 'fa' && fa_bestForTitle ? fa_bestForTitle : bestForTitle; priceTypeTitle: fa_priceTypeTitle || priceTypeTitle,
const descriptionData = bestForTitle: fa_bestForTitle || bestForTitle,
lang === 'fa' && fa_description ? fa_description : description; description: fa_description || description,
};
}
return { return {
id, id,
icon, icon,
typesCount, typesCount,
title: titleData, title,
priceTypeTitle: priceTypeTitleData, priceTypeTitle,
bestForTitle: bestForTitleData, bestForTitle,
description: descriptionData, description,
}; };
}, },
); );
-42
View File
@@ -1,42 +0,0 @@
import { NextResponse } from 'next/server';
import { cookies } from 'next/headers';
import { productCategories } from '../product_categories/data';
export async function GET(request: Request) {
const data = productCategories.find(
(productCategory) => productCategory.id === request.url.split('/').pop(),
);
const cookieStore = cookies();
const lang = (await cookieStore).get('lang')?.value || 'fa';
// if (lang === 'fa') {
// data = productCategories.find(
// ({
// id,
// title,
// fa_title,
// icon,
// typesCount,
// priceTypeTitle,
// fa_priceTypeTitle,
// bestForTitle,
// fa_bestForTitle,
// }) => {
// const titleData = lang === 'fa' && fa_title ? fa_title : title;
// const priceTypeTitleData =
// lang === 'fa' && fa_priceTypeTitle
// ? fa_priceTypeTitle
// : priceTypeTitle;
// const bestForTitleData =
// lang === 'fa' && fa_bestForTitle ? fa_bestForTitle : bestForTitle;
// return {data}
// }
return NextResponse.json({ data });
}
export async function POST(request: Request) {
const data = await request.json();
return NextResponse.json({ message: 'Received POST request', data });
}
+5
View File
@@ -6,6 +6,11 @@
html{ html{
font-size: 16px; font-size: 16px;
scroll-snap-stop: always;
}
body{
scroll-snap-stop: always;
} }
@media (width <=48rem) { @media (width <=48rem) {
+8 -4
View File
@@ -7,17 +7,19 @@ import aboutImg_1 from './about-img-1.png';
import aboutImg_2 from './about-img-2.png'; import aboutImg_2 from './about-img-2.png';
import homeProductBack from './home_product_back.jpeg'; import homeProductBack from './home_product_back.jpeg';
import whatWeDoImage from './what-we-do-image.png'; import whatWeDoImage from './what-we-do-image.png';
import blogImg_1 from './blog_img_1.jpeg' import blogImg_1 from './blog_img_1.jpeg';
import projectImg_1 from './project_img_1.jpeg' import projectImg_1 from './project_img_1.jpeg';
import product1 from './product1.png'; import product1 from './product1.png';
import notFound from './404.png' import notFound from './404.png';
import langEn from './lang_en.png';
import langFa from './lang_fa.png';
export default { export default {
homeBanner, homeBanner,
logo, logo,
aboutBanner, aboutBanner,
blogImg_1, blogImg_1,
blogBanner, blogBanner,
projectImg_1, projectImg_1,
factoryVector, factoryVector,
aboutImg_1, aboutImg_1,
aboutImg_2, aboutImg_2,
@@ -25,4 +27,6 @@ export default {
homeProductBack, homeProductBack,
whatWeDoImage, whatWeDoImage,
product1, product1,
langEn,
langFa,
}; };
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1020 B

+37 -16
View File
@@ -1,28 +1,49 @@
'use client'; 'use client';
import Link from 'next/link'; import images from '@/assets/images/images';
import { TLocales } from '@/models/layout';
import Image from 'next/image';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
export default function LanguageSwitch() { export default function LanguageSwitch() {
const pathname = usePathname(); const pathname = usePathname();
const pathWithoutLocale = pathname.replace(/^\/(en|fa)/, ''); 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];
if (currentLocale === locale) return;
let newPath = pathname.replace(/^\/(en|fa)/, '');
if (newPath === '/') newPath = '';
window.location.pathname = `/${locale}${newPath}`;
};
return ( return (
<div className="flex items-center justify-center"> <div className="flex items-center justify-center gap-3">
<Link {languages.map((language) => (
href={`/en${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`} <div
className="mx-2 text-gray-800 hover:text-blue-500" className="flex cursor-pointer flex-col items-center justify-center gap-2 px-1"
> onClick={() => changeLocale(language.locale as TLocales)}
EN key={language.title}
</Link> >
<span className="text-gray-800">|</span> <Image src={language.image} alt={language.title} />
<Link <span className="text-xs font-normal text-white">
href={`/fa${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`} {language.title}
className="mx-2 text-gray-800 hover:text-blue-500" </span>
> </div>
FA ))}
</Link>
</div> </div>
); );
} }
+1 -1
View File
@@ -11,7 +11,7 @@ import { useTranslations } from 'next-intl';
import routeFactory from '@/assets/constants/routeFactory'; import routeFactory from '@/assets/constants/routeFactory';
export default function Navbar() { export default function Navbar() {
const [open, setOpen] = useState(false); const [open, setOpen] = useState<boolean>(false);
const pathname = usePathname(); const pathname = usePathname();
const params = useParams(); const params = useParams();
const locale = params.locale as string; const locale = params.locale as string;
@@ -0,0 +1,55 @@
'use client';
import Button, { IconButton } from '@/components/uikit/button';
import React from 'react';
interface PaginationProps {
page: number;
totalPages: number;
onPageChange?: (page: number) => void;
className?: string;
}
export default function Pagination({
page,
totalPages,
onPageChange = () => {},
className,
}: PaginationProps) {
if (totalPages <= 1) return null;
const pages = [];
for (let i = 1; i <= totalPages; i++) {
pages.push(i);
}
return (
<nav
className={`mt-8 flex items-center justify-center gap-2 ${className || ''}`}
>
<IconButton
disabled={page === 1}
onClick={() => onPageChange(page - 1)}
className="disabled:opacity-50"
>
&lt;
</IconButton>
{pages.map((p) => (
<IconButton
key={p}
className={`${p === page ? 'opacity-50' : ''}`}
onClick={() => onPageChange(p)}
>
{p}
</IconButton>
))}
<IconButton
disabled={page === totalPages}
onClick={() => onPageChange(page + 1)}
className="disabled:opacity-50"
>
&gt;
</IconButton>
</nav>
);
}
@@ -0,0 +1,38 @@
'use client';
import { useState } from 'react';
import ProductCard from '../ProductCard';
import Pagination from '@/components/layout/pagination';
import { getProducts } from '@/services/products';
import { IProductListProps } from './productList';
export default function ProductList({
initialProducts,
initialPagination,
categoryId,
}: IProductListProps) {
const [products, setProducts] = useState(initialProducts);
const [pagination, setPagination] = useState(initialPagination);
const fetchPage = (page: number) => {
getProducts(categoryId, page)
.then((res) => {
setProducts(res.data);
setPagination(res.pagination);
})
.catch((error) => {
console.error('Failed to fetch products:', error);
});
};
return (
<>
<div className="grid gap-3 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5">
{products.map((product, index) => (
<ProductCard {...product} key={index} />
))}
</div>
<Pagination {...pagination} onPageChange={fetchPage} />
</>
);
}
@@ -0,0 +1,8 @@
import { IPagination } from '@/models/response';
import { IProductCardProps } from '../productCard';
export interface IProductListProps {
initialProducts: IProductCardProps[];
initialPagination: IPagination;
categoryId: string;
}
+1
View File
@@ -8,6 +8,7 @@ export interface IBaseButtonProps
disabled?: boolean; disabled?: boolean;
className?: string; className?: string;
link?: URL | string; link?: URL | string;
onClick?: () => void;
} }
export interface ITextButtonProps extends IBaseButtonProps { export interface ITextButtonProps extends IBaseButtonProps {
+9 -1
View File
@@ -11,6 +11,7 @@ const Button: React.FC<ITextButtonProps> = ({
endIcon, endIcon,
className, className,
link, link,
onClick,
...props ...props
}) => { }) => {
const wrapperClass = `group bg-primary relative h-12 cursor-pointer rounded-[0.5625rem] p-[0.125rem] flex w-fit ${className || ''}`; const wrapperClass = `group bg-primary relative h-12 cursor-pointer rounded-[0.5625rem] p-[0.125rem] flex w-fit ${className || ''}`;
@@ -22,7 +23,12 @@ const Button: React.FC<ITextButtonProps> = ({
</ButtonBody> </ButtonBody>
</Link> </Link>
) : ( ) : (
<button disabled={disabled || loading} {...props} className={wrapperClass}> <button
disabled={disabled || loading}
{...props}
onClick={onClick}
className={wrapperClass}
>
<ButtonBody {...props} loading={loading} endIcon={endIcon}> <ButtonBody {...props} loading={loading} endIcon={endIcon}>
{children} {children}
</ButtonBody> </ButtonBody>
@@ -82,6 +88,7 @@ export const IconButton: React.FC<IIconButtonProps> = ({
size = 'medium', size = 'medium',
color = 'primary', color = 'primary',
className, className,
onClick,
...props ...props
}) => { }) => {
const colorClass = const colorClass =
@@ -95,6 +102,7 @@ export const IconButton: React.FC<IIconButtonProps> = ({
className={`group relative flex cursor-pointer items-center justify-center rounded-[0.5625rem] ${colorClass} ${sizeClass} ${className || ''} `} className={`group relative flex cursor-pointer items-center justify-center rounded-[0.5625rem] ${colorClass} ${sizeClass} ${className || ''} `}
disabled={disabled || loading} disabled={disabled || loading}
{...props} {...props}
onClick={onClick}
> >
{loading ? ( {loading ? (
<svg <svg
+4 -16
View File
@@ -1,30 +1,18 @@
import axios from 'axios'; import axios from 'axios';
// Create an Axios instance
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:3000/api',
withCredentials: true, // Send cookies with requests if needed headers: {
'Content-Type': 'application/json',
},
withCredentials: true, // Enable sending cookies with requests
}); });
api.interceptors.request.use( api.interceptors.request.use(
(config) => { (config) => {
// Example: Add lang cookie from browser (client-side only)
if (typeof window !== 'undefined') {
const match = document.cookie.match(/(^| )lang=([^;]+)/);
if (match) {
config.headers['lang'] = decodeURIComponent(match[2]);
}
}
return config; return config;
}, },
(error) => Promise.reject(error), (error) => Promise.reject(error),
); );
api.interceptors.response.use(
(response) => response,
(error) => {
return Promise.reject(error);
},
);
export default api; export default api;
+39 -4
View File
@@ -1,8 +1,43 @@
import createMiddleware from 'next-intl/middleware'; import { NextResponse } from 'next/server';
import { routing } from './i18n/routing'; import type { NextRequest } from 'next/server';
export default createMiddleware(routing); const PUBLIC_FILE = /\.(.*)$/;
const locales = ['en', 'fa'];
const defaultLocale = 'en';
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// Ignore public files and API routes
if (pathname.startsWith('/api') || PUBLIC_FILE.test(pathname)) {
return NextResponse.next();
}
// Check if the pathname already includes a locale
const matchedLocale = locales.find(
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
);
const locale = matchedLocale || defaultLocale;
let response: NextResponse;
// If locale is missing, redirect and set cookie
const pathnameIsMissingLocale = !matchedLocale;
if (pathnameIsMissingLocale) {
response = NextResponse.redirect(
new URL(`/${defaultLocale}${pathname}`, request.url),
);
} else {
response = NextResponse.next();
}
// Set the locale cookie for all responses
response.cookies.set('locale', locale, { path: '/' });
return response;
}
export const config = { export const config = {
matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)', matcher: ['/((?!_next/static|_next/image|favicon.ico|api).*)'],
}; };
+11
View File
@@ -0,0 +1,11 @@
export interface IResponse<T> {
pagination: IPagination;
data: T;
}
export interface IPagination {
page: number;
perPage: number;
total: number;
totalPages: number;
}
+30 -7
View File
@@ -1,32 +1,55 @@
'use server';
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.d'; import type { IProductCardProps } from '@/components/pages/products/productCard.d';
import api from '@/lib/axios'; import api from '@/lib/axios';
import { IResponse } from '@/models/response';
import { cookies } from 'next/headers';
export function getProductCategories() { export async function getProductCategories() {
const cookiesStore = await cookies();
return api return api
.get('/product_categories') .get('/product_categories', {
headers: {
Cookie: cookiesStore.toString(),
},
})
.then(({ data }) => { .then(({ data }) => {
return Promise.resolve(data.data); return Promise.resolve(data.data);
}) })
.catch((error) => Promise.reject(error)); .catch((error) => Promise.reject(error));
} }
export function getProductCategoryById( export async function getProductCategoryById(
categoryId: string, categoryId: string,
): Promise<IProductCategoryCardProps> { ): Promise<IProductCategoryCardProps> {
const cookiesStore = await cookies();
return api return api
.get(`/product_categories/${categoryId}`) .get(`/product_categories/${categoryId}`, {
headers: {
Cookie: cookiesStore.toString(),
},
})
.then(({ data }) => { .then(({ data }) => {
return Promise.resolve(data.data); return Promise.resolve(data.data);
}) })
.catch((error) => Promise.reject(error)); .catch((error) => Promise.reject(error));
} }
export function getProducts(categoryId: string): Promise<IProductCardProps[]> { export async function getProducts(
categoryId: string,
page = 1,
): Promise<IResponse<IProductCardProps[]>> {
const cookiesStore = await cookies();
return api return api
.get(`/product_categories/${categoryId}/products`) .get(`/product_categories/${categoryId}/products`, {
headers: {
Cookie: cookiesStore.toString(),
},
params: { page },
})
.then(({ data }) => { .then(({ data }) => {
return Promise.resolve(data.data); return Promise.resolve(data);
}) })
.catch((error) => Promise.reject(error)); .catch((error) => Promise.reject(error));
} }