返回 presentation-ai
UserFontPairs.tsx
1 "use client";
2
3 import { useQuery, useQueryClient } from "@tanstack/react-query";
4 import { Check, Trash2 } from "lucide-react";
5 import { useState } from "react";
6 import { toast } from "sonner";
7
8 import {
9 deleteFontPair,
10 getUserFontPairs,
11 } from "@/app/_actions/presentation/font-pair-actions";
12 import { Skeleton } from "@/components/ui/skeleton";
13 import { cn } from "@/lib/utils";
14
15 interface FontPair {
16 id: string;
17 heading: string;
18 headingUrl?: string | null;
19 body: string;
20 bodyUrl?: string | null;
21 createdAt: Date;
22 updatedAt: Date;
23 }
24
25 interface UserFontPairsProps {
26 currentHeading?: string;
27 currentBody?: string;
28 onSelect: (
29 heading: string,
30 body: string,
31 headingUrl?: string,
32 bodyUrl?: string,
33 headingWeight?: number,
34 bodyWeight?: number,
35 ) => void;
36 }
37
38 function FontPairSkeleton() {
39 return (
40 <div className="w-full space-y-1 rounded-lg border p-3">
41 <Skeleton className="h-4 w-24" />
42 <Skeleton className="h-3 w-20" />
43 </div>
44 );
45 }
46
47 export function UserFontPairs({
48 currentHeading,
49 currentBody,
50 onSelect,
51 }: UserFontPairsProps) {
52 const queryClient = useQueryClient();
53 const [deletingId, setDeletingId] = useState<string | null>(null);
54
55 const { data: fontPairs = [], isLoading } = useQuery({
56 queryKey: ["userFontPairs"],
57 queryFn: async () => {
58 const result = await getUserFontPairs();
59 return result.success ? (result.fontPairs as FontPair[]) : [];
60 },
61 });
62
63 const handleDelete = async (e: React.MouseEvent, id: string) => {
64 e.stopPropagation();
65 if (deletingId) return;
66
67 try {
68 setDeletingId(id);
69 const result = await deleteFontPair(id);
70
71 if (result.success) {
72 toast.success("Font pair deleted");
73 queryClient.invalidateQueries({ queryKey: ["userFontPairs"] });
74 } else {
75 toast.error(result.message || "Failed to delete font pair");
76 }
77 } catch {
78 try {
79 toast.error("An error occurred while deleting");
80 } catch (reactDoctorCatchError) {
81 setDeletingId(null);
82 throw reactDoctorCatchError;
83 }
84 }
85 setDeletingId(null);
86 };
87
88 // Don't render anything if there are no font pairs and not loading
89 if (!isLoading && fontPairs.length === 0) {
90 return null;
91 }
92
93 return (
94 <div className="space-y-3">
95 <span className="text-xs font-semibold text-muted-foreground uppercase">
96 Your Font Pairs
97 </span>
98 <div className="space-y-2">
99 {isLoading ? (
100 <>
101 <FontPairSkeleton />
102 <FontPairSkeleton />
103 <FontPairSkeleton />
104 </>
105 ) : (
106 fontPairs.map((pair) => {
107 const isSelected =
108 currentHeading === pair.heading && currentBody === pair.body;
109 const isDeleting = deletingId === pair.id;
110
111 return (
112 <div
113 key={pair.id}
114 className={cn(
115 "group relative flex w-full items-center justify-between rounded-lg border p-3 transition-all hover:bg-accent/50",
116 isSelected
117 ? "border-primary bg-primary/5"
118 : "border-border hover:border-border/80",
119 )}
120 >
121 <button
122 type="button"
123 className="flex-1 text-left"
124 onClick={() =>
125 onSelect(
126 pair.heading,
127 pair.body,
128 pair.headingUrl ?? undefined,
129 pair.bodyUrl ?? undefined,
130 )
131 }
132 >
133 <div className="space-y-1">
134 <div className="text-sm font-semibold text-foreground">
135 {pair.heading}
136 </div>
137 <div className="text-xs text-muted-foreground">
138 {pair.body}
139 </div>
140 </div>
141 </button>
142
143 <div className="flex items-center gap-2">
144 {isSelected && (
145 <Check className="size-4 shrink-0 text-primary" />
146 )}
147 <button
148 type="button"
149 onClick={(e) => handleDelete(e, pair.id)}
150 disabled={isDeleting}
151 className={cn(
152 "rounded-md p-1.5 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-destructive/10 hover:text-destructive",
153 isDeleting &&
154 "animate-pulse text-destructive opacity-100",
155 "focus:opacity-100 focus:outline-none",
156 )}
157 title="Delete font pair"
158 >
159 <Trash2 className="size-4" />
160 <span className="sr-only">Delete</span>
161 </button>
162 </div>
163 </div>
164 );
165 })
166 )}
167 </div>
168 </div>
169 );
170 }
171
171 lines Plain Text