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
3. Filter nahili isleyar
4. Order nadip otmen etmeli.
5. Review feed back yazylyan yer bamy bolmalymy
6. Open Store api field ler nahili bolmaly.
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 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();
@@ -58,7 +60,7 @@ export default function FavoritesPage() {
media.images_400x400 ||
media.thumbnail
)
.filter(Boolean) || ["/placeholder-product.jpg"];
.filter(Boolean) || [Placeholder];
const formattedPrice = product.price_amount
? `${parseFloat(product.price_amount).toFixed(2)} TMT`

View File

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

View File

@@ -1,9 +1,10 @@
// Header.tsx
"use client";
import { useState, useEffect, useCallback } from "react";
import Link from "next/link";
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 Logo from "@/public/logo.webp";
import CategoryMenu from "./ui/CategoryMenu";
@@ -54,7 +55,7 @@ export default function Header({ locale = "ru" }: HeaderProps) {
return (
<>
<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">
<Link href="/" className="shrink-0">
<div className="relative h-8 w-[180px]">
@@ -69,6 +70,7 @@ export default function Header({ locale = "ru" }: HeaderProps) {
</Link>
<Button
data-catalog-trigger
onClick={toggleCategoryMenu}
className="cursor-pointer hidden gap-2 rounded-lg font-bold lg:flex hover:bg-[#005bff] bg-[#005bff] text-white"
size="lg"

View File

@@ -67,8 +67,13 @@ const cartCount = useCartCount()
return Array.isArray(ordersData) ? ordersData.length : 0;
}, [ordersData]);
const handleLogout = () => {
logout();
const handleLogout = () => {
logout(undefined, {
onSuccess: () => {
router.push(`/${locale}`);
router.refresh();
}
});
};
const buttons: ActionButtonData[] = useMemo(

View File

@@ -1,80 +1,149 @@
"use client"
import { useState } from "react"
import Link from "next/link"
import { useCategories } from "@/lib/hooks"
import { Skeleton } from "@/components/ui/skeleton"
// CategoryMenu.tsx
"use client";
import { useState, useEffect, useRef } from "react";
import Link from "next/link";
import { useCategories } from "@/lib/hooks";
import { Skeleton } from "@/components/ui/skeleton";
interface CategoryMenuProps {
isOpen: boolean
onClose: () => void
isOpen: boolean;
onClose: () => void;
}
export default function CategoryMenu({ isOpen, onClose }: CategoryMenuProps) {
const [hoveredCategory, setHoveredCategory] = useState<number | null>(null)
const { data: categories, isLoading } = useCategories()
const [hoveredCategory, setHoveredCategory] = useState<number | null>(null);
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 activeCategory = hoveredCategory !== null ? categoryList[hoveredCategory] : null
const handleClickOutside = (event: MouseEvent) => {
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 (
<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="flex">
<CategoryList
categories={categoryList}
isLoading={isLoading}
onCategoryHover={setHoveredCategory}
onCategoryClick={onClose}
/>
<>
<div className="fixed inset-0 bg-black/20 z-30" onClick={onClose} />
{activeCategory?.children && <SubcategoryList category={activeCategory} onSubcategoryClick={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">
<CategoryList
categories={categoryList}
isLoading={isLoading}
onCategoryHover={setHoveredCategory}
onCategoryClick={onClose}
/>
{activeCategory?.children && (
<SubcategoryList
category={activeCategory}
onSubcategoryClick={onClose}
/>
)}
</div>
</div>
</div>
</div>
)
</>
);
}
interface CategoryListProps {
categories: any[]
isLoading: boolean
onCategoryHover: (index: number) => void
onCategoryClick: () => void
categories: any[];
isLoading: boolean;
onCategoryHover: (index: number) => void;
onCategoryClick: () => void;
}
function CategoryList({ categories, isLoading, onCategoryHover, onCategoryClick }: CategoryListProps) {
function CategoryList({
categories,
isLoading,
onCategoryHover,
onCategoryClick,
}: CategoryListProps) {
return (
<div className="w-[280px] border-r">
<div className="max-h-[calc(100vh-4rem)] overflow-y-auto py-2">
{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) => (
<Link
key={category.id}
href={`/category/${category.slug}?category_id=${category.id}`}
onClick={onCategoryClick}
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>
</Link>
))}
</div>
</div>
)
);
}
interface SubcategoryListProps {
category: any
onSubcategoryClick: () => void
category: any;
onSubcategoryClick: () => void;
}
function SubcategoryList({ category, onSubcategoryClick }: SubcategoryListProps) {
function SubcategoryList({
category,
onSubcategoryClick,
}: SubcategoryListProps) {
return (
<div className="flex-1 p-6">
<h3 className="text-xl font-semibold mb-4">{category.name}</h3>
@@ -91,5 +160,5 @@ function SubcategoryList({ category, onSubcategoryClick }: SubcategoryListProps)
))}
</div>
</div>
)
);
}

View File

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

View File

@@ -1,38 +1,54 @@
"use client"
import Image, { type StaticImageData } from "next/image"
import { Swiper, SwiperSlide } from "swiper/react"
import { Autoplay } from "swiper/modules"
import "swiper/css"
"use client";
import Image, { type StaticImageData } from "next/image";
import Link from "next/link";
import { Swiper, SwiperSlide } from "swiper/react";
import { Autoplay } from "swiper/modules";
import "swiper/css";
type CarouselItem = {
title: string
image: StaticImageData | string
url?: string | null
}
title: string;
image: StaticImageData | string;
url?: string | null;
};
export default function HeroCarousel({ items }: { items: CarouselItem[] }) {
return (
<section className="rounded-2xl overflow-hidden">
<Swiper
modules={[Autoplay]}
slidesPerView={1}
loop
<Swiper
modules={[Autoplay]}
slidesPerView={1}
loop
autoplay={{ delay: 3000, disableOnInteraction: false }}
>
{items.map((item, i) => (
<SwiperSlide key={i}>
<div className="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}
/>
</div>
{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]">
<Image
src={item.image}
alt={item.title}
fill
className="object-cover"
priority={i === 0}
/>
</div>
)}
</SwiperSlide>
))}
</Swiper>
</section>
)
}
);
}

View File

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

View File

@@ -171,6 +171,7 @@
"share_experience": "Поделитесь опытом с этим товаром",
"rating": "Рейтинг",
"your_review": "Ваш отзыв",
"submit": "Отправить",
"submitting": "Отправляется...",
"submit_review": "Отправить отзыв",
"characters": "символы",
@@ -185,7 +186,11 @@
"collection_not_found": "Коллекция не найдена",
"added_to_favorites": "Товар добавлен в избранное",
"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ň",
"rating": "Reýting",
"your_review": "Teswiriňiz",
"sumbit": "Ugratmak",
"submitting": "Ugradylýar...",
"submit_review": "Teswiri ugrat",
"characters": "simbol",
@@ -186,7 +187,9 @@
"collection_not_found": "Kolleksiýa tapylmady",
"added_to_favorites": "Haryt saýlananlara goşuldy",
"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 {
await apiClient.post("/auth/logout", {}, { timeout: 5000 });
} catch (error) {
// Logout should succeed even if server call fails
console.warn("[Logout] Server call failed, clearing local state anyway");
}
},
onSuccess: () => {
TokenStorage.clearTokens();
queryClient.clear();
if (typeof window !== "undefined") {
window.location.href = "/";
}
},
onError: () => {
TokenStorage.clearTokens();
queryClient.clear();
if (typeof window !== "undefined") {
window.location.href = "/";
}
},
});
}