diff --git a/src/app/global-error.tsx b/src/app/global-error.tsx index 8c2b6bb..cc0f8db 100644 --- a/src/app/global-error.tsx +++ b/src/app/global-error.tsx @@ -1,5 +1,6 @@ "use client" +import Link from "next/link" import { useEffect } from "react" import { AlertTriangle, Home, RefreshCcw } from "lucide-react" import { getCurrentLocale, translate } from "@/shared/i18n/translate" @@ -73,13 +74,13 @@ export default function GlobalError({ error, reset }: GlobalErrorProps) { {translate("common.retry")} - {translate("common.go-home")} - + diff --git a/src/app/users/[username]/user-profile-client.tsx b/src/app/users/[username]/user-profile-client.tsx index 592f9a2..a31c4e2 100644 --- a/src/app/users/[username]/user-profile-client.tsx +++ b/src/app/users/[username]/user-profile-client.tsx @@ -1,6 +1,6 @@ "use client" -import { useParams, useRouter } from "next/navigation" +import { useRouter } from "next/navigation" import { motion } from "framer-motion" import { RefreshCcw, @@ -22,12 +22,12 @@ import { TiltCard } from "@/shared/components/ui/tilt-card" import { OptimizedAvatar } from "@/shared/components/optimized-avatar" import { Button } from "@/shared/components/button" import { Skeleton } from "@/shared/components/skeleton" -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/shared/components/card" +import { Card, CardContent, CardTitle, CardDescription } from "@/shared/components/card" import { toast } from "sonner" import { cn } from "@/shared/lib/utils" import { getErrorMessage } from "@/shared/lib/api-client" import { useEffect, useState } from "react" -import { TIER_STYLES, getTierStyle } from "@/shared/constants/tier-styles" +import { TIER_STYLES } from "@/shared/constants/tier-styles" import { useI18n } from "@/shared/providers/locale-provider" interface UserProfileClientProps { @@ -45,8 +45,11 @@ export function UserProfileClient({ username }: UserProfileClientProps) { useEffect(() => { if (user) { - setDisplayPercentile(0) - setTimeout(() => setDisplayPercentile(user.percentile), 100) + const timeoutId = window.setTimeout(() => { + setDisplayPercentile(user.percentile) + }, 100) + + return () => window.clearTimeout(timeoutId) } }, [user]) diff --git a/src/features/auth/store/auth-store.ts b/src/features/auth/store/auth-store.ts index 4a37ece..aa3a765 100644 --- a/src/features/auth/store/auth-store.ts +++ b/src/features/auth/store/auth-store.ts @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import { useSyncExternalStore } from 'react'; import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { User } from '@/shared/types/api'; @@ -24,17 +24,15 @@ export const useAuthStore = create()( ) ); -export const useAuthHydrated = () => { - const [hydrated, setHydrated] = useState(false); - - useEffect(() => { - if (useAuthStore.persist?.hasHydrated?.()) { - setHydrated(true); - return; - } - const unsub = useAuthStore.persist?.onFinishHydration?.(() => setHydrated(true)); - return () => unsub?.(); - }, []); +const subscribeToAuthHydration = (onStoreChange: () => void) => { + const unsubscribe = useAuthStore.persist?.onFinishHydration?.(() => onStoreChange()); + return () => unsubscribe?.(); +}; - return hydrated; +export const useAuthHydrated = () => { + return useSyncExternalStore( + subscribeToAuthHydration, + () => useAuthStore.persist?.hasHydrated?.() ?? false, + () => false + ); }; diff --git a/src/features/home/components/hero-section.tsx b/src/features/home/components/hero-section.tsx index 49c3cd4..d8723c6 100644 --- a/src/features/home/components/hero-section.tsx +++ b/src/features/home/components/hero-section.tsx @@ -1,6 +1,6 @@ "use client" -import { useState, useEffect, useRef, useCallback } from "react" +import { useState, useRef, useCallback } from "react" import { motion, AnimatePresence, useScroll, useTransform } from "framer-motion" import { useRouter } from "next/navigation" import { Search, History, X, BookOpen, TrendingUp } from "lucide-react" @@ -17,6 +17,7 @@ import { LiveTicker } from "@/shared/components/ui/live-ticker" import { toast } from "sonner" import { useI18n } from "@/shared/providers/locale-provider" import { localizePathname } from "@/shared/i18n/config" +import { useHasMounted } from "@/shared/hooks/use-has-mounted" export function HeroSection() { const { t, locale } = useI18n() @@ -25,10 +26,10 @@ export function HeroSection() { const [open, setOpen] = useState(false) const [isFocused, setIsFocused] = useState(false) const [query, setQuery] = useState("") - const [mounted, setMounted] = useState(false) const [selectedIndex, setSelectedIndex] = useState(-1) const inputRef = useRef(null) const sectionRef = useRef(null) + const mounted = useHasMounted() const isMobile = useIsMobile() const prefersReducedMotion = useReducedMotion() @@ -54,9 +55,8 @@ export function HeroSection() { ? t("home.search.placeholder.mobile", { example: placeholderText }) : t("home.search.placeholder.desktop", { example: placeholderText }) - useEffect(() => { - setMounted(true) - }, []) + const selectedSearch = selectedIndex >= 0 ? recentSearches[selectedIndex] ?? null : null + const activeQuery = selectedSearch ?? query const handleFocus = useCallback(() => { setOpen(true) @@ -92,7 +92,7 @@ export function HeroSection() { const handleKeyDown = useCallback((e: React.KeyboardEvent) => { if (!open) { - if (e.key === 'Enter') handleSearch(query) + if (e.key === 'Enter') handleSearch(activeQuery) return } switch (e.key) { @@ -107,7 +107,7 @@ export function HeroSection() { case 'Enter': e.preventDefault() if (selectedIndex >= 0) handleSearch(recentSearches[selectedIndex]) - else handleSearch(query) + else handleSearch(activeQuery) break case 'Escape': setOpen(false) @@ -116,13 +116,7 @@ export function HeroSection() { inputRef.current?.blur() break } - }, [open, query, recentSearches, selectedIndex, handleSearch]) - - useEffect(() => { - if (selectedIndex >= 0 && recentSearches[selectedIndex]) { - setQuery(recentSearches[selectedIndex]) - } - }, [selectedIndex, recentSearches]) + }, [activeQuery, open, recentSearches, selectedIndex, handleSearch]) return (
{ setQuery(e.target.value) setSelectedIndex(-1) @@ -215,7 +209,7 @@ export function HeroSection() { diff --git a/src/features/ranking/api/ranking-service.ts b/src/features/ranking/api/ranking-service.ts index ef0763c..4916d28 100644 --- a/src/features/ranking/api/ranking-service.ts +++ b/src/features/ranking/api/ranking-service.ts @@ -11,7 +11,7 @@ export const getRankingList = cache(async (page: number, tier?: Tier): Promise(`/ranking?${params.toString()}`) + return apiClient.get(`/ranking?${params.toString()}`) }) export const useRankingList = (page: number, tier?: Tier) => { @@ -20,4 +20,4 @@ export const useRankingList = (page: number, tier?: Tier) => { queryFn: () => getRankingList(page, tier), placeholderData: (previousData) => previousData, }) -} \ No newline at end of file +} diff --git a/src/features/user/components/badge-generator.tsx b/src/features/user/components/badge-generator.tsx index 291f357..531e706 100644 --- a/src/features/user/components/badge-generator.tsx +++ b/src/features/user/components/badge-generator.tsx @@ -3,7 +3,7 @@ import { useState } from "react" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/shared/components/card" import { Button } from "@/shared/components/button" -import { Check, Copy, Link2, Code2, ExternalLink } from "lucide-react" +import { Check, Copy, Link2, Code2, ExternalLink, type LucideIcon } from "lucide-react" import { toast } from "sonner" import { cn } from "@/shared/lib/utils" import { useI18n } from "@/shared/providers/locale-provider" @@ -15,6 +15,36 @@ interface BadgeGeneratorProps { } type CopyType = "markdown" | "html" | "link" | null +type BadgeCopyType = Exclude + +interface BadgeCopyButtonProps { + copied: CopyType + icon: LucideIcon + label: string + onCopy: () => void + type: BadgeCopyType +} + +function BadgeCopyButton({ copied, icon: Icon, label, onCopy, type }: BadgeCopyButtonProps) { + return ( + + ) +} export function BadgeGenerator({ nodeId, username }: BadgeGeneratorProps) { const { t, locale } = useI18n() @@ -27,7 +57,7 @@ export function BadgeGenerator({ nodeId, username }: BadgeGeneratorProps) { const markdownCode = `[![Git Ranker](${badgeUrl})](${profileUrl})` const htmlCode = `Git Ranker Badge` - const handleCopy = async (text: string, type: CopyType) => { + const handleCopy = async (text: string, type: BadgeCopyType) => { await navigator.clipboard.writeText(text) setCopied(type) toast.success( @@ -38,35 +68,6 @@ export function BadgeGenerator({ nodeId, username }: BadgeGeneratorProps) { setTimeout(() => setCopied(null), 2000) } - const CopyButton = ({ - type, - text, - icon: Icon, - label - }: { - type: CopyType - text: string - icon: React.ElementType - label: string - }) => ( - - ) - return ( @@ -104,23 +105,26 @@ export function BadgeGenerator({ nodeId, username }: BadgeGeneratorProps) { {/* Copy Buttons */}
- handleCopy(markdownCode, "markdown")} /> - handleCopy(htmlCode, "html")} /> - handleCopy(badgeUrl, "link")} />