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';