Compare commits

..

17 Commits

Author SHA1 Message Date
ahasani df34692325 debug for build 2026-07-27 11:55:36 +03:30
ahasani bfa7413dfd debug for build 2026-07-26 00:25:35 +03:30
ahasani aa5b883a1c debug for build 2026-07-25 19:58:10 +03:30
ahasani 0670f4462c debug for build 2026-07-25 19:51:20 +03:30
ahasani 27977b1018 debug window on contact page 2026-07-25 19:45:04 +03:30
ahasani 9fd23b69e3 ready for build 2026-07-21 12:26:26 +03:30
ahasani 3486da6eeb ready for build 2026-07-21 12:25:19 +03:30
ahasani 730e2d8ca8 feat: add certificate gallery and certifications section components
- Implemented CertificateGallery component to display certification images in a gallery format.
- Created CertificationsSection component to showcase certifications with titles and descriptions.
- Enhanced Gallery component to support animations and keyboard navigation.
- Introduced Select component for dropdown selection with images.
- Added utility functions for API responses and pagination handling.
- Implemented localization functions for text and content.
- Created a custom hook for detecting clicks outside of a component.
2026-07-19 07:59:16 +03:30
ahasani 478b550c51 update 2026-05-30 14:20:42 +03:30
ahasani 3cab7e3ee5 update 2026-05-30 14:16:02 +03:30
ahasani aae6a4b49b update 2026-05-25 09:12:22 +03:30
zahravaziri 19e53d4b97 fix project slider 2025-07-01 12:40:24 +03:30
zahravaziri 584bc5522e Merge remote-tracking branch 'origin/main' into navbar 2025-07-01 12:30:39 +03:30
zahravaziri 123df850a8 fix slider post and add skeleton 2025-07-01 12:25:50 +03:30
zahravaziri 21c52a84ed change design language modal 2025-06-30 17:06:27 +03:30
ahasani 6c9dd0b731 docker update 2025-06-30 14:31:08 +03:30
zahravaziri 1d5b25354d create modal language selector 2025-06-30 13:45:33 +03:30
81 changed files with 9239 additions and 6916 deletions
+5
View File
@@ -2,3 +2,8 @@
node_modules node_modules
npm-debug.log npm-debug.log
.next/cache .next/cache
.next
.git
.gitignore
README.md
*.log
+4
View File
@@ -42,3 +42,7 @@ next-env.d.ts
contactMessages.json contactMessages.json
public/sitemap.xml public/sitemap.xml
data/
node_modules/
+28 -21
View File
@@ -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"]
+18
View File
@@ -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
+11 -10
View File
@@ -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',
}, },
}, },
]; ];
+7
View File
@@ -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.",
+7
View File
@@ -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.",
+7
View File
@@ -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": "با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته. با ما بیشتر آشنا شوید؛ متعهد به تولید آجرهای باکیفیت با بهره‌گیری از فناوری‌های نوین و روش‌های ساخت پیشرفته.",
+2 -2
View File
@@ -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`,
// ], // ],
}; };
+3 -1
View File
@@ -7,6 +7,8 @@ const withNextIntl = createNextIntlPlugin({
}, },
}); });
const config: NextConfig = {}; const config: NextConfig = {
output: 'standalone',
};
export default withNextIntl(config); export default withNextIntl(config);
-6413
View File
File diff suppressed because it is too large Load Diff
+12 -12
View File
@@ -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"
} }
+4719
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 360 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 259 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

After

Width:  |  Height:  |  Size: 17 KiB

+2 -2
View File
@@ -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
+3 -2
View File
@@ -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}
+2
View File
@@ -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>
); );
} }
+16 -4
View File
@@ -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 -2
View File
@@ -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;
+3 -6
View File
@@ -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"
+2
View File
@@ -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} />
+13 -15
View File
@@ -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,
});
} }
+57 -15
View File
@@ -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',
},
},
], ],
}, },
{ {
+9 -19
View File
@@ -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),
},
});
} }
+6 -6
View File
@@ -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 },
);
} }
} }
@@ -443,19 +443,19 @@ export const sefidCategoryProducts = [
size: '', size: '',
categoryId: '50', categoryId: '50',
}, },
{ {
model: 'پلاک گرانیتی 26', model: 'پلاک گرانیتی 26',
color_code: 'سفید', color_code: 'سفید',
size: '5.5 * 26 * 2.5', size: '5.5 * 26 * 2.5',
categoryId: '50', categoryId: '50',
}, },
{ {
model: 'پلاک 7', model: 'پلاک 7',
color_code: 'سفید', color_code: 'سفید',
size: '7 * 33 * 2.5', size: '7 * 33 * 2.5',
categoryId: '50', categoryId: '50',
}, },
{ {
model: 'پلاک گرانتی 7', model: 'پلاک گرانتی 7',
color_code: 'سفید', color_code: 'سفید',
size: '7 * 33 * 2.5', size: '7 * 33 * 2.5',
@@ -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),
},
});
} }
+18 -16
View File
@@ -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);
} }
+20 -11
View File
@@ -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 },
);
} }
} }
+13 -15
View File
@@ -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,
});
} }
+10 -6
View File
@@ -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),
}), }));
);
} }
+9 -26
View File
@@ -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),
},
});
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 980 B

+11
View File
@@ -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,
}; };
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

+42
View File
@@ -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>
+26 -9
View File
@@ -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>
+5 -15
View File
@@ -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>
);
}
+16 -30
View File
@@ -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>
); );
} }
+7
View File
@@ -0,0 +1,7 @@
'use client';
import dynamic from 'next/dynamic';
const ContactMap = dynamic(() => import('./index'), { ssr: false });
export default ContactMap;
+10 -4
View File
@@ -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>
);
}
+2 -2
View File
@@ -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
+2
View File
@@ -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>
))} ))}
+14
View File
@@ -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;
}
+168
View File
@@ -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>
);
}
+135
View File
@@ -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;
+18
View File
@@ -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';
+4 -1
View File
@@ -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);
+21
View File
@@ -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]);
}
+18
View File
@@ -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 -1
View File
@@ -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',
}, },
+13
View File
@@ -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;
}
+9
View File
@@ -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;
}
+24
View File
@@ -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),
};
}
+24
View File
@@ -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;
}
+6 -2
View File
@@ -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();
+68 -9
View File
@@ -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';
}
+5 -24
View File
@@ -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));
} }
+9 -14
View File
@@ -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));
} }
+14 -38
View File
@@ -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));
} }
+12 -24
View File
@@ -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));
} }
+3023
View File
File diff suppressed because it is too large Load Diff