From 633a3c9d47ad1efaaaf27b437b47ec4173470a52 Mon Sep 17 00:00:00 2001 From: Jelaletdin12 Date: Sat, 13 Dec 2025 00:05:43 +0500 Subject: [PATCH] fixed some ui, refactored code --- app/[locale]/cart/page.tsx | 2 +- app/[locale]/category/[slug]/page.tsx | 2 +- app/[locale]/favorites/page.tsx | 5 +- app/[locale]/orders/page.tsx | 49 +- app/[locale]/page.tsx | 26 +- app/[locale]/product/[slug]/page.tsx | 3 +- components/icons.tsx | 11 +- components/layout/Header.tsx | 24 +- components/layout/MobileBar.tsx | 130 +++-- components/layout/ui/ActionButtons.tsx | 11 +- components/layout/ui/LanguageSelector.tsx | 16 +- .../category/components/CategoryClient.tsx | 497 ------------------ .../category/components/CategoryFilters.tsx | 234 +++++++++ .../components/CategoryFiltersSheet.tsx | 55 ++ .../components/CategoryPageClient.tsx | 279 ++++++++++ .../components/CategoryProductsGrid.tsx | 60 +++ features/category/hooks/useCategories.ts | 12 +- features/home/components/CategoryGrid.tsx | 9 +- features/home/components/HomePage.tsx | 55 +- features/home/components/HomeSkeleton.tsx | 30 -- features/home/components/ProductCard.tsx | 194 +++---- .../home/components/ProductCardSkeleton.tsx | 23 - features/home/components/ProductGrid.tsx | 51 +- .../home/components/ProductGridSkeleton.tsx | 24 - features/orders/components/OrderPage.tsx | 13 +- .../components/ProductPageContent.tsx | 4 +- .../profile/components/ProfilePageContent.tsx | 354 +++++++++++-- i18n/messages/ru.json | 10 +- i18n/messages/tm.json | 13 +- lib/types/api.ts | 1 + 30 files changed, 1274 insertions(+), 923 deletions(-) delete mode 100644 features/category/components/CategoryClient.tsx 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 delete mode 100644 features/home/components/HomeSkeleton.tsx delete mode 100644 features/home/components/ProductCardSkeleton.tsx delete mode 100644 features/home/components/ProductGridSkeleton.tsx diff --git a/app/[locale]/cart/page.tsx b/app/[locale]/cart/page.tsx index fea8c0c..950b6a0 100644 --- a/app/[locale]/cart/page.tsx +++ b/app/[locale]/cart/page.tsx @@ -146,7 +146,7 @@ export default function CartPage() { } return ( -
+

{t("cart")}

diff --git a/app/[locale]/category/[slug]/page.tsx b/app/[locale]/category/[slug]/page.tsx index 5b49725..e623def 100644 --- a/app/[locale]/category/[slug]/page.tsx +++ b/app/[locale]/category/[slug]/page.tsx @@ -31,6 +31,6 @@ export default async function CategoryPage(props: Props) { const params = await props.params const { slug } = params - const CategoryPageClient = (await import("../../../../features/category/components/CategoryClient")).default + const CategoryPageClient = (await import("../../../../features/category/components/CategoryPageClient")).default return } diff --git a/app/[locale]/favorites/page.tsx b/app/[locale]/favorites/page.tsx index 0dbbf05..f2f1df1 100644 --- a/app/[locale]/favorites/page.tsx +++ b/app/[locale]/favorites/page.tsx @@ -35,11 +35,10 @@ export default function FavoritesPage() { } return ( -

{t("favorite_products")}

-
+
{favorites.map((favorite: Favorite) => { const product = favorite.product; diff --git a/app/[locale]/orders/page.tsx b/app/[locale]/orders/page.tsx index 691cc15..0ee5449 100644 --- a/app/[locale]/orders/page.tsx +++ b/app/[locale]/orders/page.tsx @@ -1,20 +1,43 @@ -import type { Metadata } from "next" -import OrdersPageClient from "../../../features/orders/components/OrderPage" +import type { Metadata, ResolvingMetadata } from "next"; +import OrdersPageClient from "../../../features/orders/components/OrderPage"; -export const metadata: Metadata = { - title: "My Orders | E-Commerce", - description: "View your order history", - robots: "noindex, nofollow", // Private page -} +const metadataContent = { + tm: { + title: "Meniň Sargytlarym | Post shop", + description: "Sargytlaryňyzy görüň", + }, + ru: { + title: "Мои Заказы | Пост-магазин", + description: "Просмотр истории заказов", + }, +} as const; interface PageProps { - params: Promise<{ - locale: string - }> + params: { + locale: string; + }; +} + +export async function generateMetadata( + { params }: PageProps, + parent: ResolvingMetadata +): Promise { + const locale = params.locale as keyof typeof metadataContent; + + const content = metadataContent[locale] || metadataContent.ru; + + return { + title: content.title, + description: content.description, + + robots: { + index: false, + follow: false, + nocache: true, + }, + }; } export default async function OrdersPage({ params }: PageProps) { - const resolvedParams = await params - - return + return ; } diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 7fff3dd..79419c5 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -1,7 +1,17 @@ import type { Metadata } from "next"; import HomePage from "@/features/home/components/HomePage"; -export const revalidate = 300; +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, @@ -9,19 +19,7 @@ export async function generateMetadata({ params: Promise<{ locale: string }>; }): Promise { const { locale } = await params; - - const meta = { - ru: { - title: "Интернет магазин - Лучшие товары по низким ценам", - description: "Качественные товары с быстрой доставкой по всей стране", - }, - tm: { - title: "Satym dükanı - Iň gowy harytlar aşak bahada", - description: "Suw harytly towarnama. Elektrika, eşik, ev we bag", - }, - }; - - const { title, description } = meta[locale as keyof typeof meta] || meta.ru; + const { title, description } = META[locale as keyof typeof META] || META.ru; return { title, diff --git a/app/[locale]/product/[slug]/page.tsx b/app/[locale]/product/[slug]/page.tsx index f79947c..3beaec3 100644 --- a/app/[locale]/product/[slug]/page.tsx +++ b/app/[locale]/product/[slug]/page.tsx @@ -3,9 +3,8 @@ import { notFound } from "next/navigation"; import ProductPageContent from "../../../../features/products/components/ProductPageContent"; type Props = { - params: Promise<{ locale: string; slug: string }>; + params: { locale: string; slug: string }; }; - export const revalidate = 3600; // ISR: Revalidate every hour export async function generateMetadata({ params }: Props): Promise { diff --git a/components/icons.tsx b/components/icons.tsx index 6274558..d9f662f 100644 --- a/components/icons.tsx +++ b/components/icons.tsx @@ -2,7 +2,6 @@ export const FavoriteIcon = () => (
- - {/* Profile/Login Button */} - + {isAuthenticated ? ( + + + + ) : ( + + )}
@@ -147,7 +188,6 @@ export default function MobileBottomNav({ onClick={() => setIsCategoryOpen(false)} className="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-gray-100 transition-colors font-semibold" > - {category.icon_class && } {category.name} @@ -173,5 +213,5 @@ export default function MobileBottomNav({ - ) + ); } diff --git a/components/layout/ui/ActionButtons.tsx b/components/layout/ui/ActionButtons.tsx index 2bcd9f0..61c89c4 100644 --- a/components/layout/ui/ActionButtons.tsx +++ b/components/layout/ui/ActionButtons.tsx @@ -1,5 +1,5 @@ "use client"; - +import { useRouter } from "next/navigation"; import { useMemo } from "react"; import type React from "react"; import Link from "next/link"; @@ -47,7 +47,7 @@ export default function ActionButtons({ }: ActionButtonsProps) { const t = useTranslations(); const { mutate: logout, isPending: isLoggingOut } = useLogout(); - + const router = useRouter(); const { data: cartData, isLoading: cartLoading } = useCart(); const { data: favoritesData, isLoading: favoritesLoading } = useFavorites(); const { data: ordersData, isLoading: ordersLoading } = useOrders(); @@ -101,8 +101,7 @@ export default function ActionButtons({ href: "/cart", badgeCount: cartCount, isLoading: cartLoading, - } - + }, ], [ ordersCount, @@ -133,9 +132,7 @@ export default function ActionButtons({ - (window.location.href = `/${locale}/me`)} - > + router.push(`/${locale}/me`)}> {t("profile")} diff --git a/components/layout/ui/LanguageSelector.tsx b/components/layout/ui/LanguageSelector.tsx index 5f2c743..79e6cf9 100644 --- a/components/layout/ui/LanguageSelector.tsx +++ b/components/layout/ui/LanguageSelector.tsx @@ -2,6 +2,7 @@ import { useRouter, usePathname } from "next/navigation"; import Image from "next/image"; import { useLocale } from "next-intl"; +import { useQueryClient } from "@tanstack/react-query"; import { Select, SelectContent, @@ -26,13 +27,18 @@ const LANGUAGES: Language[] = [ export default function LanguageSelector() { const locale = useLocale(); const router = useRouter(); - const pathname = usePathname(); // Şu anki path'i al + const pathname = usePathname(); + const queryClient = useQueryClient(); + + const handleLanguageChange = async (newLocale: string) => { + if (typeof window !== "undefined") { + (window as any).i18n = { language: newLocale }; + } + + queryClient.invalidateQueries(); - const handleLanguageChange = (newLocale: string) => { - // Mevcut path'i yeni locale ile değiştir - // Örnek: /tm/cart -> /ru/cart const currentPath = pathname.replace(`/${locale}`, ""); - router.push(`/${newLocale}${currentPath}`); + router.replace(`/${newLocale}${currentPath}`); }; return ( diff --git a/features/category/components/CategoryClient.tsx b/features/category/components/CategoryClient.tsx deleted file mode 100644 index 76aae31..0000000 --- a/features/category/components/CategoryClient.tsx +++ /dev/null @@ -1,497 +0,0 @@ -"use client"; - -import { useEffect, useState, useMemo, useCallback } from "react"; -import { useRouter } from "next/navigation"; -import { SlidersHorizontal, X } from "lucide-react"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Slider } from "@/components/ui/slider"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "@/components/ui/sheet"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { Checkbox } from "@/components/ui/checkbox"; -import InfiniteScroll from "react-infinite-scroll-component"; -import ProductCard from "@/features/home/components/ProductCard"; -import { - useCategories, - useCategoryFilters, - useFilteredCategoryProducts, -} from "@/features/category/hooks/useCategories"; - -import { useTranslations } from "next-intl"; -import type { Category, Product } from "@/lib/types/api"; - -interface CategoryPageClientProps { - params: { locale: string; slug: string }; -} - -export default function CategoryPageClient({ - params, -}: CategoryPageClientProps) { - const { slug, locale } = params; - const router = useRouter(); - const [isOpen, setIsOpen] = useState(false); - const t = useTranslations(); - - const { data: categoriesData, isLoading: categoriesLoading } = - useCategories(); - - const selectedCategory = useMemo(() => { - if (!categoriesData || !slug) return null; - - const findBySlug = (categories: Category[]): Category | null => { - for (const category of categories) { - if (category.slug === slug) return category; - if (category.children) { - const found = findBySlug(category.children); - if (found) return found; - } - } - return null; - }; - - return findBySlug(categoriesData); - }, [categoriesData, slug]); - - const [currentPage, setCurrentPage] = useState(1); - const [allProducts, setAllProducts] = useState([]); - const [priceSort, setPriceSort] = useState< - "none" | "lowToHigh" | "highToLow" - >("none"); - const [priceRange, setPriceRange] = useState<[number, number]>([0, 10000]); - const [selectedBrands, setSelectedBrands] = useState>(new Set()); - const [selectedFilterCategories, setSelectedFilterCategories] = useState< - Set - >(new Set()); - - // Fetch filters - const { data: filtersData, isLoading: filtersLoading } = useCategoryFilters( - selectedCategory?.id, - { enabled: !!selectedCategory } - ); - - // Build filter params - const filterParams = useMemo(() => { - const params: any = { - page: currentPage, - limit: 6, - }; - - if (selectedBrands.size > 0) { - params.brands = Array.from(selectedBrands); - } - - if (selectedFilterCategories.size > 0) { - params.categories = Array.from(selectedFilterCategories); - } - - if (priceRange[0] > 0) { - params.min_price = priceRange[0]; - } - - if (priceRange[1] < 10000) { - params.max_price = priceRange[1]; - } - - return params; - }, [currentPage, selectedBrands, selectedFilterCategories, priceRange]); - - // Fetch filtered products - const { - data: productsData, - isLoading: productsLoading, - isFetching, - } = useFilteredCategoryProducts( - selectedCategory?.id?.toString() || "", - filterParams, - { enabled: !!selectedCategory } - ); - - // Reset on category change - useEffect(() => { - if (selectedCategory) { - setAllProducts([]); - setCurrentPage(1); - setSelectedBrands(new Set()); - setSelectedFilterCategories(new Set()); - setPriceRange([0, 10000]); - setPriceSort("none"); - } - }, [selectedCategory?.id]); - - // Update products list - useEffect(() => { - if (productsData?.data) { - setAllProducts((prev) => { - if (currentPage === 1) { - return productsData.data; - } - const existingIds = new Set(prev.map((p) => p.id)); - const newProducts = productsData.data.filter( - (p: Product) => !existingIds.has(p.id) - ); - return [...prev, ...newProducts]; - }); - } - }, [productsData, currentPage]); - - const hasMore = useMemo(() => { - return !!productsData?.pagination?.next_page_url; - }, [productsData]); - - const loadMoreData = useCallback(() => { - if (!hasMore || isFetching) return; - setCurrentPage((prev) => prev + 1); - }, [hasMore, isFetching]); - - const sortedProducts = useMemo(() => { - const products = [...allProducts]; - if (priceSort === "lowToHigh") { - return products.sort( - (a, b) => - parseFloat(a.price_amount || "0") - parseFloat(b.price_amount || "0") - ); - } - if (priceSort === "highToLow") { - return products.sort( - (a, b) => - parseFloat(b.price_amount || "0") - parseFloat(a.price_amount || "0") - ); - } - return products; - }, [allProducts, priceSort]); - - const handleBrandToggle = useCallback((brandId: number) => { - setSelectedBrands((prev) => { - const newSet = new Set(prev); - if (newSet.has(brandId)) { - newSet.delete(brandId); - } else { - newSet.add(brandId); - } - return newSet; - }); - setCurrentPage(1); - setAllProducts([]); - }, []); - - const handleCategoryToggle = useCallback((categoryId: number) => { - setSelectedFilterCategories((prev) => { - const newSet = new Set(prev); - if (newSet.has(categoryId)) { - newSet.delete(categoryId); - } else { - newSet.add(categoryId); - } - return newSet; - }); - setCurrentPage(1); - setAllProducts([]); - }, []); - - const handlePriceChange = useCallback((values: number[]) => { - setPriceRange([values[0], values[1]]); - setCurrentPage(1); - setAllProducts([]); - }, []); - - const handlePriceSortChange = useCallback( - (sortType: "none" | "lowToHigh" | "highToLow") => { - setPriceSort(sortType); - }, - [] - ); - - const resetFilters = useCallback(() => { - setSelectedBrands(new Set()); - setSelectedFilterCategories(new Set()); - setPriceRange([0, 10000]); - setPriceSort("none"); - setCurrentPage(1); - setAllProducts([]); - }, []); - - const findCategoryById = useCallback( - (categories: Category[] | undefined, id: number): Category | null => { - if (!categories) return null; - for (const category of categories) { - if (category.id === id) return category; - if (category.children) { - const found = findCategoryById(category.children, id); - if (found) return found; - } - } - return null; - }, - [] - ); - - - - const FiltersContent = useCallback( - () => ( -
- {filtersData?.categories && filtersData.categories.length > 0 && ( -
-

{t("category")}

-
- {filtersData.categories.map((category) => ( - - ))} -
-
- )} - - {filtersData?.brands && filtersData.brands.length > 0 && ( -
-

{t("brands")}

-
- {filtersData.brands.map((brand) => ( - - ))} -
-
- )} - -
-

{t("sort")}

-
- - - -
-
- - - - -
- ), - [ - filtersData, - selectedFilterCategories, - selectedBrands, - priceSort, - priceRange, - t, - handleCategoryToggle, - handleBrandToggle, - handlePriceSortChange, - handlePriceChange, - resetFilters, - ] - ); - - if (categoriesLoading) return
{t("common.loading")}
; - if (!selectedCategory) - return
{t("category_not_found")}
; - - const totalItems = - productsData?.pagination?.total || sortedProducts.length || 0; - - return ( -
-

{selectedCategory.name}

- - -
-
- - - -
- -
- {sortedProducts.length > 0 ? ( - -
{t("common.loading")}
-
- } - > -
- {sortedProducts.map((product) => ( - - ))} -
- - ) : ( -
- {t("no_results")} -
- )} -
-
- - - - - - - - {t("filter")} - - - - - - - -
- ); -} - -function PriceFilter({ - title, - priceRange, - onPriceChange, - translations, -}: { - title: string; - priceRange: [number, number]; - onPriceChange: (values: number[]) => void; - translations: { from: string; to: string }; -}) { - return ( -
-

{title}

-
-
-
- - - onPriceChange([parseInt(e.target.value) || 0, priceRange[1]]) - } - className="rounded-lg" - /> -
-
- - - onPriceChange([ - priceRange[0], - parseInt(e.target.value) || 10000, - ]) - } - className="rounded-lg" - /> -
-
- -
-
- ); -} diff --git a/features/category/components/CategoryFilters.tsx b/features/category/components/CategoryFilters.tsx new file mode 100644 index 0000000..e5f6114 --- /dev/null +++ b/features/category/components/CategoryFilters.tsx @@ -0,0 +1,234 @@ +import { useCallback } from "react"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Slider } from "@/components/ui/slider"; +import type { FilterBrand, FilterCategory } from "@/lib/types/api"; + +interface FiltersData { + categories: FilterCategory[]; + brands: FilterBrand[]; +} + +interface CategoryFiltersProps { + filtersData: FiltersData | undefined; + selectedBrands: Set; + selectedFilterCategories: Set; + priceSort: "none" | "lowToHigh" | "highToLow"; + priceRange: [number, number]; + onBrandToggle: (brandId: number) => void; + onCategoryToggle: (categoryId: number) => void; + onPriceSortChange: (sortType: "none" | "lowToHigh" | "highToLow") => void; + onPriceChange: (values: number[]) => void; + onReset: () => void; + translations: { + category: string; + brands: string; + sort: string; + default: string; + price_low_to_high: string; + price_high_to_low: string; + price: string; + price_from: string; + price_to: string; + reset: string; + }; +} + +export default function CategoryFilters({ + filtersData, + selectedBrands, + selectedFilterCategories, + priceSort, + priceRange, + onBrandToggle, + onCategoryToggle, + onPriceSortChange, + onPriceChange, + onReset, + translations, +}: CategoryFiltersProps) { + return ( +
+ {filtersData?.categories && filtersData.categories.length > 0 && ( + + {filtersData.categories.map((category) => ( + onCategoryToggle(category.id)} + label={category.name} + /> + ))} + + )} + + {filtersData?.brands && filtersData.brands.length > 0 && ( + + {filtersData.brands.map((brand) => ( + onBrandToggle(brand.id)} + label={brand.name} + /> + ))} + + )} + + + onPriceSortChange("none")} + label={translations.default} + /> + onPriceSortChange("lowToHigh")} + label={translations.price_low_to_high} + /> + onPriceSortChange("highToLow")} + label={translations.price_high_to_low} + /> + + + + + +
+ ); +} + +function FilterSection({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + return ( +
+

{title}

+
{children}
+
+ ); +} + +function CheckboxItem({ + checked, + onCheckedChange, + label, +}: { + checked: boolean; + onCheckedChange: () => void; + label: string; +}) { + return ( + + ); +} + +function RadioItem({ + name, + checked, + onChange, + label, +}: { + name: string; + checked: boolean; + onChange: () => void; + label: string; +}) { + return ( + + ); +} + +function PriceFilter({ + title, + priceRange, + onPriceChange, + translations, +}: { + title: string; + priceRange: [number, number]; + onPriceChange: (values: number[]) => void; + translations: { from: string; to: string }; +}) { + return ( +
+

{title}

+
+
+
+ + + onPriceChange([parseInt(e.target.value) || 0, priceRange[1]]) + } + className="rounded-lg" + /> +
+
+ + + onPriceChange([ + priceRange[0], + parseInt(e.target.value) || 10000, + ]) + } + className="rounded-lg" + /> +
+
+ +
+
+ ); +} \ No newline at end of file diff --git a/features/category/components/CategoryFiltersSheet.tsx b/features/category/components/CategoryFiltersSheet.tsx new file mode 100644 index 0000000..25afd98 --- /dev/null +++ b/features/category/components/CategoryFiltersSheet.tsx @@ -0,0 +1,55 @@ +import { SlidersHorizontal, X } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import { + Sheet, + SheetContent, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "@/components/ui/sheet"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +interface CategoryFiltersSheetProps { + isOpen: boolean; + onOpenChange: (open: boolean) => void; + filterLabel: string; + closeLabel: string; + children: React.ReactNode; +} + +export default function CategoryFiltersSheet({ + isOpen, + onOpenChange, + filterLabel, + closeLabel, + children, +}: CategoryFiltersSheetProps) { + return ( + + + + + + + {filterLabel} + + + + {children} + + + + ); +} \ No newline at end of file diff --git a/features/category/components/CategoryPageClient.tsx b/features/category/components/CategoryPageClient.tsx new file mode 100644 index 0000000..c222ef1 --- /dev/null +++ b/features/category/components/CategoryPageClient.tsx @@ -0,0 +1,279 @@ +"use client"; + +import { useEffect, useState, useMemo, useCallback } from "react"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { + useCategories, + useCategoryFilters, + useFilteredCategoryProducts, +} from "@/features/category/hooks/useCategories"; +import { useTranslations } from "next-intl"; +import type { Category, Product } from "@/lib/types/api"; +import CategoryFilters from "./CategoryFilters"; +import CategoryProductsGrid from "./CategoryProductsGrid"; +import CategoryFiltersSheet from "./CategoryFiltersSheet"; + +interface CategoryPageClientProps { + params: { locale: string; slug: string }; +} + +export default function CategoryPageClient({ + params, +}: CategoryPageClientProps) { + const { slug } = params; + const t = useTranslations(); + const [isSheetOpen, setIsSheetOpen] = useState(false); + + const { data: categoriesData, isLoading: categoriesLoading } = + useCategories(); + + const selectedCategory = useMemo(() => { + if (!categoriesData || !slug) return null; + + const findBySlug = (categories: Category[]): Category | null => { + for (const category of categories) { + if (category.slug === slug) return category; + if (category.children) { + const found = findBySlug(category.children); + if (found) return found; + } + } + return null; + }; + + return findBySlug(categoriesData); + }, [categoriesData, slug]); + + // State management + const [currentPage, setCurrentPage] = useState(1); + const [allProducts, setAllProducts] = useState([]); + const [priceSort, setPriceSort] = useState< + "none" | "lowToHigh" | "highToLow" + >("none"); + const [priceRange, setPriceRange] = useState<[number, number]>([0, 10000]); + const [selectedBrands, setSelectedBrands] = useState>(new Set()); + const [selectedFilterCategories, setSelectedFilterCategories] = useState< + Set + >(new Set()); + + // Fetch filters + const { data: filtersData } = useCategoryFilters(selectedCategory?.id, { + enabled: !!selectedCategory, + }); + + // Build filter params + const filterParams = useMemo(() => { + const params: any = { + page: currentPage, + limit: 6, + }; + + if (selectedBrands.size > 0) { + params.brands = Array.from(selectedBrands); + } + + if (selectedFilterCategories.size > 0) { + params.categories = Array.from(selectedFilterCategories); + } + + if (priceRange[0] > 0) { + params.min_price = priceRange[0]; + } + + if (priceRange[1] < 10000) { + params.max_price = priceRange[1]; + } + + return params; + }, [currentPage, selectedBrands, selectedFilterCategories, priceRange]); + + // Fetch filtered products + const { data: productsData, isFetching } = useFilteredCategoryProducts( + selectedCategory?.id?.toString() || "", + filterParams, + { enabled: !!selectedCategory } + ); + + // Reset on category change + useEffect(() => { + if (selectedCategory) { + setAllProducts([]); + setCurrentPage(1); + setSelectedBrands(new Set()); + setSelectedFilterCategories(new Set()); + setPriceRange([0, 10000]); + setPriceSort("none"); + } + }, [selectedCategory?.id]); + + // Update products list + useEffect(() => { + if (productsData?.data) { + setAllProducts((prev) => { + if (currentPage === 1) { + return productsData.data; + } + const existingIds = new Set(prev.map((p) => p.id)); + const newProducts = productsData.data.filter( + (p: Product) => !existingIds.has(p.id) + ); + return [...prev, ...newProducts]; + }); + } + }, [productsData, currentPage]); + + const hasMore = useMemo(() => { + return !!productsData?.pagination?.next_page_url; + }, [productsData]); + + const loadMoreData = useCallback(() => { + if (!hasMore || isFetching) return; + setCurrentPage((prev) => prev + 1); + }, [hasMore, isFetching]); + + const sortedProducts = useMemo(() => { + const products = [...allProducts]; + if (priceSort === "lowToHigh") { + return products.sort( + (a, b) => + parseFloat(a.price_amount || "0") - parseFloat(b.price_amount || "0") + ); + } + if (priceSort === "highToLow") { + return products.sort( + (a, b) => + parseFloat(b.price_amount || "0") - parseFloat(a.price_amount || "0") + ); + } + return products; + }, [allProducts, priceSort]); + + // Filter handlers + const handleBrandToggle = useCallback((brandId: number) => { + setSelectedBrands((prev) => { + const newSet = new Set(prev); + newSet.has(brandId) ? newSet.delete(brandId) : newSet.add(brandId); + return newSet; + }); + setCurrentPage(1); + setAllProducts([]); + }, []); + + const handleCategoryToggle = useCallback((categoryId: number) => { + setSelectedFilterCategories((prev) => { + const newSet = new Set(prev); + newSet.has(categoryId) + ? newSet.delete(categoryId) + : newSet.add(categoryId); + return newSet; + }); + setCurrentPage(1); + setAllProducts([]); + }, []); + + const handlePriceChange = useCallback((values: number[]) => { + setPriceRange([values[0], values[1]]); + setCurrentPage(1); + setAllProducts([]); + }, []); + + const handlePriceSortChange = useCallback( + (sortType: "none" | "lowToHigh" | "highToLow") => { + setPriceSort(sortType); + }, + [] + ); + + const resetFilters = useCallback(() => { + setSelectedBrands(new Set()); + setSelectedFilterCategories(new Set()); + setPriceRange([0, 10000]); + setPriceSort("none"); + setCurrentPage(1); + setAllProducts([]); + }, []); + + const filterTranslations = useMemo( + () => ({ + category: t("category"), + brands: t("brands"), + sort: t("sort"), + default: t("default"), + price_low_to_high: t("price_low_to_high"), + price_high_to_low: t("price_high_to_low"), + price: t("price"), + price_from: t("price_from"), + price_to: t("price_to"), + reset: t("reset"), + }), + [t] + ); + + if (categoriesLoading) return
{t("common.loading")}
; + if (!selectedCategory) + return
{t("category_not_found")}
; + + return ( +
+

+ {selectedCategory.name} +

+ +
+ {/* Desktop Filters Sidebar */} +
+ + + +
+ + {/* Products Grid */} +
+ +
+
+ + {/* Mobile Filters Sheet */} + + + +
+ ); +} \ No newline at end of file diff --git a/features/category/components/CategoryProductsGrid.tsx b/features/category/components/CategoryProductsGrid.tsx new file mode 100644 index 0000000..d13f051 --- /dev/null +++ b/features/category/components/CategoryProductsGrid.tsx @@ -0,0 +1,60 @@ +import InfiniteScroll from "react-infinite-scroll-component"; +import ProductCard from "@/features/home/components/ProductCard"; +import type { Product } from "@/lib/types/api"; + +interface CategoryProductsGridProps { + products: Product[]; + hasMore: boolean; + onLoadMore: () => void; + translations: { + loading: string; + no_results: string; + }; +} + +export default function CategoryProductsGrid({ + products, + hasMore, + onLoadMore, + translations, +}: CategoryProductsGridProps) { + if (products.length === 0) { + return ( +
+ {translations.no_results} +
+ ); + } + + return ( + +
{translations.loading}
+
+ } + > +
+ {products.map((product) => ( + + ))} +
+ + ); +} \ No newline at end of file diff --git a/features/category/hooks/useCategories.ts b/features/category/hooks/useCategories.ts index 26f1960..53fe881 100644 --- a/features/category/hooks/useCategories.ts +++ b/features/category/hooks/useCategories.ts @@ -47,7 +47,7 @@ export function useCategoryProducts( { params: { page: options?.page || 1, - limit: options?.limit + per_page: options?.limit }, } ) @@ -123,7 +123,7 @@ export function useFilteredCategoryProducts( queryFn: async () => { const params: Record = { page: filters.page || 1, - limit: filters.limit || 6, + per_page: filters.limit || 6, } if (filters.brands && filters.brands.length > 0) { @@ -166,10 +166,10 @@ export function useAllCategoryProductsPaginated( } ) { const page = options?.page || 1 - const limit = options?.limit || 6 + const per_page = options?.limit || 6 return useQuery({ - queryKey: ["category", category?.id, "paginated-products", page, limit], + queryKey: ["category", category?.id, "paginated-products", page, per_page], queryFn: async () => { if (!category) { return { @@ -186,7 +186,7 @@ export function useAllCategoryProductsPaginated( category.children.forEach((child) => categoryIds.push(child.id)) } - const perCategoryLimit = Math.ceil(limit / categoryIds.length) + const perCategoryLimit = Math.ceil(per_page / categoryIds.length) const hasMoreByCategory: Record = {} let allPageProducts: Product[] = [] @@ -196,7 +196,7 @@ export function useAllCategoryProductsPaginated( { params: { page, - limit: perCategoryLimit + per_page: perCategoryLimit } } ) diff --git a/features/home/components/CategoryGrid.tsx b/features/home/components/CategoryGrid.tsx index 8af5f48..6338035 100644 --- a/features/home/components/CategoryGrid.tsx +++ b/features/home/components/CategoryGrid.tsx @@ -2,7 +2,6 @@ import Image from "next/image"; import Link from "next/link"; import { Card, CardContent } from "@/components/ui/card"; -import { Skeleton } from "@/components/ui/skeleton"; import type { Category } from "@/lib/types/api"; type Props = { @@ -38,8 +37,8 @@ export default function CategoryGrid({
{Array.from({ length: 6 }).map((_, i) => (
- - +
+
))}
@@ -59,7 +58,9 @@ export default function CategoryGrid({
{cat.name} setMounted(true), []); + // Prefetch favorites + useFavorites(); const loadMore = () => { if (collections && visibleCount < collections.length) { @@ -43,22 +38,16 @@ export default function HomePage() { } }; - if (!mounted) return
Loading...
; - const carouselItems = - carousels?.map((carousel) => ({ - title: carousel.title || "", - image: carousel.image || carousel.thumbnail, - url: carousel.link || null, + carousels?.map((c) => ({ + title: c.title || "", + image: c.image || c.thumbnail, + url: c.link || null, })) || []; const visibleCollections = collections?.slice(0, visibleCount) || []; const hasMore = collections ? visibleCount < collections.length : false; - // Show loading indicator while favorites are being fetched - const showFavoritesLoading = - favoritesLoading && !categoriesLoading && !collectionsLoading; - return (
{!carouselsLoading && carouselItems.length > 0 && ( @@ -73,13 +62,6 @@ export default function HomePage() { title={t("categories")} /> - {showFavoritesLoading && ( -
-
-

Loading favorites...

-
- )} - {collectionsError ? (

@@ -89,17 +71,18 @@ export default function HomePage() { ) : collectionsLoading ? (

{Array.from({ length: 3 }).map((_, i) => ( -
+
-
- {Array.from({ length: 4 }).map((_, j) => ( -
+
+ {Array.from({ length: 5 }).map((_, j) => ( +
+
+
+
+
))}
-
+
))}
) : ( @@ -109,13 +92,13 @@ export default function HomePage() { hasMore={hasMore} loader={
-
-

Loading more collections...

+
+

{t("loading")}

} endMessage={
-

✓ All collections loaded

+

✓ {t("all_collections_loaded")}

} scrollThreshold={0.8} diff --git a/features/home/components/HomeSkeleton.tsx b/features/home/components/HomeSkeleton.tsx deleted file mode 100644 index ca1d72b..0000000 --- a/features/home/components/HomeSkeleton.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Skeleton } from "@/components/ui/skeleton" -import ProductGridSkeleton from "./ProductGridSkeleton" -import CategorySkeleton from "../../category/components/CategorySkeleton" - -export default function HomeSkeleton() { - return ( -
- {/* Hero Carousel Skeleton */} -
- -
- - {/* Categories Section Skeleton */} -
- -
- {Array.from({ length: 6 }).map((_, i) => ( - - ))} -
-
- - {/* Products Section Skeleton */} -
- - -
-
- ) -} diff --git a/features/home/components/ProductCard.tsx b/features/home/components/ProductCard.tsx index eb7935b..0523e89 100644 --- a/features/home/components/ProductCard.tsx +++ b/features/home/components/ProductCard.tsx @@ -1,6 +1,7 @@ "use client"; -import { useState, MouseEvent, useEffect, useRef, useCallback } from "react"; +import { useState, useEffect, useRef, useCallback, MouseEvent } from "react"; +import { useRouter } from "next/navigation"; import { Heart, ShoppingCart, Loader2, Plus, Minus } from "lucide-react"; import { toast } from "sonner"; import { @@ -21,6 +22,7 @@ import { useCart, } from "@/features/cart/hooks/useCart"; import { useTranslations } from "next-intl"; + type ProductCardProps = { id: number; name: string; @@ -42,8 +44,6 @@ export default function ProductCard({ name, price, struct_price_text, - discount, - discount_text, images, labels = [], price_color = "#005bff", @@ -52,6 +52,8 @@ export default function ProductCard({ button = false, stock, }: ProductCardProps) { + const router = useRouter(); + const t = useTranslations(); const { isFavorite, isLoading: isFavoriteLoading } = useIsFavorite(id); const { mutate: toggleFavorite, isPending: isFavoriteToggling } = useToggleFavorite(); @@ -66,54 +68,45 @@ export default function ProductCard({ const autoplayRef = useRef(null); const debounceTimerRef = useRef(undefined); - const isRequestInFlightRef = useRef(false); + const isRequestInFlightRef = useRef(false); const pendingQuantityRef = useRef(null); const hasMultipleImages = images.length > 1; - const cartItem = cartData?.data?.find((item: any) => item.product?.id === id); const isInCart = !!cartItem; - const isOutOfStock = stock !== undefined && stock === 0; + const isOutOfStock = stock === 0; const availableStock = stock || 999; - const t = useTranslations(); + + // Carousel setup useEffect(() => { if (!api) return; setCurrent(api.selectedScrollSnap()); - api.on("select", () => { - setCurrent(api.selectedScrollSnap()); - }); + const onSelect = () => setCurrent(api.selectedScrollSnap()); + api.on("select", onSelect); + return () => { + api.off("select", onSelect); + }; }, [api]); + // Autoplay useEffect(() => { if (!api || !hasMultipleImages) return; - const startAutoplay = () => { - autoplayRef.current = setInterval(() => { - if (api.canScrollNext()) { - api.scrollNext(); - } else { - api.scrollTo(0); - } - }, 3000); - }; + autoplayRef.current = setInterval(() => { + api.canScrollNext() ? api.scrollNext() : api.scrollTo(0); + }, 3000); - startAutoplay(); return () => { - if (autoplayRef.current) { - clearInterval(autoplayRef.current); - } + if (autoplayRef.current) clearInterval(autoplayRef.current); }; }, [api, hasMultipleImages]); - // Sync localQuantity with cart + // Sync local quantity with cart useEffect(() => { - if (cartItem?.product_quantity) { - setLocalQuantity(cartItem.product_quantity); - } else { - setLocalQuantity(1); - } + setLocalQuantity(cartItem?.product_quantity || 1); }, [cartItem]); + // Server sync function const syncToServer = useCallback( async (quantity: number) => { if (isRequestInFlightRef.current) { @@ -125,13 +118,7 @@ export default function ProductCard({ setIsSyncing(true); try { - await updateCartMutation.mutateAsync({ - productId: id, - quantity: quantity, - }); - - isRequestInFlightRef.current = false; - setIsSyncing(false); + await updateCartMutation.mutateAsync({ productId: id, quantity }); await refetchCart(); if (pendingQuantityRef.current !== null) { @@ -141,9 +128,13 @@ export default function ProductCard({ } } catch (error) { console.error("Sync failed:", error); + setLocalQuantity(cartItem?.product_quantity || 1); + toast.error("Failed to update quantity", { + description: "Please try again", + }); + } finally { isRequestInFlightRef.current = false; setIsSyncing(false); - setLocalQuantity(cartItem?.product_quantity || 1); } }, [id, updateCartMutation, cartItem, refetchCart] @@ -151,24 +142,17 @@ export default function ProductCard({ // Debounced sync useEffect(() => { - if (!isInCart) return; - - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current); - } - - if (localQuantity === (cartItem?.product_quantity || 1)) { + if (!isInCart || localQuantity === (cartItem?.product_quantity || 1)) return; - } + + if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current); debounceTimerRef.current = setTimeout(() => { syncToServer(localQuantity); }, 800); return () => { - if (debounceTimerRef.current) { - clearTimeout(debounceTimerRef.current); - } + if (debounceTimerRef.current) clearTimeout(debounceTimerRef.current); }; }, [localQuantity, isInCart, cartItem, syncToServer]); @@ -180,14 +164,11 @@ export default function ProductCard({ toggleFavorite( { productId: id, isFavorite }, { - onSuccess: (data) => { + onSuccess: (data) => toast.success( data.wasAdded ? "Added to favorites" : "Removed from favorites" - ); - }, - onError: () => { - toast.error("Error. Try again"); - }, + ), + onError: () => toast.error("Error. Try again"), } ); }, @@ -199,6 +180,16 @@ export default function ProductCard({ e.preventDefault(); e.stopPropagation(); + // Stock kontrolü + if (localQuantity > availableStock) { + toast.error("Insufficient Stock", { + description: `Only ${availableStock} items available in stock`, + duration: 4000, + }); + setLocalQuantity(availableStock); + return; + } + setIsSyncing(true); try { @@ -206,58 +197,51 @@ export default function ProductCard({ productId: id, quantity: localQuantity, }); - await refetchCart(); - setIsSyncing(false); - toast.success("Added to cart", { description: `${name} has been added to your cart`, }); } catch (error) { console.error("Add to cart error:", error); - setIsSyncing(false); toast.error("Failed to add to cart"); + } finally { + setIsSyncing(false); } }, - [id, name, localQuantity, addToCartMutation, refetchCart] + [id, name, localQuantity, availableStock, addToCartMutation, refetchCart] ); - const handleQuantityIncrease = useCallback( - (e: MouseEvent) => { + const handleQuantityChange = useCallback( + (e: MouseEvent, delta: number) => { e.preventDefault(); e.stopPropagation(); - if (localQuantity >= availableStock) { - toast.error("Stock limit reached", { - description: `Only ${availableStock} items available`, + const newQuantity = localQuantity + delta; + + if (newQuantity < 1) return; + + if (newQuantity > availableStock) { + toast.error("Stock Limit Reached", { + description: `Maximum ${availableStock} items available`, + duration: 4000, }); return; } - setLocalQuantity((prev) => prev + 1); + setLocalQuantity(newQuantity); }, [localQuantity, availableStock] ); - const handleQuantityDecrease = useCallback( - (e: MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - - if (localQuantity <= 1) return; - setLocalQuantity((prev) => prev - 1); - }, - [localQuantity] - ); - - const handleCardClick = (e: MouseEvent) => { + const handleCardClick = (e: MouseEvent) => { const target = e.target as HTMLElement; if ( target.closest("button") || target.closest('[data-carousel-control="true"]') ) { - e.preventDefault(); + return; } + router.push(`/product/${id}`); }; const handleNavClick = (e: MouseEvent, action: () => void) => { @@ -267,32 +251,27 @@ export default function ProductCard({ }; return ( -
- {images.map((image, index) => ( - + {images.map((image, idx) => ( +
{`${name} @@ -317,7 +296,6 @@ export default function ProductCard({ )} - {/* Favorite button */} {hasMultipleImages && (
- {images.map((_, index) => ( + {images.map((_, idx) => (
)} - {labels?.length > 0 && ( + {labels.length > 0 && (
{labels.map((label, idx) => ( )} - {/* Out of Stock Overlay */} {isOutOfStock && (
@@ -383,14 +362,13 @@ export default function ProductCard({

- {/* Cart controls - show on hover if button enabled */} {button && !isOutOfStock && ( -
); } diff --git a/features/home/components/ProductCardSkeleton.tsx b/features/home/components/ProductCardSkeleton.tsx deleted file mode 100644 index 8593d7f..0000000 --- a/features/home/components/ProductCardSkeleton.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Skeleton } from "@/components/ui/skeleton" -import { Card } from "@/components/ui/card" - -export default function ProductCardSkeleton() { - return ( - - {/* Image Skeleton */} - - - {/* Content Skeleton */} -
- {/* Title skeleton - 2 lines */} -
- - -
- - {/* Price skeleton */} - -
-
- ) -} diff --git a/features/home/components/ProductGrid.tsx b/features/home/components/ProductGrid.tsx index eb7d232..7a18f07 100644 --- a/features/home/components/ProductGrid.tsx +++ b/features/home/components/ProductGrid.tsx @@ -1,9 +1,7 @@ "use client"; -import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { ChevronRight } from "lucide-react"; import ProductCard from "@/features/home/components/ProductCard"; -import { Skeleton } from "@/components/ui/skeleton"; import { useCollectionProducts } from "@/lib/hooks"; import type { Collection } from "@/lib/types/api"; @@ -14,48 +12,42 @@ type Props = { export default function CollectionSection({ collection, locale }: Props) { const router = useRouter(); - const [shouldRender, setShouldRender] = useState(true); - const { data: productsData, isLoading, isError, - } = useCollectionProducts(collection.id, { enabled: shouldRender }); - - useEffect(() => { - if (!isLoading && productsData) { - const hasProducts = productsData.data && productsData.data.length > 0; - setShouldRender(hasProducts); - } - }, [isLoading, productsData]); - - if (!isLoading && (!productsData?.data || productsData.data.length === 0)) { - return null; - } + } = useCollectionProducts(collection.id); const handleTitleClick = () => { router.push(`/${locale}/collections/${collection.id}`); }; + // Hide section if no products + if (!isLoading && (!productsData?.data || productsData.data.length === 0)) { + return null; + } + if (isLoading) { return (
- - +
+
-
- {Array.from({ length: 4 }).map((_, i) => ( - +
+ {Array.from({ length: 5 }).map((_, i) => ( +
+
+
+
+
))}
); } - if (isError) { - return null; - } + if (isError) return null; const displayProducts = productsData?.data.slice(0, 10) || []; @@ -75,11 +67,11 @@ export default function CollectionSection({ collection, locale }: Props) { {displayProducts.map((product) => { const allImages = product.media ?.map( - (media) => - media.images_800x800 || - media.images_720x720 || - media.images_400x400 || - media.thumbnail + (m) => + m.images_800x800 || + m.images_720x720 || + m.images_400x400 || + m.thumbnail ) .filter(Boolean) || ["/placeholder-product.jpg"]; @@ -101,7 +93,6 @@ export default function CollectionSection({ collection, locale }: Props) { price_color="#0059ff" height={360} width={250} - /> ); })} diff --git a/features/home/components/ProductGridSkeleton.tsx b/features/home/components/ProductGridSkeleton.tsx deleted file mode 100644 index 7532a93..0000000 --- a/features/home/components/ProductGridSkeleton.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import ProductCardSkeleton from "./ProductCardSkeleton" - -interface ProductGridSkeletonProps { - count?: number - columns?: "2" | "3" | "4" | "5" -} - -export default function ProductGridSkeleton({ count = 8, columns = "4" }: ProductGridSkeletonProps) { - const gridClass = - { - "2": "grid-cols-2", - "3": "md:grid-cols-3", - "4": "md:grid-cols-4 lg:grid-cols-4", - "5": "md:grid-cols-4 xl:grid-cols-5", - }[columns] || "md:grid-cols-4" - - return ( -
- {Array.from({ length: count }).map((_, i) => ( - - ))} -
- ) -} diff --git a/features/orders/components/OrderPage.tsx b/features/orders/components/OrderPage.tsx index 841a963..825eafa 100644 --- a/features/orders/components/OrderPage.tsx +++ b/features/orders/components/OrderPage.tsx @@ -191,8 +191,8 @@ export default function OrdersPageClient({ locale }: OrdersPageClientProps) { } return ( -
-

{t("my_orders")}

+
+

{t("my_orders")}

@@ -314,10 +314,10 @@ function CompactOrderCard({ const itemCount = order.orderItems.length; return ( - + {/* Compact Header - Always Visible */}
@@ -325,7 +325,7 @@ function CompactOrderCard({
-

+

{t("order_number")} {order.id}

@@ -336,12 +336,15 @@ function CompactOrderCard({

+
+ {getStatusBadge(order.status)}

{total.toFixed(2)} TMT

+
{isExpanded ? ( ) : ( diff --git a/features/products/components/ProductPageContent.tsx b/features/products/components/ProductPageContent.tsx index c20dac4..4a18de2 100644 --- a/features/products/components/ProductPageContent.tsx +++ b/features/products/components/ProductPageContent.tsx @@ -432,8 +432,8 @@ export default function ProductPageContent({ slug }: ProductDetailProps) { return ( <> -
-
+
+
diff --git a/features/profile/components/ProfilePageContent.tsx b/features/profile/components/ProfilePageContent.tsx index 9fb043c..a385228 100644 --- a/features/profile/components/ProfilePageContent.tsx +++ b/features/profile/components/ProfilePageContent.tsx @@ -1,15 +1,22 @@ "use client"; -import { useCallback, useMemo } from "react"; -import { LogOut } from "lucide-react"; +import { useCallback, useMemo, useState, useEffect } from "react"; +import { LogOut, Edit2, Save, X, User, Phone, MapPin, Mail } 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 { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import { Skeleton } from "@/components/ui/skeleton"; -import { useUserProfile } from "@/lib/hooks"; +import { useUserProfile, useUpdateProfile } from "@/lib/hooks"; import { clearAuthToken } from "@/lib/api"; import { useTranslations } from "next-intl"; +import { toast } from "sonner"; interface ProfilePageProps { params: Promise<{ locale: string }>; @@ -17,34 +24,118 @@ interface ProfilePageProps { export default function ClientProfilePage(props: ProfilePageProps) { const { data: user, isLoading, error } = useUserProfile(); + const updateProfile = useUpdateProfile(); const t = useTranslations(); + const [isEditing, setIsEditing] = useState(false); + const [formData, setFormData] = useState({ + first_name: "", + last_name: "", + phone_number: "", + address: "", + }); + + useEffect(() => { + if (user && !isEditing) { + setFormData({ + first_name: user.first_name || "", + last_name: user.last_name || "", + phone_number: user.phone_number || "", + address: user.address || "", + }); + } + }, [user, isEditing]); + + const prepareDataForAPI = useCallback(() => { + return { + name: `${formData.first_name} ${formData.last_name}`.trim(), + phone_number: formData.phone_number, + address: formData.address, + }; + }, [formData]); + const handleLogout = useCallback(() => { clearAuthToken(); window.location.href = "/"; }, []); - const loadingSkeleton = useMemo(() => ( -
-
- - - - - - - - {[1, 2, 3, 4].map((i) => ( -
- - -
- ))} -
-
+ const handleEdit = useCallback(() => { + if (user) { + setFormData({ + first_name: user.first_name || "", + last_name: user.last_name || "", + phone_number: user.phone_number || "", + address: user.address || "", + }); + setIsEditing(true); + } + }, [user]); + + const handleCancel = useCallback(() => { + setIsEditing(false); + if (user) { + setFormData({ + first_name: user.first_name || "", + last_name: user.last_name || "", + phone_number: user.phone_number || "", + address: user.address || "", + }); + } + }, [user]); + + const handleSave = useCallback(async () => { + const apiData = prepareDataForAPI(); + + if (!apiData.name) { + toast.error(t("name_required") || "Name is required"); + return; + } + + try { + await updateProfile.mutateAsync(apiData); + setIsEditing(false); + toast.success(t("profile_updated_success") || "Profile updated successfully"); + } catch (err: any) { + const errorMessage = err?.response?.data?.message || t("profile_update_error") || "Failed to update profile"; + toast.error(errorMessage); + console.error("Profile update error:", err); + } + }, [formData, updateProfile, t, prepareDataForAPI]); + + const handleInputChange = useCallback( + (field: keyof typeof formData, value: string) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }, + [] + ); + + const loadingSkeleton = useMemo( + () => ( +
+
+
+ + +
+ + + + + + + {[1, 2, 3, 4].map((i) => ( +
+ + +
+ ))} +
+
+
-
- ), []); + ), + [] + ); if (isLoading) { return loadingSkeleton; @@ -53,10 +144,20 @@ export default function ClientProfilePage(props: ProfilePageProps) { if (error) { return (
- + -

{t("error_loading_profile")}

- +
+ +
+

+ {t("error_loading_profile")} +

+
@@ -64,51 +165,194 @@ export default function ClientProfilePage(props: ProfilePageProps) { } return ( -
-
-

{t("profile")}

+
+
+ {/* Header Section */} +
+
+
+

+ {t("profile")} +

+

+ {isEditing ? t("edit_your_information") : t("view_your_information")} +

+
+
+ +
+
+
- - - {t("personal_info")} - {t("profile_description")} + {/* Profile Card */} + + +
+
+ + {t("personal_info")} + + + {t("profile_description")} + +
+ {!isEditing && ( + + )} +
- + + {user && ( <> -
- - + {/* Name Fields - Grid on larger screens */} +
+
+ + + handleInputChange("first_name", e.target.value) + } + disabled={!isEditing} + className={`h-10 sm:h-11 text-sm sm:text-base ${ + isEditing + ? "border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white" + : "bg-gray-50 border-gray-200 text-gray-700" + }`} + placeholder={t("enter_first_name")} + /> +
+ +
+ + + handleInputChange("last_name", e.target.value) + } + disabled={!isEditing} + className={`h-10 sm:h-11 text-sm sm:text-base ${ + isEditing + ? "border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white" + : "bg-gray-50 border-gray-200 text-gray-700" + }`} + placeholder={t("enter_last_name")} + /> +
+ {/* Phone Field */}
- - + + + handleInputChange("phone_number", e.target.value) + } + disabled={!isEditing} + className={`h-10 sm:h-11 text-sm sm:text-base ${ + isEditing + ? "border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white" + : "bg-gray-50 border-gray-200 text-gray-700" + }`} + placeholder={t("enter_phone_number")} + />
+ {/* Address Field */}
- - + + + handleInputChange("address", e.target.value) + } + disabled={!isEditing} + className={`h-10 sm:h-11 text-sm sm:text-base ${ + isEditing + ? "border-gray-300 focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white" + : "bg-gray-50 border-gray-200 text-gray-700" + }`} + placeholder={t("enter_address")} + />
-
- - -
+ {/* Action Buttons - Edit Mode */} + {isEditing && ( +
+ + +
+ )} )} - + {/* Logout Button */} +
+ +
); diff --git a/i18n/messages/ru.json b/i18n/messages/ru.json index eecf6a2..e5d7013 100644 --- a/i18n/messages/ru.json +++ b/i18n/messages/ru.json @@ -16,7 +16,8 @@ "send": "Отправить", "enterPhone": "Введите свой номер телефона", "weWillSendCode": "Мы вышлем вам код", - "loading": "Загрузка..." + "loading": "Загрузка...", + "all_collections_loaded": "Все коллекции загружены" }, "category": "Категория", "checkout": "Оформить заказ", @@ -26,6 +27,7 @@ "total_price": "Общая цена:", "profile": "Профиль", "cart_orders": "Корзина заказов", + "shipping_method": "Способ доставки", "product_description_title": "Описание к товару", "recommended": "Рекомендуем также", "address_search": "Поиск адреса", @@ -148,5 +150,9 @@ "only_left": "Осталось {count} шт.", "stock_limit_title": "Недостаточно на складе", "stock_limit_message": "{product} закончился. Можно купить только {stock} шт.", - "understood": "Понятно" + "understood": "Понятно", + "loading": "Загрузка...", + "customer_information": "Информация о клиенте", + "name": "Имя" + } \ No newline at end of file diff --git a/i18n/messages/tm.json b/i18n/messages/tm.json index cf5caa5..d08664e 100644 --- a/i18n/messages/tm.json +++ b/i18n/messages/tm.json @@ -16,7 +16,8 @@ "send": "Ugrat", "enterPhone": "Telefon belgisini giriziň", "weWillSendCode": "Biz size kod ugradarys", - "loading": "Ýüklenýär..." + "loading": "Ýüklenýär...", + "all_collections_loaded": "Bütüň koleksiyonlar ýüklendi" }, "category": "Bölümler", "checkout": "Sargyt et", @@ -25,6 +26,7 @@ "discount": "Arzanladyş:", "total_price": "Jemi baha:", "profile": "Profil", + "shipping_method": "Eltip bermek usuly", "cart_orders": "Sargyt sebedi", "product_description_title": "Haryt barada maglumat", "recommended": "Maslahat berilýän harytlar", @@ -148,5 +150,12 @@ "only_left": "Diňe {count} sany galdy", "stock_limit_title": "Çäkli sanda", "stock_limit_message": "{product} harytdan diňe {stock} sany bar. Mundan köp sebediňize goşup bilmersiňiz.", - "understood": "Düşündim" + "understood": "Düşündim", + "loading": "Ýüklenýär...", + "customer_information": "Müşteri maglumatlary", + "name": "Ady" + + + + } \ No newline at end of file diff --git a/lib/types/api.ts b/lib/types/api.ts index b550c49..990e430 100644 --- a/lib/types/api.ts +++ b/lib/types/api.ts @@ -9,6 +9,7 @@ export interface ProductMedia { images_720x720: string; images_800x800: string; images_1200x1200: string; + } export type DeliveryType = "SELECTED_DELIVERY" | "PICK_UP";