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/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 245ed7b..f980f9b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -28,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",
@@ -59,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",
@@ -4654,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 cab244c..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",
@@ -29,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/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
index 8f9fe90..569c61c 100644
--- a/src/app/[...not_found]/layout.tsx
+++ b/src/app/[...not_found]/layout.tsx
@@ -1,21 +1,23 @@
import '@/app/globals.css';
import { notFound } from 'next/navigation';
import { NextIntlClientProvider } from 'next-intl';
-import type { ILayoutLocaleParams } from '@/models/layout.d';
+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 RootLayout({
+export default async function ErrorLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
- params: ILayoutLocaleParams;
+ params: IPageParams;
}>) {
- const locale = await params.locale;
+ 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;
@@ -25,7 +27,7 @@ export default async function RootLayout({
return (
-
+
{children}
diff --git a/src/app/[...not_found]/page.tsx b/src/app/[...not_found]/page.tsx
index 8b96a88..ce79a51 100644
--- a/src/app/[...not_found]/page.tsx
+++ b/src/app/[...not_found]/page.tsx
@@ -1,16 +1,13 @@
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 (
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 58409af..eb4cbf1 100644
--- a/src/app/[locale]/layout.tsx
+++ b/src/app/[locale]/layout.tsx
@@ -2,24 +2,18 @@ import Footer from '@/components/layout/footer/Footer';
import '@/app/globals.css';
import 'leaflet/dist/leaflet.css';
import { notFound } from 'next/navigation';
-// import { cookies } from 'next/headers';
import { NextIntlClientProvider } from 'next-intl';
-import type { ILayoutLocaleParams } from '@/models/layout.d';
-import { cookies } from 'next/headers';
+import type { IPageParams } from '@/models/layout.d';
export default async function RootLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
- params: ILayoutLocaleParams;
+ params: IPageParams;
}>) {
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';
diff --git a/src/app/[locale]/page.tsx b/src/app/[locale]/page.tsx
index 2dd18f0..dfd4207 100644
--- a/src/app/[locale]/page.tsx
+++ b/src/app/[locale]/page.tsx
@@ -2,17 +2,13 @@ import Header from '@/components/layout/header';
import AboutTopInfo from '@/components/pages/about/TopInfo';
import HomeStorySection from '@/components/pages/home/HomeStorySection';
import HomePageProducts from '@/components/pages/home/Products';
-import { ILayoutLocaleParams } from '@/models/layout';
+import type { IPageParams } from '@/models/layout.d';
import { getTranslations } from 'next-intl/server';
-interface IPageParams {
- params: ILayoutLocaleParams;
-}
-
-export default async function Home({ params }: IPageParams) {
+export default async function Home({ params }: { params: IPageParams }) {
const { locale } = await params;
- const t = await getTranslations({ locale });
+ await getTranslations({ locale });
return (
diff --git a/src/app/[locale]/product-categories/[product_category_id]/page.tsx b/src/app/[locale]/product-categories/[product_category_id]/page.tsx
index 4083280..d7d6768 100644
--- a/src/app/[locale]/product-categories/[product_category_id]/page.tsx
+++ b/src/app/[locale]/product-categories/[product_category_id]/page.tsx
@@ -3,22 +3,20 @@ import images from '@/assets/images/images';
import Breadcrumb from '@/components/layout/breadcrumb';
import { IBreadcrumbItem } from '@/components/layout/breadcrumb/breadcrumb';
import InnerPageBanner from '@/components/layout/innerPages/banner';
-import Pagination from '@/components/layout/pagination';
import ProductCategoryCardInProducts from '@/components/pages/productCategories/ProductCategoryCardInProducts';
-import ProductCard from '@/components/pages/products/ProductCard';
import ProductList from '@/components/pages/products/productList';
-import { ILayoutLocaleParams } from '@/models/layout';
+import { IPageParams } from '@/models/layout';
import { getProductCategoryById, getProducts } from '@/services/products';
import { getTranslations } from 'next-intl/server';
-interface IParams extends ILayoutLocaleParams {
+interface IParams {
product_category_id: string;
}
export default async function ProductPage({
params,
}: {
- params: Promise;
+ params: IPageParams;
}) {
const { product_category_id: productCategoryId, locale } = await params;
const t = await getTranslations({ locale });
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
index efa1295..e83f9be 100644
--- a/src/app/api/product_categories/[id]/products/data.ts
+++ b/src/app/api/product_categories/[id]/products/data.ts
@@ -1,5 +1,5 @@
import { productCategories } from '../../data';
-import type { IProductCardProps } from '@/components/pages/products/productCard.d';
+import type { IProductCardProps } from '@/components/pages/products/productCard/productCard.d';
// Helper to generate 50 products for a category
export function generateProductsForCategory(
diff --git a/src/app/api/product_categories/[id]/products/route.ts b/src/app/api/product_categories/[id]/products/route.ts
index a3377cb..ac1704d 100644
--- a/src/app/api/product_categories/[id]/products/route.ts
+++ b/src/app/api/product_categories/[id]/products/route.ts
@@ -7,7 +7,7 @@ export async function GET(request: NextRequest, context: any) {
const cookieStore = await cookies();
const lang = cookieStore.get('locale')?.value || 'en';
- const params = context.params;
+ const params = await context.params;
const category = productCategories.find((cat) => cat.id === params.id);
if (!category) {
diff --git a/src/app/api/product_categories/[id]/route.ts b/src/app/api/product_categories/[id]/route.ts
index fb289e9..f005af1 100644
--- a/src/app/api/product_categories/[id]/route.ts
+++ b/src/app/api/product_categories/[id]/route.ts
@@ -4,14 +4,19 @@ import { NextResponse, NextRequest } from 'next/server';
import { cookies } from 'next/headers';
import { productCategories } from '../data';
-export async function GET(
- request: NextRequest,
- context: { params: { id: string } },
-) {
+interface IContext {
+ params: {
+ id: string;
+ };
+}
+
+interface IParams {}
+
+export async function GET(request: NextRequest, context: any) {
const cookieStore = await cookies();
const lang = cookieStore.get('locale')?.value || 'en';
- const params = await context.params;
+ const params = (await context).params;
const { id } = params;
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/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
index 29de78c..c0741af 100644
--- a/src/components/pages/products/productList/index.tsx
+++ b/src/components/pages/products/productList/index.tsx
@@ -1,10 +1,10 @@
'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';
+import ProductCard from '../productCard';
export default function ProductList({
initialProducts,
diff --git a/src/components/pages/products/productList/productList.d.ts b/src/components/pages/products/productList/productList.d.ts
index 2916fa8..d8b290e 100644
--- a/src/components/pages/products/productList/productList.d.ts
+++ b/src/components/pages/products/productList/productList.d.ts
@@ -1,5 +1,5 @@
import { IPagination } from '@/models/response';
-import { IProductCardProps } from '../productCard';
+import { IProductCardProps } from '../productCard/productCard.d';
export interface IProductListProps {
initialProducts: IProductCardProps[];
diff --git a/src/models/layout.d.ts b/src/models/layout.d.ts
index a021256..4e8a611 100644
--- a/src/models/layout.d.ts
+++ b/src/models/layout.d.ts
@@ -1,3 +1,4 @@
+export interface IPageParams extends Promise {}
export interface ILayoutLocaleParams {
locale: TLocales;
}
diff --git a/src/services/products.ts b/src/services/products.ts
index 9e23364..87820dc 100644
--- a/src/services/products.ts
+++ b/src/services/products.ts
@@ -1,7 +1,7 @@
'use server';
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/productCard.d';
import api from '@/lib/axios';
import { IResponse } from '@/models/response';
import { cookies } from 'next/headers';