1"use client"
2
3import { useState } from "react"
4import {
5 Check,
6 FrameCorners,
7 Palette,
8 Sticker as StickerIcon,
9 TextT,
10} from "@phosphor-icons/react"
11
12import { Switch } from "@/components/ui/switch"
13import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
14import { cn } from "@/lib/utils"
15import { TEXT_LENGTH_MAX, useBoothStore } from "@/stores/booth"
16import { EFFECTS, FRAMES, STICKERS, type EffectPreset } from "./presets"
17
18/* DESIGN-SYSTEM §10.3 dan acuan `customize.html`: rel kiri halaman Hias.
19 Tab pil (toggle-group) dengan kisi tiga kolom. Semua kontrol bisa dipakai
20 dengan keyboard; keadaan terpilih selalu diiringi lencana centang, bukan
21 hanya warna (DESIGN-SYSTEM §2). */
22
23type DecorTab = "frame" | "efek" | "stiker" | "teks"
24
25const TABS: { value: DecorTab; label: string; icon: typeof FrameCorners }[] = [
26 { value: "frame", label: "Frame", icon: FrameCorners },
27 { value: "efek", label: "Efek", icon: Palette },
28 { value: "stiker", label: "Stiker", icon: StickerIcon },
29 { value: "teks", label: "Teks", icon: TextT },
30]
31
32const SWATCH =
33 "relative box-border border-2 border-ink outline-none transition-transform duration-[120ms] focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-violet motion-reduce:transition-none"
34
35const FRAME_BARS = [0, 1, 2, 3]
36
37const EFFECT_GRADIENT =
38 "linear-gradient(135deg, var(--color-pink) 0%, var(--color-pink) 50%, var(--color-lime) 50%, var(--color-lime) 100%)"
39const EFFECT_PIXEL =
40 "repeating-conic-gradient(var(--color-pink) 0% 25%, var(--color-lime) 0% 50%)"
41
42function effectSwatchStyle(effect: EffectPreset) {
43 if (effect.params.kind === "pixelate") {
44 return {
45 backgroundImage: EFFECT_PIXEL,
46 backgroundSize: "16px 16px",
47 }
48 }
49 return {
50 backgroundImage: EFFECT_GRADIENT,
51 filter: effect.cssPreview.length > 0 ? effect.cssPreview : undefined,
52 }
53}
54
55function SelectedBadge() {
56 return (
57 <span className="absolute -top-2.5 -right-2.5 flex size-6 items-center justify-center rounded-pill border-2 border-ink bg-violet text-paper">
58 <Check weight="bold" className="size-3" aria-hidden />
59 </span>
60 )
61}
62
63export function DecorPanel() {
64 const frameId = useBoothStore((state) => state.frameId)
65 const effectId = useBoothStore((state) => state.effectId)
66 const textValue = useBoothStore((state) => state.texts[0]?.value ?? "")
67 const showDate = useBoothStore((state) => state.showDate)
68 const setFrame = useBoothStore((state) => state.setFrame)
69 const setEffect = useBoothStore((state) => state.setEffect)
70 const addSticker = useBoothStore((state) => state.addSticker)
71 const setStripText = useBoothStore((state) => state.setStripText)
72 const setShowDate = useBoothStore((state) => state.setShowDate)
73
74 const [tab, setTab] = useState<DecorTab>("frame")
75
76 return (
77 <div className="flex flex-col gap-4">
78 <ToggleGroup
79 type="single"
80 value={tab}
81 onValueChange={(value) => {
82 if (value !== "") {
83 setTab(value as DecorTab)
84 }
85 }}
86 aria-label="Pilihan hiasan"
87 className="w-full flex-wrap justify-start gap-2 rounded-none border-0 bg-transparent p-0"
88 >
89 {TABS.map((item) => (
90 <ToggleGroupItem key={item.value} value={item.value}>
91 <item.icon weight="bold" className="size-[18px]" aria-hidden />
92 {item.label}
93 </ToggleGroupItem>
94 ))}
95 </ToggleGroup>
96
97 {tab === "frame" ? (
98 <section className="flex flex-col gap-3">
99 <h2 className="font-mono text-xs font-bold tracking-[0.06em] uppercase">
100 Pilih frame
101 </h2>
102 <div className="grid grid-cols-3 gap-3.5">
103 {FRAMES.map((frame) => (
104 <div key={frame.id} className="flex flex-col items-center gap-1.5">
105 <button
106 type="button"
107 aria-pressed={frame.id === frameId}
108 aria-label={frame.label}
109 onClick={() => setFrame(frame.id)}
110 style={{ backgroundColor: `var(${frame.bg})` }}
111 className={cn(
112 SWATCH,
113 "flex aspect-[2/3] w-full flex-col gap-1 rounded-thumb p-2 pb-3.5 hover:-translate-y-px",
114 frame.id === frameId && "ring-selected"
115 )}
116 >
117 {FRAME_BARS.map((bar) => (
118 <span key={bar} className="flex-1 rounded-sm bg-cream" />
119 ))}
120 {frame.id === frameId ? <SelectedBadge /> : null}
121 </button>
122 <span className="font-mono text-xs font-bold">
123 {frame.label}
124 </span>
125 </div>
126 ))}
127 </div>
128 </section>
129 ) : null}
130
131 {tab === "efek" ? (
132 <section className="flex flex-col gap-3">
133 <h2 className="font-mono text-xs font-bold tracking-[0.06em] uppercase">
134 Efek foto
135 </h2>
136 <div className="grid grid-cols-3 gap-3.5">
137 {EFFECTS.map((effect) => (
138 <div key={effect.id} className="flex flex-col items-center gap-1.5">
139 <button
140 type="button"
141 aria-pressed={effect.id === effectId}
142 aria-label={effect.label}
143 onClick={() => setEffect(effect.id)}
144 className={cn(
145 SWATCH,
146 "aspect-square w-full rounded-thumb hover:-translate-y-px",
147 effect.id === effectId && "ring-selected"
148 )}
149 >
150 <span
151 className="block size-full overflow-hidden rounded-sm"
152 style={effectSwatchStyle(effect)}
153 />
154 {effect.id === effectId ? <SelectedBadge /> : null}
155 </button>
156 <span className="font-mono text-xs font-bold">
157 {effect.label}
158 </span>
159 </div>
160 ))}
161 </div>
162 </section>
163 ) : null}
164
165 {tab === "stiker" ? (
166 <section className="flex flex-col gap-3">
167 <h2 className="font-mono text-xs font-bold tracking-[0.06em] uppercase">
168 Stiker
169 </h2>
170 <div className="grid grid-cols-3 gap-3.5">
171 {STICKERS.map((sticker) => (
172 <button
173 key={sticker.id}
174 type="button"
175 aria-label={`Tempel ${sticker.label}`}
176 onClick={() => addSticker(sticker.id)}
177 className={cn(
178 SWATCH,
179 "flex aspect-square w-full items-center justify-center rounded-card bg-paper hover:-translate-y-px"
180 )}
181 >
182 <span
183 aria-hidden
184 className="size-9 bg-center bg-contain bg-no-repeat"
185 style={{ backgroundImage: `url(/stickers/${sticker.file})` }}
186 />
187 </button>
188 ))}
189 </div>
190 <p className="text-sm leading-[1.4] text-muted">
191 Ketuk untuk menempel. Geser, putar, dan ubah ukurannya di strip,
192 atau pakai daftar di bawah kanvas.
193 </p>
194 </section>
195 ) : null}
196
197 {tab === "teks" ? (
198 <section className="flex flex-col gap-4">
199 <h2 className="font-mono text-xs font-bold tracking-[0.06em] uppercase">
200 Teks strip
201 </h2>
202 <div className="flex flex-col gap-1.5">
203 <label htmlFor="strip-text" className="text-sm font-bold">
204 Teks
205 </label>
206 <input
207 id="strip-text"
208 value={textValue}
209 maxLength={TEXT_LENGTH_MAX}
210 placeholder="Tulis sesuatu"
211 onChange={(event) => setStripText(event.target.value)}
212 className="h-11 rounded-pill border-2 border-ink bg-paper px-4 font-sans text-[15px] font-medium outline-none focus-visible:outline-3 focus-visible:outline-offset-3 focus-visible:outline-violet"
213 />
214 <span className="font-mono text-xs text-muted">
215 {Array.from(textValue).length}/{TEXT_LENGTH_MAX}
216 </span>
217 </div>
218 <div className="flex items-center justify-between gap-3">
219 <span id="strip-date-label" className="text-sm font-bold">
220 Tanggal
221 </span>
222 <span className="flex items-center gap-2">
223 <Switch
224 aria-labelledby="strip-date-label"
225 checked={showDate}
226 onCheckedChange={setShowDate}
227 />
228 <span className="font-mono text-xs font-bold tracking-[0.06em] uppercase">
229 {showDate ? "Nyala" : "Mati"}
230 </span>
231 </span>
232 </div>
233 </section>
234 ) : null}
235 </div>
236 )
237}
238