feat: refactor product components and add sitemap support

- Updated product components to use new IPageParams interface for better type safety.
- Added next-sitemap configuration and generated sitemap.xml and robots.txt files.
- Removed deprecated ProductCard component and its associated types.
- Enhanced internationalization support across various pages.
This commit is contained in:
2025-06-09 11:29:54 +03:30
parent 7073310a22
commit 429256f512
27 changed files with 187 additions and 87 deletions
+9 -15
View File
@@ -1,25 +1,19 @@
# Use official Node.js image as the base # Build stage
FROM node:20-alpine AS builder FROM node:20-alpine AS builder
# Set working directory
WORKDIR /app WORKDIR /app
# Install dependencies
COPY package.json package-lock.json ./ COPY package.json package-lock.json ./
RUN npm ci RUN npm ci
# Copy the rest of the app
COPY . . COPY . .
# Build the Next.js app
RUN npm run build RUN npm run build
# Production stage
# Set environment variables (optional) FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production ENV NODE_ENV=production
ENV PORT=5000
# Expose port 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 EXPOSE 5000
# Start the Next.js app
CMD ["npm", "start"] CMD ["npm", "start"]
+18
View File
@@ -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`,
// ],
};
+40
View File
@@ -28,6 +28,7 @@
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.4.1", "eslint-plugin-prettier": "^5.4.1",
"next-sitemap": "^4.2.3",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.12", "prettier-plugin-tailwindcss": "^0.6.12",
"tailwindcss": "^4", "tailwindcss": "^4",
@@ -59,6 +60,12 @@
"node": ">=6.0.0" "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": { "node_modules/@emnapi/core": {
"version": "1.4.3", "version": "1.4.3",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz", "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": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
+3 -1
View File
@@ -6,7 +6,8 @@
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint",
"sitemap": "next-sitemap"
}, },
"dependencies": { "dependencies": {
"axios": "^1.9.0", "axios": "^1.9.0",
@@ -29,6 +30,7 @@
"eslint-config-prettier": "^10.1.5", "eslint-config-prettier": "^10.1.5",
"eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-prettier": "^5.4.1", "eslint-plugin-prettier": "^5.4.1",
"next-sitemap": "^4.2.3",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-tailwindcss": "^0.6.12", "prettier-plugin-tailwindcss": "^0.6.12",
"tailwindcss": "^4", "tailwindcss": "^4",
+9
View File
@@ -0,0 +1,9 @@
# *
User-agent: *
Allow: /
# Host
Host: http://localhost:3000
# Sitemaps
Sitemap: http://localhost:3000/sitemap.xml
+3
View File
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
</sitemapindex>
+7 -5
View File
@@ -1,21 +1,23 @@
import '@/app/globals.css'; import '@/app/globals.css';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
import { NextIntlClientProvider } from 'next-intl'; import { NextIntlClientProvider } from 'next-intl';
import type { ILayoutLocaleParams } from '@/models/layout.d'; import type { IPageParams } from '@/models/layout.d';
// export const metadata: Metadata = { // export const metadata: Metadata = {
// title: 'Create Next App', // title: 'Create Next App',
// description: 'Generated by create next app', // description: 'Generated by create next app',
// }; // };
export default async function RootLayout({ export default async function ErrorLayout({
children, children,
params, params,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; 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; let messages;
try { try {
messages = (await import(`../../../messages/${locale}.json`)).default; messages = (await import(`../../../messages/${locale}.json`)).default;
@@ -25,7 +27,7 @@ export default async function RootLayout({
return ( return (
<html lang={locale} dir={locale === 'fa' ? 'rtl' : 'ltr'}> <html lang={locale} dir={locale === 'fa' ? 'rtl' : 'ltr'}>
<body className="min-h-svh w-full overflow-x-hidden"> <body className={`min-h-svh w-full overflow-x-hidden ${fontClass}`}>
<NextIntlClientProvider locale={locale} messages={messages}> <NextIntlClientProvider locale={locale} messages={messages}>
{children} {children}
</NextIntlClientProvider> </NextIntlClientProvider>
+5 -8
View File
@@ -1,16 +1,13 @@
import routeFactory from '@/assets/constants/routeFactory'; import routeFactory from '@/assets/constants/routeFactory';
import images from '@/assets/images/images'; import images from '@/assets/images/images';
import Button from '@/components/uikit/button'; import Button from '@/components/uikit/button';
import { ILayoutLocaleParams } from '@/models/layout'; import { IPageParams } from '@/models/layout';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
export default async function NotFound({ export default async function NotFound({ params }: { params: IPageParams }) {
params, const paramsData = await params;
}: { const t = await getTranslations({ locale: paramsData.locale });
params: ILayoutLocaleParams; const routes = routeFactory(t, paramsData.locale);
}) {
const t = await getTranslations({ locale: params.locale });
const routes = routeFactory(t, params.locale);
return ( return (
<div className="container mx-auto flex h-svh flex-col items-center justify-center gap-6"> <div className="container mx-auto flex h-svh flex-col items-center justify-center gap-6">
+2 -3
View File
@@ -1,15 +1,14 @@
import images from '@/assets/images/images'; import images from '@/assets/images/images';
import InnerPageBanner from '@/components/layout/innerPages/banner'; import InnerPageBanner from '@/components/layout/innerPages/banner';
import AboutTopInfo from '@/components/pages/about/TopInfo'; import AboutTopInfo from '@/components/pages/about/TopInfo';
import { ILayoutLocaleParams } from '@/models/layout'; import { IPageParams } from '@/models/layout';
import { useTranslations } from 'next-intl';
import { setRequestLocale } from 'next-intl/server'; import { setRequestLocale } from 'next-intl/server';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
export default async function AboutUs({ export default async function AboutUs({
params, params,
}: Readonly<{ }: Readonly<{
params: ILayoutLocaleParams; params: IPageParams;
}>) { }>) {
const { locale } = await params; const { locale } = await params;
+4 -7
View File
@@ -1,15 +1,12 @@
import images from '@/assets/images/images'; import images from '@/assets/images/images';
import InnerPageBanner from '@/components/layout/innerPages/banner'; import InnerPageBanner from '@/components/layout/innerPages/banner';
import PostsGrid from '@/components/pages/blog/PostsGrid'; import PostsGrid from '@/components/pages/blog/PostsGrid';
import { ILayoutLocaleParams } from '@/models/layout'; import { IPageParams } from '@/models/layout';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
export default async function Blog({ export default async function Blog({ params }: { params: IPageParams }) {
params, const paramsData = await params;
}: { const t = await getTranslations({ locale: paramsData.locale });
params: ILayoutLocaleParams;
}) {
const t = await getTranslations({ locale: params.locale });
return ( return (
<main className="flex flex-col items-center justify-between"> <main className="flex flex-col items-center justify-between">
+5 -7
View File
@@ -5,14 +5,12 @@ import ContactForm from '@/components/pages/contact/ContactMeForm';
import TouchUs from '@/components/pages/contact/TouchUs'; import TouchUs from '@/components/pages/contact/TouchUs';
import Image from 'next/image'; import Image from 'next/image';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { ILayoutLocaleParams } from '@/models/layout'; import { IPageParams } from '@/models/layout';
export default async function Contact({ export default async function Contact({ params }: { params: IPageParams }) {
params, const { locale } = await params;
}: {
params: ILayoutLocaleParams; const t = await getTranslations({ locale });
}) {
const t = await getTranslations({ locale: params.locale });
return ( return (
<main className="flex flex-col items-center justify-between"> <main className="flex flex-col items-center justify-between">
+2 -8
View File
@@ -2,24 +2,18 @@ import Footer from '@/components/layout/footer/Footer';
import '@/app/globals.css'; import '@/app/globals.css';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
// import { cookies } from 'next/headers';
import { NextIntlClientProvider } from 'next-intl'; import { NextIntlClientProvider } from 'next-intl';
import type { ILayoutLocaleParams } from '@/models/layout.d'; import type { IPageParams } from '@/models/layout.d';
import { cookies } from 'next/headers';
export default async function RootLayout({ export default async function RootLayout({
children, children,
params, params,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
params: ILayoutLocaleParams; params: IPageParams;
}>) { }>) {
const paramsData = await params; const paramsData = await params;
const locale = paramsData.locale; const locale = paramsData.locale;
// const cookieStore = await cookies();
// // Set locale cookie on the server
// cookieStore.set('locale', locale, { path: '/', httpOnly: false });
const fontClass = locale === 'fa' ? 'font-fa' : 'font-en'; const fontClass = locale === 'fa' ? 'font-fa' : 'font-en';
+3 -7
View File
@@ -2,17 +2,13 @@ import Header from '@/components/layout/header';
import AboutTopInfo from '@/components/pages/about/TopInfo'; import AboutTopInfo from '@/components/pages/about/TopInfo';
import HomeStorySection from '@/components/pages/home/HomeStorySection'; import HomeStorySection from '@/components/pages/home/HomeStorySection';
import HomePageProducts from '@/components/pages/home/Products'; 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'; import { getTranslations } from 'next-intl/server';
interface IPageParams { export default async function Home({ params }: { params: IPageParams }) {
params: ILayoutLocaleParams;
}
export default async function Home({ params }: IPageParams) {
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale }); await getTranslations({ locale });
return ( return (
<main className="flex flex-col items-center justify-between"> <main className="flex flex-col items-center justify-between">
@@ -3,22 +3,20 @@ import images from '@/assets/images/images';
import Breadcrumb from '@/components/layout/breadcrumb'; import Breadcrumb from '@/components/layout/breadcrumb';
import { IBreadcrumbItem } from '@/components/layout/breadcrumb/breadcrumb'; import { IBreadcrumbItem } from '@/components/layout/breadcrumb/breadcrumb';
import InnerPageBanner from '@/components/layout/innerPages/banner'; import InnerPageBanner from '@/components/layout/innerPages/banner';
import Pagination from '@/components/layout/pagination';
import ProductCategoryCardInProducts from '@/components/pages/productCategories/ProductCategoryCardInProducts'; import ProductCategoryCardInProducts from '@/components/pages/productCategories/ProductCategoryCardInProducts';
import ProductCard from '@/components/pages/products/ProductCard';
import ProductList from '@/components/pages/products/productList'; import ProductList from '@/components/pages/products/productList';
import { ILayoutLocaleParams } from '@/models/layout'; import { IPageParams } from '@/models/layout';
import { getProductCategoryById, getProducts } from '@/services/products'; import { getProductCategoryById, getProducts } from '@/services/products';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
interface IParams extends ILayoutLocaleParams { interface IParams {
product_category_id: string; product_category_id: string;
} }
export default async function ProductPage({ export default async function ProductPage({
params, params,
}: { }: {
params: Promise<IParams>; params: IPageParams<IParams>;
}) { }) {
const { product_category_id: productCategoryId, locale } = await params; const { product_category_id: productCategoryId, locale } = await params;
const t = await getTranslations({ locale }); const t = await getTranslations({ locale });
+4 -4
View File
@@ -2,17 +2,17 @@ import images from '@/assets/images/images';
import InnerPageBanner from '@/components/layout/innerPages/banner'; import InnerPageBanner from '@/components/layout/innerPages/banner';
import ProductsCategoryCard from '@/components/pages/productCategories/ProductCategoryCard'; import ProductsCategoryCard from '@/components/pages/productCategories/ProductCategoryCard';
import type { IProductCategoryCardProps } from '@/components/pages/productCategories/productCategoryCard.d'; 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 { getProductCategories } from '@/services/products';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
export default async function ProductsPage({ export default async function ProductsPage({
params, params,
}: { }: {
params: ILayoutLocaleParams; params: IPageParams;
}) { }) {
const awaitedParams = await params; const { locale } = await params;
const t = await getTranslations({ locale: awaitedParams.locale }); const t = await getTranslations({ locale });
const productCategories: IProductCategoryCardProps[] = const productCategories: IProductCategoryCardProps[] =
await getProductCategories(); await getProductCategories();
+4 -7
View File
@@ -1,15 +1,12 @@
import images from '@/assets/images/images'; import images from '@/assets/images/images';
import InnerPageBanner from '@/components/layout/innerPages/banner'; import InnerPageBanner from '@/components/layout/innerPages/banner';
import ProjectsGrid from '@/components/pages/project/ProjectGrid'; import ProjectsGrid from '@/components/pages/project/ProjectGrid';
import { ILayoutLocaleParams } from '@/models/layout'; import { IPageParams } from '@/models/layout';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
export default async function Projects({ export default async function Projects({ params }: { params: IPageParams }) {
params, const { locale } = await params;
}: { const t = await getTranslations({ locale });
params: ILayoutLocaleParams;
}) {
const t = await getTranslations({ locale: params.locale });
return ( return (
<main className="flex flex-col items-center justify-between"> <main className="flex flex-col items-center justify-between">
@@ -1,5 +1,5 @@
import { productCategories } from '../../data'; 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 // Helper to generate 50 products for a category
export function generateProductsForCategory( export function generateProductsForCategory(
@@ -7,7 +7,7 @@ export async function GET(request: NextRequest, context: any) {
const cookieStore = await cookies(); const cookieStore = await cookies();
const lang = cookieStore.get('locale')?.value || 'en'; 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); const category = productCategories.find((cat) => cat.id === params.id);
if (!category) { if (!category) {
+10 -5
View File
@@ -4,14 +4,19 @@ import { NextResponse, NextRequest } from 'next/server';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';
import { productCategories } from '../data'; import { productCategories } from '../data';
export async function GET( interface IContext {
request: NextRequest, params: {
context: { params: { id: string } }, id: string;
) { };
}
interface IParams {}
export async function GET(request: NextRequest, context: any) {
const cookieStore = await cookies(); const cookieStore = await cookies();
const lang = cookieStore.get('locale')?.value || 'en'; const lang = cookieStore.get('locale')?.value || 'en';
const params = await context.params; const params = (await context).params;
const { id } = params; const { id } = params;
@@ -0,0 +1 @@
[]
@@ -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 },
);
}
}
@@ -1,10 +1,10 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState } from 'react';
import ProductCard from '../ProductCard';
import Pagination from '@/components/layout/pagination'; import Pagination from '@/components/layout/pagination';
import { getProducts } from '@/services/products'; import { getProducts } from '@/services/products';
import { IProductListProps } from './productList'; import { IProductListProps } from './productList';
import ProductCard from '../productCard';
export default function ProductList({ export default function ProductList({
initialProducts, initialProducts,
+1 -1
View File
@@ -1,5 +1,5 @@
import { IPagination } from '@/models/response'; import { IPagination } from '@/models/response';
import { IProductCardProps } from '../productCard'; import { IProductCardProps } from '../productCard/productCard.d';
export interface IProductListProps { export interface IProductListProps {
initialProducts: IProductCardProps[]; initialProducts: IProductCardProps[];
+1
View File
@@ -1,3 +1,4 @@
export interface IPageParams<T = {}> extends Promise<ILayoutLocaleParams & T> {}
export interface ILayoutLocaleParams { export interface ILayoutLocaleParams {
locale: TLocales; locale: TLocales;
} }
+1 -1
View File
@@ -1,7 +1,7 @@
'use server'; 'use server';
import type { IProductCategoryCardProps } from '@/components/pages/productCategories/productCategoryCard.d'; import type { IProductCategoryCardProps } from '@/components/pages/productCategories/productCategoryCard.d';
import type { IProductCardProps } from '@/components/pages/products/productCard.d'; import type { IProductCardProps } from '@/components/pages/products/productCard/productCard.d';
import api from '@/lib/axios'; import api from '@/lib/axios';
import { IResponse } from '@/models/response'; import { IResponse } from '@/models/response';
import { cookies } from 'next/headers'; import { cookies } from 'next/headers';