diff --git a/app/[locale]/favicon.ico b/app/[locale]/favicon.ico deleted file mode 100644 index 718d6fe..0000000 Binary files a/app/[locale]/favicon.ico and /dev/null differ diff --git a/app/[locale]/favorites/page.tsx b/app/[locale]/favorites/page.tsx index 33fe7cc..595f993 100644 --- a/app/[locale]/favorites/page.tsx +++ b/app/[locale]/favorites/page.tsx @@ -26,30 +26,12 @@ export default function FavoritesPage() { useRemoveFromFavorites(); const { mutate: addToCart, isPending: isAddingToCart } = useAddToCart(); - const handleRemoveFromFavorites = useCallback((productId: number) => { - removeFromFavorites(productId, { - onSuccess: () => { - toast({ - title: t("removed_from_favorites"), - }); - }, - onError: (error) => { - toast({ - title: t("error"), - description: error.message, - variant: "destructive", - }); - }, - }); - }, [removeFromFavorites, toast, t]); - - const handleAddToCart = useCallback((productId: number) => { - addToCart( - { productId }, - { + const handleRemoveFromFavorites = useCallback( + (productId: number) => { + removeFromFavorites(productId, { onSuccess: () => { toast({ - title: t("added_to_cart"), + title: t("removed_from_favorites"), }); }, onError: (error) => { @@ -59,20 +41,47 @@ export default function FavoritesPage() { variant: "destructive", }); }, - } - ); - }, [addToCart, toast, t]); + }); + }, + [removeFromFavorites, toast, t] + ); - const loadingSkeleton = useMemo(() => ( -
-

{t("favorite_products")}

-
- {Array.from({ length: 10 }).map((_, i) => ( - - ))} + const handleAddToCart = useCallback( + (productId: number) => { + addToCart( + { productId }, + { + onSuccess: () => { + toast({ + title: t("added_to_cart"), + }); + }, + onError: (error) => { + toast({ + title: t("error"), + description: error.message, + variant: "destructive", + }); + }, + } + ); + }, + [addToCart, toast, t] + ); + + const loadingSkeleton = useMemo( + () => ( +
+

{t("favorite_products")}

+
+ {Array.from({ length: 10 }).map((_, i) => ( + + ))} +
-
- ), [t]); + ), + [t] + ); if (isLoading) { return loadingSkeleton; @@ -95,7 +104,7 @@ export default function FavoritesPage() {
{favorites.map((favorite: Favorite) => ( handleRemoveFromFavorites(favorite.product.id)} @@ -170,7 +179,6 @@ function ProductCard({ >
- {/* Favorite Button */} - {/* Product Image */} {product.name} - {/* Out of Stock Badge */} {product.stock === 0 && (
@@ -203,7 +209,6 @@ function ProductCard({ )}
- {/* Product Info */}

{product.name} @@ -217,7 +222,6 @@ function ProductCard({

- {/* Add to Cart Button - показывается при hover */} {isHovered && product.stock > 0 && (
diff --git a/components/layout/ui/ActionButtons.tsx b/components/layout/ui/ActionButtons.tsx index 902bf93..cf870ae 100644 --- a/components/layout/ui/ActionButtons.tsx +++ b/components/layout/ui/ActionButtons.tsx @@ -16,6 +16,7 @@ import { useCart, useFavorites, useOrders } from "@/lib/hooks"; import { Skeleton } from "@/components/ui/skeleton"; import { useTranslations } from "next-intl"; import { useLogout } from "@/lib/hooks/useAuth"; +import { CartIcon, FavoriteIcon, OrderIcon, ProfileIcon } from "@/components/icons"; interface ActionButtonsProps { isAuthenticated: boolean; @@ -70,21 +71,21 @@ export default function ActionButtons({ const buttons: ActionButtonData[] = useMemo(() => [ { - icon: , + icon: , label: t("common.orders"), href: "/orders", badgeCount: ordersCount, isLoading: ordersLoading, }, { - icon: , + icon: , label: t("common.favorites"), href: "/favorites", badgeCount: favoritesCount, isLoading: favoritesLoading, }, { - icon: , + icon: , label: t("common.cart"), href: "/cart", badgeCount: cartCount, @@ -101,7 +102,7 @@ export default function ActionButtons({ @@ -118,7 +119,7 @@ export default function ActionButtons({ ) : ( )} diff --git a/components/layout/ui/AuthDialog.tsx b/components/layout/ui/AuthDialog.tsx index 422e51c..ad07be7 100644 --- a/components/layout/ui/AuthDialog.tsx +++ b/components/layout/ui/AuthDialog.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"; import { toast } from "sonner"; -import Logo from "@/public/logo.png"; +import Logo from "@/public/logo.webp"; import { useLogin, useVerifyToken } from "@/lib/hooks/useAuth"; import { useTranslations } from "next-intl"; diff --git a/components/layout/ui/SearchBar.tsx b/components/layout/ui/SearchBar.tsx index 30eea7d..fcf8f93 100644 --- a/components/layout/ui/SearchBar.tsx +++ b/components/layout/ui/SearchBar.tsx @@ -13,6 +13,7 @@ import { import { useRouter } from "next/navigation"; import { useSearchProducts } from "@/features/search/hooks/useSearch"; import Image from "next/image"; +import { SearchIcon } from "@/components/icons"; interface SearchBarProps { isMobile: boolean; @@ -158,7 +159,7 @@ export default function SearchBar({ size="icon" className="h-auto hover:bg-[#005bff] cursor-pointer bg-transparent flex items-center mr-1.5 text-white" > - +
diff --git a/context/AuthWrapper.tsx b/context/AuthWrapper.tsx index 84ead5f..4b7ae08 100644 --- a/context/AuthWrapper.tsx +++ b/context/AuthWrapper.tsx @@ -4,6 +4,7 @@ import { useEffect, type ReactNode } from "react"; import { useRouter, usePathname } from "next/navigation"; import { useAuthStatus, useGetGuestToken } from "@/lib/hooks/useAuth"; import { useUserProfile } from "@/features/profile/hooks/useUserProfile"; +import Preloader from "@/components/PageLoader/PreLoader"; interface AuthWrapperProps { children: ReactNode; @@ -58,12 +59,7 @@ export default function AuthWrapper({ if (isLoading || (requireAuth && !isAuthenticated)) { return ( -
-
-
-

Yükleniyor...

-
-
+ ); } diff --git a/features/favorites/hooks/useFavorites.ts b/features/favorites/hooks/useFavorites.ts index 69398e4..0dea131 100644 --- a/features/favorites/hooks/useFavorites.ts +++ b/features/favorites/hooks/useFavorites.ts @@ -2,69 +2,148 @@ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { apiClient } from "@/lib/api"; import type { Favorite } from "@/lib/types/api"; -// Response tiplerini tanımlayalım interface FavoritesResponse { data?: Favorite[]; [key: string]: any; } -interface FavoriteActionResponse { - data?: string | Favorite[]; - [key: string]: any; -} - -// Response'u transform eden yardımcı fonksiyon function transformFavoritesResponse(response: any): Favorite[] { if (typeof response === "object" && response.data) { return response.data; } - if (typeof response === "string") { try { const parsed = JSON.parse(response); return parsed.data || []; - } catch (error) { - console.error("Failed to parse favorites response:", error); + } catch { return []; } } - return []; } -function transformActionResponse(response: any, defaultValue: string): string { - if (typeof response === "object" && response.data) { - return response.data; - } +// Fetch ALL favorite products (handle pagination on backend) +async function fetchAllFavorites(): Promise { + const allFavorites: Favorite[] = []; + let currentPage = 1; + let hasMorePages = true; - if (typeof response === "string") { + while (hasMorePages) { try { - const parsed = JSON.parse(response); - return parsed.data || defaultValue; - } catch (error) { - if (response.includes("")) { - return defaultValue; + const response = await apiClient.get("/favorites", { + params: { page: currentPage, perPage: 100 }, + }); + + const favorites = transformFavoritesResponse(response.data); + allFavorites.push(...favorites); + + // Check pagination + const pagination = response.data?.pagination; + if (pagination?.next_page_url) { + currentPage++; + } else { + hasMorePages = false; } - console.error(`Failed to parse favorite response:`, error); - return defaultValue; + } catch (error) { + // If pagination not supported, return what we have + hasMorePages = false; } } - return defaultValue; + return allFavorites; } +// Get all favorites with automatic pagination export function useFavorites() { return useQuery({ queryKey: ["favorites"], - queryFn: async () => { - const response = await apiClient.get("/favorites"); - return transformFavoritesResponse(response.data); - }, + queryFn: fetchAllFavorites, staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, // Keep in cache for 10 minutes retry: 1, }); } +// Get favorite product IDs as Set for O(1) lookup - ALWAYS loads favorites first +export function useFavoriteIds() { + const { data: favorites, isLoading } = useFavorites(); + + // Return Set with IDs, empty Set while loading + return { + favoriteIds: new Set(favorites?.map((fav) => fav.product.id) || []), + isLoading, + }; +} + +// Check if product is favorited - with loading state +export function useIsFavorite(productId: number) { + const { favoriteIds, isLoading } = useFavoriteIds(); + + return { + isFavorite: favoriteIds.has(productId), + isLoading, + }; +} + +// Toggle favorite (add/remove) with optimistic updates +export function useToggleFavorite() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ + productId, + isFavorite, + }: { + productId: number; + isFavorite: boolean; + }) => { + const formData = new URLSearchParams({ + product_id: productId.toString(), + }); + + await apiClient.post("/favorites", formData, { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + }, + }); + + return { productId, wasAdded: !isFavorite }; + }, + onMutate: async ({ productId, isFavorite }) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: ["favorites"] }); + + // Snapshot previous + const previousFavorites = queryClient.getQueryData([ + "favorites", + ]); + + // Optimistically update + queryClient.setQueryData(["favorites"], (old = []) => { + if (isFavorite) { + // Remove from favorites + return old.filter((fav) => fav.product.id !== productId); + } + // For add, we'll refetch to get full product data + return old; + }); + + return { previousFavorites }; + }, + onError: (err, variables, context) => { + // Rollback on error + if (context?.previousFavorites) { + queryClient.setQueryData(["favorites"], context.previousFavorites); + } + }, + onSettled: () => { + // Refetch to ensure consistency + queryClient.invalidateQueries({ queryKey: ["favorites"] }); + }, + }); +} + +// Add to favorites export function useAddToFavorites() { const queryClient = useQueryClient(); @@ -74,13 +153,13 @@ export function useAddToFavorites() { product_id: productId.toString(), }); - const response = await apiClient.post("/favorites", formData, { + await apiClient.post("/favorites", formData, { headers: { "Content-Type": "application/x-www-form-urlencoded", }, }); - return transformActionResponse(response.data, "Added"); + return productId; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["favorites"] }); @@ -88,6 +167,7 @@ export function useAddToFavorites() { }); } +// Remove from favorites export function useRemoveFromFavorites() { const queryClient = useQueryClient(); @@ -97,13 +177,13 @@ export function useRemoveFromFavorites() { product_id: productId.toString(), }); - const response = await apiClient.post("/favorites", formData, { + await apiClient.post("/favorites", formData, { headers: { "Content-Type": "application/x-www-form-urlencoded", }, }); - return transformActionResponse(response.data, "Removed"); + return productId; }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["favorites"] }); diff --git a/features/home/components/Carousel.tsx b/features/home/components/Carousel.tsx index aaca9a1..6884b71 100644 --- a/features/home/components/Carousel.tsx +++ b/features/home/components/Carousel.tsx @@ -21,7 +21,7 @@ export default function HeroCarousel({ items }: { items: CarouselItem[] }) { > {items.map((item, i) => ( -
+
{item.title} setMounted(true), []); const loadMore = () => { @@ -48,8 +55,12 @@ export default function HomePage() { 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 && ( )} @@ -62,6 +73,13 @@ export default function HomePage() { title={t("categories")} /> + {showFavoritesLoading && ( +
+
+

Loading favorites...

+
+ )} + {collectionsError ? (

@@ -115,4 +133,4 @@ export default function HomePage() { )}

); -} \ No newline at end of file +} diff --git a/features/home/components/ProductCard.tsx b/features/home/components/ProductCard.tsx index 762289e..1b105b8 100644 --- a/features/home/components/ProductCard.tsx +++ b/features/home/components/ProductCard.tsx @@ -13,6 +13,7 @@ import { } from "@/components/ui/carousel"; import { Card, CardContent } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; +import { useToggleFavorite, useIsFavorite } from "@/lib/hooks"; type ProductCardProps = { id: number; @@ -22,12 +23,10 @@ type ProductCardProps = { discount?: number | null; discount_text?: string | null; images: string[]; - is_favorite: boolean; labels?: { text: string; bg_color: string }[]; price_color?: string; height?: number; width?: number; - button?: boolean; }; export default function ProductCard({ @@ -38,32 +37,29 @@ export default function ProductCard({ discount, discount_text, images, - is_favorite, labels = [], price_color = "#005bff", height = 360, width = 280, - button = true, }: ProductCardProps) { - const [favorite, setFavorite] = useState(is_favorite); + const { isFavorite, isLoading: isFavoriteLoading } = useIsFavorite(id); + const { mutate: toggleFavorite, isPending } = useToggleFavorite(); + const [api, setApi] = useState(); const [current, setCurrent] = useState(0); const autoplayRef = useRef(null); const hasMultipleImages = images.length > 1; - // Track carousel current slide useEffect(() => { if (!api) return; setCurrent(api.selectedScrollSnap()); - api.on("select", () => { setCurrent(api.selectedScrollSnap()); }); }, [api]); - // Auto-play functionality - 3 seconds useEffect(() => { if (!api || !hasMultipleImages) return; @@ -85,28 +81,34 @@ export default function ProductCard({ }; startAutoplay(); - return () => stopAutoplay(); }, [api, hasMultipleImages]); - const handleFavorite = async (e: MouseEvent) => { + const handleFavorite = (e: MouseEvent) => { e.preventDefault(); e.stopPropagation(); - const newFavoriteState = !favorite; - setFavorite(newFavoriteState); - - if (newFavoriteState) { - toast.success("Товар добавлен в избранное"); - } else { - toast.success("Товар удален из избранного"); - } + toggleFavorite( + { productId: id, isFavorite }, + { + onSuccess: (data) => { + toast.success( + data.wasAdded + ? "Товар добавлен в избранное" + : "Товар удален из избранного" + ); + }, + onError: () => { + toast.error("Ошибка. Попробуйте снова"); + }, + } + ); }; const handleCardClick = (e: MouseEvent) => { const target = e.target as HTMLElement; if ( - target.closest('button') || + target.closest("button") || target.closest('[data-carousel-control="true"]') ) { e.preventDefault(); @@ -122,20 +124,19 @@ export default function ProductCard({ return ( - {/* Image Section with Carousel */}
{images.map((image, index) => ( -
+
{`${name} - {/* Navigation Arrows - Only show if multiple images */} {hasMultipleImages && ( <> - {/* Favorite Button */} + {/* Favorite button - show skeleton while loading favorites */} - {/* Image Indicators */} {hasMultipleImages && (
{images.map((_, index) => ( @@ -200,7 +202,6 @@ export default function ProductCard({
)} - {/* Labels */} {labels?.length > 0 && (
{labels.map((label, idx) => ( @@ -216,17 +217,18 @@ export default function ProductCard({ )}
- {/* Content */}

{struct_price_text}

-

{name}

+

+ {name} +

); -} \ No newline at end of file +} diff --git a/features/home/components/ProductGrid.tsx b/features/home/components/ProductGrid.tsx index 448996a..f529770 100644 --- a/features/home/components/ProductGrid.tsx +++ b/features/home/components/ProductGrid.tsx @@ -60,7 +60,7 @@ export default function CollectionSection({ collection, locale }: Props) { const displayProducts = productsData?.data.slice(0, 10) || []; return ( -
+
{displayProducts.map((product) => { - // 🔥 TÜM RESİMLERİ AL - Burada değişiklik! - const allImages = product.media?.map( - (media) => - media.images_800x800 || - media.images_720x720 || - media.images_400x400 || - media.thumbnail - ).filter(Boolean) || ["/placeholder-product.jpg"]; + const allImages = product.media + ?.map( + (media) => + media.images_800x800 || + media.images_720x720 || + media.images_400x400 || + media.thumbnail + ) + .filter(Boolean) || ["/placeholder-product.jpg"]; const formattedPrice = product.price_amount ? `${parseFloat(product.price_amount).toFixed(2)} TMT` @@ -95,17 +96,15 @@ export default function CollectionSection({ collection, locale }: Props) { product.price_amount ? parseFloat(product.price_amount) : null } struct_price_text={formattedPrice} - images={allImages} // 🔥 Array olarak tüm resimler - is_favorite={false} + images={allImages} labels={[]} - price_color="#111" + price_color="#0059ff" height={360} width={250} - button={false} /> ); })}
); -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 2d41069..6f6afb7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "baseline-browser-mapping": "^2.9.5", "eslint": "^9", "eslint-config-next": "16.0.1", "tailwindcss": "^4", @@ -3868,9 +3869,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.8.21", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.21.tgz", - "integrity": "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q==", + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.5.tgz", + "integrity": "sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/package.json b/package.json index aee9a66..f6aff0e 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", + "baseline-browser-mapping": "^2.9.5", "eslint": "^9", "eslint-config-next": "16.0.1", "tailwindcss": "^4", diff --git a/public/logo.png b/public/logo.png deleted file mode 100644 index c7c8c4d..0000000 Binary files a/public/logo.png and /dev/null differ diff --git a/public/logo.webp b/public/logo.webp new file mode 100644 index 0000000..e1debe6 Binary files /dev/null and b/public/logo.webp differ diff --git a/public/seller.png b/public/seller.png new file mode 100644 index 0000000..a8aa0aa Binary files /dev/null and b/public/seller.png differ