refactored some code

This commit is contained in:
Jelaletdin12
2025-12-24 22:04:44 +05:00
parent 342fb31906
commit 7538bdb813
12 changed files with 197 additions and 111 deletions

View File

@@ -2,12 +2,10 @@
2. Harytlar kem kas bolanu ucin home page doly gorkezenok 2. Harytlar kem kas bolanu ucin home page doly gorkezenok
3. Filter nahili isleyar
4. Order nadip otmen etmeli. 4. Order nadip otmen etmeli.
5. Review feed back yazylyan yer bamy bolmalymy 5. Review feed back yazylyan yer bamy bolmalymy
6. Open Store api field ler nahili bolmaly.
7. Delivery type soramaly, type lar yok 7. Delivery type soramaly, type lar yok

View File

@@ -7,6 +7,8 @@ import ProductCard from "@/features/home/components/ProductCard";
import type { Favorite } from "@/lib/types/api"; import type { Favorite } from "@/lib/types/api";
import EmptyFavorites from "@/features/favorites/components/EmptyFavorites"; import EmptyFavorites from "@/features/favorites/components/EmptyFavorites";
import ErrorPage from "@/components/ErrorPage"; import ErrorPage from "@/components/ErrorPage";
import Placeholder from "@/public/logo.webp";
export default function FavoritesPage() { export default function FavoritesPage() {
const t = useTranslations(); const t = useTranslations();
const { data: favorites, isLoading, isError } = useFavorites(); const { data: favorites, isLoading, isError } = useFavorites();
@@ -58,7 +60,7 @@ export default function FavoritesPage() {
media.images_400x400 || media.images_400x400 ||
media.thumbnail media.thumbnail
) )
.filter(Boolean) || ["/placeholder-product.jpg"]; .filter(Boolean) || [Placeholder];
const formattedPrice = product.price_amount const formattedPrice = product.price_amount
? `${parseFloat(product.price_amount).toFixed(2)} TMT` ? `${parseFloat(product.price_amount).toFixed(2)} TMT`

View File

@@ -176,7 +176,7 @@ export default function OpenStorePage({
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
{/* First Name */} {/* First Name */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="firstName">{t("firstName")}</Label> <Label htmlFor="firstName">{t("enter_first_name")}</Label>
<Input <Input
id="firstName" id="firstName"
name="firstName" name="firstName"
@@ -191,7 +191,7 @@ export default function OpenStorePage({
{/* Last Name */} {/* Last Name */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="lastName">{t("lastName")}</Label> <Label htmlFor="lastName">{t("enter_last_name")}</Label>
<Input <Input
id="lastName" id="lastName"
name="lastName" name="lastName"
@@ -206,7 +206,7 @@ export default function OpenStorePage({
{/* Email */} {/* Email */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="email">{t("email")}</Label> <Label htmlFor="email">{t("enter_email")}</Label>
<Input <Input
id="email" id="email"
name="email" name="email"
@@ -222,7 +222,7 @@ export default function OpenStorePage({
{/* Phone */} {/* Phone */}
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="phone">{t("phone")}</Label> <Label htmlFor="phone">{t("enter_phone")}</Label>
<Input <Input
id="phone" id="phone"
name="phone" name="phone"
@@ -273,7 +273,7 @@ export default function OpenStorePage({
className="w-full cursor-pointer bg-[#005bff] hover:bg-[#0041c4]" className="w-full cursor-pointer bg-[#005bff] hover:bg-[#0041c4]"
disabled={loading} disabled={loading}
> >
{loading ? "Загрузка..." : t("submit")} {loading ? t("submitting") : t("submit")}
</Button> </Button>
</form> </form>
</CardContent> </CardContent>

View File

@@ -1,9 +1,10 @@
// Header.tsx
"use client"; "use client";
import { useState, useEffect, useCallback } from "react"; import { useState, useEffect, useCallback } from "react";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { X, Search, User as UserIcon } from "lucide-react"; import { X, Search } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Logo from "@/public/logo.webp"; import Logo from "@/public/logo.webp";
import CategoryMenu from "./ui/CategoryMenu"; import CategoryMenu from "./ui/CategoryMenu";
@@ -54,7 +55,7 @@ export default function Header({ locale = "ru" }: HeaderProps) {
return ( return (
<> <>
<header className="sticky top-0 z-50 w-full border-b bg-white shadow-sm"> <header className="sticky top-0 z-50 w-full border-b bg-white shadow-sm">
<div className=" mx-auto px-4"> <div className="mx-auto px-4">
<div className="flex h-16 items-center justify-between gap-3"> <div className="flex h-16 items-center justify-between gap-3">
<Link href="/" className="shrink-0"> <Link href="/" className="shrink-0">
<div className="relative h-8 w-[180px]"> <div className="relative h-8 w-[180px]">
@@ -69,6 +70,7 @@ export default function Header({ locale = "ru" }: HeaderProps) {
</Link> </Link>
<Button <Button
data-catalog-trigger
onClick={toggleCategoryMenu} onClick={toggleCategoryMenu}
className="cursor-pointer hidden gap-2 rounded-lg font-bold lg:flex hover:bg-[#005bff] bg-[#005bff] text-white" className="cursor-pointer hidden gap-2 rounded-lg font-bold lg:flex hover:bg-[#005bff] bg-[#005bff] text-white"
size="lg" size="lg"

View File

@@ -68,7 +68,12 @@ const cartCount = useCartCount()
}, [ordersData]); }, [ordersData]);
const handleLogout = () => { const handleLogout = () => {
logout(); logout(undefined, {
onSuccess: () => {
router.push(`/${locale}`);
router.refresh();
}
});
}; };
const buttons: ActionButtonData[] = useMemo( const buttons: ActionButtonData[] = useMemo(

View File

@@ -1,29 +1,80 @@
"use client" // CategoryMenu.tsx
"use client";
import { useState } from "react"
import Link from "next/link"
import { useCategories } from "@/lib/hooks"
import { Skeleton } from "@/components/ui/skeleton"
import { useState, useEffect, useRef } from "react";
import Link from "next/link";
import { useCategories } from "@/lib/hooks";
import { Skeleton } from "@/components/ui/skeleton";
interface CategoryMenuProps { interface CategoryMenuProps {
isOpen: boolean isOpen: boolean;
onClose: () => void onClose: () => void;
} }
export default function CategoryMenu({ isOpen, onClose }: CategoryMenuProps) { export default function CategoryMenu({ isOpen, onClose }: CategoryMenuProps) {
const [hoveredCategory, setHoveredCategory] = useState<number | null>(null) const [hoveredCategory, setHoveredCategory] = useState<number | null>(null);
const { data: categories, isLoading } = useCategories() const { data: categories, isLoading } = useCategories();
const menuRef = useRef<HTMLDivElement>(null);
if (!isOpen) return null // Click outside to close
useEffect(() => {
if (!isOpen) return;
const categoryList = categories || [] const handleClickOutside = (event: MouseEvent) => {
const activeCategory = hoveredCategory !== null ? categoryList[hoveredCategory] : null const target = event.target as HTMLElement;
if (target.closest("[data-catalog-trigger]")) {
return;
}
if (menuRef.current && !menuRef.current.contains(target)) {
onClose();
}
};
// Add listener after a small delay to prevent immediate closing
const timeoutId = setTimeout(() => {
document.addEventListener("mousedown", handleClickOutside);
}, 100);
return () => {
clearTimeout(timeoutId);
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isOpen, onClose]);
// ESC key to close
useEffect(() => {
if (!isOpen) return;
const handleEscape = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onClose();
}
};
document.addEventListener("keydown", handleEscape);
return () => document.removeEventListener("keydown", handleEscape);
}, [isOpen, onClose]);
if (!isOpen) return null;
const categoryList = categories || [];
const activeCategory =
hoveredCategory !== null ? categoryList[hoveredCategory] : null;
return ( return (
<div className="fixed left-0 right-0 top-15 z-40 bg-white border-b shadow-lg max-w-[1504px] mx-auto"> <>
<div className=" mx-auto px-4"> <div className="fixed inset-0 bg-black/20 z-30" onClick={onClose} />
{/* Menu */}
<div
ref={menuRef}
className="fixed left-0 right-0 top-16 z-40 bg-white border-b rounded-b-lg shadow-lg max-w-[1504px] mx-auto"
>
<div className="mx-auto px-4">
<div className="flex"> <div className="flex">
<CategoryList <CategoryList
categories={categoryList} categories={categoryList}
@@ -32,49 +83,67 @@ export default function CategoryMenu({ isOpen, onClose }: CategoryMenuProps) {
onCategoryClick={onClose} onCategoryClick={onClose}
/> />
{activeCategory?.children && <SubcategoryList category={activeCategory} onSubcategoryClick={onClose} />} {activeCategory?.children && (
<SubcategoryList
category={activeCategory}
onSubcategoryClick={onClose}
/>
)}
</div> </div>
</div> </div>
</div> </div>
) </>
);
} }
interface CategoryListProps { interface CategoryListProps {
categories: any[] categories: any[];
isLoading: boolean isLoading: boolean;
onCategoryHover: (index: number) => void onCategoryHover: (index: number) => void;
onCategoryClick: () => void onCategoryClick: () => void;
} }
function CategoryList({ categories, isLoading, onCategoryHover, onCategoryClick }: CategoryListProps) { function CategoryList({
categories,
isLoading,
onCategoryHover,
onCategoryClick,
}: CategoryListProps) {
return ( return (
<div className="w-[280px] border-r"> <div className="w-[280px] border-r">
<div className="max-h-[calc(100vh-4rem)] overflow-y-auto py-2"> <div className="max-h-[calc(100vh-4rem)] overflow-y-auto py-2">
{isLoading {isLoading
? [1, 2, 3, 4, 5].map((i) => <Skeleton key={i} className="h-10 mx-4 my-2 rounded" />) ? [1, 2, 3, 4, 5].map((i) => (
<Skeleton key={i} className="h-10 mx-4 my-2 rounded" />
))
: categories.map((category, index) => ( : categories.map((category, index) => (
<Link <Link
key={category.id} key={category.id}
href={`/category/${category.slug}?category_id=${category.id}`} href={`/category/${category.slug}?category_id=${category.id}`}
onClick={onCategoryClick} onClick={onCategoryClick}
onMouseEnter={() => onCategoryHover(index)} onMouseEnter={() => onCategoryHover(index)}
className="flex items-center gap-3 px-4 py-3 rounded-xl hover:bg-gray-100 hover:text-primary transition-colors" className="flex items-center gap-3 px-4 py-3 rounded-lg hover:bg-gray-100 hover:text-primary transition-colors"
> >
{category.icon_class && <i className={`${category.icon_class} text-xl`}></i>} {category.icon_class && (
<i className={`${category.icon_class} text-xl`} />
)}
<span>{category.name}</span> <span>{category.name}</span>
</Link> </Link>
))} ))}
</div> </div>
</div> </div>
) );
} }
interface SubcategoryListProps { interface SubcategoryListProps {
category: any category: any;
onSubcategoryClick: () => void onSubcategoryClick: () => void;
} }
function SubcategoryList({ category, onSubcategoryClick }: SubcategoryListProps) { function SubcategoryList({
category,
onSubcategoryClick,
}: SubcategoryListProps) {
return ( return (
<div className="flex-1 p-6"> <div className="flex-1 p-6">
<h3 className="text-xl font-semibold mb-4">{category.name}</h3> <h3 className="text-xl font-semibold mb-4">{category.name}</h3>
@@ -91,5 +160,5 @@ function SubcategoryList({ category, onSubcategoryClick }: SubcategoryListProps)
))} ))}
</div> </div>
</div> </div>
) );
} }

View File

@@ -3,7 +3,7 @@
"use client"; "use client";
import { useEffect, type ReactNode } from "react"; import { useEffect, type ReactNode } from "react";
import { useRouter, usePathname } from "next/navigation"; import { useRouter } from "next/navigation";
import { useAuthStatus, useGetGuestToken } from "@/lib/hooks/useAuth"; import { useAuthStatus, useGetGuestToken } from "@/lib/hooks/useAuth";
import { useUserProfile } from "@/features/profile/hooks/useUserProfile"; import { useUserProfile } from "@/features/profile/hooks/useUserProfile";
import Preloader from "@/components/PageLoader/PreLoader"; import Preloader from "@/components/PageLoader/PreLoader";
@@ -23,14 +23,12 @@ export default function AuthWrapper({
locale, locale,
}: AuthWrapperProps) { }: AuthWrapperProps) {
const router = useRouter(); const router = useRouter();
const pathname = usePathname();
const { isAuthenticated, isLoading } = useAuthStatus(); const { isAuthenticated, isLoading } = useAuthStatus();
const { mutate: getGuestToken, isPending: isGettingGuestToken } = useGetGuestToken(); const { mutate: getGuestToken, isPending: isGettingGuestToken } =
useGetGuestToken();
// Fetch user profile only if authenticated
useUserProfile(); useUserProfile();
// Initialize guest token if needed
useEffect(() => { useEffect(() => {
if (isLoading) return; if (isLoading) return;
@@ -39,23 +37,20 @@ export default function AuthWrapper({
} }
}, [isLoading, getGuestToken, isGettingGuestToken]); }, [isLoading, getGuestToken, isGettingGuestToken]);
// Handle redirects
useEffect(() => { useEffect(() => {
if (isLoading || isGettingGuestToken) return; if (isLoading || isGettingGuestToken) return;
// Redirect to login if auth required but not authenticated
if (requireAuth && !isAuthenticated) { if (requireAuth && !isAuthenticated) {
const redirect = redirectTo || `/${locale}/login`; router.push(`/${locale}`);
const returnUrl = pathname !== redirect ? `?returnUrl=${encodeURIComponent(pathname)}` : "";
router.push(`${redirect}${returnUrl}`);
return; return;
} }
}, [
if (isAuthenticated && (pathname.includes("/login") || pathname.includes("/register"))) { isAuthenticated,
router.push(`/${locale}`); isLoading,
} requireAuth,
}, [isAuthenticated, isLoading, requireAuth, pathname, router, locale, redirectTo, isGettingGuestToken]); router,
locale,
isGettingGuestToken,
]);
if (isLoading || (requireAuth && !isAuthenticated)) { if (isLoading || (requireAuth && !isAuthenticated)) {
return <Preloader />; return <Preloader />;
} }

View File

@@ -1,14 +1,15 @@
"use client" "use client";
import Image, { type StaticImageData } from "next/image" import Image, { type StaticImageData } from "next/image";
import { Swiper, SwiperSlide } from "swiper/react" import Link from "next/link";
import { Autoplay } from "swiper/modules" import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css" import { Autoplay } from "swiper/modules";
import "swiper/css";
type CarouselItem = { type CarouselItem = {
title: string title: string;
image: StaticImageData | string image: StaticImageData | string;
url?: string | null url?: string | null;
} };
export default function HeroCarousel({ items }: { items: CarouselItem[] }) { export default function HeroCarousel({ items }: { items: CarouselItem[] }) {
return ( return (
@@ -21,6 +22,20 @@ export default function HeroCarousel({ items }: { items: CarouselItem[] }) {
> >
{items.map((item, i) => ( {items.map((item, i) => (
<SwiperSlide key={i}> <SwiperSlide key={i}>
{item.url ? (
<Link
href={item.url}
className="block relative w-full h-[200px] sm:h-[300px] md:h-[496px]"
>
<Image
src={item.image}
alt={item.title}
fill
className="object-cover"
priority={i === 0}
/>
</Link>
) : (
<div className="relative w-full h-[200px] sm:h-[300px] md:h-[496px]"> <div className="relative w-full h-[200px] sm:h-[300px] md:h-[496px]">
<Image <Image
src={item.image} src={item.image}
@@ -30,9 +45,10 @@ export default function HeroCarousel({ items }: { items: CarouselItem[] }) {
priority={i === 0} priority={i === 0}
/> />
</div> </div>
)}
</SwiperSlide> </SwiperSlide>
))} ))}
</Swiper> </Swiper>
</section> </section>
) );
} }

View File

@@ -347,7 +347,7 @@ export default function ProductCard({
{isOutOfStock && ( {isOutOfStock && (
<div className="absolute inset-0 bg-black/50 flex items-center justify-center z-10"> <div className="absolute inset-0 bg-black/50 flex items-center justify-center z-10">
<Badge variant="secondary" className="text-sm font-bold"> <Badge variant="secondary" className="text-sm font-bold">
Out of Stock {t("outOfStock")}
</Badge> </Badge>
</div> </div>
)} )}

View File

@@ -171,6 +171,7 @@
"share_experience": "Поделитесь опытом с этим товаром", "share_experience": "Поделитесь опытом с этим товаром",
"rating": "Рейтинг", "rating": "Рейтинг",
"your_review": "Ваш отзыв", "your_review": "Ваш отзыв",
"submit": "Отправить",
"submitting": "Отправляется...", "submitting": "Отправляется...",
"submit_review": "Отправить отзыв", "submit_review": "Отправить отзыв",
"characters": "символы", "characters": "символы",
@@ -185,7 +186,11 @@
"collection_not_found": "Коллекция не найдена", "collection_not_found": "Коллекция не найдена",
"added_to_favorites": "Товар добавлен в избранное", "added_to_favorites": "Товар добавлен в избранное",
"submit_success": "Отзыв отправлен", "submit_success": "Отзыв отправлен",
"submit_error": "Произошла ошибка" "submit_error": "Произошла ошибка",
"title": "Открыть магазин",
"enter_email": "Введите email",
"uploadPatent": "Загрузить патент",
"outOfStock": "Нет в наличии"
} }

View File

@@ -172,6 +172,7 @@
"share_experience": "Bu haryt barada öz teswiriňizi ýazyň", "share_experience": "Bu haryt barada öz teswiriňizi ýazyň",
"rating": "Reýting", "rating": "Reýting",
"your_review": "Teswiriňiz", "your_review": "Teswiriňiz",
"sumbit": "Ugratmak",
"submitting": "Ugradylýar...", "submitting": "Ugradylýar...",
"submit_review": "Teswiri ugrat", "submit_review": "Teswiri ugrat",
"characters": "simbol", "characters": "simbol",
@@ -186,7 +187,9 @@
"collection_not_found": "Kolleksiýa tapylmady", "collection_not_found": "Kolleksiýa tapylmady",
"added_to_favorites": "Haryt saýlananlara goşuldy", "added_to_favorites": "Haryt saýlananlara goşuldy",
"submit_success": "Üstünlikli ugradyldy", "submit_success": "Üstünlikli ugradyldy",
"submit_error": "Ýalňyşlyk ýüze çykdy" "submit_error": "Ýalňyşlyk ýüze çykdy",
"title": "Magazin aç",
"enter_email": "Poçtaňyzy ýazyň",
"uploadPatent": "Patent goş",
"outOfStock": "Ammarda ýok"
} }

View File

@@ -222,25 +222,16 @@ export function useLogout() {
try { try {
await apiClient.post("/auth/logout", {}, { timeout: 5000 }); await apiClient.post("/auth/logout", {}, { timeout: 5000 });
} catch (error) { } catch (error) {
// Logout should succeed even if server call fails
console.warn("[Logout] Server call failed, clearing local state anyway"); console.warn("[Logout] Server call failed, clearing local state anyway");
} }
}, },
onSuccess: () => { onSuccess: () => {
TokenStorage.clearTokens(); TokenStorage.clearTokens();
queryClient.clear(); queryClient.clear();
if (typeof window !== "undefined") {
window.location.href = "/";
}
}, },
onError: () => { onError: () => {
TokenStorage.clearTokens(); TokenStorage.clearTokens();
queryClient.clear(); queryClient.clear();
if (typeof window !== "undefined") {
window.location.href = "/";
}
}, },
}); });
} }