diff --git a/Dockerfile b/Dockerfile index 1e9db6a..9659d11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,19 @@ -# Use official Node.js image as the base +# Build stage FROM node:20-alpine AS builder - -# Set working directory WORKDIR /app - -# Install dependencies COPY package.json package-lock.json ./ RUN npm ci - -# Copy the rest of the app COPY . . - -# Build the Next.js app RUN npm run build - -# Set environment variables (optional) +# Production stage +FROM node:20-alpine AS runner +WORKDIR /app ENV NODE_ENV=production - -# Expose port +ENV PORT=5000 +COPY --from=builder /app/.next ./.next +COPY --from=builder /app/public ./public +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/node_modules ./node_modules EXPOSE 5000 - -# Start the Next.js app CMD ["npm", "start"] diff --git a/messages/fa.json b/messages/fa.json index 2d04522..0c37b13 100644 --- a/messages/fa.json +++ b/messages/fa.json @@ -1,59 +1,63 @@ { - "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-
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", + "pages_home_title": "خانه", + "pages_home_description": "به صفحه اصلی خوش آمدید", + "pages_about_title": "درباره ما", + "pages_about_description": "در این صفحه بیشتر درباره ما بدانید", + "pages_contact_title": "تماس با ما", + "pages_product_categories_title": "محصولات", + "pages_products_title": "محصولات", + "pages_blog_title": "وبلاگ", + "pages_projects_title": "پروژه‌ها", + "pages_contact_description": "از طریق این صفحه با ما در تماس باشید", + "com_home_banner_title": "نوآوری در تعالی", + "com_home_banner_title_bold": "صنعت برای امروز", + "com_home_banner_description": "در قلب فعالیت‌های ما تعهد به ارائه محصولات برتر از طریق فناوری پیشرفته و فرآیندهای نوآورانه قرار دارد.", + "com_home_banner_button": "بیشتر بدانید", + "com_home_Banner_link1": "راهکارهای پیشرفته تولید", + "com_home_Banner_link2": "سیستم‌های تضمین کیفیت", + "com_home_Banner_link3": "فناوری روز دنیا", - "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_about_info_title": "ساخت کیفیت از طریق", + "com_about_info_title_bold": "نوآوری صنعتی", + "com_about_info_description": "در قلب فعالیت‌های ما تعهد به ارائه محصولات برتر از طریق فناوری پیشرفته و فرآیندهای نوآورانه قرار دارد.", + "com_about_info_years_experience": "سال تجربه", + "com_about_info_achievements_1": "تولید پایدار", + "com_about_info_achievements_2": "اتوماسیون پیشرفته", + "com_about_info_achievements_3": "فرآیندهای تولید کارآمد", + "com_about_info_achievements_4": "خدمات تحویل قابل اعتماد", - "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.", + "com_home_products_title": "آجرهای با کیفیت برای پروژه‌های شما", + "com_home_products_description": "لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است.", - "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 today’s 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", + "footer_title": "راهکارهای صنعتی برای دنیای مدرن", + "footer_description": "ما متعهد به ارائه راهکارهای صنعتی شخصی‌سازی شده هستیم که نیازهای منحصر به فرد مشتریانمان را برآورده می‌کند. تیم متخصص ما به ارائه محصولات و خدمات با کیفیت بالا که موفقیت را در بازار رقابتی امروز تضمین می‌کند، متعهد است.", + "footer_slogan": "ما متعهد به ارائه راهکارهای صنعتی شخصی‌سازی شده هستیم.", + "footer_links_quick_links": "لینک‌های سریع", + "footer_links_who_we_are": "ما که هستیم", + "footer_links_contact": "تماس", + "footer_copyright": "کلیه حقوق محفوظ است © ۲۰۲۵", + "footer_links_address": "خیابان صنعتی ۱۲۳، واحد ۴۵۶، شهر، کشور", - "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 today’s 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_home_story_title": "داستان ما", + "com_home_story_slogan": "ساخت کیفیت از طریق", + "com_home_story_slogan_bold": "نوآوری صنعتی", + "com_home_story_description": "ما ارائه‌دهنده پیشرو راهکارهای صنعتی هستیم که به ارائه محصولات و خدمات با کیفیت بالا متعهدیم تا نیازهای منحصر به فرد مشتریانمان را برآورده کنیم. تیم متخصص ما با نوآوری و تعالی، موفقیت را در بازار رقابتی امروز تضمین می‌کند.", + "com_home_story_projects_count": "پروژه‌های انجام شده", + "com_home_story_customer_count": "مشتریان راضی", + "com_home_story_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_category_Ghazaghi_title": "غذاقی", + "com_products_category_Ghermez_title": "قرمز", + "com_products_category_Semirom_title": "سمیرم", + "com_products_category_Zard_title": "زرد", + "com_products_category_types_count": "تعداد محصولات {count}", + "com_products_category_show_all_CTA": "نمایش محصولات", + "pages_products_content_title": "طبقه‌بندی آجر: درک تنوع و کاربردها", + "pages_products_content_description": "آجرها به عنوان یکی از قدیمی‌ترین و پرکاربردترین مصالح ساختمانی بشر، در طول تاریخ دچار تحول زیادی شده‌اند. امروزه این مصالح در طیف بسیار متنوعی تولید و عرضه می‌شوند و به مهندسان و معماران این امکان را می‌دهند تا مناسب‌ترین آجر را برای هر پروژه و نیازی، از سازه‌های سنتی و تاریخی تا ساختمان‌های مدرن و پیشرفته، انتخاب کنند. برای درک بهتر این تنوع، می‌توان آجرها را بر اساس چند معیار کلیدی طبقه‌بندی کرد که هر یک ویژگی‌ها و کاربردهای خاص خود را دارند.\n۱. طبقه‌بندی بر اساس روش تولید: از سنت تا صنعت پیشرفته\nروش تولید آجرها مستقیماً بر ظاهر، بافت و ویژگی‌های آن‌ها تأثیر می‌گذارد.\nآجرهای دستی/قالبی: این آجرها با فشار دادن دستی خاک رس در قالب تولید می‌شوند. نتیجه، آجری با بافت نسبتاً زبر، لبه‌های نامنظم و ظاهری سنتی‌تر است که هر قطعه حس منحصر به فردی دارد. این نوع آجر اغلب برای پروژه‌های مرمت یا ساخت ساختمان‌هایی با سبک روستایی یا قدیمی استفاده می‌شود.\nآجرهای ماشینی: این آجرها با استفاده از ماشین‌آلات صنعتی تولید می‌شوند که دقت ابعادی و یکنواختی ظاهری بسیار بالاتری دارند. آجرهای ماشینی خود به دو دسته اصلی تقسیم می‌شوند:\nآجرهای سوراخ‌دار/توخالی: این آجرها دارای سوراخ‌هایی هستند که چندین مزیت دارند: کاهش وزن ساختمان، بهبود عایق حرارتی و صوتی و افزایش چسبندگی ملات.\nآجرهای توپر: این آجرها بدون سوراخ بوده و مقاومت فشاری بسیار بالایی دارند. آن‌ها برای سازه‌های باربر و دیوارهایی که به حداکثر استحکام نیاز دارند، ایده‌آل هستند.\n۲. طبقه‌بندی بر اساس نوع و ترکیب مواد اولیه: از خاک رس تا بتن\nجنس آجر تعیین‌کننده ویژگی‌های فیزیکی و شیمیایی آن است.", - "com_products_products_category_title": "Products of {title} Category" + "com_products_products_category_title": "محصولات دسته {title}", + + "pages_notFound_title": "صفحه ۴۰۴ پیدا نشد", + "pages_notFound_description": "صفحه‌ای که به دنبال آن هستید وجود ندارد یا منتقل شده است. لطفاً آدرس را بررسی کنید یا به صفحه اصلی بازگردید.", + "pages_notFound_button": "بازگشت به صفحه اصلی" } diff --git a/middleware.ts b/middleware.ts deleted file mode 100644 index 63fa951..0000000 --- a/middleware.ts +++ /dev/null @@ -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).*)', - ], -}; \ No newline at end of file diff --git a/next-sitemap.config.js b/next-sitemap.config.js new file mode 100644 index 0000000..1015340 --- /dev/null +++ b/next-sitemap.config.js @@ -0,0 +1,18 @@ +module.exports = { + siteUrl: process.env.SITE_URL || 'http://localhost:3000', + generateRobotsTxt: true, + changefreq: 'weekly', + priority: 0.7, + sitemapSize: 7000, + // Enable i18n support + i18n: { + locales: ['en', 'fa'], + defaultLocale: 'en', + }, + // Optionally, exclude certain paths (like API routes) + exclude: ['/api/*', '/_next/*'], + // If you want to include additional sitemaps (for subdirectories, etc.) + // additionalSitemaps: [ + // `${process.env.SITE_URL || 'http://localhost:5000'}/custom-sitemap.xml`, + // ], +}; diff --git a/package-lock.json b/package-lock.json index c75439e..f980f9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/leaflet": "^1.9.18", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -27,6 +28,7 @@ "eslint-config-prettier": "^10.1.5", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-prettier": "^5.4.1", + "next-sitemap": "^4.2.3", "prettier": "^3.5.3", "prettier-plugin-tailwindcss": "^0.6.12", "tailwindcss": "^4", @@ -58,6 +60,12 @@ "node": ">=6.0.0" } }, + "node_modules/@corex/deepmerge": { + "version": "4.0.43", + "resolved": "https://registry.npmjs.org/@corex/deepmerge/-/deepmerge-4.0.43.tgz", + "integrity": "sha512-N8uEMrMPL0cu/bdboEWpQYb/0i2K5Qn8eCsxzOmxSggJbbQte7ljMRoXm917AbntqTGOzdTu+vP3KOOzoC70HQ==", + "dev": true + }, "node_modules/@emnapi/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", @@ -1289,6 +1297,12 @@ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "dev": true }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -1301,6 +1315,15 @@ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", "dev": true }, + "node_modules/@types/leaflet": { + "version": "1.9.18", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.18.tgz", + "integrity": "sha512-ht2vsoPjezor5Pmzi5hdsA7F++v5UGq9OlUduWHmMZiuQGIpJ2WS5+Gg9HaAA79gNh1AIPtCqhzejcIZ3lPzXQ==", + "dev": true, + "dependencies": { + "@types/geojson": "*" + } + }, "node_modules/@types/node": { "version": "20.17.57", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.57.tgz", @@ -4638,6 +4661,39 @@ } } }, + "node_modules/next-sitemap": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/next-sitemap/-/next-sitemap-4.2.3.tgz", + "integrity": "sha512-vjdCxeDuWDzldhCnyFCQipw5bfpl4HmZA7uoo3GAaYGjGgfL4Cxb1CiztPuWGmS+auYs7/8OekRS8C2cjdAsjQ==", + "dev": true, + "funding": [ + { + "url": "https://github.com/iamvishnusankar/next-sitemap.git" + } + ], + "dependencies": { + "@corex/deepmerge": "^4.0.43", + "@next/env": "^13.4.3", + "fast-glob": "^3.2.12", + "minimist": "^1.2.8" + }, + "bin": { + "next-sitemap": "bin/next-sitemap.mjs", + "next-sitemap-cjs": "bin/next-sitemap.cjs" + }, + "engines": { + "node": ">=14.18" + }, + "peerDependencies": { + "next": "*" + } + }, + "node_modules/next-sitemap/node_modules/@next/env": { + "version": "13.5.11", + "resolved": "https://registry.npmjs.org/@next/env/-/env-13.5.11.tgz", + "integrity": "sha512-fbb2C7HChgM7CemdCY+y3N1n8pcTKdqtQLbC7/EQtPdLvlMUT9JX/dBYl8MMZAtYG4uVMyPFHXckb68q/NRwqg==", + "dev": true + }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", diff --git a/package.json b/package.json index af4798b..f22724a 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "dev": "next dev --turbopack", "build": "next build", "start": "next start", - "lint": "next lint" + "lint": "next lint", + "sitemap": "next-sitemap" }, "dependencies": { "axios": "^1.9.0", @@ -20,6 +21,7 @@ "devDependencies": { "@eslint/eslintrc": "^3", "@tailwindcss/postcss": "^4", + "@types/leaflet": "^1.9.18", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", @@ -28,6 +30,7 @@ "eslint-config-prettier": "^10.1.5", "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-prettier": "^5.4.1", + "next-sitemap": "^4.2.3", "prettier": "^3.5.3", "prettier-plugin-tailwindcss": "^0.6.12", "tailwindcss": "^4", diff --git a/public/product1.png b/public/images/product1.png similarity index 100% rename from public/product1.png rename to public/images/product1.png diff --git a/public/images/product2.png b/public/images/product2.png new file mode 100644 index 0000000..dc9d261 Binary files /dev/null and b/public/images/product2.png differ diff --git a/public/images/product3.png b/public/images/product3.png new file mode 100644 index 0000000..0f02dbc Binary files /dev/null and b/public/images/product3.png differ diff --git a/public/images/product4.png b/public/images/product4.png new file mode 100644 index 0000000..5902cbd Binary files /dev/null and b/public/images/product4.png differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..3a72385 --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,9 @@ +# * +User-agent: * +Allow: / + +# Host +Host: http://localhost:3000 + +# Sitemaps +Sitemap: http://localhost:3000/sitemap.xml diff --git a/public/sitemap.xml b/public/sitemap.xml new file mode 100644 index 0000000..39fed00 --- /dev/null +++ b/public/sitemap.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/src/app/[...not_found]/layout.tsx b/src/app/[...not_found]/layout.tsx new file mode 100644 index 0000000..569c61c --- /dev/null +++ b/src/app/[...not_found]/layout.tsx @@ -0,0 +1,37 @@ +import '@/app/globals.css'; +import { notFound } from 'next/navigation'; +import { NextIntlClientProvider } from 'next-intl'; +import type { IPageParams } from '@/models/layout.d'; + +// export const metadata: Metadata = { +// title: 'Create Next App', +// description: 'Generated by create next app', +// }; + +export default async function ErrorLayout({ + children, + params, +}: Readonly<{ + children: React.ReactNode; + params: IPageParams; +}>) { + const paramsData = await params; + const locale = paramsData.locale; + const fontClass = locale === 'fa' ? 'font-fa' : 'font-en'; + let messages; + try { + messages = (await import(`../../../messages/${locale}.json`)).default; + } catch (error) { + notFound(); + } + + return ( + + + + {children} + + + + ); +} diff --git a/src/app/[...not_found]/page.tsx b/src/app/[...not_found]/page.tsx index 7802305..ce79a51 100644 --- a/src/app/[...not_found]/page.tsx +++ b/src/app/[...not_found]/page.tsx @@ -1,19 +1,16 @@ import routeFactory from '@/assets/constants/routeFactory'; import images from '@/assets/images/images'; import Button from '@/components/uikit/button'; -import { ILayoutLocaleParams } from '@/models/layout'; +import { IPageParams } from '@/models/layout'; import { getTranslations } from 'next-intl/server'; -export default async function NotFound({ - params, -}: { - params: ILayoutLocaleParams; -}) { - const t = await getTranslations({ locale: params.locale }); - const routes = routeFactory(t, params.locale); +export default async function NotFound({ params }: { params: IPageParams }) { + const paramsData = await params; + const t = await getTranslations({ locale: paramsData.locale }); + const routes = routeFactory(t, paramsData.locale); return ( -
+

