commit b8c871750a9fb11ceff3cc8ecc43e2f89e50b27c Author: Jelaletdin12 Date: Sun Feb 1 20:55:57 2026 +0500 first commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ef6a52 --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# env files (can opt-in for committing if needed) +.env* + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/Soraglar.txt b/Soraglar.txt new file mode 100644 index 0000000..72796c8 --- /dev/null +++ b/Soraglar.txt @@ -0,0 +1,11 @@ +1. Home page category suratlar acanok + +2. Harytlar kem kas bolanu ucin home page doly gorkezenok + + + +4. Order nadip otmen etmeli. + +5. Review feed back yazylyan yer bamy bolmalymy + +7. Delivery type soramaly, type lar yok \ No newline at end of file diff --git a/app/[locale]/cart/page.tsx b/app/[locale]/cart/page.tsx new file mode 100644 index 0000000..103cf34 --- /dev/null +++ b/app/[locale]/cart/page.tsx @@ -0,0 +1,261 @@ +"use client"; +import { useState, useEffect, useMemo } from "react"; +import { Card } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; +import CartItemCard from "../../../features/cart/components/CartItemCard"; +import CartItemSkeleton from "../../../features/cart/components/CartItemSkeleton"; +import OrderSummary from "../../../features/cart/components/OrderSummary"; +import OrderSummarySkeleton from "../../../features/cart/components/OrderSummarySkeleton"; +import { + useCart, + useCreateOrder, + useRegions, + usePaymentTypes, +} from "@/lib/hooks"; +import { useTranslations } from "next-intl"; +import { useRouter } from "next/navigation"; +import type { DeliveryType, PaymentType } from "@/lib/types/api"; +import EmptyCart from "@/features/cart/components/EmptyCart"; +import ErrorPage from "@/components/ErrorPage"; + +export default function CartPage() { + const [isClient, setIsClient] = useState(false); + const [paymentType, setPaymentType] = useState(null); + const [deliveryType, setDeliveryType] = + useState("SELECTED_DELIVERY"); + const [selectedRegion, setSelectedRegion] = useState(""); + const [selectedProvince, setSelectedProvince] = useState(null); + const [note, setNote] = useState(""); + const [phone, setPhone] = useState("+993 "); + const [name, setName] = useState(""); + const [lastName, setLastName] = useState(""); + const router = useRouter(); + const t = useTranslations(); + + const { data: cartResponse, isLoading: cartLoading, isError } = useCart(); + const { data: provinces = [], isLoading: provincesLoading } = useRegions(); + const { data: paymentTypes = [], isLoading: paymentTypesLoading } = + usePaymentTypes(); + const { mutate: createOrder, isPending: isCreatingOrder } = useCreateOrder(); + + const cartItems = cartResponse?.data || []; + const isLoading = cartLoading || provincesLoading || paymentTypesLoading; + + useEffect(() => { + setIsClient(true); + }, []); + + const regionGroups = useMemo(() => { + return provinces.reduce((acc, province) => { + if (!acc[province.region]) { + acc[province.region] = []; + } + acc[province.region].push(province); + return acc; + }, {} as Record); + }, [provinces]); + + const availableRegions = useMemo( + () => Object.keys(regionGroups), + [regionGroups] + ); + + const itemsBySeller = useMemo(() => { + return cartItems.reduce((acc, item) => { + const sellerId = item.product.channel?.[0]?.id || 0; + const sellerName = item.product.channel?.[0]?.name || "Unknown Seller"; + + if (!acc[sellerId]) { + acc[sellerId] = { + seller: { id: sellerId, name: sellerName }, + items: [], + }; + } + acc[sellerId].items.push(item); + return acc; + }, {} as Record); + }, [cartItems]); + + const totalAmount = useMemo(() => { + return cartItems.reduce((sum, item) => { + const price = parseFloat(item.product.price_amount || "0"); + return sum + price * item.product_quantity; + }, 0); + }, [cartItems]); + + const handleDeliveryTypeChange = (type: DeliveryType) => { + setDeliveryType(type); + setSelectedProvince(null); + }; + + + const formatPhoneForBackend = (phoneNumber: string): string => { + + return phoneNumber.replace(/^\+993\s*/, "").replace(/\s+/g, ""); + }; + + const handleCompleteOrder = () => { + if (!selectedRegion || !selectedProvince || !paymentType || !phone || !name) { + console.warn("Missing required fields for order"); + return; + } + + const phoneDigits = formatPhoneForBackend(phone); + if (phoneDigits.length !== 8) { + console.warn("Phone number must be exactly 8 digits"); + return; + } + + const selectedProvinceData = provinces.find((p) => p.id === selectedProvince); + if (!selectedProvinceData) return; + + createOrder( + { + customer_name: `${name} ${lastName}`.trim(), + customer_phone: parseInt(phoneDigits, 10), + customer_address: selectedProvinceData.name, + shipping_method: "standart", + payment_type_id: paymentType.id, + region: selectedRegion, + note: note || undefined, + }, + { + onSuccess: () => { + router.push(`/orders`); + }, + } + ); +}; + + if (!isClient) return null; + + if (isLoading) { + return ( +
+

