返回 presentation-ai
FormatsSection.tsx
1 "use client";
2
3 import { Globe2 as GlobeIcon, Monitor, Smartphone } from "lucide-react";
4 import { useMemo } from "react";
5 import { VscLayers } from "react-icons/vsc";
6
7 import {
8 Select,
9 SelectContent,
10 SelectGroup,
11 SelectItem,
12 SelectLabel,
13 SelectTrigger,
14 SelectValue,
15 } from "@/components/ui/select";
16 import { cn } from "@/lib/utils";
17 import { useCommonValues } from "../hooks/useCommonValues";
18 import { useUpdateAllSlides } from "../hooks/useUpdateAllSlides";
19
20 export function FormatsSection() {
21 const { currentFormatCategory, currentAspectRatio } = useCommonValues();
22 const updateAllSlides = useUpdateAllSlides();
23
24 // aspect options per top-level format
25 const aspectOptions = useMemo(() => {
26 if (currentFormatCategory === "presentation") {
27 return [
28 { value: "fluid", label: "Default", sub: "Fluid", icon: "monitor" },
29 { value: "16:9", label: "Traditional", sub: "16:9", icon: "monitor" },
30 { value: "tall", label: "Tall", sub: "85vh", icon: "smartphone" },
31 ];
32 }
33 if (currentFormatCategory === "webpage") {
34 return [
35 {
36 value: "fluid",
37 label: "Fluid",
38 sub: "Responsive width",
39 icon: "globe",
40 },
41 {
42 value: "16:9",
43 label: "16:9 (wide section)",
44 sub: "16:9",
45 icon: "monitor",
46 },
47 { value: "A4", label: "A4", sub: "210×297mm", icon: "monitor" },
48 ];
49 }
50 if (currentFormatCategory === "social") {
51 return [
52 { value: "1:1", label: "Square", sub: "1:1", icon: "monitor" },
53 { value: "4:5", label: "Portrait", sub: "4:5", icon: "smartphone" },
54 { value: "9:16", label: "Story", sub: "9:16", icon: "smartphone" },
55 ];
56 }
57 // document
58 return [
59 { value: "A4", label: "A4", sub: "210×297mm", icon: "monitor" },
60 { value: "Letter", label: "Letter", sub: "8.5×11in", icon: "monitor" },
61 ];
62 }, [currentFormatCategory]);
63
64 const currentAspectValue = useMemo(() => {
65 // Figure out the actual aspect value based on currentAspectRatio
66 if (!currentAspectRatio) {
67 // Defaults: presentation/fluid, social/1:1, document/A4, webpage/fluid
68 if (
69 currentFormatCategory === "presentation" ||
70 currentFormatCategory === "webpage"
71 )
72 return "fluid";
73 if (currentFormatCategory === "social") return "1:1";
74 return "A4";
75 }
76 if (currentAspectRatio.type === "fluid") return "fluid";
77 if (currentAspectRatio.type === "tall") return "tall";
78 if (currentAspectRatio.type === "preset")
79 return currentAspectRatio.value ?? "A4";
80 if (currentAspectRatio.type === "ratio")
81 return currentAspectRatio.value ?? "16:9";
82 return "fluid";
83 }, [currentAspectRatio, currentFormatCategory]);
84
85 return (
86 <div className="space-y-4">
87 <div className="space-y-2">
88 <label className="flex items-center gap-2 text-sm font-semibold text-foreground">
89 <VscLayers className="h-4 w-4 text-muted-foreground" />
90 Format
91 </label>
92 <Select
93 value={currentFormatCategory}
94 onValueChange={(val) => {
95 // Types now allow for 'webpage'
96 const category = val as
97 | "presentation"
98 | "social"
99 | "document"
100 | "webpage";
101 // Enforce aspect ratio defaults for each category
102 if (category === "presentation") {
103 updateAllSlides({
104 formatCategory: category,
105 aspectRatio: { type: "fluid" },
106 });
107 } else if (category === "webpage") {
108 updateAllSlides({
109 formatCategory: category,
110 aspectRatio: { type: "fluid" },
111 });
112 } else if (category === "social") {
113 updateAllSlides({
114 formatCategory: category,
115 aspectRatio: { type: "ratio", value: "1:1" },
116 });
117 } else {
118 // document
119 updateAllSlides({
120 formatCategory: category,
121 aspectRatio: { type: "preset", value: "A4" },
122 });
123 }
124 }}
125 >
126 <SelectTrigger className="rounded-full">
127 <SelectValue placeholder="Choose format" />
128 </SelectTrigger>
129 <SelectContent>
130 <SelectGroup>
131 <SelectLabel>Format</SelectLabel>
132 <SelectItem value="presentation">Presentation</SelectItem>
133 {/* <SelectItem value="webpage">Webpage</SelectItem> */}
134 <SelectItem value="document">Document</SelectItem>
135 <SelectItem value="social">Social</SelectItem>
136 </SelectGroup>
137 </SelectContent>
138 </Select>
139 </div>
140
141 <div className="space-y-2">
142 <div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
143 {aspectOptions.map((o) => {
144 const active = currentAspectValue === o.value;
145 let Icon;
146 if (o.icon === "globe") {
147 Icon = GlobeIcon;
148 } else if (o.icon === "smartphone") {
149 Icon = Smartphone;
150 } else {
151 Icon = Monitor;
152 }
153
154 const onSelect = () => {
155 // "fluid" and "tall" aspect ratio
156 if (o.value === "fluid") {
157 updateAllSlides({ aspectRatio: { type: "fluid" } });
158 } else if (o.value === "tall") {
159 updateAllSlides({ aspectRatio: { type: "tall" } });
160 } else if (o.value === "A4" || o.value === "Letter") {
161 updateAllSlides({
162 aspectRatio: { type: "preset", value: o.value },
163 });
164 } else {
165 updateAllSlides({
166 aspectRatio: { type: "ratio", value: o.value },
167 });
168 }
169 };
170 return (
171 <button
172 key={o.value}
173 type="button"
174 onClick={onSelect}
175 className={cn(
176 "w-full rounded-xl border p-4 text-left transition-colors",
177 active
178 ? "border-primary bg-muted/30 ring ring-primary"
179 : "border-border hover:bg-muted/20",
180 )}
181 >
182 <div className="flex h-full flex-col items-start gap-2">
183 <Icon
184 className={cn(
185 "h-5 w-5",
186 active ? "text-primary" : "text-muted-foreground",
187 )}
188 />
189 <div className="text-base font-semibold">{o.label}</div>
190 <div className="text-sm text-muted-foreground">{o.sub}</div>
191 </div>
192 </button>
193 );
194 })}
195 </div>
196 </div>
197 </div>
198 );
199 }
200
200 lines Plain Text