From b8c871750a9fb11ceff3cc8ecc43e2f89e50b27c Mon Sep 17 00:00:00 2001 From: Jelaletdin12 Date: Sun, 1 Feb 2026 20:55:57 +0500 Subject: [PATCH] first commit --- .gitignore | 41 + README.md | 36 + Soraglar.txt | 11 + app/[locale]/cart/page.tsx | 261 + app/[locale]/category/[slug]/page.tsx | 52 + app/[locale]/collections/[slug]/page.tsx | 64 + app/[locale]/favorites/page.tsx | 91 + app/[locale]/globals.css | 236 + app/[locale]/layout.tsx | 67 + app/[locale]/me/page.tsx | 16 + app/[locale]/openStore/page.tsx | 280 + app/[locale]/orders/page.tsx | 43 + app/[locale]/page.tsx | 33 + app/[locale]/product/[slug]/page.tsx | 37 + app/favicon.ico | Bin 0 -> 15086 bytes components.json | 22 + components/ErrorPage/index.tsx | 126 + components/PageLoader/PreLoader.tsx | 79 + components/icons.tsx | 65 + components/layout/Header.tsx | 132 + components/layout/MobileBar.tsx | 232 + components/layout/ui/ActionButtons.tsx | 206 + components/layout/ui/AuthDialog.tsx | 197 + components/layout/ui/CategoryMenu.tsx | 164 + components/layout/ui/LanguageSelector.tsx | 80 + components/layout/ui/SearchBar.tsx | 167 + components/ui/avatar.tsx | 53 + components/ui/badge.tsx | 46 + components/ui/button.tsx | 60 + components/ui/card.tsx | 92 + components/ui/carousel.tsx | 241 + components/ui/checkbox.tsx | 32 + components/ui/dialog.tsx | 143 + components/ui/dropdown-menu.tsx | 257 + components/ui/input.tsx | 21 + components/ui/label.tsx | 24 + components/ui/radio-group.tsx | 45 + components/ui/scroll-area.tsx | 58 + components/ui/select.tsx | 187 + components/ui/separator.tsx | 28 + components/ui/sheet.tsx | 139 + components/ui/skeleton.tsx | 13 + components/ui/slider.tsx | 63 + components/ui/sonner.tsx | 47 + components/ui/tabs.tsx | 66 + components/ui/textarea.tsx | 18 + context/AuthWrapper.tsx | 59 + context/Provider.tsx | 22 + eslint.config.mjs | 54 + features/cart/components/CartItemCard.tsx | 458 + features/cart/components/CartItemSkeleton.tsx | 36 + .../cart/components/DeliveryTypeSelector.tsx | 58 + features/cart/components/EmptyCart.tsx | 30 + features/cart/components/OrderSummary.tsx | 348 + .../cart/components/OrderSummarySkeleton.tsx | 81 + features/cart/hooks/useAddresses.ts | 25 + features/cart/hooks/useCart.ts | 508 + features/cart/hooks/usePaymentTypes.ts | 23 + .../category/components/CategoryFilters.tsx | 234 + .../components/CategoryFiltersSheet.tsx | 55 + .../components/CategoryPageClient.tsx | 395 + .../components/CategoryProductsGrid.tsx | 83 + .../category/components/CategorySkeleton.tsx | 17 + features/category/hooks/useCategories.ts | 222 + .../components/CollectionFilters.tsx | 233 + .../components/CollectionFiltersSheet.tsx | 55 + .../components/CollectionPageClient.tsx | 385 + .../components/CollectionProductsGrid.tsx | 82 + features/collections/hooks/useCollections.ts | 161 + .../favorites/components/EmptyFavorites.tsx | 30 + features/favorites/hooks/useFavorites.ts | 203 + features/home/components/Carousel.tsx | 54 + features/home/components/CategoryGrid.tsx | 81 + features/home/components/HomePage.tsx | 126 + features/home/components/ProductCard.tsx | 452 + features/home/components/ProductGrid.tsx | 103 + features/home/hooks/useCollections.ts | 150 + features/home/hooks/useMedia.ts | 29 + features/openStore/hooks/useOpenStore.ts | 47 + features/orders/components/EmptyOrders.tsx | 30 + features/orders/components/OrderPage.tsx | 506 + features/orders/hooks/useOrders.ts | 77 + .../components/ProductImageGallery.tsx | 90 + .../products/components/ProductInfoCard.tsx | 126 + .../components/ProductPageContent.tsx | 553 + .../components/ProductPurchaseCard.tsx | 190 + .../components/ProductReviewsSection.tsx | 94 + .../components/RelatedProductsSection.tsx | 74 + features/products/components/ReviewModal.tsx | 124 + .../products/components/StockLimitModal.tsx | 56 + features/products/hooks/useProducts.ts | 236 + .../profile/components/ProfilePageContent.tsx | 368 + features/profile/hooks/useUserProfile.ts | 37 + features/search/hooks/useSearch.ts | 30 + features/search/types.ts | 30 + i18n/i18n.ts | 32 + i18n/messages/ru.json | 196 + i18n/messages/tm.json | 195 + lib/api.ts | 170 + lib/hooks/index.ts | 25 + lib/hooks/useAuth.ts | 237 + lib/i18n-utils.ts | 29 + lib/queryClient.ts | 19 + lib/tokenStorage.ts | 56 + lib/types/api.ts | 524 + lib/utils.ts | 6 + next.config.ts | 22 + package-lock.json | 9460 +++++++++++++++++ package.json | 52 + postcss.config.mjs | 7 + proxy.ts | 19 + public/file.svg | 1 + public/globe.svg | 1 + public/logo.webp | Bin 0 -> 7888 bytes public/next.svg | 1 + public/ru.png | Bin 0 -> 344 bytes ...-market-post-tm-ru-2025-05-12-15_18_42.png | Bin 0 -> 6039031 bytes ...-market-post-tm-ru-2025-05-12-15_20_20.png | Bin 0 -> 3131190 bytes ...tm-ru-cart-summary-2025-05-12-15_19_28.png | Bin 0 -> 399600 bytes ...tegory-elektronika-2025-05-12-15_19_05.png | Bin 0 -> 17737369 bytes ...tegory-elektronika-2025-05-12-15_20_56.png | Bin 0 -> 7081513 bytes public/seller.png | Bin 0 -> 24175 bytes public/styr.text | 101 + public/temp3.jpg | Bin 0 -> 79259 bytes public/tm.png | Bin 0 -> 1749 bytes public/vercel.svg | 1 + public/window.svg | 1 + tsconfig.json | 68 + 128 files changed, 23114 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 Soraglar.txt create mode 100644 app/[locale]/cart/page.tsx create mode 100644 app/[locale]/category/[slug]/page.tsx create mode 100644 app/[locale]/collections/[slug]/page.tsx create mode 100644 app/[locale]/favorites/page.tsx create mode 100644 app/[locale]/globals.css create mode 100644 app/[locale]/layout.tsx create mode 100644 app/[locale]/me/page.tsx create mode 100644 app/[locale]/openStore/page.tsx create mode 100644 app/[locale]/orders/page.tsx create mode 100644 app/[locale]/page.tsx create mode 100644 app/[locale]/product/[slug]/page.tsx create mode 100644 app/favicon.ico create mode 100644 components.json create mode 100644 components/ErrorPage/index.tsx create mode 100644 components/PageLoader/PreLoader.tsx create mode 100644 components/icons.tsx create mode 100644 components/layout/Header.tsx create mode 100644 components/layout/MobileBar.tsx create mode 100644 components/layout/ui/ActionButtons.tsx create mode 100644 components/layout/ui/AuthDialog.tsx create mode 100644 components/layout/ui/CategoryMenu.tsx create mode 100644 components/layout/ui/LanguageSelector.tsx create mode 100644 components/layout/ui/SearchBar.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 context/AuthWrapper.tsx create mode 100644 context/Provider.tsx create mode 100644 eslint.config.mjs create mode 100644 features/cart/components/CartItemCard.tsx create mode 100644 features/cart/components/CartItemSkeleton.tsx create mode 100644 features/cart/components/DeliveryTypeSelector.tsx create mode 100644 features/cart/components/EmptyCart.tsx create mode 100644 features/cart/components/OrderSummary.tsx create mode 100644 features/cart/components/OrderSummarySkeleton.tsx create mode 100644 features/cart/hooks/useAddresses.ts create mode 100644 features/cart/hooks/useCart.ts create mode 100644 features/cart/hooks/usePaymentTypes.ts create mode 100644 features/category/components/CategoryFilters.tsx create mode 100644 features/category/components/CategoryFiltersSheet.tsx create mode 100644 features/category/components/CategoryPageClient.tsx create mode 100644 features/category/components/CategoryProductsGrid.tsx create mode 100644 features/category/components/CategorySkeleton.tsx create mode 100644 features/category/hooks/useCategories.ts create mode 100644 features/collections/components/CollectionFilters.tsx create mode 100644 features/collections/components/CollectionFiltersSheet.tsx create mode 100644 features/collections/components/CollectionPageClient.tsx create mode 100644 features/collections/components/CollectionProductsGrid.tsx create mode 100644 features/collections/hooks/useCollections.ts create mode 100644 features/favorites/components/EmptyFavorites.tsx create mode 100644 features/favorites/hooks/useFavorites.ts create mode 100644 features/home/components/Carousel.tsx create mode 100644 features/home/components/CategoryGrid.tsx create mode 100644 features/home/components/HomePage.tsx create mode 100644 features/home/components/ProductCard.tsx create mode 100644 features/home/components/ProductGrid.tsx create mode 100644 features/home/hooks/useCollections.ts create mode 100644 features/home/hooks/useMedia.ts create mode 100644 features/openStore/hooks/useOpenStore.ts create mode 100644 features/orders/components/EmptyOrders.tsx create mode 100644 features/orders/components/OrderPage.tsx create mode 100644 features/orders/hooks/useOrders.ts create mode 100644 features/products/components/ProductImageGallery.tsx create mode 100644 features/products/components/ProductInfoCard.tsx create mode 100644 features/products/components/ProductPageContent.tsx create mode 100644 features/products/components/ProductPurchaseCard.tsx create mode 100644 features/products/components/ProductReviewsSection.tsx create mode 100644 features/products/components/RelatedProductsSection.tsx create mode 100644 features/products/components/ReviewModal.tsx create mode 100644 features/products/components/StockLimitModal.tsx create mode 100644 features/products/hooks/useProducts.ts create mode 100644 features/profile/components/ProfilePageContent.tsx create mode 100644 features/profile/hooks/useUserProfile.ts create mode 100644 features/search/hooks/useSearch.ts create mode 100644 features/search/types.ts create mode 100644 i18n/i18n.ts create mode 100644 i18n/messages/ru.json create mode 100644 i18n/messages/tm.json create mode 100644 lib/api.ts create mode 100644 lib/hooks/index.ts create mode 100644 lib/hooks/useAuth.ts create mode 100644 lib/i18n-utils.ts create mode 100644 lib/queryClient.ts create mode 100644 lib/tokenStorage.ts create mode 100644 lib/types/api.ts create mode 100644 lib/utils.ts create mode 100644 next.config.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.mjs create mode 100644 proxy.ts create mode 100644 public/file.svg create mode 100644 public/globe.svg create mode 100644 public/logo.webp create mode 100644 public/next.svg create mode 100644 public/ru.png create mode 100644 public/screencapture-market-post-tm-ru-2025-05-12-15_18_42.png create mode 100644 public/screencapture-market-post-tm-ru-2025-05-12-15_20_20.png create mode 100644 public/screencapture-market-post-tm-ru-cart-summary-2025-05-12-15_19_28.png create mode 100644 public/screencapture-market-post-tm-ru-category-elektronika-2025-05-12-15_19_05.png create mode 100644 public/screencapture-market-post-tm-ru-category-elektronika-2025-05-12-15_20_56.png create mode 100644 public/seller.png create mode 100644 public/styr.text create mode 100644 public/temp3.jpg create mode 100644 public/tm.png create mode 100644 public/vercel.svg create mode 100644 public/window.svg create mode 100644 tsconfig.json 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 0000000000000000000000000000000000000000..17789e2408773b61638ca2ceb339731ac16e0bd9 GIT binary patch literal 15086 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 { + const timer = setTimeout(() => setIsLoading(false), 1000); + return () => clearTimeout(timer); + }, []); + + return ( +
+