+ {t("cart")} +

+ +
+
+ +
+ {Array.from({ length: 3 }).map((_, i) => ( + + ))} +
+
+
+ +
+
+ ); + } + + if (isError ) { + return ; + } + if (cartItems.length === 0) { + return ; + } + + return ( +
+

+ {t("cart")} +

+ +
+
+ + {Object.entries(itemsBySeller).map( + ([sellerId, { seller, items }]) => ( +
+

{seller.name}

+
+ {items.map((item) => { + const price = parseFloat( + item.product.price_amount || "0" + ); + const quantity = item.product_quantity; + const total = price * quantity; + + return ( + m.images_800x800 || m.thumbnail + ) || [], + }, + }} + /> + ); + })} +
+ {Object.entries(itemsBySeller).length > 1 && ( + + )} +
+ ) + )} +
+
+ + +
+
+ ); +} \ No newline at end of file diff --git a/app/[locale]/category/[slug]/page.tsx b/app/[locale]/category/[slug]/page.tsx new file mode 100644 index 0000000..ce96469 --- /dev/null +++ b/app/[locale]/category/[slug]/page.tsx @@ -0,0 +1,52 @@ +import type { Metadata } from "next"; + +type Props = { + params: Promise<{ locale: string; slug: string }>; +}; + +export const revalidate = 600; // ISR: Revalidate every 10 minutes + +const CATEGORY_META = { + tm: { + suffix: " | Post shop", + description: "Kategoriýa boýunça harytlary gözläň", + ogLocale: "tk_TM", + }, + ru: { + suffix: " | Post shop", + description: "Просмотр товаров в данной категории", + ogLocale: "ru_RU", + }, +} as const; + +export async function generateMetadata({ params }: Props): Promise { + const { locale, slug } = await params; + + const meta = + CATEGORY_META[locale as keyof typeof CATEGORY_META] ?? CATEGORY_META.ru; + + return { + title: `${slug}${meta.suffix}`, + description: meta.description, + openGraph: { + locale: meta.ogLocale, + title: `${slug}${meta.suffix}`, + description: meta.description, + }, + }; +} + +export async function generateStaticParams() { + const categories = ["electronics", "clothing", "home-garden"]; + return categories.map((slug) => ({ slug })); +} + +export default async function CategoryPage(props: Props) { + const params = await props.params; + const { slug } = params; + + const CategoryPageClient = ( + await import("../../../../features/category/components/CategoryPageClient") + ).default; + return ; +} diff --git a/app/[locale]/collections/[slug]/page.tsx b/app/[locale]/collections/[slug]/page.tsx new file mode 100644 index 0000000..309e4f8 --- /dev/null +++ b/app/[locale]/collections/[slug]/page.tsx @@ -0,0 +1,64 @@ +import type { Metadata } from "next"; + +type Props = { + params: Promise<{ locale: string; slug: string }>; +}; + +export const revalidate = 600; // ISR: 10 minutes + +const META = { + tm: { + titleSuffix: " | Post shop", + description: (name: string) => `${name} kolleksiýasyndaky harytlary gözläň`, + ogLocale: "tk_TM", + }, + ru: { + titleSuffix: " | Post shop", + description: (name: string) => `Просмотр товаров из коллекции «${name}»`, + ogLocale: "ru_RU", + }, +} as const; + +function formatSlug(slug: string) { + return slug + .split("-") + .map((w) => w.charAt(0).toUpperCase() + w.slice(1)) + .join(" "); +} + +export async function generateMetadata({ params }: Props): Promise { + const { locale, slug } = await params; + + const meta = META[locale as keyof typeof META] ?? META.ru; + const collectionName = formatSlug(slug); + const title = `${collectionName}${meta.titleSuffix}`; + const description = meta.description(collectionName); + + return { + title, + description, + openGraph: { + type: "website", + locale: meta.ogLocale, + title, + description, + }, + }; +} + +export async function generateStaticParams() { + const collections = ["new-arrivals", "best-sellers", "featured"]; + return collections.map((slug) => ({ slug })); +} + +export default async function CollectionPage(props: Props) { + const params = await props.params; + + const CollectionPageClient = ( + await import( + "../../../../features/collections/components/CollectionPageClient" + ) + ).default; + + return ; +} diff --git a/app/[locale]/favorites/page.tsx b/app/[locale]/favorites/page.tsx new file mode 100644 index 0000000..4bfcb84 --- /dev/null +++ b/app/[locale]/favorites/page.tsx @@ -0,0 +1,91 @@ +"use client"; + +import { useFavorites } from "@/lib/hooks"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useTranslations } from "next-intl"; +import ProductCard from "@/features/home/components/ProductCard"; +import type { Favorite } from "@/lib/types/api"; +import EmptyFavorites from "@/features/favorites/components/EmptyFavorites"; +import ErrorPage from "@/components/ErrorPage"; +import Placeholder from "@/public/logo.webp"; + +export default function FavoritesPage() { + const t = useTranslations(); + const { data: favorites, isLoading, isError } = useFavorites(); + + if (isLoading) { + return ( +
+

