Compare commits
17 Commits
864d5c51dc
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
| df34692325 | |||
| bfa7413dfd | |||
| aa5b883a1c | |||
| 0670f4462c | |||
| 27977b1018 | |||
| 9fd23b69e3 | |||
| 3486da6eeb | |||
| 730e2d8ca8 | |||
| 478b550c51 | |||
| 3cab7e3ee5 | |||
| aae6a4b49b | |||
| 19e53d4b97 | |||
| 584bc5522e | |||
| 123df850a8 | |||
| 21c52a84ed | |||
| 6c9dd0b731 | |||
| 1d5b25354d |
@@ -2,3 +2,8 @@
|
|||||||
node_modules
|
node_modules
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
.next/cache
|
.next/cache
|
||||||
|
.next
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
README.md
|
||||||
|
*.log
|
||||||
|
|||||||
@@ -42,3 +42,7 @@ next-env.d.ts
|
|||||||
|
|
||||||
contactMessages.json
|
contactMessages.json
|
||||||
public/sitemap.xml
|
public/sitemap.xml
|
||||||
|
|
||||||
|
data/
|
||||||
|
|
||||||
|
node_modules/
|
||||||
|
|||||||
@@ -1,28 +1,35 @@
|
|||||||
# Build stage
|
FROM node:20-alpine AS base
|
||||||
FROM node:20-alpine AS builder
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json package-lock.json ./
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
RUN npm ci
|
|
||||||
|
FROM base AS deps
|
||||||
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
# RUN npm config set registry https://hub.megan.ir/npm
|
||||||
|
RUN npm i -g pnpm
|
||||||
|
RUN pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
FROM base AS builder
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
# RUN npm config set registry https://hub.megan.ir/npm
|
||||||
|
RUN npm install -g pnpm
|
||||||
|
RUN pnpm build
|
||||||
|
|
||||||
ENV PORT=5000
|
|
||||||
ENV VERCEL_PROJECT_PRODUCTION_URL=http://194.59.214.243:5000
|
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=http://194.59.214.243:5000/api
|
|
||||||
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM node:20-alpine AS runner
|
FROM node:20-alpine AS runner
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
ARG PORT=3000
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV PORT=5000
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
ENV VERCEL_PROJECT_PRODUCTION_URL=http://194.59.214.243:5000
|
ENV PORT=$PORT
|
||||||
ENV NEXT_PUBLIC_API_BASE_URL=http://194.59.214.243:5000/api
|
|
||||||
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
|
|
||||||
VOLUME ["/app/contactMessages.json"]
|
|
||||||
|
|
||||||
EXPOSE 5000
|
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
|
||||||
CMD ["npm", "start"]
|
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/public ./public
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
RUN touch /app/contactMessages.json && chown nextjs:nodejs /app/contactMessages.json
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
EXPOSE ${PORT}
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
services:
|
||||||
|
web:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
args:
|
||||||
|
PORT: ${PORT:-3000}
|
||||||
|
image: pasargad-bricks:latest
|
||||||
|
container_name: pasargad-bricks
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "${PORT:-3000}:${PORT:-3000}"
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
volumes:
|
||||||
|
- ./data/contactMessages.json:/app/data/contactMessages.json
|
||||||
@@ -10,19 +10,20 @@ const compat = new FlatCompat({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const eslintConfig = [
|
const eslintConfig = [
|
||||||
...compat.extends("next/core-web-vitals", "next/typescript"),
|
...compat.extends("next/core-web-vitals", "next/typescript", "plugin:prettier/recommended", "plugin:jsx-a11y/recommended"),
|
||||||
{
|
{
|
||||||
extends: [
|
|
||||||
'plugin:prettier/recommended',
|
|
||||||
'plugin:jsx-a11y/recommended', // Add this line
|
|
||||||
],
|
|
||||||
rules: {
|
rules: {
|
||||||
// Example: Enforce using `const` for variables that are never reassigned
|
'prefer-const': 'warn',
|
||||||
'prefer-const': 'error',
|
|
||||||
// Example: Disallow unused variables (already often included in recommended configs, but shown for illustration)
|
|
||||||
'no-unused-vars': ['warn', { 'args': 'none' }],
|
'no-unused-vars': ['warn', { 'args': 'none' }],
|
||||||
// Example: Disable a rule if it conflicts with your style or Prettier (less common after plugin:prettier/recommended)
|
'@typescript-eslint/no-unused-vars': ['warn', { 'args': 'none' }],
|
||||||
// 'indent': 'off',
|
'@typescript-eslint/no-explicit-any': 'warn',
|
||||||
|
'@typescript-eslint/ban-ts-comment': 'warn',
|
||||||
|
'@typescript-eslint/no-empty-object-type': 'warn',
|
||||||
|
'prettier/prettier': 'warn',
|
||||||
|
'jsx-a11y/click-events-have-key-events': 'warn',
|
||||||
|
'jsx-a11y/no-static-element-interactions': 'warn',
|
||||||
|
'jsx-a11y/alt-text': 'warn',
|
||||||
|
'jsx-a11y/no-noninteractive-element-interactions': 'warn',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -102,9 +102,16 @@ declare const messages: {
|
|||||||
"com_contact_textarea_error": "Message must be at least 10 characters long.",
|
"com_contact_textarea_error": "Message must be at least 10 characters long.",
|
||||||
"com_contact_button_submit": "Submit Message",
|
"com_contact_button_submit": "Submit Message",
|
||||||
"pages_notFound_title": "404 Not Found Page",
|
"pages_notFound_title": "404 Not Found Page",
|
||||||
|
|
||||||
|
"com_modal_language_title": "Please select language",
|
||||||
|
"com_modal_language_close": "Close",
|
||||||
"pages_notFound_description": "You can either head back to the Homepage or try a search.",
|
"pages_notFound_description": "You can either head back to the Homepage or try a search.",
|
||||||
"pages_notFound_button": "Go To Home",
|
"pages_notFound_button": "Go To Home",
|
||||||
|
|
||||||
|
"com_about_certifications_title": "Our Certifications",
|
||||||
|
"com_about_certifications_title_bold": "& Standards",
|
||||||
|
"com_about_certifications_description": "We maintain the highest quality standards through internationally recognized certifications.",
|
||||||
|
|
||||||
"page_about_metadata_title": "About Us",
|
"page_about_metadata_title": "About Us",
|
||||||
"page_about_metadata_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.",
|
"page_about_metadata_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.",
|
||||||
"page_about_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.",
|
"page_about_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.",
|
||||||
|
|||||||
@@ -99,9 +99,16 @@
|
|||||||
"com_contact_textarea_error": "Message must be at least 10 characters long.",
|
"com_contact_textarea_error": "Message must be at least 10 characters long.",
|
||||||
"com_contact_button_submit": "Submit Message",
|
"com_contact_button_submit": "Submit Message",
|
||||||
"pages_notFound_title": "404 Not Found Page",
|
"pages_notFound_title": "404 Not Found Page",
|
||||||
|
|
||||||
|
"com_modal_language_title": "Please select language",
|
||||||
|
"com_modal_language_close": "Close",
|
||||||
"pages_notFound_description": "You can either head back to the Homepage or try a search.",
|
"pages_notFound_description": "You can either head back to the Homepage or try a search.",
|
||||||
"pages_notFound_button": "Go To Home",
|
"pages_notFound_button": "Go To Home",
|
||||||
|
|
||||||
|
"com_about_certifications_title": "Our Certifications",
|
||||||
|
"com_about_certifications_title_bold": "& Standards",
|
||||||
|
"com_about_certifications_description": "We maintain the highest quality standards through internationally recognized certifications.",
|
||||||
|
|
||||||
"page_about_metadata_title": "About Us",
|
"page_about_metadata_title": "About Us",
|
||||||
"page_about_metadata_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.",
|
"page_about_metadata_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.",
|
||||||
"page_about_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.",
|
"page_about_description": "Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques. Learn more about our commitment to producing high-quality bricks using both traditional and modern manufacturing techniques.",
|
||||||
|
|||||||
@@ -98,10 +98,17 @@
|
|||||||
"com_contact_phone_error": "شماره تماس معتبر نیست (مثلاً: 09123456789)",
|
"com_contact_phone_error": "شماره تماس معتبر نیست (مثلاً: 09123456789)",
|
||||||
"com_contact_textarea_error": "پیام باید حداقل ۱۰ کاراکتر باشد",
|
"com_contact_textarea_error": "پیام باید حداقل ۱۰ کاراکتر باشد",
|
||||||
"com_contact_button_submit": "ارسال پیام",
|
"com_contact_button_submit": "ارسال پیام",
|
||||||
|
|
||||||
|
"com_modal_language_title": "لطفاً زبان را انتخاب کنید",
|
||||||
|
"com_modal_language_close": "بستن",
|
||||||
"pages_notFound_title": "صفحه مورد نظر پیدا نشد.",
|
"pages_notFound_title": "صفحه مورد نظر پیدا نشد.",
|
||||||
"pages_notFound_description": "صفحهای که به دنبال آن هستید وجود ندارد یا منتقل شده است. لطفاً آدرس را بررسی کنید یا به صفحه اصلی بازگردید.",
|
"pages_notFound_description": "صفحهای که به دنبال آن هستید وجود ندارد یا منتقل شده است. لطفاً آدرس را بررسی کنید یا به صفحه اصلی بازگردید.",
|
||||||
"pages_notFound_button": "بازگشت به صفحه اصلی",
|
"pages_notFound_button": "بازگشت به صفحه اصلی",
|
||||||
|
|
||||||
|
"com_about_certifications_title": "گواهینامهها",
|
||||||
|
"com_about_certifications_title_bold": "و استانداردهای ما",
|
||||||
|
"com_about_certifications_description": "ما بالاترین استانداردهای کیفیت را از طریق گواهینامههای معتبر بینالمللی حفظ میکنیم.",
|
||||||
|
|
||||||
"page_about_metadata_title": "درباره ما",
|
"page_about_metadata_title": "درباره ما",
|
||||||
"page_about_metadata_description": "با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهرهگیری از فناوریهای نوین و روشهای ساخت پیشرفته.",
|
"page_about_metadata_description": "با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهرهگیری از فناوریهای نوین و روشهای ساخت پیشرفته.",
|
||||||
"page_about_description": "با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهرهگیری از فناوریهای نوین و روشهای ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهرهگیری از فناوریهای نوین و روشهای ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهرهگیری از فناوریهای نوین و روشهای ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهرهگیری از فناوریهای نوین و روشهای ساخت پیشرفته.",
|
"page_about_description": "با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهرهگیری از فناوریهای نوین و روشهای ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهرهگیری از فناوریهای نوین و روشهای ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهرهگیری از فناوریهای نوین و روشهای ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهرهگیری از فناوریهای نوین و روشهای ساخت پیشرفته.",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
siteUrl: process.env.SITE_URL || 'http://localhost:3000',
|
siteUrl: process.env.SITE_URL || 'http://localhost:4000',
|
||||||
generateRobotsTxt: true,
|
generateRobotsTxt: true,
|
||||||
changefreq: 'weekly',
|
changefreq: 'weekly',
|
||||||
priority: 0.7,
|
priority: 0.7,
|
||||||
@@ -13,6 +13,6 @@ module.exports = {
|
|||||||
exclude: ['/api/*', '/_next/*'],
|
exclude: ['/api/*', '/_next/*'],
|
||||||
// If you want to include additional sitemaps (for subdirectories, etc.)
|
// If you want to include additional sitemaps (for subdirectories, etc.)
|
||||||
// additionalSitemaps: [
|
// additionalSitemaps: [
|
||||||
// `${process.env.SITE_URL || 'http://localhost:5000'}/custom-sitemap.xml`,
|
// `${process.env.SITE_URL || 'http://localhost:3000'}/custom-sitemap.xml`,
|
||||||
// ],
|
// ],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ const withNextIntl = createNextIntlPlugin({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const config: NextConfig = {};
|
const config: NextConfig = {
|
||||||
|
output: 'standalone',
|
||||||
|
};
|
||||||
|
|
||||||
export default withNextIntl(config);
|
export default withNextIntl(config);
|
||||||
|
|||||||
@@ -1,15 +1,4 @@
|
|||||||
{
|
{
|
||||||
"name": "pasargad_bricks",
|
|
||||||
"version": "0.1.0",
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "next dev --turbopack",
|
|
||||||
"build": "next build",
|
|
||||||
"start": "next start",
|
|
||||||
"lint": "next lint",
|
|
||||||
"sitemap": "ts-node scripts/generate-sitemap.ts",
|
|
||||||
"postbuild": "npm run sitemap"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
@@ -28,6 +17,7 @@
|
|||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
|
"dotenv-cli": "^11.0.0",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.3",
|
"eslint-config-next": "15.3.3",
|
||||||
"eslint-config-prettier": "^10.1.5",
|
"eslint-config-prettier": "^10.1.5",
|
||||||
@@ -40,5 +30,15 @@
|
|||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"tsconfig-paths": "^4.2.0",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3"
|
||||||
}
|
},
|
||||||
|
"name": "pasargad_bricks",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "next build",
|
||||||
|
"dev": "dotenv -- next dev --turbopack",
|
||||||
|
"lint": "next lint",
|
||||||
|
"sitemap": "ts-node scripts/generate-sitemap.ts",
|
||||||
|
"start": "dotenv -- next start"
|
||||||
|
},
|
||||||
|
"version": "0.1.0"
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 360 KiB After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 360 KiB |
|
Before Width: | Height: | Size: 259 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 578 KiB After Width: | Height: | Size: 17 KiB |
@@ -3,7 +3,7 @@ User-agent: *
|
|||||||
Allow: /
|
Allow: /
|
||||||
|
|
||||||
# Host
|
# Host
|
||||||
Host: http://localhost:3000
|
Host: http://localhost:4000
|
||||||
|
|
||||||
# Sitemaps
|
# Sitemaps
|
||||||
Sitemap: http://localhost:3000/sitemap.xml
|
Sitemap: http://localhost:4000/sitemap.xml
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import '@/app/globals.css';
|
import '@/app/globals.css';
|
||||||
|
import { isRtl } from '@/config';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
import { NextIntlClientProvider } from 'next-intl';
|
import { NextIntlClientProvider } from 'next-intl';
|
||||||
import type { IPageParams } from '@/models/layout.d';
|
import type { IPageParams } from '@/models/layout.d';
|
||||||
@@ -17,7 +18,7 @@ export default async function ErrorLayout({
|
|||||||
}>) {
|
}>) {
|
||||||
const paramsData = await params;
|
const paramsData = await params;
|
||||||
const locale = paramsData.locale;
|
const locale = paramsData.locale;
|
||||||
const fontClass = locale === 'fa' ? 'font-fa' : 'font-en';
|
const fontClass = isRtl(locale) ? 'font-fa' : 'font-en';
|
||||||
let messages;
|
let messages;
|
||||||
try {
|
try {
|
||||||
messages = (await import(`../../../../messages/${locale}.json`)).default;
|
messages = (await import(`../../../../messages/${locale}.json`)).default;
|
||||||
@@ -26,7 +27,7 @@ export default async function ErrorLayout({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} dir={locale === 'fa' ? 'rtl' : 'ltr'}>
|
<html lang={locale} dir={isRtl(locale) ? 'rtl' : 'ltr'}>
|
||||||
<body className={`min-h-svh w-full overflow-x-hidden ${fontClass}`}>
|
<body className={`min-h-svh w-full overflow-x-hidden ${fontClass}`}>
|
||||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
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 AboutDescription from '@/components/pages/about/AboutDescription';
|
import AboutDescription from '@/components/pages/about/AboutDescription';
|
||||||
|
import CertificationsSection from '@/components/pages/about/CertificationsSection';
|
||||||
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 { IPageParams } from '@/models/layout';
|
import { IPageParams } from '@/models/layout';
|
||||||
@@ -36,6 +37,7 @@ export default async function AboutUs({
|
|||||||
<AboutTopInfo />
|
<AboutTopInfo />
|
||||||
<AboutDescription className="mb-8 md:mb-14" locale={locale} />
|
<AboutDescription className="mb-8 md:mb-14" locale={locale} />
|
||||||
<HomeStorySection />
|
<HomeStorySection />
|
||||||
|
<CertificationsSection locale={locale} />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import InnerPageBanner from '@/components/layout/innerPages/banner';
|
|||||||
import { IPageParams } from '@/models/layout';
|
import { IPageParams } from '@/models/layout';
|
||||||
import { getPost } from '@/services/blog';
|
import { getPost } from '@/services/blog';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
interface IParams {
|
interface IParams {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -20,7 +21,14 @@ export async function generateMetadata({
|
|||||||
const paramsData = await params;
|
const paramsData = await params;
|
||||||
const { id: postId, locale } = paramsData;
|
const { id: postId, locale } = paramsData;
|
||||||
const t = await getTranslations({ locale });
|
const t = await getTranslations({ locale });
|
||||||
const postData = await getPost(postId);
|
const postData = await getPost(postId).catch(() => null);
|
||||||
|
|
||||||
|
if (!postData) {
|
||||||
|
return {
|
||||||
|
title: t('page_blog_metadata_title'),
|
||||||
|
description: t('page_blog_metadata_description'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: postData.title,
|
title: postData.title,
|
||||||
@@ -35,11 +43,15 @@ export default async function PostPage({
|
|||||||
}) {
|
}) {
|
||||||
const paramsData = await params;
|
const paramsData = await params;
|
||||||
const t = await getTranslations({ locale: paramsData.locale });
|
const t = await getTranslations({ locale: paramsData.locale });
|
||||||
|
const { id: postId, locale } = paramsData;
|
||||||
const { id: postId, locale } = await params;
|
|
||||||
const routes = routeFactory(t, locale);
|
const routes = routeFactory(t, locale);
|
||||||
|
|
||||||
const postData = await getPost(postId);
|
const postData = await getPost(postId).catch((error) => {
|
||||||
|
if (error?.response?.status === 404) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
});
|
||||||
|
|
||||||
const breadcrumbItems = [
|
const breadcrumbItems = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
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 ContactMap from '@/components/layout/map';
|
import ContactMap from '@/components/layout/map/MapWrapper';
|
||||||
import ContactForm from '@/components/pages/contact/ContactMeForm';
|
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, setRequestLocale } from 'next-intl/server';
|
import { getTranslations, setRequestLocale } from 'next-intl/server';
|
||||||
import { IPageParams } from '@/models/layout';
|
import { IPageParams } from '@/models/layout';
|
||||||
import Head from 'next/head';
|
|
||||||
|
|
||||||
export async function generateMetadata({ params }: { params: IPageParams }) {
|
export async function generateMetadata({ params }: { params: IPageParams }) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { NextIntlClientProvider } from 'next-intl';
|
|||||||
import type { IPageParams } from '@/models/layout.d';
|
import type { IPageParams } from '@/models/layout.d';
|
||||||
import Head from 'next/head';
|
import Head from 'next/head';
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from 'next-intl/server';
|
||||||
import { host } from '@/config';
|
import { host, isRtl } from '@/config';
|
||||||
|
|
||||||
export async function generateMetadata({ params }: { params: IPageParams }) {
|
export async function generateMetadata({ params }: { params: IPageParams }) {
|
||||||
const { locale } = await params;
|
const { locale } = await params;
|
||||||
@@ -41,7 +41,7 @@ export default async function RootLayout({
|
|||||||
const paramsData = await params;
|
const paramsData = await params;
|
||||||
const locale = paramsData.locale;
|
const locale = paramsData.locale;
|
||||||
|
|
||||||
const fontClass = locale === 'fa' ? 'font-fa' : 'font-en';
|
const fontClass = isRtl(locale) ? 'font-fa' : 'font-en';
|
||||||
|
|
||||||
let messages;
|
let messages;
|
||||||
try {
|
try {
|
||||||
@@ -51,12 +51,9 @@ export default async function RootLayout({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang={locale} dir={locale === 'fa' ? 'rtl' : 'ltr'}>
|
<html lang={locale} dir={isRtl(locale) ? 'rtl' : 'ltr'}>
|
||||||
<Head>
|
<Head>
|
||||||
<meta name="robots" content="index, follow" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<meta property="og:site_name" content="کارخانه آجرپزی پاسارگاد" />
|
|
||||||
<meta name="author" content="پاسارگاد" />
|
|
||||||
{/* <link rel="icon" href="/favicon.ico" /> */}
|
{/* <link rel="icon" href="/favicon.ico" /> */}
|
||||||
<link
|
<link
|
||||||
rel="icon"
|
rel="icon"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import Header from '@/components/layout/header';
|
import Header from '@/components/layout/header';
|
||||||
|
import ModalLanguageSelector from '@/components/layout/languageModal';
|
||||||
import AboutTopInfo from '@/components/pages/about/TopInfo';
|
import AboutTopInfo from '@/components/pages/about/TopInfo';
|
||||||
import HomePageBlog from '@/components/pages/home/HomePageBlog';
|
import HomePageBlog from '@/components/pages/home/HomePageBlog';
|
||||||
import HomeStorySection from '@/components/pages/home/HomeStorySection';
|
import HomeStorySection from '@/components/pages/home/HomeStorySection';
|
||||||
@@ -24,6 +25,7 @@ export default async function Home({ params }: { params: IPageParams }) {
|
|||||||
const t = await getTranslations({ locale });
|
const t = await getTranslations({ locale });
|
||||||
return (
|
return (
|
||||||
<main className="flex flex-col items-center justify-between">
|
<main className="flex flex-col items-center justify-between">
|
||||||
|
<ModalLanguageSelector />
|
||||||
<Header />
|
<Header />
|
||||||
<AboutTopInfo />
|
<AboutTopInfo />
|
||||||
<HomePageProducts locale={locale} />
|
<HomePageProducts locale={locale} />
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { posts } from '../data';
|
import { posts } from '../data';
|
||||||
|
import { getLocaleFromCookies } from '@/lib/locale';
|
||||||
|
import { localizeContent, localizeText } from '@/lib/domain';
|
||||||
|
import { notFound, ok } from '@/lib/api-response';
|
||||||
|
|
||||||
export async function GET(request: Request, context: any) {
|
export async function GET(
|
||||||
const params = await context.params;
|
request: Request,
|
||||||
const postId = params.id;
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
|
) {
|
||||||
|
const { id: postId } = await params;
|
||||||
|
|
||||||
const cookieStore = await cookies();
|
const locale = await getLocaleFromCookies();
|
||||||
const lang = cookieStore.get('locale')?.value || 'en';
|
|
||||||
|
|
||||||
let post = posts.find((post) => post.id === postId);
|
let post = posts.find((post) => post.id === postId);
|
||||||
if (!post) {
|
if (!post) {
|
||||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lang === 'fa') {
|
|
||||||
post = {
|
post = {
|
||||||
...post,
|
...post,
|
||||||
title: post.fa_title || post.title,
|
title: localizeText(locale, post.title, post.fa_title),
|
||||||
content: post.fa_content || post.content,
|
content: localizeContent(locale, post.content, post.fa_content),
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return ok(post);
|
||||||
data: post,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,21 +27,21 @@ export const posts: IPostData[] = [
|
|||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: 'title',
|
type: 'title',
|
||||||
data: 'روشهای پایدار برای کاهش ضایعات در تولید صنعتی',
|
data: 'Sustainable practices to reduce waste in industrial production',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'content',
|
type: 'content',
|
||||||
data: 'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است. لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است.',
|
data: 'Sustainable manufacturing focuses on minimizing waste, optimizing resource use, and improving process efficiency. By adopting circular design, material recovery, and energy-efficient processes, manufacturers can reduce environmental impact while maintaining productivity and cost-effectiveness.',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'bullet',
|
type: 'bullet',
|
||||||
data: [
|
data: [
|
||||||
'روشهای پایدار برای کاهش ضایعات در تولید صنعتی',
|
'Sustainable practices to reduce waste in industrial production',
|
||||||
'رباتیک پیشرفته و تحول فرآیندهای صنعتی',
|
'Advanced robotics transforming industrial processes',
|
||||||
'مزایای اصلی استفاده از رباتیک در تولید',
|
'Key benefits of using robotics in manufacturing',
|
||||||
'استفاده از تحلیل داده برای تولید هوشمندتر',
|
'Using data analytics for smarter production',
|
||||||
'کاهش هزینهها با اتوماسیون',
|
'Reducing costs through automation',
|
||||||
'مزایای راهحلهای سفارشیسازی در تولید',
|
'Advantages of customized manufacturing solutions',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -50,20 +50,41 @@ export const posts: IPostData[] = [
|
|||||||
src: '/images/project2.jpeg',
|
src: '/images/project2.jpeg',
|
||||||
alt: '',
|
alt: '',
|
||||||
thumbTitle:
|
thumbTitle:
|
||||||
'لورم ایپسوم متن ساختگی با تولید سادگی نامفهوم از صنعت چاپ و با استفاده از طراحان گرافیک است.',
|
'Sample image illustrating sustainable manufacturing practices.',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'numberList',
|
type: 'numberList',
|
||||||
data: [
|
data: [
|
||||||
'روشهای پایدار برای کاهش ضایعات در تولید صنعتی',
|
'Sustainable practices to reduce waste in industrial production',
|
||||||
'رباتیک پیشرفته و تحول فرآیندهای صنعتی',
|
'Advanced robotics transforming industrial processes',
|
||||||
'مزایای اصلی استفاده از رباتیک در تولید',
|
'Key benefits of using robotics in manufacturing',
|
||||||
'استفاده از تحلیل داده برای تولید هوشمندتر',
|
'Using data analytics for smarter production',
|
||||||
'کاهش هزینهها با اتوماسیون',
|
'Reducing costs through automation',
|
||||||
'مزایای راهحلهای سفارشیسازی در تولید',
|
'Advantages of customized manufacturing solutions',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'quote',
|
||||||
|
data: {
|
||||||
|
text: 'Sustainability starts with measurable, small operational improvements.',
|
||||||
|
author: 'Pasargad Bricks Team',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subtitle',
|
||||||
|
data: 'Explore related projects',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'cta',
|
||||||
|
data: {
|
||||||
|
label: 'View Projects',
|
||||||
|
link: '/projects',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
fa_content: [
|
fa_content: [
|
||||||
{
|
{
|
||||||
@@ -118,6 +139,27 @@ export const posts: IPostData[] = [
|
|||||||
'مزایای راهحلهای سفارشیسازی در تولید',
|
'مزایای راهحلهای سفارشیسازی در تولید',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'quote',
|
||||||
|
data: {
|
||||||
|
text: 'پایداری با بهبودهای کوچک اما مداوم در عملیات آغاز میشود.',
|
||||||
|
author: 'تیم آجر پاسارگاد',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subtitle',
|
||||||
|
data: 'مشاهده پروژههای مرتبط',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'cta',
|
||||||
|
data: {
|
||||||
|
label: 'مشاهده پروژهها',
|
||||||
|
link: '/projects',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,33 +1,23 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { generatePosts } from './data';
|
||||||
import { cookies } from 'next/headers';
|
import { getLocaleFromCookies } from '@/lib/locale';
|
||||||
import { generatePosts, IPostData } from './data';
|
import { localizeText } from '@/lib/domain';
|
||||||
|
import { buildPagination, getPaginationParams } from '@/lib/pagination';
|
||||||
|
import { okPaginated } from '@/lib/api-response';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
const { searchParams } = new URL(request.url);
|
const locale = await getLocaleFromCookies();
|
||||||
const lang = (await cookies()).get('lang')?.value || 'fa';
|
const { page, perPage, start, end } = getPaginationParams(request, 10);
|
||||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
|
||||||
const perPage = 10;
|
|
||||||
|
|
||||||
const posts = generatePosts();
|
const posts = generatePosts();
|
||||||
const start = (page - 1) * perPage;
|
|
||||||
const end = start + perPage;
|
|
||||||
const paginatedPosts = posts.slice(start, end);
|
const paginatedPosts = posts.slice(start, end);
|
||||||
|
|
||||||
const data = paginatedPosts.map((post) => {
|
const data = paginatedPosts.map((post) => {
|
||||||
return {
|
return {
|
||||||
id: post.id,
|
id: post.id,
|
||||||
imageSrc: post.imageSrc,
|
imageSrc: post.imageSrc,
|
||||||
title: post.fa_title || post.title,
|
title: localizeText(locale, post.title, post.fa_title),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return okPaginated(data, buildPagination(page, perPage, posts.length));
|
||||||
data,
|
|
||||||
pagination: {
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
total: posts.length,
|
|
||||||
totalPages: Math.ceil(posts.length / perPage),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { writeFile, readFile } from 'fs/promises';
|
import { mkdir, writeFile, readFile } from 'fs/promises';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
import { serverError } from '@/lib/api-response';
|
||||||
|
|
||||||
const filePath = path.resolve(process.cwd(), 'contactMessages.json');
|
const filePath = path.resolve(process.cwd(), 'data', 'contactMessages.json');
|
||||||
|
|
||||||
export async function POST(request: NextRequest) {
|
export async function POST(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
|
await mkdir(path.dirname(filePath), { recursive: true });
|
||||||
|
|
||||||
const body = await request.json();
|
const body = await request.json();
|
||||||
const { first_name, last_name, phone, email, message } = body;
|
const { first_name, last_name, phone, email, message } = body;
|
||||||
|
|
||||||
@@ -43,9 +46,6 @@ export async function POST(request: NextRequest) {
|
|||||||
message: 'Your message has been received.',
|
message: 'Your message has been received.',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return NextResponse.json(
|
return serverError('مشکلی پیش آمده.');
|
||||||
{ error, message: 'مشکلی پیش آمده.' },
|
|
||||||
{ status: 400 },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -566,7 +566,7 @@ export const meshkiCategoryProducts = [
|
|||||||
size: '',
|
size: '',
|
||||||
categoryId: '51',
|
categoryId: '51',
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
export const rusticiCategoryProducts = [
|
export const rusticiCategoryProducts = [
|
||||||
{
|
{
|
||||||
@@ -672,8 +672,7 @@ export const rusticiCategoryProducts = [
|
|||||||
size: '7 * 25 * 2.5',
|
size: '7 * 25 * 2.5',
|
||||||
categoryId: '52',
|
categoryId: '52',
|
||||||
},
|
},
|
||||||
|
];
|
||||||
]
|
|
||||||
|
|
||||||
export const ghazaghiCategoryProducts = [
|
export const ghazaghiCategoryProducts = [
|
||||||
{
|
{
|
||||||
@@ -684,7 +683,6 @@ export const ghazaghiCategoryProducts = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
export const ghazaghizardCategoryProducts = [
|
export const ghazaghizardCategoryProducts = [
|
||||||
{
|
{
|
||||||
model: 'پلاک قزاقی کامل',
|
model: 'پلاک قزاقی کامل',
|
||||||
@@ -798,7 +796,7 @@ export const ghazaghipiyaziCategoryProducts = [
|
|||||||
size: '5.5 * 20 * 5.5',
|
size: '5.5 * 20 * 5.5',
|
||||||
categoryId: '54',
|
categoryId: '54',
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|
||||||
export const ghazaghighermezCategoryProducts = [
|
export const ghazaghighermezCategoryProducts = [
|
||||||
{
|
{
|
||||||
@@ -837,4 +835,4 @@ export const ghazaghighermezCategoryProducts = [
|
|||||||
size: '10 * 20 * 2.5',
|
size: '10 * 20 * 2.5',
|
||||||
categoryId: '55',
|
categoryId: '55',
|
||||||
},
|
},
|
||||||
]
|
];
|
||||||
|
|||||||
@@ -1,35 +1,26 @@
|
|||||||
import { NextResponse, NextRequest } from 'next/server';
|
import { NextRequest } from 'next/server';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { generateProductsForCategory } from './data';
|
import { generateProductsForCategory } from './data';
|
||||||
import { productCategoriesData } from '../../data';
|
import { productCategoriesData } from '../../data';
|
||||||
|
import { buildPagination, getPaginationParams } from '@/lib/pagination';
|
||||||
|
import { notFound, okPaginated } from '@/lib/api-response';
|
||||||
|
|
||||||
export async function GET(request: NextRequest, context: any) {
|
export async function GET(
|
||||||
const cookieStore = await cookies();
|
request: NextRequest,
|
||||||
const lang = cookieStore.get('locale')?.value || 'en';
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
|
) {
|
||||||
const params = await context.params;
|
const { id } = await params;
|
||||||
const category = productCategoriesData.find((cat) => cat.id === params.id);
|
const category = productCategoriesData.find((cat) => cat.id === id);
|
||||||
|
|
||||||
if (!category) {
|
if (!category) {
|
||||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const { searchParams } = new URL(request.url);
|
const { page, perPage, start, end } = getPaginationParams(request, 5);
|
||||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
|
||||||
const perPage = 5;
|
|
||||||
|
|
||||||
const allProducts = generateProductsForCategory(category.id);
|
const allProducts = generateProductsForCategory(category.id);
|
||||||
const start = (page - 1) * perPage;
|
|
||||||
const end = start + perPage;
|
|
||||||
const paginatedProducts = allProducts.slice(start, end);
|
const paginatedProducts = allProducts.slice(start, end);
|
||||||
|
|
||||||
return NextResponse.json({
|
return okPaginated(
|
||||||
data: paginatedProducts,
|
paginatedProducts,
|
||||||
pagination: {
|
buildPagination(page, perPage, allProducts.length),
|
||||||
page,
|
);
|
||||||
perPage,
|
|
||||||
total: allProducts.length,
|
|
||||||
totalPages: Math.ceil(allProducts.length / perPage),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
'use server';
|
import { NextRequest } from 'next/server';
|
||||||
|
|
||||||
import { NextResponse, NextRequest } from 'next/server';
|
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { productCategoriesData } from '../data';
|
import { productCategoriesData } from '../data';
|
||||||
|
import { getLocaleFromCookies } from '@/lib/locale';
|
||||||
|
import { localizeText } from '@/lib/domain';
|
||||||
|
import { notFound, ok } from '@/lib/api-response';
|
||||||
|
|
||||||
export async function GET(request: NextRequest, context: any) {
|
export async function GET(
|
||||||
const cookieStore = await cookies();
|
request: NextRequest,
|
||||||
const lang = cookieStore.get('locale')?.value || 'en';
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
|
) {
|
||||||
const params = (await context).params;
|
const locale = await getLocaleFromCookies();
|
||||||
|
const { id } = await params;
|
||||||
const { id } = params;
|
|
||||||
|
|
||||||
let category = productCategoriesData.find((cat) => cat.id === id);
|
let category = productCategoriesData.find((cat) => cat.id === id);
|
||||||
|
|
||||||
if (!category) {
|
if (!category) {
|
||||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lang === 'fa') {
|
if (locale === 'fa') {
|
||||||
category = {
|
category = {
|
||||||
...category,
|
...category,
|
||||||
title: category.fa_title || category.title,
|
title: category.fa_title || category.title,
|
||||||
@@ -28,7 +27,10 @@ export async function GET(request: NextRequest, context: any) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return NextResponse.json({
|
category = {
|
||||||
data: category,
|
...category,
|
||||||
});
|
title: localizeText(locale, category.title, category.fa_title),
|
||||||
|
};
|
||||||
|
|
||||||
|
return ok(category);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { APIProductCategoryEntity } from '@/models/api';
|
import { APIProductCategoryEntity } from '@/models/api';
|
||||||
import { productCategoriesData } from './data';
|
import { productCategoriesData } from './data';
|
||||||
import { productsRelatedByCategory } from './[id]/products/data';
|
import { productsRelatedByCategory } from './[id]/products/data';
|
||||||
|
import { getLocaleFromCookies } from '@/lib/locale';
|
||||||
|
import { localizeText } from '@/lib/domain';
|
||||||
|
import { ok, serverError } from '@/lib/api-response';
|
||||||
|
|
||||||
const getTypesCount = (categoryId: string) =>
|
const getTypesCount = (categoryId: string) =>
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
productsRelatedByCategory[categoryId]?.length || 0;
|
productsRelatedByCategory[categoryId]?.length || 0;
|
||||||
|
|
||||||
const setRelatedLocaleData = (productCategory: APIProductCategoryEntity) => ({
|
const setRelatedLocaleData = (productCategory: APIProductCategoryEntity) => ({
|
||||||
title: productCategory.fa_title || productCategory.title,
|
title: localizeText(
|
||||||
|
'fa',
|
||||||
|
productCategory.title,
|
||||||
|
productCategory.fa_title,
|
||||||
|
),
|
||||||
priceTypeTitle:
|
priceTypeTitle:
|
||||||
productCategory.fa_priceTypeTitle || productCategory.priceTypeTitle,
|
productCategory.fa_priceTypeTitle || productCategory.priceTypeTitle,
|
||||||
bestForTitle: productCategory.fa_bestForTitle || productCategory.bestForTitle,
|
bestForTitle: productCategory.fa_bestForTitle || productCategory.bestForTitle,
|
||||||
@@ -20,21 +25,25 @@ export async function GET(request: Request) {
|
|||||||
try {
|
try {
|
||||||
let data = productCategoriesData as APIProductCategoryEntity[];
|
let data = productCategoriesData as APIProductCategoryEntity[];
|
||||||
|
|
||||||
const cookieStore = await cookies();
|
const locale = await getLocaleFromCookies();
|
||||||
const lang = (await cookieStore).get('locale')?.value || 'en';
|
|
||||||
|
|
||||||
data = productCategoriesData.map((productCategory) => ({
|
data = productCategoriesData.map((productCategory) => ({
|
||||||
...productCategory,
|
...productCategory,
|
||||||
...(lang === 'fa' ? setRelatedLocaleData(productCategory) : {}),
|
...(locale === 'fa'
|
||||||
|
? setRelatedLocaleData(productCategory)
|
||||||
|
: {
|
||||||
|
title: localizeText(
|
||||||
|
locale,
|
||||||
|
productCategory.title,
|
||||||
|
productCategory.fa_title,
|
||||||
|
),
|
||||||
|
}),
|
||||||
typesCount: getTypesCount(productCategory.id),
|
typesCount: getTypesCount(productCategory.id),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return NextResponse.json({ data });
|
return ok(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching product categories:', error);
|
console.error('Error fetching product categories:', error);
|
||||||
return NextResponse.json(
|
return serverError('Failed to fetch product categories');
|
||||||
{ error: 'Failed to fetch product categories' },
|
|
||||||
{ status: 500 },
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import { projects } from '../data';
|
import { projects } from '../data';
|
||||||
|
import { getLocaleFromCookies } from '@/lib/locale';
|
||||||
|
import { localizeContent, localizeText } from '@/lib/domain';
|
||||||
|
import { notFound, ok } from '@/lib/api-response';
|
||||||
|
|
||||||
export async function GET(request: Request, context: any) {
|
export async function GET(
|
||||||
const params = await context.params;
|
request: Request,
|
||||||
const projectId = params.id;
|
{ params }: { params: Promise<{ id: string }> },
|
||||||
|
) {
|
||||||
|
const { id: projectId } = await params;
|
||||||
|
|
||||||
const cookieStore = await cookies();
|
const locale = await getLocaleFromCookies();
|
||||||
const lang = cookieStore.get('locale')?.value || 'en';
|
|
||||||
|
|
||||||
let project = projects.find((project) => project.id === projectId);
|
let project = projects.find((project) => project.id === projectId);
|
||||||
if (!project) {
|
if (!project) {
|
||||||
return NextResponse.json({ error: 'Not found' }, { status: 404 });
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lang === 'fa') {
|
|
||||||
project = {
|
project = {
|
||||||
...project,
|
...project,
|
||||||
title: project.fa_title || project.title,
|
title: localizeText(locale, project.title, project.fa_title),
|
||||||
content: project.fa_content || project.content,
|
content: localizeContent(locale, project.content, project.fa_content),
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
return NextResponse.json({
|
return ok(project);
|
||||||
data: project,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import images from '@/assets/images/images';
|
|
||||||
import { tempContent } from '@/assets/temp/tempContent';
|
import { tempContent } from '@/assets/temp/tempContent';
|
||||||
import { IDynamicContent } from '@/models/dynamicContent';
|
import { IDynamicContent } from '@/models/dynamicContent';
|
||||||
|
|
||||||
@@ -38,7 +37,7 @@ export const projects = [
|
|||||||
{
|
{
|
||||||
id: '3',
|
id: '3',
|
||||||
title: 'Top Benefits of the Robotics in Manufacturing',
|
title: 'Top Benefits of the Robotics in Manufacturing',
|
||||||
fa_title: 'مزایای اصلی استفاده از رباتیک در تولید',
|
fa_title: 'مزایای اصلی استفاده از در تولید',
|
||||||
imageSrc: `/images/project3.jpeg`,
|
imageSrc: `/images/project3.jpeg`,
|
||||||
...tempContent,
|
...tempContent,
|
||||||
},
|
},
|
||||||
@@ -63,13 +62,18 @@ export const projects = [
|
|||||||
imageSrc: `/images/project1.jpeg`,
|
imageSrc: `/images/project1.jpeg`,
|
||||||
...tempContent,
|
...tempContent,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '7',
|
||||||
|
title: 'The Advantages of Customized Manufacturing Solutions',
|
||||||
|
fa_title: 'پروژهی تست',
|
||||||
|
imageSrc: `/images/project1.jpeg`,
|
||||||
|
...tempContent,
|
||||||
|
},
|
||||||
] as IProjectData[];
|
] as IProjectData[];
|
||||||
|
|
||||||
export function generateProjects(): IProjectData[] {
|
export function generateProjects(): IProjectData[] {
|
||||||
return [...projects, ...projects, ...projects, ...projects, ...projects].map(
|
return [...projects].map((e, i) => ({
|
||||||
(e, i) => ({
|
|
||||||
...e,
|
...e,
|
||||||
id: String(i + 1),
|
id: String(i + 1),
|
||||||
}),
|
}));
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,47 +1,30 @@
|
|||||||
import { NextResponse } from 'next/server';
|
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
import {
|
import {
|
||||||
generateProjects,
|
generateProjects,
|
||||||
IProjectData,
|
IProjectData,
|
||||||
IProjectThumbResponseData,
|
IProjectThumbResponseData,
|
||||||
} from './data';
|
} from './data';
|
||||||
|
import { getLocaleFromCookies } from '@/lib/locale';
|
||||||
|
import { localizeText } from '@/lib/domain';
|
||||||
|
import { buildPagination, getPaginationParams } from '@/lib/pagination';
|
||||||
|
import { okPaginated } from '@/lib/api-response';
|
||||||
|
|
||||||
export async function GET(request: Request) {
|
export async function GET(request: Request) {
|
||||||
let projects = generateProjects();
|
let projects = generateProjects();
|
||||||
let data = projects as IProjectThumbResponseData[];
|
let data = projects as IProjectThumbResponseData[];
|
||||||
|
|
||||||
const cookieStore = await cookies();
|
const locale = await getLocaleFromCookies();
|
||||||
const lang = (await cookieStore).get('lang')?.value || 'fa';
|
const { page, perPage, start, end } = getPaginationParams(request, 6);
|
||||||
|
|
||||||
// Pagination logic
|
|
||||||
const { searchParams } = new URL(request.url);
|
|
||||||
|
|
||||||
const page = parseInt(searchParams.get('page') || '1', 10);
|
|
||||||
const perPage = 6;
|
|
||||||
|
|
||||||
const start = (page - 1) * perPage;
|
|
||||||
const end = start + perPage;
|
|
||||||
const paginatedProjects = projects.slice(start, end);
|
const paginatedProjects = projects.slice(start, end);
|
||||||
|
|
||||||
const setRelatedLocaleData = (project: IProjectData) => ({
|
const setRelatedLocaleData = (project: IProjectData) => ({
|
||||||
title: project.fa_title || project.title,
|
title: localizeText(locale, project.title, project.fa_title),
|
||||||
});
|
});
|
||||||
|
|
||||||
data = paginatedProjects.map((project) => ({
|
data = paginatedProjects.map((project) => ({
|
||||||
id: project.id,
|
id: project.id,
|
||||||
imageSrc: project.imageSrc,
|
imageSrc: project.imageSrc,
|
||||||
...(lang === 'fa'
|
...setRelatedLocaleData(project),
|
||||||
? setRelatedLocaleData(project)
|
|
||||||
: { title: project.title }),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return NextResponse.json({
|
return okPaginated(data, buildPagination(page, perPage, projects.length));
|
||||||
data,
|
|
||||||
pagination: {
|
|
||||||
page,
|
|
||||||
perPage,
|
|
||||||
total: projects.length,
|
|
||||||
totalPages: Math.ceil(projects.length / perPage),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 980 B |
@@ -13,6 +13,12 @@ import product1 from './product1.png';
|
|||||||
import notFound from './404.png';
|
import notFound from './404.png';
|
||||||
import langEn from './lang_en.png';
|
import langEn from './lang_en.png';
|
||||||
import langFa from './lang_fa.png';
|
import langFa from './lang_fa.png';
|
||||||
|
import en from './en_square.png';
|
||||||
|
import fa from './ir_square.png';
|
||||||
|
import certificateIso9001 from './certificates/iso9001.jpg';
|
||||||
|
import certificateIso14001 from './certificates/iso14001.jpg';
|
||||||
|
import certificateStandard from './certificates/standard.jpg';
|
||||||
|
import certificateLia from './certificates/lia.jpg';
|
||||||
export default {
|
export default {
|
||||||
homeBanner,
|
homeBanner,
|
||||||
logo,
|
logo,
|
||||||
@@ -27,6 +33,11 @@ export default {
|
|||||||
homeProductBack,
|
homeProductBack,
|
||||||
whatWeDoImage,
|
whatWeDoImage,
|
||||||
product1,
|
product1,
|
||||||
|
fa,en,
|
||||||
langEn,
|
langEn,
|
||||||
langFa,
|
langFa,
|
||||||
|
certificateIso9001,
|
||||||
|
certificateIso14001,
|
||||||
|
certificateStandard,
|
||||||
|
certificateLia,
|
||||||
};
|
};
|
||||||
|
|||||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -39,6 +39,27 @@ export const tempContent = {
|
|||||||
'مزایای راهحلهای سفارشیسازی در تولید',
|
'مزایای راهحلهای سفارشیسازی در تولید',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'quote',
|
||||||
|
data: {
|
||||||
|
text: 'پایداری با بهینهسازی مداوم فرآیندها ممکن میشود.',
|
||||||
|
author: 'تیم پاسارگاد',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subtitle',
|
||||||
|
data: 'پروژههای بیشتر',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'cta',
|
||||||
|
data: {
|
||||||
|
label: 'رفتن به پروژهها',
|
||||||
|
link: '/projects',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
fa_content: [
|
fa_content: [
|
||||||
{
|
{
|
||||||
@@ -93,5 +114,26 @@ export const tempContent = {
|
|||||||
'مزایای راهحلهای سفارشیسازی در تولید',
|
'مزایای راهحلهای سفارشیسازی در تولید',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
type: 'quote',
|
||||||
|
data: {
|
||||||
|
text: 'پایداری با بهینهسازی مداوم فرآیندها ممکن میشود.',
|
||||||
|
author: 'تیم پاسارگاد',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'divider',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'subtitle',
|
||||||
|
data: 'پروژههای بیشتر',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'cta',
|
||||||
|
data: {
|
||||||
|
label: 'رفتن به پروژهها',
|
||||||
|
link: '/projects',
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import images from '@/assets/images/images';
|
||||||
|
import Gallery from '@/components/uikit/gallery';
|
||||||
|
import { GalleryItem } from '@/components/uikit/gallery/gallery';
|
||||||
|
import { isRtl } from '@/config';
|
||||||
|
import { TLocales } from '@/models/layout';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const certificates: GalleryItem[] = [
|
||||||
|
{ src: images.certificateIso9001, alt: 'ISO 9001' },
|
||||||
|
{ src: images.certificateIso14001, alt: 'ISO 14001' },
|
||||||
|
{ src: images.certificateStandard, alt: 'Standard' },
|
||||||
|
{ src: images.certificateLia, alt: 'LIA' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function CertificateGallery() {
|
||||||
|
const params = useParams();
|
||||||
|
const locale = params.locale as TLocales;
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-wrap items-center justify-center gap-3">
|
||||||
|
{certificates.map((cert, i) => (
|
||||||
|
<button
|
||||||
|
key={cert.alt}
|
||||||
|
onClick={() => setSelectedIndex(i)}
|
||||||
|
className="group relative h-16 w-16 cursor-pointer overflow-hidden rounded-md border border-white/20 transition-transform hover:scale-105"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={cert.src}
|
||||||
|
alt={cert.alt}
|
||||||
|
fill
|
||||||
|
className="object-contain p-1"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedIndex !== null && (
|
||||||
|
<Gallery
|
||||||
|
items={certificates}
|
||||||
|
initialIndex={selectedIndex}
|
||||||
|
onClose={() => setSelectedIndex(null)}
|
||||||
|
rtl={isRtl(locale)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
|
IContentCTA,
|
||||||
IContentData,
|
IContentData,
|
||||||
IContentDataLink,
|
IContentDataLink,
|
||||||
IContentImage,
|
IContentImage,
|
||||||
|
IContentQuote,
|
||||||
IDynamicContent,
|
IDynamicContent,
|
||||||
} from '@/models/dynamicContent';
|
} from '@/models/dynamicContent';
|
||||||
|
|
||||||
function ImageContent(data: IContentImage) {
|
function imageContent(data: IContentImage) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center gap-3">
|
<div className="flex flex-col items-center gap-3">
|
||||||
<img
|
<img
|
||||||
@@ -26,12 +28,23 @@ function titleContent(data: string) {
|
|||||||
return <h2 className="text-2xl font-bold text-gray-500">{data}</h2>;
|
return <h2 className="text-2xl font-bold text-gray-500">{data}</h2>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function fullContent(data: IContentData) {
|
function subtitleContent(data: string) {
|
||||||
const splittedData = data.data?.split('\n') || [];
|
return <h3 className="text-xl font-semibold text-gray-500">{data}</h3>;
|
||||||
return splittedData.map((e) => {
|
}
|
||||||
|
|
||||||
|
function fullContent(data: IContentData | string) {
|
||||||
|
const normalizedData =
|
||||||
|
typeof data === 'string'
|
||||||
|
? ({
|
||||||
|
data,
|
||||||
|
} as IContentData)
|
||||||
|
: data;
|
||||||
|
|
||||||
|
const splittedData = normalizedData.data?.split('\n') || [];
|
||||||
|
return splittedData.map((e, index) => {
|
||||||
return (
|
return (
|
||||||
<p className="indent-5 text-base text-gray-400">
|
<p key={index} className="indent-5 text-base text-gray-400">
|
||||||
{generateLinkedContent(e, data.links)}
|
{generateLinkedContent(e, normalizedData.links)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -80,6 +93,34 @@ function listContent(data: string[], type: 'bullet' | 'numberList') {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function quoteContent(data: IContentQuote) {
|
||||||
|
return (
|
||||||
|
<blockquote className="rounded-xl border-l-4 border-primary bg-gray-100 px-4 py-3 text-gray-500">
|
||||||
|
<p className="text-base italic">{data.text}</p>
|
||||||
|
{data.author ? (
|
||||||
|
<cite className="mt-2 block text-sm text-gray-400">— {data.author}</cite>
|
||||||
|
) : null}
|
||||||
|
</blockquote>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ctaContent(data: IContentCTA) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={data.link}
|
||||||
|
target={data.target || '_self'}
|
||||||
|
rel={data.target === '_blank' ? 'noopener noreferrer' : undefined}
|
||||||
|
className="bg-primary inline-flex w-fit rounded-lg px-4 py-2 text-sm text-white"
|
||||||
|
>
|
||||||
|
{data.label}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function dividerContent() {
|
||||||
|
return <hr className="my-2 border-gray-200" />;
|
||||||
|
}
|
||||||
|
|
||||||
export default function ContentGenerator({
|
export default function ContentGenerator({
|
||||||
data,
|
data,
|
||||||
className,
|
className,
|
||||||
@@ -89,17 +130,27 @@ export default function ContentGenerator({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className={`flex flex-col gap-3 md:gap-6 ${className}`}>
|
<div className={`flex flex-col gap-3 md:gap-6 ${className}`}>
|
||||||
{data.map((e) => {
|
{data.map((e, index) => {
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case 'title':
|
case 'title':
|
||||||
return titleContent(e.data as string);
|
return <div key={index}>{titleContent(e.data)}</div>;
|
||||||
|
case 'subtitle':
|
||||||
|
return <div key={index}>{subtitleContent(e.data)}</div>;
|
||||||
case 'content':
|
case 'content':
|
||||||
return <div>{fullContent(e.data as IContentData)}</div>;
|
return <div key={index}>{fullContent(e.data)}</div>;
|
||||||
case 'image':
|
case 'image':
|
||||||
return ImageContent(e.data as IContentImage);
|
return <div key={index}>{imageContent(e.data)}</div>;
|
||||||
case 'bullet':
|
case 'bullet':
|
||||||
case 'numberList':
|
case 'numberList':
|
||||||
return listContent(e.data as string[], e.type);
|
return <div key={index}>{listContent(e.data, e.type)}</div>;
|
||||||
|
case 'quote':
|
||||||
|
return <div key={index}>{quoteContent(e.data)}</div>;
|
||||||
|
case 'cta':
|
||||||
|
return <div key={index}>{ctaContent(e.data)}</div>;
|
||||||
|
case 'divider':
|
||||||
|
return <div key={index}>{dividerContent()}</div>;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import images from '@/assets/images/images';
|
import { appConstants } from '@/assets/constants';
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import FooterMenuBuilder from './FooterMenuBuilder';
|
|
||||||
import FooterMenuBuilderWrapper from './FooterMenuBuilderWrapper';
|
|
||||||
import FooterInfo from './FooterInfo';
|
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import { useParams } from 'next/navigation';
|
|
||||||
import { getQuickLinks } from '@/assets/constants/footerMenu/quickLinks.const';
|
import { getQuickLinks } from '@/assets/constants/footerMenu/quickLinks.const';
|
||||||
import { getWhoAreWeLinks } from '@/assets/constants/footerMenu/whoAreWe.const';
|
import { getWhoAreWeLinks } from '@/assets/constants/footerMenu/whoAreWe.const';
|
||||||
|
import images from '@/assets/images/images';
|
||||||
|
import { Icon, IconName } from '@/components/uikit/icons';
|
||||||
import { TLocales } from '@/models/layout';
|
import { TLocales } from '@/models/layout';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import FooterInfo from './FooterInfo';
|
||||||
|
import FooterMenuBuilder from './FooterMenuBuilder';
|
||||||
|
import FooterMenuBuilderWrapper from './FooterMenuBuilderWrapper';
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
@@ -41,7 +43,7 @@ export default function Footer() {
|
|||||||
</h5>
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-start justify-between gap-8 border-y border-gray-300/50 py-14 max-lg:flex-col-reverse">
|
<div className="flex items-start justify-between gap-8 border-y border-gray-300/50 py-14 max-lg:flex-col-reverse">
|
||||||
<FooterInfo withLogo className="lg:max-w-[200px]" />
|
<FooterInfo withLogo className="max-lg:mt-3 lg:max-w-[200px]" />
|
||||||
<div className="grid w-full grid-cols-2 gap-5 md:grid-cols-3 md:gap-10 lg:max-w-3/5">
|
<div className="grid w-full grid-cols-2 gap-5 md:grid-cols-3 md:gap-10 lg:max-w-3/5">
|
||||||
<FooterMenuBuilder
|
<FooterMenuBuilder
|
||||||
title={t('footer_links_quick_links')}
|
title={t('footer_links_quick_links')}
|
||||||
@@ -74,6 +76,21 @@ export default function Footer() {
|
|||||||
{t('footer_links_address')}
|
{t('footer_links_address')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4 max-lg:justify-center">
|
||||||
|
{appConstants.socials.map((social) => (
|
||||||
|
<Link
|
||||||
|
href={social.link}
|
||||||
|
key={social.title}
|
||||||
|
className="text-white lg:flex-1"
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name={social.icon as IconName}
|
||||||
|
className="text-primary"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</FooterMenuBuilderWrapper>
|
</FooterMenuBuilderWrapper>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { appConstants } from '@/assets/constants';
|
|
||||||
import images from '@/assets/images/images';
|
import images from '@/assets/images/images';
|
||||||
import { Icon, IconName } from '@/components/uikit/icons';
|
import CertificateGallery from '@/components/layout/certificateGallery';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useTranslations } from 'next-intl';
|
|
||||||
import LanguageSwitch from '../languageSwitch';
|
import LanguageSwitch from '../languageSwitch';
|
||||||
|
|
||||||
export default function FooterInfo({
|
export default function FooterInfo({
|
||||||
@@ -32,18 +31,9 @@ export default function FooterInfo({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<p className="text-sm tracking-normal">{t('footer_slogan')}</p>
|
<p className="text-sm tracking-normal">{t('footer_slogan')}</p>
|
||||||
<div className="flex items-center gap-4 max-lg:justify-center">
|
|
||||||
{appConstants.socials.map((social) => (
|
{withLogo && <CertificateGallery />}
|
||||||
<Link
|
{!withLogo && <LanguageSwitch />}
|
||||||
href={social.link}
|
|
||||||
key={social.title}
|
|
||||||
className="text-white lg:flex-1"
|
|
||||||
>
|
|
||||||
<Icon name={social.icon as IconName} className="text-primary" />
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<LanguageSwitch />
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import Link from 'next/link';
|
import routeFactory from '@/assets/constants/routeFactory';
|
||||||
import Image from 'next/image';
|
|
||||||
import images from '@/assets/images/images';
|
import images from '@/assets/images/images';
|
||||||
import { usePathname, useParams } from 'next/navigation';
|
|
||||||
import { Icon } from '@/components/uikit/icons';
|
import { Icon } from '@/components/uikit/icons';
|
||||||
import { useTranslations } from 'next-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useParams, usePathname } from 'next/navigation';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import FooterInfo from '../footer/FooterInfo';
|
import FooterInfo from '../footer/FooterInfo';
|
||||||
import routeFactory from '@/assets/constants/routeFactory';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -66,7 +66,9 @@ export default function HamburgerMenu({ isOpen, onClose }: Props) {
|
|||||||
|
|
||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-x-0 top-0 z-50 flex h-screen">
|
<div
|
||||||
|
className={`fixed inset-x-0 start-0 top-0 z-50 flex h-screen justify-end`}
|
||||||
|
>
|
||||||
<div className="absolute inset-0 bg-black/60" />
|
<div className="absolute inset-0 bg-black/60" />
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { TLocales } from '@/models/layout';
|
||||||
|
import images from '@/assets/images/images';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{
|
||||||
|
title: 'EN',
|
||||||
|
image: images.en,
|
||||||
|
locale: 'en',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'FA',
|
||||||
|
image: images.fa,
|
||||||
|
locale: 'fa',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function ModalLanguageSelector() {
|
||||||
|
const [show, setShow] = useState(false);
|
||||||
|
const t = useTranslations();
|
||||||
|
useEffect(() => {
|
||||||
|
const selected = localStorage.getItem('selectedLocale');
|
||||||
|
const cookieLocale = document.cookie
|
||||||
|
.split('; ')
|
||||||
|
.find((item) => item.startsWith('locale='))
|
||||||
|
?.split('=')[1];
|
||||||
|
|
||||||
|
if (!selected && !cookieLocale) setShow(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const changeLocale = (locale: TLocales) => {
|
||||||
|
localStorage.setItem('selectedLocale', locale);
|
||||||
|
document.cookie = `locale=${locale}; path=/; max-age=31536000; samesite=lax`;
|
||||||
|
const path = window.location.pathname.replace(/^\/(en|fa)/, '');
|
||||||
|
window.location.pathname = `/${locale}${path}`;
|
||||||
|
};
|
||||||
|
const closeModal = () => setShow(false);
|
||||||
|
|
||||||
|
if (!show) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-100 flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
||||||
|
onClick={closeModal}
|
||||||
|
>
|
||||||
|
<div className="relative h-auto w-100 rounded-3xl bg-white p-6 shadow-2xl">
|
||||||
|
<div className="flex justify-around gap-4">
|
||||||
|
{languages.map((lang) => (
|
||||||
|
<div
|
||||||
|
key={lang.locale}
|
||||||
|
onClick={() => changeLocale(lang.locale as TLocales)}
|
||||||
|
className="flex w-32 flex-col items-center justify-between gap-2 rounded-xl border border-gray-300 bg-gray-50 px-4 py-3 text-sm font-medium text-gray-700 shadow-sm transition-all hover:border-blue-400 hover:bg-blue-100"
|
||||||
|
>
|
||||||
|
<span className="text-2xl">
|
||||||
|
<Image src={lang.image} alt={lang.title} className="w-100" />
|
||||||
|
</span>
|
||||||
|
{lang.title}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,28 +2,22 @@
|
|||||||
|
|
||||||
import images from '@/assets/images/images';
|
import images from '@/assets/images/images';
|
||||||
import { TLocales } from '@/models/layout';
|
import { TLocales } from '@/models/layout';
|
||||||
import Image from 'next/image';
|
import Select from '@/components/uikit/select';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
const languages = [
|
||||||
|
{ value: 'en', label: 'English', image: images.en },
|
||||||
|
{ value: 'fa', label: 'فارسی', image: images.fa },
|
||||||
|
];
|
||||||
|
|
||||||
export default function LanguageSwitch() {
|
export default function LanguageSwitch() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const currentLocale = pathname.split('/')[1] as TLocales;
|
||||||
|
|
||||||
const languages = [
|
const changeLocale = (locale: string) => {
|
||||||
{
|
|
||||||
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;
|
if (currentLocale === locale) return;
|
||||||
|
|
||||||
|
document.cookie = `locale=${locale}; path=/; max-age=31536000; samesite=lax`;
|
||||||
let newPath = pathname.replace(/^\/(en|fa)/, '');
|
let newPath = pathname.replace(/^\/(en|fa)/, '');
|
||||||
if (newPath === '/') newPath = '';
|
if (newPath === '/') newPath = '';
|
||||||
|
|
||||||
@@ -31,19 +25,11 @@ export default function LanguageSwitch() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center gap-3">
|
<Select
|
||||||
{languages.map((language) => (
|
options={languages}
|
||||||
<div
|
value={currentLocale}
|
||||||
className="flex cursor-pointer flex-col items-center justify-center gap-2 px-1"
|
onChange={changeLocale}
|
||||||
onClick={() => changeLocale(language.locale as TLocales)}
|
size="small"
|
||||||
key={language.title}
|
/>
|
||||||
>
|
|
||||||
<Image src={language.image} alt={language.title} />
|
|
||||||
<span className="text-xs font-normal text-white">
|
|
||||||
{language.title}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
const ContactMap = dynamic(() => import('./index'), { ssr: false });
|
||||||
|
|
||||||
|
export default ContactMap;
|
||||||
@@ -1,14 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import routeFactory from '@/assets/constants/routeFactory';
|
||||||
import images from '@/assets/images/images';
|
import images from '@/assets/images/images';
|
||||||
import { Icon } from '@/components/uikit/icons';
|
import { Icon } from '@/components/uikit/icons';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname, useParams } from 'next/navigation';
|
import { useParams, usePathname } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import HamburgerMenu from '../hamburgerMenu';
|
import HamburgerMenu from '../hamburgerMenu';
|
||||||
import { useTranslations } from 'next-intl';
|
import LanguageSwitch from '../languageSwitch';
|
||||||
import routeFactory from '@/assets/constants/routeFactory';
|
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
@@ -50,7 +51,7 @@ export default function Navbar() {
|
|||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<ul className="hidden space-x-6 text-sm lg:flex lg:text-base">
|
<ul className="hidden items-center space-x-6 text-sm lg:flex lg:text-base">
|
||||||
{links.map(({ route, title }) => (
|
{links.map(({ route, title }) => (
|
||||||
<li key={route()}>
|
<li key={route()}>
|
||||||
<Link
|
<Link
|
||||||
@@ -61,12 +62,17 @@ export default function Navbar() {
|
|||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
<div className="h-4 w-px bg-white opacity-35"></div>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a className="flex items-center gap-2" href="tel:+098123456789">
|
<a className="flex items-center gap-2" href="tel:+098123456789">
|
||||||
<Icon name="phone" className="text-primary" />
|
<Icon name="phone" className="text-primary" />
|
||||||
<p>+098 123456789</p>
|
<p>+098 123456789</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<LanguageSwitch />
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{!open && (
|
{!open && (
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
count?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SliderThumbSkeleton({ count = 3 }: Props) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-4 overflow-hidden">
|
||||||
|
{Array.from({ length: count }).map((_, i) => (
|
||||||
|
<div
|
||||||
|
key={i}
|
||||||
|
className="flex w-full animate-pulse flex-col gap-4 overflow-hidden rounded-xl bg-white"
|
||||||
|
>
|
||||||
|
<div className="aspect-[1.09] w-full bg-gray-200" />
|
||||||
|
<div className="mx-auto mb-4 h-4 w-3/4 rounded bg-gray-200" />
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import images from '@/assets/images/images';
|
||||||
|
import SectionTitle from '@/components/layout/sectionTitle';
|
||||||
|
import Gallery from '@/components/uikit/gallery';
|
||||||
|
import { GalleryItem } from '@/components/uikit/gallery/gallery';
|
||||||
|
import { isRtl } from '@/config';
|
||||||
|
import { TLocales } from '@/models/layout';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
const certifications: GalleryItem[] = [
|
||||||
|
{ src: images.certificateIso9001, alt: 'ISO 9001', title: 'ISO 9001' },
|
||||||
|
{ src: images.certificateIso14001, alt: 'ISO 14001', title: 'ISO 14001' },
|
||||||
|
{ src: images.certificateStandard, alt: 'Standard', title: 'Standard' },
|
||||||
|
{ src: images.certificateLia, alt: 'LIA', title: 'LIA' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function CertificationsSection({
|
||||||
|
locale,
|
||||||
|
className = '',
|
||||||
|
}: {
|
||||||
|
locale: TLocales;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const t = useTranslations();
|
||||||
|
const [selectedIndex, setSelectedIndex] = useState<number | null>(null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`w-full py-20 ${className}`}>
|
||||||
|
<div className="container mx-auto">
|
||||||
|
<SectionTitle
|
||||||
|
title={t('com_about_certifications_title')}
|
||||||
|
description={t('com_about_certifications_title_bold')}
|
||||||
|
locale={locale}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<p className="mt-4 max-w-2xl text-base font-light text-gray-300">
|
||||||
|
{t('com_about_certifications_description')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-10 grid grid-cols-2 gap-6 sm:grid-cols-4">
|
||||||
|
{certifications.map((cert, i) => (
|
||||||
|
<button
|
||||||
|
key={cert.alt}
|
||||||
|
onClick={() => setSelectedIndex(i)}
|
||||||
|
className="group cursor-pointer"
|
||||||
|
>
|
||||||
|
<div className="relative aspect-[3/4] overflow-hidden rounded-xl border border-gray-100 bg-white transition-all group-hover:shadow-lg">
|
||||||
|
<Image
|
||||||
|
src={cert.src}
|
||||||
|
alt={cert.alt}
|
||||||
|
fill
|
||||||
|
className="object-contain p-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{cert.title && (
|
||||||
|
<p className="mt-3 text-center text-sm font-medium text-gray-500">
|
||||||
|
{cert.title}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{selectedIndex !== null && (
|
||||||
|
<Gallery
|
||||||
|
items={certifications}
|
||||||
|
initialIndex={selectedIndex}
|
||||||
|
onClose={() => setSelectedIndex(null)}
|
||||||
|
rtl={isRtl(locale)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import Button from '@/components/uikit/button';
|
|||||||
import { TLocales } from '@/models/layout';
|
import { TLocales } from '@/models/layout';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useTranslations } from 'use-intl';
|
import { useTranslations } from 'next-intl';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
post: IPostThumbResponseData;
|
post: IPostThumbResponseData;
|
||||||
@@ -17,7 +17,7 @@ const PostThumb = ({ post, locale }: Props) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={post.id}
|
key={post.id}
|
||||||
className="group flex flex-col gap-3 overflow-hidden bg-white md:gap-5"
|
className="group flex w-full flex-col gap-3 overflow-hidden bg-white md:gap-5"
|
||||||
>
|
>
|
||||||
<div className="relative aspect-[1.09] w-full overflow-hidden rounded-2xl">
|
<div className="relative aspect-[1.09] w-full overflow-hidden rounded-2xl">
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -1,14 +1,58 @@
|
|||||||
|
// 'use client';
|
||||||
|
|
||||||
|
// import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
|
// import { FreeMode } from 'swiper/modules';
|
||||||
|
// import 'swiper/css';
|
||||||
|
// import 'swiper/css/free-mode';
|
||||||
|
// import { IPostThumbResponseData } from '@/app/api/blog/data';
|
||||||
|
// import { useTranslations } from 'use-intl';
|
||||||
|
// import routeFactory from '@/assets/constants/routeFactory';
|
||||||
|
// import { TLocales } from '@/models/layout';
|
||||||
|
// import PostThumb from './PostThumb';
|
||||||
|
|
||||||
|
// interface Props {
|
||||||
|
// posts: IPostThumbResponseData[];
|
||||||
|
// locale: TLocales;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default function PostsGridViewSlider({ posts, locale }: Props) {
|
||||||
|
// const t = useTranslations();
|
||||||
|
// const routes = routeFactory(t, locale);
|
||||||
|
// return (
|
||||||
|
// <div className="w-full">
|
||||||
|
// <Swiper
|
||||||
|
// modules={[FreeMode]}
|
||||||
|
// freeMode
|
||||||
|
// spaceBetween={16}
|
||||||
|
// breakpoints={{
|
||||||
|
// 0: { slidesPerView: 1.1 },
|
||||||
|
// 768: { slidesPerView: Math.min(posts.length, 3), spaceBetween: 24 },
|
||||||
|
// }}
|
||||||
|
// className="w-full"
|
||||||
|
// >
|
||||||
|
// {posts.map((post) => (
|
||||||
|
// <SwiperSlide key={post.id} className="w-auto">
|
||||||
|
// <PostThumb post={post} locale={locale} />
|
||||||
|
// </SwiperSlide>
|
||||||
|
// ))}
|
||||||
|
// </Swiper>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
import { FreeMode } from 'swiper/modules';
|
import { FreeMode } from 'swiper/modules';
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
import 'swiper/css/free-mode';
|
import 'swiper/css/free-mode';
|
||||||
import { IPostThumbResponseData } from '@/app/api/blog/data';
|
|
||||||
import { useTranslations } from 'use-intl';
|
|
||||||
import routeFactory from '@/assets/constants/routeFactory';
|
|
||||||
import { TLocales } from '@/models/layout';
|
|
||||||
import PostThumb from './PostThumb';
|
import PostThumb from './PostThumb';
|
||||||
|
import { IPostThumbResponseData } from '@/app/api/blog/data';
|
||||||
|
import { TLocales } from '@/models/layout';
|
||||||
|
import { useTranslations } from 'next-intl';
|
||||||
|
import routeFactory from '@/assets/constants/routeFactory';
|
||||||
|
import SliderThumbSkeleton from '@/components/layout/sliderSkeleton';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
posts: IPostThumbResponseData[];
|
posts: IPostThumbResponseData[];
|
||||||
@@ -16,8 +60,18 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function PostsGridViewSlider({ posts, locale }: Props) {
|
export default function PostsGridViewSlider({ posts, locale }: Props) {
|
||||||
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const routes = routeFactory(t, locale);
|
const routes = routeFactory(t, locale);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!hasMounted) {
|
||||||
|
return <SliderThumbSkeleton count={3} />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Swiper
|
<Swiper
|
||||||
@@ -26,12 +80,12 @@ export default function PostsGridViewSlider({ posts, locale }: Props) {
|
|||||||
spaceBetween={16}
|
spaceBetween={16}
|
||||||
breakpoints={{
|
breakpoints={{
|
||||||
0: { slidesPerView: 1.1 },
|
0: { slidesPerView: 1.1 },
|
||||||
768: { slidesPerView: posts.length, spaceBetween: 24 },
|
768: { slidesPerView: Math.min(posts.length, 3), spaceBetween: 24 },
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{posts.map((post) => (
|
{posts.map((post) => (
|
||||||
<SwiperSlide key={post.id}>
|
<SwiperSlide key={post.id} className="w-auto">
|
||||||
<PostThumb post={post} locale={locale} />
|
<PostThumb post={post} locale={locale} />
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -57,6 +57,8 @@ const ContactForm = ({ className = '' }) => {
|
|||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (!validate()) return;
|
||||||
|
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setContact(formData).finally(() => {
|
setContact(formData).finally(() => {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export default async function HomePageBlog({ locale, className }: Props) {
|
|||||||
<h4 className="text-base font-extralight tracking-normal max-lg:hidden">
|
<h4 className="text-base font-extralight tracking-normal max-lg:hidden">
|
||||||
{t('com_home_blog_description')}
|
{t('com_home_blog_description')}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
link={routes.blog.route()}
|
link={routes.blog.route()}
|
||||||
endIcon="showMore"
|
endIcon="showMore"
|
||||||
@@ -37,6 +39,7 @@ export default async function HomePageBlog({ locale, className }: Props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<PostsGridViewSlider
|
<PostsGridViewSlider
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export default async function HomePageProjects({ locale }: Props) {
|
|||||||
<h4 className="text-base font-extralight tracking-normal max-lg:hidden">
|
<h4 className="text-base font-extralight tracking-normal max-lg:hidden">
|
||||||
{t('com_home_projects_description')}
|
{t('com_home_projects_description')}
|
||||||
</h4>
|
</h4>
|
||||||
|
<div className="flex w-full justify-end">
|
||||||
<Button
|
<Button
|
||||||
link={routes.projects.route()}
|
link={routes.projects.route()}
|
||||||
endIcon="showMore"
|
endIcon="showMore"
|
||||||
@@ -38,6 +39,7 @@ export default async function HomePageProjects({ locale }: Props) {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<ProjectsGridSlider
|
<ProjectsGridSlider
|
||||||
projects={projects.data?.slice(0, 3)}
|
projects={projects.data?.slice(0, 3)}
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
import { useState } from 'react';
|
||||||
import type { IProductCardProps } from './productCard.d';
|
import type { IProductCardProps } from './productCard.d';
|
||||||
|
|
||||||
export default function ProductCard({
|
export default function ProductCard({
|
||||||
@@ -5,8 +9,27 @@ export default function ProductCard({
|
|||||||
imageSrc,
|
imageSrc,
|
||||||
size,
|
size,
|
||||||
}: IProductCardProps) {
|
}: IProductCardProps) {
|
||||||
|
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isPreviewOpen) return;
|
||||||
|
|
||||||
|
const handleEsc = (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setIsPreviewOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('keydown', handleEsc);
|
||||||
|
return () => window.removeEventListener('keydown', handleEsc);
|
||||||
|
}, [isPreviewOpen]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="overflow-hidden rounded-xl bg-gray-200 p-4">
|
<>
|
||||||
|
<div
|
||||||
|
className="cursor-pointer overflow-hidden rounded-xl bg-gray-200 p-4"
|
||||||
|
onClick={() => setIsPreviewOpen(true)}
|
||||||
|
>
|
||||||
<div className="aspect-square w-full">
|
<div className="aspect-square w-full">
|
||||||
<img
|
<img
|
||||||
src={imageSrc}
|
src={imageSrc}
|
||||||
@@ -17,5 +40,29 @@ export default function ProductCard({
|
|||||||
<h3 className="mt-4 mb-10 text-xl font-bold text-gray-500">{model}</h3>
|
<h3 className="mt-4 mb-10 text-xl font-bold text-gray-500">{model}</h3>
|
||||||
<span className="text-base font-normal text-gray-300">{size}</span>
|
<span className="text-base font-normal text-gray-300">{size}</span>
|
||||||
</div>
|
</div>
|
||||||
|
{isPreviewOpen ? (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-100 flex items-center justify-center bg-black/70 p-4 transition-opacity duration-200"
|
||||||
|
onClick={() => setIsPreviewOpen(false)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative max-h-[90vh] w-full max-w-3xl scale-100 rounded-2xl bg-white p-4 opacity-100 transition-all duration-200 ease-out"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="absolute top-3 right-3 cursor-pointer rounded-full bg-black/70 px-3 py-1 text-sm text-white"
|
||||||
|
onClick={() => setIsPreviewOpen(false)}
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
<img
|
||||||
|
src={imageSrc}
|
||||||
|
alt={model}
|
||||||
|
className="max-h-[80vh] w-full rounded-xl object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Button from '@/components/uikit/button';
|
import Button from '@/components/uikit/button';
|
||||||
@@ -11,12 +13,13 @@ interface Props {
|
|||||||
locale: TLocales;
|
locale: TLocales;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectThumb = ({ project, locale }: Props) => {
|
export default function ProjectThumb({ project, locale }: Props) {
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const routes = routeFactory(t, locale);
|
const routes = routeFactory(t, locale);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="group flex flex-col gap-3 overflow-hidden bg-white md:gap-5">
|
<div className="group flex flex-col gap-3 overflow-hidden bg-white md:gap-5">
|
||||||
<div className="relative aspect-[1.4] w-full overflow-hidden rounded-2xl">
|
<div className="relative aspect-[1.09] w-full overflow-hidden rounded-2xl">
|
||||||
<Image
|
<Image
|
||||||
src={project.imageSrc}
|
src={project.imageSrc}
|
||||||
alt={project.title}
|
alt={project.title}
|
||||||
@@ -24,7 +27,7 @@ const ProjectThumb = ({ project, locale }: Props) => {
|
|||||||
className="h-auto w-full object-cover"
|
className="h-auto w-full object-cover"
|
||||||
/>
|
/>
|
||||||
<div className="invisible absolute inset-0 flex items-center justify-center bg-black/20 opacity-0 backdrop-blur-xs transition-all group-hover:visible group-hover:opacity-100">
|
<div className="invisible absolute inset-0 flex items-center justify-center bg-black/20 opacity-0 backdrop-blur-xs transition-all group-hover:visible group-hover:opacity-100">
|
||||||
<Button endIcon="showMore" link={routes.project.route(project.id)}>
|
<Button link={routes.project.route(project.id)} endIcon="showMore">
|
||||||
{t('com_home_projects_details_CTA')}
|
{t('com_home_projects_details_CTA')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -38,6 +41,4 @@ const ProjectThumb = ({ project, locale }: Props) => {
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
|
||||||
export default ProjectThumb;
|
|
||||||
|
|||||||
@@ -1,22 +1,36 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||||
import { FreeMode } from 'swiper/modules';
|
import { FreeMode } from 'swiper/modules';
|
||||||
import 'swiper/css';
|
import 'swiper/css';
|
||||||
import 'swiper/css/free-mode';
|
import 'swiper/css/free-mode';
|
||||||
|
|
||||||
import ProjectThumb from './ProjectThumb';
|
import ProjectThumb from './ProjectThumb';
|
||||||
import { TLocales } from '@/models/layout';
|
|
||||||
import { IProjectThumbResponseData } from '@/app/api/projects/data';
|
import { IProjectThumbResponseData } from '@/app/api/projects/data';
|
||||||
|
import { TLocales } from '@/models/layout';
|
||||||
|
import SliderThumbSkeleton from '@/components/layout/sliderSkeleton';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
projects: IProjectThumbResponseData[];
|
||||||
|
locale: TLocales;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function ProjectsGridSlider({
|
export default function ProjectsGridSlider({
|
||||||
projects,
|
projects,
|
||||||
locale,
|
locale,
|
||||||
className = '',
|
className = '',
|
||||||
}: {
|
}: Props) {
|
||||||
projects: IProjectThumbResponseData[];
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
locale: TLocales;
|
|
||||||
className?: string;
|
useEffect(() => {
|
||||||
}) {
|
setHasMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!hasMounted) return <SliderThumbSkeleton count={3} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<Swiper
|
<Swiper
|
||||||
@@ -25,12 +39,15 @@ export default function ProjectsGridSlider({
|
|||||||
spaceBetween={16}
|
spaceBetween={16}
|
||||||
breakpoints={{
|
breakpoints={{
|
||||||
0: { slidesPerView: 1.1 },
|
0: { slidesPerView: 1.1 },
|
||||||
768: { slidesPerView: projects.length, spaceBetween: 24 },
|
768: {
|
||||||
|
slidesPerView: Math.min(projects.length, 3),
|
||||||
|
spaceBetween: 24,
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
>
|
>
|
||||||
{projects.map((project) => (
|
{projects.map((project) => (
|
||||||
<SwiperSlide key={project.id}>
|
<SwiperSlide key={project.id} className="w-auto">
|
||||||
<ProjectThumb project={project} locale={locale} />
|
<ProjectThumb project={project} locale={locale} />
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { StaticImageData } from 'next/image';
|
||||||
|
|
||||||
|
export interface GalleryItem {
|
||||||
|
src: string | StaticImageData;
|
||||||
|
alt: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GalleryProps {
|
||||||
|
items: GalleryItem[];
|
||||||
|
initialIndex?: number;
|
||||||
|
onClose: () => void;
|
||||||
|
rtl?: boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { GalleryProps } from './gallery';
|
||||||
|
|
||||||
|
export default function Gallery({
|
||||||
|
items,
|
||||||
|
initialIndex = 0,
|
||||||
|
onClose,
|
||||||
|
rtl = false,
|
||||||
|
}: GalleryProps) {
|
||||||
|
const [currentIndex, setCurrentIndex] = useState(initialIndex);
|
||||||
|
const [direction, setDirection] = useState<'next' | 'prev'>('next');
|
||||||
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
const timeoutRef = useRef<NodeJS.Timeout>(null);
|
||||||
|
|
||||||
|
const animate = useCallback(
|
||||||
|
(dir: 'next' | 'prev', newIndex: number) => {
|
||||||
|
if (isAnimating) return;
|
||||||
|
setDirection(dir);
|
||||||
|
setIsAnimating(true);
|
||||||
|
setCurrentIndex(newIndex);
|
||||||
|
timeoutRef.current = setTimeout(() => setIsAnimating(false), 300);
|
||||||
|
},
|
||||||
|
[isAnimating],
|
||||||
|
);
|
||||||
|
|
||||||
|
const goNext = useCallback(() => {
|
||||||
|
animate('next', (currentIndex + 1) % items.length);
|
||||||
|
}, [currentIndex, items.length, animate]);
|
||||||
|
|
||||||
|
const goPrev = useCallback(() => {
|
||||||
|
animate('prev', (currentIndex - 1 + items.length) % items.length);
|
||||||
|
}, [currentIndex, items.length, animate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKey = (e: KeyboardEvent) => {
|
||||||
|
if (rtl) {
|
||||||
|
if (e.key === 'ArrowLeft') goNext();
|
||||||
|
else if (e.key === 'ArrowRight') goPrev();
|
||||||
|
} else {
|
||||||
|
if (e.key === 'ArrowRight') goNext();
|
||||||
|
else if (e.key === 'ArrowLeft') goPrev();
|
||||||
|
}
|
||||||
|
if (e.key === 'Escape') onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKey);
|
||||||
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('keydown', handleKey);
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
};
|
||||||
|
}, [goNext, goPrev, onClose, rtl]);
|
||||||
|
|
||||||
|
const current = items[currentIndex];
|
||||||
|
|
||||||
|
const slideInClass =
|
||||||
|
direction === 'next'
|
||||||
|
? 'animate-[slideInRight_300ms_ease-out]'
|
||||||
|
: 'animate-[slideInLeft_300ms_ease-out]';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 p-4"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="relative flex items-center gap-4"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={goPrev}
|
||||||
|
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-white/20 text-white transition-colors hover:bg-white/40"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={`h-6 w-6 ${rtl ? 'scale-x-[-1]' : ''}`}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div className="relative max-h-[90vh] max-w-[90vw] overflow-hidden">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="absolute top-0 right-0 z-10 flex h-8 w-8 items-center justify-center rounded-full bg-white text-gray-800 shadow-lg transition-colors hover:bg-gray-200"
|
||||||
|
>
|
||||||
|
<svg className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{current.title && (
|
||||||
|
<div className="absolute bottom-4 left-4 z-10 rounded-md bg-black/60 px-3 py-1.5 text-sm text-white">
|
||||||
|
{current.title}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div key={currentIndex} className={slideInClass}>
|
||||||
|
<Image
|
||||||
|
src={current.src}
|
||||||
|
alt={current.alt}
|
||||||
|
className="max-h-[85vh] rounded-lg object-contain"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={goNext}
|
||||||
|
className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-white/20 text-white transition-colors hover:bg-white/40"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
className={`h-6 w-6 ${rtl ? 'scale-x-[-1]' : ''}`}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style jsx global>{`
|
||||||
|
@keyframes slideInRight {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes slideInLeft {
|
||||||
|
from {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import useClickOutside from '@/hooks/useClickOutside';
|
||||||
|
import Image from 'next/image';
|
||||||
|
import React, { useCallback, useRef, useState } from 'react';
|
||||||
|
import { SelectProps, SelectSize } from './select';
|
||||||
|
|
||||||
|
const sizeClasses: Record<
|
||||||
|
SelectSize,
|
||||||
|
{ trigger: string; option: string; icon: number }
|
||||||
|
> = {
|
||||||
|
small: {
|
||||||
|
trigger: 'h-8 px-2 text-xs gap-1.5',
|
||||||
|
option: 'px-2 py-1.5 text-xs gap-1.5',
|
||||||
|
icon: 16,
|
||||||
|
},
|
||||||
|
medium: {
|
||||||
|
trigger: 'h-10 px-3 text-sm gap-2',
|
||||||
|
option: 'px-3 py-2 text-sm gap-2',
|
||||||
|
icon: 20,
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
trigger: 'h-12 px-4 text-base gap-2.5',
|
||||||
|
option: 'px-4 py-2.5 text-base gap-2.5',
|
||||||
|
icon: 24,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const DROPDOWN_MAX_HEIGHT = 240;
|
||||||
|
|
||||||
|
const Select: React.FC<SelectProps> = ({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
size = 'medium',
|
||||||
|
placeholder = 'Select...',
|
||||||
|
className = '',
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const [dropUp, setDropUp] = useState(false);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
|
useClickOutside(ref, () => setOpen(false));
|
||||||
|
|
||||||
|
const selected = options.find((opt) => opt.value === value);
|
||||||
|
const classes = sizeClasses[size];
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
setOpen((prev) => {
|
||||||
|
if (!prev && triggerRef.current) {
|
||||||
|
const rect = triggerRef.current.getBoundingClientRect();
|
||||||
|
const spaceBelow = window.innerHeight - rect.bottom;
|
||||||
|
const spaceAbove = rect.top;
|
||||||
|
setDropUp(
|
||||||
|
spaceBelow < DROPDOWN_MAX_HEIGHT && spaceAbove > spaceBelow,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return !prev;
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref} className={`relative ${className}`}>
|
||||||
|
<button
|
||||||
|
ref={triggerRef}
|
||||||
|
type="button"
|
||||||
|
onClick={toggle}
|
||||||
|
className={`flex w-full items-center justify-between rounded-lg border border-white/20 bg-transparent text-white transition-colors hover:border-white/40 ${classes.trigger}`}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-1 truncate">
|
||||||
|
{selected ? (
|
||||||
|
<>
|
||||||
|
{selected.image && (
|
||||||
|
<Image
|
||||||
|
src={selected.image}
|
||||||
|
alt={selected.label}
|
||||||
|
width={classes.icon}
|
||||||
|
height={classes.icon}
|
||||||
|
className="rounded-sm object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{selected.label}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<span className="text-white/50">{placeholder}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
className={`h-4 w-4 shrink-0 transition-transform ${open ? 'rotate-180' : ''}`}
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fillRule="evenodd"
|
||||||
|
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
||||||
|
clipRule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && (
|
||||||
|
<ul
|
||||||
|
className={`absolute z-50 w-full overflow-y-auto rounded-lg border border-white/20 bg-gray-800 shadow-lg ${dropUp ? 'bottom-full mb-1' : 'top-full mt-1'}`}
|
||||||
|
style={{ maxHeight: DROPDOWN_MAX_HEIGHT }}
|
||||||
|
>
|
||||||
|
{options.map((opt) => (
|
||||||
|
<li
|
||||||
|
key={opt.value}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(opt.value);
|
||||||
|
setOpen(false);
|
||||||
|
}}
|
||||||
|
className={`flex cursor-pointer items-center transition-colors hover:bg-white/10 ${classes.option} ${opt.value === value ? 'bg-white/5' : ''}`}
|
||||||
|
>
|
||||||
|
{opt.image && (
|
||||||
|
<Image
|
||||||
|
src={opt.image}
|
||||||
|
alt={opt.label}
|
||||||
|
width={classes.icon}
|
||||||
|
height={classes.icon}
|
||||||
|
className="rounded-sm object-cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<span>{opt.label}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Select;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { StaticImageData } from 'next/image';
|
||||||
|
|
||||||
|
export interface SelectOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
image?: string | StaticImageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectProps {
|
||||||
|
options: SelectOption[];
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
size?: 'small' | 'medium' | 'large';
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SelectSize = 'small' | 'medium' | 'large';
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
export const port = process.env.PORT || 3000;
|
export const port = process.env.PORT || 4000;
|
||||||
export const host = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
export const host = process.env.VERCEL_PROJECT_PRODUCTION_URL
|
||||||
? `${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
? `${process.env.VERCEL_PROJECT_PRODUCTION_URL}`
|
||||||
: `http://localhost:${port}`;
|
: `http://localhost:${port}`;
|
||||||
|
|
||||||
|
export const rtlLocales = ['fa'];
|
||||||
|
export const isRtl = (locale: string) => rtlLocales.includes(locale);
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { RefObject, useEffect } from 'react';
|
||||||
|
|
||||||
|
export default function useClickOutside<T extends HTMLElement>(
|
||||||
|
ref: RefObject<T | null>,
|
||||||
|
handler: () => void,
|
||||||
|
) {
|
||||||
|
useEffect(() => {
|
||||||
|
const listener = (e: MouseEvent | TouchEvent) => {
|
||||||
|
if (!ref.current || ref.current.contains(e.target as Node)) return;
|
||||||
|
handler();
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousedown', listener);
|
||||||
|
document.addEventListener('touchstart', listener);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mousedown', listener);
|
||||||
|
document.removeEventListener('touchstart', listener);
|
||||||
|
};
|
||||||
|
}, [ref, handler]);
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { NextResponse } from 'next/server';
|
||||||
|
import type { IPagination } from '@/models/response';
|
||||||
|
|
||||||
|
export function ok<T>(data: T) {
|
||||||
|
return NextResponse.json({ data });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function okPaginated<T>(data: T, pagination: IPagination) {
|
||||||
|
return NextResponse.json({ data, pagination });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notFound(message = 'Not found') {
|
||||||
|
return NextResponse.json({ error: message }, { status: 404 });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serverError(message = 'Internal server error') {
|
||||||
|
return NextResponse.json({ error: message }, { status: 500 });
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
const api = axios.create({
|
const api = axios.create({
|
||||||
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000/api',
|
baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:4000/api',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
export function localizeText(
|
||||||
|
locale: string,
|
||||||
|
defaultValue: string,
|
||||||
|
faValue?: string,
|
||||||
|
) {
|
||||||
|
if (locale === 'fa') return faValue || defaultValue;
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function localizeContent<T>(locale: string, defaultValue: T, faValue?: T) {
|
||||||
|
if (locale === 'fa') return faValue || defaultValue;
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { routing } from '@/i18n/routing';
|
||||||
|
|
||||||
|
export async function getLocaleFromCookies() {
|
||||||
|
const locale = (await cookies()).get('locale')?.value;
|
||||||
|
return locale && routing.locales.includes(locale as 'en' | 'fa')
|
||||||
|
? locale
|
||||||
|
: routing.defaultLocale;
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import type { IPagination } from '@/models/response';
|
||||||
|
|
||||||
|
export function getPaginationParams(request: Request, defaultPerPage: number) {
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const page = Math.max(1, parseInt(searchParams.get('page') || '1', 10) || 1);
|
||||||
|
const perPage = defaultPerPage;
|
||||||
|
const start = (page - 1) * perPage;
|
||||||
|
const end = start + perPage;
|
||||||
|
|
||||||
|
return { page, perPage, start, end };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildPagination(
|
||||||
|
page: number,
|
||||||
|
perPage: number,
|
||||||
|
total: number,
|
||||||
|
): IPagination {
|
||||||
|
return {
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
total,
|
||||||
|
totalPages: Math.ceil(total / perPage),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import api from '@/lib/axios';
|
||||||
|
|
||||||
|
type RequestConfig = {
|
||||||
|
params?: Record<string, string | number | boolean | undefined>;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function withCookieHeader(config?: RequestConfig) {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...config,
|
||||||
|
headers: {
|
||||||
|
Cookie: cookieStore.toString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function serverGet<T>(url: string, config?: RequestConfig) {
|
||||||
|
const { data } = await api.get<T>(url, await withCookieHeader(config));
|
||||||
|
return data;
|
||||||
|
}
|
||||||
@@ -18,7 +18,11 @@ export function middleware(request: NextRequest) {
|
|||||||
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
|
(locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const locale = matchedLocale || defaultLocale;
|
const cookieLocale = request.cookies.get('locale')?.value;
|
||||||
|
const preferredLocale = locales.includes(cookieLocale || '')
|
||||||
|
? cookieLocale!
|
||||||
|
: defaultLocale;
|
||||||
|
const locale = matchedLocale || preferredLocale;
|
||||||
|
|
||||||
let response: NextResponse;
|
let response: NextResponse;
|
||||||
|
|
||||||
@@ -26,7 +30,7 @@ export function middleware(request: NextRequest) {
|
|||||||
const pathnameIsMissingLocale = !matchedLocale;
|
const pathnameIsMissingLocale = !matchedLocale;
|
||||||
if (pathnameIsMissingLocale) {
|
if (pathnameIsMissingLocale) {
|
||||||
response = NextResponse.redirect(
|
response = NextResponse.redirect(
|
||||||
new URL(`/${defaultLocale}${pathname}`, request.url),
|
new URL(`/${preferredLocale}${pathname}`, request.url),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
response = NextResponse.next();
|
response = NextResponse.next();
|
||||||
|
|||||||
@@ -1,14 +1,62 @@
|
|||||||
export interface IDynamicContent {
|
export type DynamicContentType =
|
||||||
type: keyof IDynamicContentItem;
|
| 'title'
|
||||||
data: string[] | string | IContentImage | IContentData;
|
| 'subtitle'
|
||||||
|
| 'content'
|
||||||
|
| 'image'
|
||||||
|
| 'bullet'
|
||||||
|
| 'numberList'
|
||||||
|
| 'quote'
|
||||||
|
| 'cta'
|
||||||
|
| 'divider';
|
||||||
|
|
||||||
|
export type IDynamicContent =
|
||||||
|
| ITitleContentBlock
|
||||||
|
| ISubtitleContentBlock
|
||||||
|
| IParagraphContentBlock
|
||||||
|
| IImageContentBlock
|
||||||
|
| IListContentBlock
|
||||||
|
| IQuoteContentBlock
|
||||||
|
| ICTAContentBlock
|
||||||
|
| IDividerContentBlock;
|
||||||
|
|
||||||
|
export interface ITitleContentBlock {
|
||||||
|
type: 'title';
|
||||||
|
data: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IDynamicContentItem {
|
export interface ISubtitleContentBlock {
|
||||||
bullet?: string[];
|
type: 'subtitle';
|
||||||
numberList?: string[];
|
data: string;
|
||||||
image?: IContentImage;
|
}
|
||||||
title?: string;
|
|
||||||
content?: IContentData;
|
export interface IParagraphContentBlock {
|
||||||
|
type: 'content';
|
||||||
|
data: IContentData | string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IImageContentBlock {
|
||||||
|
type: 'image';
|
||||||
|
data: IContentImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IListContentBlock {
|
||||||
|
type: 'bullet' | 'numberList';
|
||||||
|
data: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IQuoteContentBlock {
|
||||||
|
type: 'quote';
|
||||||
|
data: IContentQuote;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ICTAContentBlock {
|
||||||
|
type: 'cta';
|
||||||
|
data: IContentCTA;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IDividerContentBlock {
|
||||||
|
type: 'divider';
|
||||||
|
data?: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IContentImage {
|
export interface IContentImage {
|
||||||
@@ -28,3 +76,14 @@ export interface IContentDataLink {
|
|||||||
link: string;
|
link: string;
|
||||||
label: string;
|
label: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IContentQuote {
|
||||||
|
text: string;
|
||||||
|
author?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IContentCTA {
|
||||||
|
label: string;
|
||||||
|
link: string;
|
||||||
|
target?: '_self' | '_blank';
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +1,18 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { IPostResponseData, IPostThumbResponseData } from '@/app/api/blog/data';
|
import { IPostResponseData, IPostThumbResponseData } from '@/app/api/blog/data';
|
||||||
import api from '@/lib/axios';
|
import { serverGet } from '@/lib/server-api';
|
||||||
import { IResponse } from '@/models/response';
|
import { IResponse } from '@/models/response';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
|
|
||||||
export async function getPosts(
|
export async function getPosts(
|
||||||
page = 1,
|
page = 1,
|
||||||
): Promise<IResponse<IPostThumbResponseData[]>> {
|
): Promise<IResponse<IPostThumbResponseData[]>> {
|
||||||
const cookiesStore = await cookies();
|
return serverGet<IResponse<IPostThumbResponseData[]>>('/blog', {
|
||||||
return api
|
|
||||||
.get('/blog', {
|
|
||||||
headers: {
|
|
||||||
Cookie: cookiesStore.toString(),
|
|
||||||
},
|
|
||||||
params: { page },
|
params: { page },
|
||||||
})
|
});
|
||||||
.then(({ data }) => {
|
|
||||||
return Promise.resolve(data);
|
|
||||||
})
|
|
||||||
.catch((error) => Promise.reject(error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getPost(postId: string): Promise<IPostResponseData> {
|
export async function getPost(postId: string): Promise<IPostResponseData> {
|
||||||
const cookiesStore = await cookies();
|
const data = await serverGet<{ data: IPostResponseData }>(`/blog/${postId}`);
|
||||||
return api
|
return data.data;
|
||||||
.get(`/blog/${postId}`, {
|
|
||||||
headers: {
|
|
||||||
Cookie: cookiesStore.toString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(({ data }) => {
|
|
||||||
return Promise.resolve(data.data);
|
|
||||||
})
|
|
||||||
.catch((error) => Promise.reject(error));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,13 @@
|
|||||||
import api from "@/lib/axios";
|
import api from '@/lib/axios';
|
||||||
|
|
||||||
interface payload {
|
interface Payload {
|
||||||
first_name: string,
|
first_name: string;
|
||||||
last_name: string,
|
last_name: string;
|
||||||
email: string,
|
email: string;
|
||||||
phone: string,
|
phone: string;
|
||||||
message: string,
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setContact(payload:payload) {
|
export async function setContact(payload: Payload) {
|
||||||
return api
|
await api.post('/contact', payload);
|
||||||
.post('/contact', payload)
|
|
||||||
.then(({ data }) => {
|
|
||||||
return Promise.resolve();
|
|
||||||
})
|
|
||||||
.catch((error) => Promise.reject(error));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,57 +2,33 @@
|
|||||||
|
|
||||||
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/productCard.d';
|
import type { IProductCardProps } from '@/components/pages/products/productCard/productCard.d';
|
||||||
import api from '@/lib/axios';
|
import { serverGet } from '@/lib/server-api';
|
||||||
import { IResponse } from '@/models/response';
|
import { IResponse } from '@/models/response';
|
||||||
import { cookies } from 'next/headers';
|
|
||||||
|
|
||||||
export async function getProductCategories() {
|
export async function getProductCategories() {
|
||||||
const cookiesStore = await cookies();
|
const data = await serverGet<{ data: IProductCategoryCardProps[] }>(
|
||||||
return api
|
'/product_categories',
|
||||||
.get('/product_categories', {
|
);
|
||||||
headers: {
|
return data.data;
|
||||||
Cookie: cookiesStore.toString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(({ data }) => {
|
|
||||||
return Promise.resolve(data.data);
|
|
||||||
})
|
|
||||||
.catch((error) => Promise.reject(error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProductCategoryById(
|
export async function getProductCategoryById(
|
||||||
categoryId: string,
|
categoryId: string,
|
||||||
): Promise<IProductCategoryCardProps> {
|
): Promise<IProductCategoryCardProps> {
|
||||||
const cookiesStore = await cookies();
|
const data = await serverGet<{ data: IProductCategoryCardProps }>(
|
||||||
return api
|
`/product_categories/${categoryId}`,
|
||||||
.get(`/product_categories/${categoryId}`, {
|
);
|
||||||
headers: {
|
return data.data;
|
||||||
Cookie: cookiesStore.toString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(({ data }) => {
|
|
||||||
return Promise.resolve(data.data);
|
|
||||||
})
|
|
||||||
.catch((error) => Promise.reject(error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProducts(
|
export async function getProducts(
|
||||||
categoryId: string,
|
categoryId: string,
|
||||||
page = 1,
|
page = 1,
|
||||||
): Promise<IResponse<IProductCardProps[]>> {
|
): Promise<IResponse<IProductCardProps[]>> {
|
||||||
const cookiesStore = await cookies();
|
return serverGet<IResponse<IProductCardProps[]>>(
|
||||||
console.log('request');
|
`/product_categories/${categoryId}/products`,
|
||||||
console.log(page);
|
{
|
||||||
|
|
||||||
return api
|
|
||||||
.get(`/product_categories/${categoryId}/products`, {
|
|
||||||
headers: {
|
|
||||||
Cookie: cookiesStore.toString(),
|
|
||||||
},
|
|
||||||
params: { page },
|
params: { page },
|
||||||
})
|
},
|
||||||
.then(({ data }) => {
|
);
|
||||||
return Promise.resolve(data);
|
|
||||||
})
|
|
||||||
.catch((error) => Promise.reject(error));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,23 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { IProjectResponseData } from '@/app/api/projects/data';
|
import {
|
||||||
import api from '@/lib/axios';
|
IProjectResponseData,
|
||||||
import { cookies } from 'next/headers';
|
IProjectThumbResponseData,
|
||||||
|
} from '@/app/api/projects/data';
|
||||||
|
import { serverGet } from '@/lib/server-api';
|
||||||
|
import { IResponse } from '@/models/response';
|
||||||
|
|
||||||
export async function getProjects(page = 1 as number) {
|
export async function getProjects(page = 1 as number) {
|
||||||
const cookiesStore = await cookies();
|
return serverGet<IResponse<IProjectThumbResponseData[]>>('/projects', {
|
||||||
|
|
||||||
return api
|
|
||||||
.get('/projects', {
|
|
||||||
headers: {
|
|
||||||
Cookie: cookiesStore.toString(),
|
|
||||||
},
|
|
||||||
params: { page },
|
params: { page },
|
||||||
})
|
});
|
||||||
.then(({ data }) => Promise.resolve(data))
|
|
||||||
.catch((error) => Promise.reject(error));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProject(
|
export async function getProject(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
): Promise<IProjectResponseData> {
|
): Promise<IProjectResponseData> {
|
||||||
const cookiesStore = await cookies();
|
const data = await serverGet<{ data: IProjectResponseData }>(
|
||||||
return api
|
`/projects/${projectId}`,
|
||||||
.get(`/projects/${projectId}`, {
|
);
|
||||||
headers: {
|
return data.data;
|
||||||
Cookie: cookiesStore.toString(),
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(({ data }) => {
|
|
||||||
return Promise.resolve(data.data);
|
|
||||||
})
|
|
||||||
.catch((error) => Promise.reject(error));
|
|
||||||
}
|
}
|
||||||
|
|||||||