{t('pages_notFound_title')}

diff --git a/src/app/[locale]/about-us/page.tsx b/src/app/[locale]/about-us/page.tsx index 3a711c3..1c9c0ee 100644 --- a/src/app/[locale]/about-us/page.tsx +++ b/src/app/[locale]/about-us/page.tsx @@ -1,15 +1,14 @@ import images from '@/assets/images/images'; import InnerPageBanner from '@/components/layout/innerPages/banner'; import AboutTopInfo from '@/components/pages/about/TopInfo'; -import { ILayoutLocaleParams } from '@/models/layout'; -import { useTranslations } from 'next-intl'; +import { IPageParams } from '@/models/layout'; import { setRequestLocale } from 'next-intl/server'; import { getTranslations } from 'next-intl/server'; export default async function AboutUs({ params, }: Readonly<{ - params: ILayoutLocaleParams; + params: IPageParams; }>) { const { locale } = await params; diff --git a/src/app/[locale]/blog/page.tsx b/src/app/[locale]/blog/page.tsx index aab6f67..bbb0ffc 100644 --- a/src/app/[locale]/blog/page.tsx +++ b/src/app/[locale]/blog/page.tsx @@ -1,15 +1,12 @@ import images from '@/assets/images/images'; import InnerPageBanner from '@/components/layout/innerPages/banner'; import PostsGrid from '@/components/pages/blog/PostsGrid'; -import { ILayoutLocaleParams } from '@/models/layout'; +import { IPageParams } from '@/models/layout'; import { getTranslations } from 'next-intl/server'; -export default async function Blog({ - params, -}: { - params: ILayoutLocaleParams; -}) { - const t = await getTranslations({ locale: params.locale }); +export default async function Blog({ params }: { params: IPageParams }) { + const paramsData = await params; + const t = await getTranslations({ locale: paramsData.locale }); return (
diff --git a/src/app/[locale]/contact-us/page.tsx b/src/app/[locale]/contact-us/page.tsx index 9120969..1edcafa 100644 --- a/src/app/[locale]/contact-us/page.tsx +++ b/src/app/[locale]/contact-us/page.tsx @@ -5,14 +5,12 @@ import ContactForm from '@/components/pages/contact/ContactMeForm'; import TouchUs from '@/components/pages/contact/TouchUs'; import Image from 'next/image'; import { getTranslations } from 'next-intl/server'; -import { ILayoutLocaleParams } from '@/models/layout'; +import { IPageParams } from '@/models/layout'; -export default async function Contact({ - params, -}: { - params: ILayoutLocaleParams; -}) { - const t = await getTranslations({ locale: params.locale }); +export default async function Contact({ params }: { params: IPageParams }) { + const { locale } = await params; + + const t = await getTranslations({ locale }); return (
diff --git a/src/app/[locale]/layout.tsx b/src/app/[locale]/layout.tsx index 4cfb0ff..eb4cbf1 100644 --- a/src/app/[locale]/layout.tsx +++ b/src/app/[locale]/layout.tsx @@ -3,36 +3,30 @@ import '@/app/globals.css'; import 'leaflet/dist/leaflet.css'; import { notFound } from 'next/navigation'; import { NextIntlClientProvider } from 'next-intl'; -import type { ILayoutLocaleParams } from '@/models/layout.d'; - -// export const metadata: Metadata = { -// title: 'Create Next App', -// description: 'Generated by create next app', -// }; +import type { IPageParams } from '@/models/layout.d'; export default async function RootLayout({ children, params, }: Readonly<{ children: React.ReactNode; - params: ILayoutLocaleParams; + params: IPageParams; }>) { - const locale = params.locale; - console.log(locale); + const paramsData = await params; + const locale = paramsData.locale; + + const fontClass = locale === 'fa' ? 'font-fa' : 'font-en'; let messages; try { - console.log(await import(`../../../messages/en.json`)); - messages = (await import(`../../../messages/en.json`)).default; + messages = (await import(`../../../messages/${locale}.json`)).default; } catch (error) { - console.log(error); - notFound(); } return ( - + {children}
diff --git a/src/app/[locale]/product-categories/page.tsx b/src/app/[locale]/product-categories/page.tsx index 1f90830..0cfc7b8 100644 --- a/src/app/[locale]/product-categories/page.tsx +++ b/src/app/[locale]/product-categories/page.tsx @@ -2,17 +2,17 @@ import images from '@/assets/images/images'; import InnerPageBanner from '@/components/layout/innerPages/banner'; import ProductsCategoryCard from '@/components/pages/productCategories/ProductCategoryCard'; import type { IProductCategoryCardProps } from '@/components/pages/productCategories/productCategoryCard.d'; -import { ILayoutLocaleParams } from '@/models/layout'; +import { IPageParams } from '@/models/layout'; import { getProductCategories } from '@/services/products'; import { getTranslations } from 'next-intl/server'; export default async function ProductsPage({ params, }: { - params: ILayoutLocaleParams; + params: IPageParams; }) { - const awaitedParams = await params; - const t = await getTranslations({ locale: awaitedParams.locale }); + const { locale } = await params; + const t = await getTranslations({ locale }); const productCategories: IProductCategoryCardProps[] = await getProductCategories(); diff --git a/src/app/[locale]/projects/page.tsx b/src/app/[locale]/projects/page.tsx index 14baf7b..08d9dda 100644 --- a/src/app/[locale]/projects/page.tsx +++ b/src/app/[locale]/projects/page.tsx @@ -1,15 +1,12 @@ import images from '@/assets/images/images'; import InnerPageBanner from '@/components/layout/innerPages/banner'; import ProjectsGrid from '@/components/pages/project/ProjectGrid'; -import { ILayoutLocaleParams } from '@/models/layout'; +import { IPageParams } from '@/models/layout'; import { getTranslations } from 'next-intl/server'; -export default async function Projects({ - params, -}: { - params: ILayoutLocaleParams; -}) { - const t = await getTranslations({ locale: params.locale }); +export default async function Projects({ params }: { params: IPageParams }) { + const { locale } = await params; + const t = await getTranslations({ locale }); return (
diff --git a/src/app/api/product_categories/[id]/products/data.ts b/src/app/api/product_categories/[id]/products/data.ts new file mode 100644 index 0000000..e83f9be --- /dev/null +++ b/src/app/api/product_categories/[id]/products/data.ts @@ -0,0 +1,25 @@ +import { productCategories } from '../../data'; +import type { IProductCardProps } from '@/components/pages/products/productCard/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 = + Object.fromEntries( + productCategories.map((cat) => [ + cat.id, + generateProductsForCategory(cat.id, cat.title), + ]), + ); diff --git a/src/app/api/product_categories/[id]/products/route.ts b/src/app/api/product_categories/[id]/products/route.ts index 508e0d2..ac1704d 100644 --- a/src/app/api/product_categories/[id]/products/route.ts +++ b/src/app/api/product_categories/[id]/products/route.ts @@ -1,29 +1,36 @@ import { NextResponse, NextRequest } from 'next/server'; -// import type { NextApiRequestContext } from 'next'; - import { cookies } from 'next/headers'; import { productCategories } from '../../data'; -import images from '@/assets/images/images'; +import { generateProductsForCategory } from './data'; export async function GET(request: NextRequest, context: any) { const cookieStore = await cookies(); - const lang = cookieStore.get('lang')?.value || 'en'; + const lang = cookieStore.get('locale')?.value || 'en'; - const category = productCategories.find( - (cat) => cat.id === context.params.id, - ); + const params = await context.params; + const category = productCategories.find((cat) => cat.id === params.id); if (!category) { return NextResponse.json({ error: 'Not found' }, { status: 404 }); } - const product = { - title: 'Product title', - size: 'W 18 * L10', - imageSrc: '/product1.png', - }; - const products = Array.from({ length: 10 }, () => product); + + // Pagination logic + const { searchParams } = new URL(request.url); + const page = parseInt(searchParams.get('page') || '1', 10); + const perPage = 20; + + 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({ - data: products, + data: paginatedProducts, + pagination: { + page, + perPage, + total: allProducts.length, + totalPages: Math.ceil(allProducts.length / perPage), + }, }); } diff --git a/src/app/api/product_categories/[id]/route.ts b/src/app/api/product_categories/[id]/route.ts index b7a7a76..f005af1 100644 --- a/src/app/api/product_categories/[id]/route.ts +++ b/src/app/api/product_categories/[id]/route.ts @@ -1,45 +1,42 @@ -import { NextResponse, NextRequest } from 'next/server'; -// import type { NextApiRequestContext } from 'next'; +'use server'; +import { NextResponse, NextRequest } from 'next/server'; import { cookies } from 'next/headers'; import { productCategories } from '../data'; +interface IContext { + params: { + id: string; + }; +} + +interface IParams {} + export async function GET(request: NextRequest, context: any) { const cookieStore = await cookies(); - const lang = cookieStore.get('lang')?.value || 'en'; + const lang = cookieStore.get('locale')?.value || 'en'; - const category = productCategories.find( - (cat) => cat.id === context.params.id, - ); + const params = (await context).params; + + const { id } = params; + + let category = productCategories.find((cat) => cat.id === id); if (!category) { return NextResponse.json({ error: 'Not found' }, { status: 404 }); } - const titleData = - lang === 'fa' && category.fa_title ? category.fa_title : category.title; - const priceTypeTitleData = - lang === 'fa' && category.fa_priceTypeTitle - ? category.fa_priceTypeTitle - : category.priceTypeTitle; - const bestForTitleData = - lang === 'fa' && category.fa_bestForTitle - ? category.fa_bestForTitle - : category.bestForTitle; - const descriptionData = - lang === 'fa' && category.fa_description - ? category.fa_description - : category.description; + if (lang === 'fa') { + category = { + ...category, + title: category.fa_title || category.title, + priceTypeTitle: category.fa_priceTypeTitle || category.priceTypeTitle, + bestForTitle: category.fa_bestForTitle || category.bestForTitle, + description: category.fa_description || category.description, + }; + } return NextResponse.json({ - data: { - id: category.id, - icon: category.icon, - typesCount: category.typesCount, - title: titleData, - priceTypeTitle: priceTypeTitleData, - bestForTitle: bestForTitleData, - description: descriptionData, - }, + data: category, }); } diff --git a/src/app/api/product_categories/contact/contactMessages.json b/src/app/api/product_categories/contact/contactMessages.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/src/app/api/product_categories/contact/contactMessages.json @@ -0,0 +1 @@ +[] diff --git a/src/app/api/product_categories/contact/route.ts b/src/app/api/product_categories/contact/route.ts new file mode 100644 index 0000000..1f42807 --- /dev/null +++ b/src/app/api/product_categories/contact/route.ts @@ -0,0 +1,49 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { writeFile, readFile } from 'fs/promises'; +import path from 'path'; + +const filePath = path.resolve(process.cwd(), 'contactMessages.json'); + +export async function POST(request: NextRequest) { + try { + const body = await request.json(); + const { first_name, last_name, phone, email, message } = body; + + if (!first_name || !last_name || !phone || !email || !message) { + return NextResponse.json( + { error: 'مقدار تمامی فیلدها را پر کنید' }, + { status: 400 }, + ); + } + + let messages = []; + try { + const fileData = await readFile(filePath, 'utf-8'); + messages = JSON.parse(fileData); + } catch { + messages = []; + } + + const newMessage = { + name, + email, + message, + createdAt: new Date().toISOString(), + }; + + messages.push(newMessage); + + // Write updated messages back to file + await writeFile(filePath, JSON.stringify(messages, null, 2), 'utf-8'); + + return NextResponse.json({ + success: true, + message: 'Your message has been received.', + }); + } catch (error) { + return NextResponse.json( + { error, message: 'مشکلی پیش آمده.' }, + { status: 400 }, + ); + } +} diff --git a/src/app/api/product_categories/route.ts b/src/app/api/product_categories/route.ts index 7ce6d5f..4cb1e56 100644 --- a/src/app/api/product_categories/route.ts +++ b/src/app/api/product_categories/route.ts @@ -23,23 +23,25 @@ export async function GET(request: Request) { description, fa_description, }) => { - 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; - const descriptionData = - lang === 'fa' && fa_description ? fa_description : description; + if (lang === 'fa') { + return { + id, + icon, + typesCount, + title: fa_title || title, + priceTypeTitle: fa_priceTypeTitle || priceTypeTitle, + bestForTitle: fa_bestForTitle || bestForTitle, + description: fa_description || description, + }; + } return { id, icon, typesCount, - title: titleData, - priceTypeTitle: priceTypeTitleData, - bestForTitle: bestForTitleData, - description: descriptionData, + title, + priceTypeTitle, + bestForTitle, + description, }; }, ); diff --git a/src/app/api/product_category/route.ts b/src/app/api/product_category/route.ts deleted file mode 100644 index 1c6477a..0000000 --- a/src/app/api/product_category/route.ts +++ /dev/null @@ -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 }); -} diff --git a/src/app/globals.css b/src/app/globals.css index 11274b0..1f3d808 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,116 +1,16 @@ @import "tailwindcss"; @import "tailwindcss/utilities"; -/* manrope-200 - latin */ -@font-face { - font-display: swap; +@import "../assets/fonts/manrope/manrope.css"; +@import "../assets/fonts/vazir/vazir.css"; - font-family: 'Manrope'; - font-style: normal; - font-weight: 200; - src: url('../assets/fonts/manrope/manrope-v15-latin-200.eot'); - src: url('../assets/fonts/manrope/manrope-v15-latin-200.eot?#iefix') format('embedded-opentype'), - url('../assets/fonts/manrope/manrope-v15-latin-200.woff2') format('woff2'), - url('../assets/fonts/manrope/manrope-v15-latin-200.woff') format('woff'), - url('../assets/fonts/manrope/manrope-v15-latin-200.ttf') format('truetype'), - url('../assets/fonts/manrope/manrope-v15-latin-200.svg#Manrope') format('svg'); -} - -/* manrope-300 - latin */ -@font-face { - font-display: swap; - - font-family: 'Manrope'; - font-style: normal; - font-weight: 300; - src: url('../assets/fonts/manrope/manrope-v15-latin-300.eot'); - src: url('../assets/fonts/manrope/manrope-v15-latin-300.eot?#iefix') format('embedded-opentype'), - url('../assets/fonts/manrope/manrope-v15-latin-300.woff2') format('woff2'), - url('../assets/fonts/manrope/manrope-v15-latin-300.woff') format('woff'), - url('../assets/fonts/manrope/manrope-v15-latin-300.ttf') format('truetype'), - url('../assets/fonts/manrope/manrope-v15-latin-300.svg#Manrope') format('svg'); -} - -/* manrope-regular - latin */ -@font-face { - font-display: swap; - - font-family: 'Manrope'; - font-style: normal; - font-weight: 400; - src: url('../assets/fonts/manrope/manrope-v15-latin-regular.eot'); - src: url('../assets/fonts/manrope/manrope-v15-latin-regular.eot?#iefix') format('embedded-opentype'), - url('../assets/fonts/manrope/manrope-v15-latin-regular.woff2') format('woff2'), - url('../assets/fonts/manrope/manrope-v15-latin-regular.woff') format('woff'), - url('../assets/fonts/manrope/manrope-v15-latin-regular.ttf') format('truetype'), - url('../assets/fonts/manrope/manrope-v15-latin-regular.svg#Manrope') format('svg'); -} - -/* manrope-500 - latin */ -@font-face { - font-display: swap; - - font-family: 'Manrope'; - font-style: normal; - font-weight: 500; - src: url('../assets/fonts/manrope/manrope-v15-latin-500.eot'); - src: url('../assets/fonts/manrope/manrope-v15-latin-500.eot?#iefix') format('embedded-opentype'), - url('../assets/fonts/manrope/manrope-v15-latin-500.woff2') format('woff2'), - url('../assets/fonts/manrope/manrope-v15-latin-500.woff') format('woff'), - url('../assets/fonts/manrope/manrope-v15-latin-500.ttf') format('truetype'), - url('../assets/fonts/manrope/manrope-v15-latin-500.svg#Manrope') format('svg'); -} - -/* manrope-600 - latin */ -@font-face { - font-display: swap; - - font-family: 'Manrope'; - font-style: normal; - font-weight: 600; - src: url('../assets/fonts/manrope/manrope-v15-latin-600.eot'); - src: url('../assets/fonts/manrope/manrope-v15-latin-600.eot?#iefix') format('embedded-opentype'), - url('../assets/fonts/manrope/manrope-v15-latin-600.woff2') format('woff2'), - url('../assets/fonts/manrope/manrope-v15-latin-600.woff') format('woff'), - url('../assets/fonts/manrope/manrope-v15-latin-600.ttf') format('truetype'), - url('../assets/fonts/manrope/manrope-v15-latin-600.svg#Manrope') format('svg'); -} - -/* manrope-700 - latin */ -@font-face { - font-display: swap; - - font-family: 'Manrope'; - font-style: normal; - font-weight: 700; - src: url('../assets/fonts/manrope/manrope-v15-latin-700.eot'); - src: url('../assets/fonts/manrope/manrope-v15-latin-700.eot?#iefix') format('embedded-opentype'), - url('../assets/fonts/manrope/manrope-v15-latin-700.woff2') format('woff2'), - url('../assets/fonts/manrope/manrope-v15-latin-700.woff') format('woff'), - url('../assets/fonts/manrope/manrope-v15-latin-700.ttf') format('truetype'), - url('../assets/fonts/manrope/manrope-v15-latin-700.svg#Manrope') format('svg'); -} - -/* manrope-800 - latin */ -@font-face { - font-display: swap; - - font-family: 'Manrope'; - font-style: normal; - font-weight: 800; - src: url('../assets/fonts/manrope/manrope-v15-latin-800.eot'); - src: url('../assets/fonts/manrope/manrope-v15-latin-800.eot?#iefix') format('embedded-opentype'), - url('../assets/fonts/manrope/manrope-v15-latin-800.woff2') format('woff2'), - url('../assets/fonts/manrope/manrope-v15-latin-800.woff') format('woff'), - url('../assets/fonts/manrope/manrope-v15-latin-800.ttf') format('truetype'), - url('../assets/fonts/manrope/manrope-v15-latin-800.svg#Manrope') format('svg'); -} - -* { - font-family: 'Manrope'; -} html{ font-size: 16px; + scroll-snap-stop: always; +} + +body{ + scroll-snap-stop: always; } @media (width <=48rem) { @@ -119,6 +19,14 @@ html{ } } +body.font-en *{ + font-family: 'Manrope'; +} + +body.font-fa *{ + font-family: 'Vazirmatn'; +} + :root { --primary: #FA003F; diff --git a/src/assets/constants/footerMenu/index.ts b/src/assets/constants/footerMenu/index.ts index 5f88dbf..9c25c34 100644 --- a/src/assets/constants/footerMenu/index.ts +++ b/src/assets/constants/footerMenu/index.ts @@ -5,7 +5,7 @@ import { useParams } from 'next/navigation'; import { getQuickLinks } from '@/assets/constants/footerMenu/quickLinks.const'; import { getWhoAreWeLinks } from './whoAreWe.const'; -import { TLocales } from '@/locales/translates'; +import { TLocales } from '@/models/layout'; export default function FooterMenu() { const t = useTranslations(); diff --git a/src/assets/constants/footerMenu/quickLinks.const.ts b/src/assets/constants/footerMenu/quickLinks.const.ts index 4f483b4..b9e328b 100644 --- a/src/assets/constants/footerMenu/quickLinks.const.ts +++ b/src/assets/constants/footerMenu/quickLinks.const.ts @@ -1,7 +1,8 @@ import routeFactory from '../routeFactory'; -export function getQuickLinks(t: (key: string) => string, locale: string) { +export function getQuickLinks(t: any, locale: string) { const routes = routeFactory(t, locale); + return [ { title: routes.home.title, diff --git a/src/assets/constants/footerMenu/whoAreWe.const.ts b/src/assets/constants/footerMenu/whoAreWe.const.ts index c66c8b5..a240016 100644 --- a/src/assets/constants/footerMenu/whoAreWe.const.ts +++ b/src/assets/constants/footerMenu/whoAreWe.const.ts @@ -1,6 +1,6 @@ import routeFactory from '../routeFactory'; -export function getWhoAreWeLinks(t: (key: string) => string, locale: string) { +export function getWhoAreWeLinks(t: any, locale: string) { const routes = routeFactory(t, locale); return [ { diff --git a/src/assets/constants/routeFactory/index.ts b/src/assets/constants/routeFactory/index.ts index fef0986..8db0ada 100644 --- a/src/assets/constants/routeFactory/index.ts +++ b/src/assets/constants/routeFactory/index.ts @@ -1,5 +1,5 @@ type RouteFactory = ( - t: (key: string) => string, + t: any, locale: string, ) => { home: { title: string; route: () => string }; diff --git a/src/assets/fonts/manrope/manrope.css b/src/assets/fonts/manrope/manrope.css new file mode 100644 index 0000000..fe3eda9 --- /dev/null +++ b/src/assets/fonts/manrope/manrope.css @@ -0,0 +1,104 @@ +/* manrope-200 - latin */ +@font-face { + font-display: swap; + + font-family: 'Manrope'; + font-style: normal; + font-weight: 200; + src: url('./manrope-v15-latin-200.eot'); + src: url('./manrope-v15-latin-200.eot?#iefix') format('embedded-opentype'), + url('./manrope-v15-latin-200.woff2') format('woff2'), + url('./manrope-v15-latin-200.woff') format('woff'), + url('./manrope-v15-latin-200.ttf') format('truetype'), + url('./manrope-v15-latin-200.svg#Manrope') format('svg'); +} + +/* manrope-300 - latin */ +@font-face { + font-display: swap; + + font-family: 'Manrope'; + font-style: normal; + font-weight: 300; + src: url('./manrope-v15-latin-300.eot'); + src: url('./manrope-v15-latin-300.eot?#iefix') format('embedded-opentype'), + url('./manrope-v15-latin-300.woff2') format('woff2'), + url('./manrope-v15-latin-300.woff') format('woff'), + url('./manrope-v15-latin-300.ttf') format('truetype'), + url('./manrope-v15-latin-300.svg#Manrope') format('svg'); +} + +/* manrope-regular - latin */ +@font-face { + font-display: swap; + + font-family: 'Manrope'; + font-style: normal; + font-weight: 400; + src: url('./manrope-v15-latin-regular.eot'); + src: url('./manrope-v15-latin-regular.eot?#iefix') format('embedded-opentype'), + url('./manrope-v15-latin-regular.woff2') format('woff2'), + url('./manrope-v15-latin-regular.woff') format('woff'), + url('./manrope-v15-latin-regular.ttf') format('truetype'), + url('./manrope-v15-latin-regular.svg#Manrope') format('svg'); +} + +/* manrope-500 - latin */ +@font-face { + font-display: swap; + + font-family: 'Manrope'; + font-style: normal; + font-weight: 500; + src: url('./manrope-v15-latin-500.eot'); + src: url('./manrope-v15-latin-500.eot?#iefix') format('embedded-opentype'), + url('./manrope-v15-latin-500.woff2') format('woff2'), + url('./manrope-v15-latin-500.woff') format('woff'), + url('./manrope-v15-latin-500.ttf') format('truetype'), + url('./manrope-v15-latin-500.svg#Manrope') format('svg'); +} + +/* manrope-600 - latin */ +@font-face { + font-display: swap; + + font-family: 'Manrope'; + font-style: normal; + font-weight: 600; + src: url('./manrope-v15-latin-600.eot'); + src: url('./manrope-v15-latin-600.eot?#iefix') format('embedded-opentype'), + url('./manrope-v15-latin-600.woff2') format('woff2'), + url('./manrope-v15-latin-600.woff') format('woff'), + url('./manrope-v15-latin-600.ttf') format('truetype'), + url('./manrope-v15-latin-600.svg#Manrope') format('svg'); +} + +/* manrope-700 - latin */ +@font-face { + font-display: swap; + + font-family: 'Manrope'; + font-style: normal; + font-weight: 700; + src: url('./manrope-v15-latin-700.eot'); + src: url('./manrope-v15-latin-700.eot?#iefix') format('embedded-opentype'), + url('./manrope-v15-latin-700.woff2') format('woff2'), + url('./manrope-v15-latin-700.woff') format('woff'), + url('./manrope-v15-latin-700.ttf') format('truetype'), + url('./manrope-v15-latin-700.svg#Manrope') format('svg'); +} + +/* manrope-800 - latin */ +@font-face { + font-display: swap; + + font-family: 'Manrope'; + font-style: normal; + font-weight: 800; + src: url('./manrope-v15-latin-800.eot'); + src: url('./manrope-v15-latin-800.eot?#iefix') format('embedded-opentype'), + url('./manrope-v15-latin-800.woff2') format('woff2'), + url('./manrope-v15-latin-800.woff') format('woff'), + url('./manrope-v15-latin-800.ttf') format('truetype'), + url('./manrope-v15-latin-800.svg#Manrope') format('svg'); +} diff --git a/src/assets/fonts/vazir/Vazirmatn-FD-Black.woff2 b/src/assets/fonts/vazir/Vazirmatn-FD-Black.woff2 new file mode 100644 index 0000000..251a923 Binary files /dev/null and b/src/assets/fonts/vazir/Vazirmatn-FD-Black.woff2 differ diff --git a/src/assets/fonts/vazir/Vazirmatn-FD-Bold.woff2 b/src/assets/fonts/vazir/Vazirmatn-FD-Bold.woff2 new file mode 100644 index 0000000..f97f0db Binary files /dev/null and b/src/assets/fonts/vazir/Vazirmatn-FD-Bold.woff2 differ diff --git a/src/assets/fonts/vazir/Vazirmatn-FD-ExtraBold.woff2 b/src/assets/fonts/vazir/Vazirmatn-FD-ExtraBold.woff2 new file mode 100644 index 0000000..e2b2d31 Binary files /dev/null and b/src/assets/fonts/vazir/Vazirmatn-FD-ExtraBold.woff2 differ diff --git a/src/assets/fonts/vazir/Vazirmatn-FD-ExtraLight.woff2 b/src/assets/fonts/vazir/Vazirmatn-FD-ExtraLight.woff2 new file mode 100644 index 0000000..d019c92 Binary files /dev/null and b/src/assets/fonts/vazir/Vazirmatn-FD-ExtraLight.woff2 differ diff --git a/src/assets/fonts/vazir/Vazirmatn-FD-Light.woff2 b/src/assets/fonts/vazir/Vazirmatn-FD-Light.woff2 new file mode 100644 index 0000000..41d75fb Binary files /dev/null and b/src/assets/fonts/vazir/Vazirmatn-FD-Light.woff2 differ diff --git a/src/assets/fonts/vazir/Vazirmatn-FD-Medium.woff2 b/src/assets/fonts/vazir/Vazirmatn-FD-Medium.woff2 new file mode 100644 index 0000000..2721237 Binary files /dev/null and b/src/assets/fonts/vazir/Vazirmatn-FD-Medium.woff2 differ diff --git a/src/assets/fonts/vazir/Vazirmatn-FD-Regular.woff2 b/src/assets/fonts/vazir/Vazirmatn-FD-Regular.woff2 new file mode 100644 index 0000000..ac7cff8 Binary files /dev/null and b/src/assets/fonts/vazir/Vazirmatn-FD-Regular.woff2 differ diff --git a/src/assets/fonts/vazir/Vazirmatn-FD-SemiBold.woff2 b/src/assets/fonts/vazir/Vazirmatn-FD-SemiBold.woff2 new file mode 100644 index 0000000..359e57e Binary files /dev/null and b/src/assets/fonts/vazir/Vazirmatn-FD-SemiBold.woff2 differ diff --git a/src/assets/fonts/vazir/Vazirmatn-FD-Thin.woff2 b/src/assets/fonts/vazir/Vazirmatn-FD-Thin.woff2 new file mode 100644 index 0000000..d9cfb37 Binary files /dev/null and b/src/assets/fonts/vazir/Vazirmatn-FD-Thin.woff2 differ diff --git a/src/assets/fonts/vazir/vazir.css b/src/assets/fonts/vazir/vazir.css new file mode 100644 index 0000000..a758278 --- /dev/null +++ b/src/assets/fonts/vazir/vazir.css @@ -0,0 +1,63 @@ +@font-face { + font-family: 'Vazirmatn'; + src: url('./Vazirmatn-FD-Thin.woff2') format('woff2'); + font-weight: 100; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Vazirmatn'; + src: url('./Vazirmatn-FD-ExtraLight.woff2') format('woff2'); + font-weight: 200; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Vazirmatn'; + src: url('./Vazirmatn-FD-Light.woff2') format('woff2'); + font-weight: 300; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Vazirmatn'; + src: url('./Vazirmatn-FD-Regular.woff2') format('woff2'); + font-weight: 400; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Vazirmatn'; + src: url('./Vazirmatn-FD-Medium.woff2') format('woff2'); + font-weight: 500; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Vazirmatn'; + src: url('./Vazirmatn-FD-SemiBold.woff2') format('woff2'); + font-weight: 600; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Vazirmatn'; + src: url('./Vazirmatn-FD-Bold.woff2') format('woff2'); + font-weight: 700; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Vazirmatn'; + src: url('./Vazirmatn-FD-ExtraBold.woff2') format('woff2'); + font-weight: 800; + font-style: normal; + font-display: swap; +} +@font-face { + font-family: 'Vazirmatn'; + src: url('./Vazirmatn-FD-Black.woff2') format('woff2'); + font-weight: 900; + font-style: normal; + font-display: swap; +} diff --git a/src/assets/images/images.ts b/src/assets/images/images.ts index 4c4c24d..538d61b 100644 --- a/src/assets/images/images.ts +++ b/src/assets/images/images.ts @@ -7,17 +7,19 @@ import aboutImg_1 from './about-img-1.png'; import aboutImg_2 from './about-img-2.png'; import homeProductBack from './home_product_back.jpeg'; import whatWeDoImage from './what-we-do-image.png'; -import blogImg_1 from './blog_img_1.jpeg' -import projectImg_1 from './project_img_1.jpeg' +import blogImg_1 from './blog_img_1.jpeg'; +import projectImg_1 from './project_img_1.jpeg'; 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 { homeBanner, logo, aboutBanner, blogImg_1, blogBanner, - projectImg_1, + projectImg_1, factoryVector, aboutImg_1, aboutImg_2, @@ -25,4 +27,6 @@ export default { homeProductBack, whatWeDoImage, product1, + langEn, + langFa, }; diff --git a/src/assets/images/lang_en.png b/src/assets/images/lang_en.png new file mode 100644 index 0000000..9273797 Binary files /dev/null and b/src/assets/images/lang_en.png differ diff --git a/src/assets/images/lang_fa.png b/src/assets/images/lang_fa.png new file mode 100644 index 0000000..9c31298 Binary files /dev/null and b/src/assets/images/lang_fa.png differ diff --git a/src/components/layout/footer/Footer.tsx b/src/components/layout/footer/Footer.tsx index acea285..4894683 100644 --- a/src/components/layout/footer/Footer.tsx +++ b/src/components/layout/footer/Footer.tsx @@ -10,12 +10,12 @@ import { useTranslations } from 'next-intl'; import { useParams } from 'next/navigation'; import { getQuickLinks } from '@/assets/constants/footerMenu/quickLinks.const'; import { getWhoAreWeLinks } from '@/assets/constants/footerMenu/whoAreWe.const'; -import { TLocales } from '@/locales/translates'; +import { TLocales } from '@/models/layout'; export default function Footer() { const t = useTranslations(); const params = useParams(); - const locale = params.locale as TLocales; + const locale = (params.locale as TLocales) || 'en'; const quickLinks = getQuickLinks(t, locale); const whoAreWeLinks = getWhoAreWeLinks(t, locale); diff --git a/src/components/layout/footer/FooterInfo.tsx b/src/components/layout/footer/FooterInfo.tsx index 6f5b81f..dca7faf 100644 --- a/src/components/layout/footer/FooterInfo.tsx +++ b/src/components/layout/footer/FooterInfo.tsx @@ -6,6 +6,7 @@ import { Icon, IconName } from '@/components/uikit/icons'; import Image from 'next/image'; import Link from 'next/link'; import { useTranslations } from 'next-intl'; +import LanguageSwitch from '../languageSwitch'; export default function FooterInfo({ withLogo, @@ -42,6 +43,7 @@ export default function FooterInfo({ ))} + ); } diff --git a/src/components/layout/languageSwitch/index.tsx b/src/components/layout/languageSwitch/index.tsx new file mode 100644 index 0000000..cb6be6a --- /dev/null +++ b/src/components/layout/languageSwitch/index.tsx @@ -0,0 +1,49 @@ +'use client'; + +import images from '@/assets/images/images'; +import { TLocales } from '@/models/layout'; +import Image from 'next/image'; +import { usePathname } from 'next/navigation'; + +export default function LanguageSwitch() { + const pathname = usePathname(); + + 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 ( +
+ {languages.map((language) => ( +
changeLocale(language.locale as TLocales)} + key={language.title} + > + {language.title} + + {language.title} + +
+ ))} +
+ ); +} diff --git a/src/components/layout/map/index.tsx b/src/components/layout/map/index.tsx index a9045e8..d5119d1 100644 --- a/src/components/layout/map/index.tsx +++ b/src/components/layout/map/index.tsx @@ -3,6 +3,7 @@ import { MapContainer, TileLayer, Marker, Popup } from 'react-leaflet'; import L from 'leaflet'; import 'leaflet/dist/leaflet.css'; +import 'leaflet/dist/leaflet.css'; delete (L.Icon.Default.prototype as any)._getIconUrl; L.Icon.Default.mergeOptions({ @@ -30,7 +31,7 @@ const ContactMap = () => { url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" /> - lastminute.com London Eye + Pasargad Bricks diff --git a/src/components/layout/navbar/index.tsx b/src/components/layout/navbar/index.tsx index 86628d4..186b6f4 100644 --- a/src/components/layout/navbar/index.tsx +++ b/src/components/layout/navbar/index.tsx @@ -11,7 +11,7 @@ import { useTranslations } from 'next-intl'; import routeFactory from '@/assets/constants/routeFactory'; export default function Navbar() { - const [open, setOpen] = useState(false); + const [open, setOpen] = useState(false); const pathname = usePathname(); const params = useParams(); const locale = params.locale as string; diff --git a/src/components/layout/pagination/index.tsx b/src/components/layout/pagination/index.tsx new file mode 100644 index 0000000..47518db --- /dev/null +++ b/src/components/layout/pagination/index.tsx @@ -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 ( + + ); +} diff --git a/src/components/pages/home/Products.tsx b/src/components/pages/home/Products.tsx index 9763939..6aa7418 100644 --- a/src/components/pages/home/Products.tsx +++ b/src/components/pages/home/Products.tsx @@ -6,7 +6,7 @@ import Link from 'next/link'; import routeFactory from '@/assets/constants/routeFactory'; import { getTranslations } from 'next-intl/server'; import { ILayoutLocaleParams } from '@/models/layout'; -import { TLocales } from '@/locales/translates'; +import { TLocales } from '@/models/layout'; interface Props { locale: TLocales; diff --git a/src/components/pages/products/ProductCard.tsx b/src/components/pages/products/productCard/index.tsx similarity index 100% rename from src/components/pages/products/ProductCard.tsx rename to src/components/pages/products/productCard/index.tsx diff --git a/src/components/pages/products/productCard.d.ts b/src/components/pages/products/productCard/productCard.d.ts similarity index 100% rename from src/components/pages/products/productCard.d.ts rename to src/components/pages/products/productCard/productCard.d.ts diff --git a/src/components/pages/products/productList/index.tsx b/src/components/pages/products/productList/index.tsx new file mode 100644 index 0000000..c0741af --- /dev/null +++ b/src/components/pages/products/productList/index.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { useState } from 'react'; +import Pagination from '@/components/layout/pagination'; +import { getProducts } from '@/services/products'; +import { IProductListProps } from './productList'; +import ProductCard from '../productCard'; + +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 ( + <> +
+ {products.map((product, index) => ( + + ))} +
+ + + ); +} diff --git a/src/components/pages/products/productList/productList.d.ts b/src/components/pages/products/productList/productList.d.ts new file mode 100644 index 0000000..d8b290e --- /dev/null +++ b/src/components/pages/products/productList/productList.d.ts @@ -0,0 +1,8 @@ +import { IPagination } from '@/models/response'; +import { IProductCardProps } from '../productCard/productCard.d'; + +export interface IProductListProps { + initialProducts: IProductCardProps[]; + initialPagination: IPagination; + categoryId: string; +} diff --git a/src/components/uikit/button/button.d.ts b/src/components/uikit/button/button.d.ts index 940c1d5..9b7dcc4 100644 --- a/src/components/uikit/button/button.d.ts +++ b/src/components/uikit/button/button.d.ts @@ -8,6 +8,7 @@ export interface IBaseButtonProps disabled?: boolean; className?: string; link?: URL | string; + onClick?: () => void; } export interface ITextButtonProps extends IBaseButtonProps { diff --git a/src/components/uikit/button/index.tsx b/src/components/uikit/button/index.tsx index 0b762b1..1cd5db2 100644 --- a/src/components/uikit/button/index.tsx +++ b/src/components/uikit/button/index.tsx @@ -11,6 +11,7 @@ const Button: React.FC = ({ endIcon, className, link, + onClick, ...props }) => { 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 = ({ ) : ( -