+ {t("favorite_products")} +

+
+ {Array.from({ length: 10 }).map((_, i) => ( +
+ + + +
+ ))} +
+
+ ); + } + + if (isError) { + return ; + } + + if (!favorites || favorites.length === 0) { + return ; + } + + return ( +
+

+ {t("favorite_products")} +

+
+ {favorites.map((favorite: Favorite) => { + const product = favorite.product; + + const allImages = product.media + ?.map( + (media) => + media.images_800x800 || + media.images_720x720 || + media.images_400x400 || + media.thumbnail + ) + .filter(Boolean) || [Placeholder]; + + const formattedPrice = product.price_amount + ? `${parseFloat(product.price_amount).toFixed(2)} TMT` + : "Price not available"; + + return ( + + ); + })} +
+
+ ); +} diff --git a/app/[locale]/globals.css b/app/[locale]/globals.css new file mode 100644 index 0000000..ec3033f --- /dev/null +++ b/app/[locale]/globals.css @@ -0,0 +1,236 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --radius: 0.625rem; + --background: #eff3f6; + --foreground: oklch(0.141 0.005 285.823); + --card: oklch(1 0 0); + --card-foreground: oklch(0.141 0.005 285.823); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.141 0.005 285.823); + --primary: oklch(0.21 0.006 285.885); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.967 0.001 286.375); + --secondary-foreground: oklch(0.21 0.006 285.885); + --muted: oklch(0.967 0.001 286.375); + --muted-foreground: oklch(0.552 0.016 285.938); + --accent: oklch(0.967 0.001 286.375); + --accent-foreground: oklch(0.21 0.006 285.885); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.92 0.004 286.32); + --input: oklch(0.92 0.004 286.32); + --ring: oklch(0.705 0.015 286.067); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.141 0.005 285.823); + --sidebar-primary: oklch(0.21 0.006 285.885); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.967 0.001 286.375); + --sidebar-accent-foreground: oklch(0.21 0.006 285.885); + --sidebar-border: oklch(0.92 0.004 286.32); + --sidebar-ring: oklch(0.705 0.015 286.067); +} + +.dark { + --background: oklch(0.141 0.005 285.823); + --foreground: oklch(0.985 0 0); + --card: oklch(0.21 0.006 285.885); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.21 0.006 285.885); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.92 0.004 286.32); + --primary-foreground: oklch(0.21 0.006 285.885); + --secondary: oklch(0.274 0.006 286.033); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.274 0.006 286.033); + --muted-foreground: oklch(0.705 0.015 286.067); + --accent: oklch(0.274 0.006 286.033); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.552 0.016 285.938); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.21 0.006 285.885); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.274 0.006 286.033); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.552 0.016 285.938); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} + +[data-sonner-toast] [data-description] { + color: #000 !important; + opacity: 0.9; +} + +@layer utilities { + .text-fg { + color: var(--fg); + } + .bg-bg { + background-color: var(--bg); + } + .stroke-primary { + stroke: #005bff; + } + + .stroke-track { + stroke: hsla(var(--hue), 10%, 10%, 0.1); + transition: stroke var(--trans-dur); + } + @media (prefers-color-scheme: dark) { + .stroke-track { + stroke: hsla(var(--hue), 10%, 90%, 0.1); + } + } + + .animate-msg { + animation: msg 0.3s 13.7s linear forwards; + } + .animate-msgLast { + animation: msg 0.3s 14s linear reverse forwards; + } + .animate-cartLines { + animation: cartLines 2s ease-in-out infinite; + } + .animate-cartTop { + animation: cartTop 2s ease-in-out infinite; + } + .animate-cartWheel1 { + animation: cartWheel1 2s ease-in-out infinite; + transform: rotate(-0.25turn); + transform-origin: 43px 111px; + } + .animate-cartWheel2 { + animation: cartWheel2 2s ease-in-out infinite; + transform: rotate(0.25turn); + transform-origin: 102px 111px; + } + .animate-cartWheelStroke { + animation: cartWheelStroke 2s ease-in-out infinite; + } +} + +@keyframes msg { + from { + opacity: 1; + visibility: visible; + } + 99.9% { + opacity: 0; + visibility: visible; + } + to { + opacity: 0; + visibility: hidden; + } +} +@keyframes cartLines { + from, + to { + opacity: 0; + } + 8%, + 92% { + opacity: 1; + } +} +@keyframes cartTop { + from { + stroke-dashoffset: -338; + } + 50% { + stroke-dashoffset: 0; + } + to { + stroke-dashoffset: 338; + } +} +@keyframes cartWheel1 { + from { + transform: rotate(-0.25turn); + } + to { + transform: rotate(2.75turn); + } +} +@keyframes cartWheel2 { + from { + transform: rotate(0.25turn); + } + to { + transform: rotate(3.25turn); + } +} +@keyframes cartWheelStroke { + from, + to { + stroke-dashoffset: 81.68; + } + 50% { + stroke-dashoffset: 40.84; + } +} diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx new file mode 100644 index 0000000..e63f00c --- /dev/null +++ b/app/[locale]/layout.tsx @@ -0,0 +1,67 @@ +import type React from "react" +import type { Metadata } from "next" +import { Geist, Geist_Mono } from "next/font/google" +import { notFound } from "next/navigation" +import { NextIntlClientProvider } from "next-intl" +import "./globals.css" +import Header from "@/components/layout/Header" +import MobileBottomNav from "@/components/layout/MobileBar" +import { Toaster } from "@/components/ui/sonner" +import { Providers } from "@/context/Provider" +import AuthWrapper from "@/context/AuthWrapper" + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}) + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}) + +export const metadata: Metadata = { + title: "Postshop", + description: "E-commerce platform", +} + +type Props = { + children: React.ReactNode + params: Promise<{ locale: string }> +} + +const locales = ["ru", "tm"] + +export function generateStaticParams() { + return locales.map((locale) => ({ locale })) +} + +export default async function RootLayout({ children, params }: Props) { + const { locale } = await params + + if (!locales.includes(locale)) notFound() + + let messages + try { + messages = (await import(`../../i18n/messages/${locale}.json`)).default + } catch { + messages = {} + } + + return ( + + + + + +
+ {children} + + + + + + + + ) +} \ No newline at end of file diff --git a/app/[locale]/me/page.tsx b/app/[locale]/me/page.tsx new file mode 100644 index 0000000..c794193 --- /dev/null +++ b/app/[locale]/me/page.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from "next" +import ClientProfilePage from "../../../features/profile/components/ProfilePageContent" + +export const metadata: Metadata = { + title: "My Profile | E-Commerce", + description: "Manage your profile settings", + robots: "noindex, nofollow", // Private page +} + +export default function ProfilePage({ + params, +}: { + params: Promise<{ locale: string }> +}) { + return +} diff --git a/app/[locale]/openStore/page.tsx b/app/[locale]/openStore/page.tsx new file mode 100644 index 0000000..b30f59b --- /dev/null +++ b/app/[locale]/openStore/page.tsx @@ -0,0 +1,280 @@ +"use client"; + +import type React from "react"; +import { useState } from "react"; +import { Upload } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { useOpenStore } from "@/lib/hooks"; +import { toast } from "sonner"; +import { useTranslations } from "next-intl"; + +interface OpenStorePageProps { + locale?: string; + translations?: { + title: string; + firstName: string; + lastName: string; + email: string; + phone: string; + uploadPatent: string; + submit: string; + selectedFile: string; + firstNameRequired: string; + lastNameRequired: string; + emailInvalid: string; + phoneInvalid: string; + fileRequired: string; + fileSizeError: string; + fileTypeError: string; + }; +} + +interface FormData { + firstName: string; + lastName: string; + email: string; + phone: string; + file: File | null; +} + +interface FormErrors { + firstName?: string; + lastName?: string; + email?: string; + phone?: string; + file?: string; +} + +export default function OpenStorePage({}: OpenStorePageProps) { + const [formData, setFormData] = useState({ + firstName: "", + lastName: "", + email: "", + phone: "+993", + file: null, + }); + const [errors, setErrors] = useState({}); + const [fileName, setFileName] = useState(""); + + const { mutate: submitOpenStore, isPending: loading } = useOpenStore(); + + const t = useTranslations(); + + const validateForm = (): boolean => { + const newErrors: FormErrors = {}; + + if (!formData.firstName.trim()) { + newErrors.firstName = t("requiredField"); + } + + if (!formData.lastName.trim()) { + newErrors.lastName = t("requiredField"); + } + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(formData.email)) { + newErrors.email = t("requiredField"); + } + + const phoneRegex = /^\+?[0-9]{6,15}$/; + if (!phoneRegex.test(formData.phone)) { + newErrors.phone = t("requiredField"); + } + + if (!formData.file) { + newErrors.file = t("fileRequired"); + } else { + const allowedTypes = ["image/jpeg", "image/jpg", "application/pdf"]; + if (!allowedTypes.includes(formData.file.type)) { + newErrors.file = t("fileTypeError"); + } + if (formData.file.size > 25 * 1024 * 1024) { + newErrors.file = t("fileSizeError"); + } + } + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { name, value } = e.target; + setFormData((prev) => ({ ...prev, [name]: value })); + if (errors[name as keyof FormErrors]) { + setErrors((prev) => ({ ...prev, [name]: undefined })); + } + }; + + const handleFileChange = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + setFormData((prev) => ({ ...prev, file })); + setFileName(file.name); + if (errors.file) { + setErrors((prev) => ({ ...prev, file: undefined })); + } + } + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!validateForm()) return; + + if (formData.file) { + submitOpenStore( + { + firstName: formData.firstName, + lastName: formData.lastName, + email: formData.email, + phone: formData.phone, + patentFile: formData.file, + }, + { + onSuccess: () => { + toast.success(t("submit_success")); + + setFormData({ + firstName: "", + lastName: "", + email: "", + phone: "+993", + file: null, + }); + setFileName(""); + }, + onError: (error: any) => { + toast.error(error?.message || t("submit_error")); + }, + } + ); + } + }; + + return ( +
+ + + {t("title")} + + Заполните форму для подачи заявления + + + +
+ {/* First Name */} +
+ + + {errors.firstName && ( +

{errors.firstName}

+ )} +
+ + {/* Last Name */} +
+ + + {errors.lastName && ( +

{errors.lastName}

+ )} +
+ + {/* Email */} +
+ + + {errors.email && ( +

{errors.email}

+ )} +
+ + {/* Phone */} +
+ + + {errors.phone && ( +

{errors.phone}

+ )} +
+ + {/* File Upload */} +
+ +
+ + + {fileName && ( +

+ {t("selectedFile")}: {fileName} +

+ )} + {errors.file && ( +

{errors.file}

+ )} +
+
+ + {/* Submit Button */} + +
+
+
+
+ ); +} diff --git a/app/[locale]/orders/page.tsx b/app/[locale]/orders/page.tsx new file mode 100644 index 0000000..eb5a770 --- /dev/null +++ b/app/[locale]/orders/page.tsx @@ -0,0 +1,43 @@ +import type { Metadata, ResolvingMetadata } from "next"; +import OrdersPageClient from "../../../features/orders/components/OrderPage"; + +const metadataContent = { + tm: { + title: "Meniň Sargytlarym | Post shop", + description: "Sargytlaryňyzy görüň", + }, + ru: { + title: "Мои Заказы | Пост-магазин", + description: "Просмотр истории заказов", + }, +} as const; + +interface PageProps { + params: Promise<{ + locale: string; + }>; +} + +export async function generateMetadata( + { params }: PageProps, + parent: ResolvingMetadata +): Promise { + const { locale } = await params; + const localeKey = locale as keyof typeof metadataContent; + const content = metadataContent[localeKey] || metadataContent.ru; + + return { + title: content.title, + description: content.description, + robots: { + index: false, + follow: false, + nocache: true, + }, + }; +} + +export default async function OrdersPage({ params }: PageProps) { + const { locale } = await params; + return ; +} \ No newline at end of file diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx new file mode 100644 index 0000000..79419c5 --- /dev/null +++ b/app/[locale]/page.tsx @@ -0,0 +1,33 @@ +import type { Metadata } from "next"; +import HomePage from "@/features/home/components/HomePage"; + +const META = { + ru: { + title: "Интернет магазин - Лучшие товары по низким ценам", + description: "Качественные товары с быстрой доставкой по всей стране", + }, + tm: { + title: "Post shop - Iň gowy harytlar, amatly bahada", + description: + "Ýokary hilli harytlar. Elektronika, eşik, arassaçylyk, sport, kosmetika", + }, +} as const; + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }>; +}): Promise { + const { locale } = await params; + const { title, description } = META[locale as keyof typeof META] || META.ru; + + return { + title, + description, + openGraph: { type: "website", locale, title, description }, + }; +} + +export default function Page() { + return ; +} diff --git a/app/[locale]/product/[slug]/page.tsx b/app/[locale]/product/[slug]/page.tsx new file mode 100644 index 0000000..bc43420 --- /dev/null +++ b/app/[locale]/product/[slug]/page.tsx @@ -0,0 +1,37 @@ +import type { Metadata } from "next"; +import { notFound } from "next/navigation"; +import ProductPageContent from "../../../../features/products/components/ProductPageContent"; + +type Props = { + params: { locale: string; slug: string }; +}; +export const revalidate = 3600; // ISR: Revalidate every hour + +export async function generateMetadata({ params }: Props): Promise { + const { locale, slug } = await params; + + return { + title: `Product ${slug} | E-Commerce`, + description: `View details for product ${slug}`, + openGraph: { + locale, + type: "website", + title: `Product ${slug} | E-Commerce`, + description: `View details for product ${slug}`, + }, + }; +} + +export async function generateStaticParams() { + return [{ slug: "nike-air-max" }, { slug: "adidas-ultraboost" }]; +} + +export default async function ProductPage(props: Props) { + const params = await props.params; + + if (!params.slug) { + notFound(); + } + + return ; +} \ No newline at end of file diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..17789e2 Binary files /dev/null and b/app/favicon.ico differ diff --git a/components.json b/components.json new file mode 100644 index 0000000..d5005f0 --- /dev/null +++ b/components.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/components/ErrorPage/index.tsx b/components/ErrorPage/index.tsx new file mode 100644 index 0000000..617fb3f --- /dev/null +++ b/components/ErrorPage/index.tsx @@ -0,0 +1,126 @@ +"use client"; + +import React, { useEffect, useState } from "react"; + +// Google Fonts'u içe aktarmak için bileşen dışına ekliyoruz +const fontImport = ` + @import url('https://fonts.googleapis.com/css?family=Encode+Sans+Semi+Condensed:100,200,300,400'); + + @keyframes clockwise { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } + } + @keyframes anticlockwise { + 0% { transform: rotate(360deg); } + 100% { transform: rotate(0deg); } + } + @keyframes clockwiseError { + 0% { transform: rotate(0deg); } + 20% { transform: rotate(30deg); } + 40% { transform: rotate(25deg); } + 60% { transform: rotate(30deg); } + 100% { transform: rotate(0deg); } + } + @keyframes anticlockwiseError { + 0% { transform: rotate(0deg); } + 20% { transform: rotate(-30deg); } + 40% { transform: rotate(-25deg); } + 60% { transform: rotate(-30deg); } + 100% { transform: rotate(0deg); } + } + @keyframes anticlockwiseErrorStop { + 0% { transform: rotate(0deg); } + 20% { transform: rotate(-30deg); } + 60% { transform: rotate(-30deg); } + 100% { transform: rotate(0deg); } + } +`; + +export default function ErrorPage() { + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + const timer = setTimeout(() => setIsLoading(false), 1000); + return () => clearTimeout(timer); + }, []); + + return ( +
+