|
| 1 | +"use client"; |
| 2 | + |
| 3 | +import { useEffect, useState } from "react"; |
| 4 | +import { Check } from "lucide-react"; |
| 5 | + |
| 6 | +export function RealtimeVotingAnimation() { |
| 7 | + const [hovered, setHovered] = useState<number | null>(null); |
| 8 | + const [selected, setSelected] = useState<number | null>(null); |
| 9 | + |
| 10 | + useEffect(() => { |
| 11 | + let isMounted = true; |
| 12 | + const play = async () => { |
| 13 | + while (isMounted) { |
| 14 | + setHovered(null); |
| 15 | + setSelected(null); |
| 16 | + await new Promise((r) => setTimeout(r, 1000)); |
| 17 | + if (!isMounted) break; |
| 18 | + |
| 19 | + setHovered(2); // Card '5' |
| 20 | + await new Promise((r) => setTimeout(r, 400)); |
| 21 | + if (!isMounted) break; |
| 22 | + |
| 23 | + setSelected(2); |
| 24 | + await new Promise((r) => setTimeout(r, 2500)); |
| 25 | + } |
| 26 | + }; |
| 27 | + play(); |
| 28 | + return () => { |
| 29 | + isMounted = false; |
| 30 | + }; |
| 31 | + }, []); |
| 32 | + |
| 33 | + const cards = [1, 3, 5, 8]; |
| 34 | + |
| 35 | + return ( |
| 36 | + <div className="absolute inset-0 flex items-end justify-center pr-0 pb-4 sm:pb-16 pointer-events-none"> |
| 37 | + <div className="absolute right-1/4 bottom-0 w-96 h-96 bg-primary/5 rounded-full blur-3xl"></div> |
| 38 | + <div className="flex gap-4 sm:gap-8 z-10 translate-y-2 sm:translate-y-4 opacity-90"> |
| 39 | + {cards.map((c, i) => ( |
| 40 | + <div |
| 41 | + key={c} |
| 42 | + className={`w-16 h-24 sm:w-28 sm:h-40 rounded-2xl flex items-center justify-center font-bold text-2xl sm:text-5xl transition-all duration-300 |
| 43 | + ${ |
| 44 | + selected === i |
| 45 | + ? "bg-primary text-primary-foreground -translate-y-8 shadow-2xl scale-110" |
| 46 | + : hovered === i |
| 47 | + ? "bg-white dark:bg-zinc-800 text-primary -translate-y-4 shadow-xl border border-primary/30" |
| 48 | + : "bg-white dark:bg-zinc-800 text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-zinc-700 shadow-md" |
| 49 | + } |
| 50 | + `} |
| 51 | + > |
| 52 | + {c} |
| 53 | + </div> |
| 54 | + ))} |
| 55 | + </div> |
| 56 | + </div> |
| 57 | + ); |
| 58 | +} |
| 59 | + |
| 60 | +export function AnalyticsAnimation() { |
| 61 | + const [revealed, setRevealed] = useState(false); |
| 62 | + |
| 63 | + useEffect(() => { |
| 64 | + let isMounted = true; |
| 65 | + const play = async () => { |
| 66 | + while (isMounted) { |
| 67 | + setRevealed(false); |
| 68 | + await new Promise((r) => setTimeout(r, 1000)); |
| 69 | + if (!isMounted) break; |
| 70 | + |
| 71 | + setRevealed(true); |
| 72 | + await new Promise((r) => setTimeout(r, 3500)); |
| 73 | + } |
| 74 | + }; |
| 75 | + play(); |
| 76 | + return () => { |
| 77 | + isMounted = false; |
| 78 | + }; |
| 79 | + }, []); |
| 80 | + |
| 81 | + const bars = [ |
| 82 | + { height: "h-6", target: "h-12", value: "3" }, |
| 83 | + { height: "h-6", target: "h-24", value: "5", active: true }, |
| 84 | + { height: "h-6", target: "h-16", value: "8" }, |
| 85 | + { height: "h-6", target: "h-8", value: "13" }, |
| 86 | + ]; |
| 87 | + |
| 88 | + return ( |
| 89 | + <div className="absolute inset-0 flex items-end justify-center pr-0 pb-4 sm:pb-10 pointer-events-none"> |
| 90 | + <div className="absolute right-1/4 bottom-0 w-80 h-80 bg-emerald-500/5 rounded-full blur-3xl"></div> |
| 91 | + <div className="flex items-end gap-8 sm:gap-16 z-10 opacity-90 translate-y-4 w-full px-12 sm:px-24 justify-center"> |
| 92 | + {bars.map((bar, i) => ( |
| 93 | + <div key={i} className="flex flex-col items-center gap-3"> |
| 94 | + <div className="w-12 sm:w-20 bg-white dark:bg-zinc-800 rounded-t-xl border-t border-x border-gray-200 dark:border-zinc-700 flex flex-col justify-end overflow-hidden shadow-sm"> |
| 95 | + <div |
| 96 | + className={`w-full transition-all duration-1000 ease-out ${ |
| 97 | + revealed ? bar.target : bar.height |
| 98 | + } ${ |
| 99 | + bar.active |
| 100 | + ? "bg-emerald-500" |
| 101 | + : "bg-gray-200 dark:bg-zinc-700" |
| 102 | + }`} |
| 103 | + ></div> |
| 104 | + </div> |
| 105 | + <span className="text-sm font-bold text-gray-500">{bar.value}</span> |
| 106 | + </div> |
| 107 | + ))} |
| 108 | + </div> |
| 109 | + </div> |
| 110 | + ); |
| 111 | +} |
| 112 | + |
| 113 | +export function TimerAnimation() { |
| 114 | + const [time, setTime] = useState(60); |
| 115 | + |
| 116 | + useEffect(() => { |
| 117 | + let isMounted = true; |
| 118 | + let interval: NodeJS.Timeout; |
| 119 | + |
| 120 | + const play = async () => { |
| 121 | + setTime(60); |
| 122 | + interval = setInterval(() => { |
| 123 | + if (!isMounted) return; |
| 124 | + setTime((t) => (t > 0 ? t - 1 : 60)); |
| 125 | + }, 50); // Fast countdown for effect |
| 126 | + }; |
| 127 | + |
| 128 | + play(); |
| 129 | + return () => { |
| 130 | + isMounted = false; |
| 131 | + clearInterval(interval); |
| 132 | + }; |
| 133 | + }, []); |
| 134 | + |
| 135 | + return ( |
| 136 | + <div className="absolute inset-0 flex items-end justify-center pb-4 sm:pb-8 pointer-events-none"> |
| 137 | + <div className="absolute left-1/2 -translate-x-1/2 bottom-0 w-48 h-48 bg-fuchsia-500/5 rounded-full blur-3xl"></div> |
| 138 | + <div className="w-24 h-24 sm:w-28 sm:h-28 rounded-full border-4 border-fuchsia-100 dark:border-fuchsia-900/30 flex items-center justify-center relative z-10 bg-white/50 dark:bg-zinc-900/50 backdrop-blur-sm shadow-md translate-y-2"> |
| 139 | + <svg className="absolute inset-0 w-full h-full -rotate-90" viewBox="0 0 100 100"> |
| 140 | + <circle |
| 141 | + cx="50" |
| 142 | + cy="50" |
| 143 | + r="46" |
| 144 | + fill="none" |
| 145 | + stroke="currentColor" |
| 146 | + strokeWidth="8" |
| 147 | + className="text-fuchsia-500 transition-all duration-75" |
| 148 | + strokeDasharray="289" |
| 149 | + strokeDashoffset={289 - (time / 60) * 289} |
| 150 | + /> |
| 151 | + </svg> |
| 152 | + <span className="text-xl sm:text-2xl font-bold font-mono text-fuchsia-600 dark:text-fuchsia-400"> |
| 153 | + 00:{time.toString().padStart(2, '0')} |
| 154 | + </span> |
| 155 | + </div> |
| 156 | + </div> |
| 157 | + ); |
| 158 | +} |
| 159 | + |
| 160 | +export function CanvasAnimation() { |
| 161 | + const [pos, setPos] = useState({ x: 0, y: 0 }); |
| 162 | + |
| 163 | + useEffect(() => { |
| 164 | + let isMounted = true; |
| 165 | + const play = async () => { |
| 166 | + while (isMounted) { |
| 167 | + setPos({ x: 0, y: 0 }); |
| 168 | + await new Promise((r) => setTimeout(r, 1000)); |
| 169 | + if (!isMounted) break; |
| 170 | + |
| 171 | + setPos({ x: -20, y: -30 }); |
| 172 | + await new Promise((r) => setTimeout(r, 800)); |
| 173 | + if (!isMounted) break; |
| 174 | + |
| 175 | + setPos({ x: -40, y: -10 }); |
| 176 | + await new Promise((r) => setTimeout(r, 1500)); |
| 177 | + } |
| 178 | + }; |
| 179 | + play(); |
| 180 | + return () => { |
| 181 | + isMounted = false; |
| 182 | + }; |
| 183 | + }, []); |
| 184 | + |
| 185 | + return ( |
| 186 | + <div className="absolute inset-0 flex items-end justify-center sm:justify-end pr-0 sm:pr-8 pb-4 sm:pb-8 pointer-events-none"> |
| 187 | + <div className="absolute right-0 bottom-0 w-64 h-64 bg-cyan-500/5 rounded-full blur-3xl"></div> |
| 188 | + <div |
| 189 | + className="w-32 h-20 bg-white dark:bg-zinc-800 rounded-xl shadow-lg border border-cyan-200 dark:border-cyan-900/50 flex flex-col justify-between p-3 transition-transform duration-700 ease-in-out z-10" |
| 190 | + style={{ transform: `translate(${pos.x}px, ${pos.y}px)` }} |
| 191 | + > |
| 192 | + <div className="h-2 w-12 bg-cyan-100 dark:bg-cyan-900/50 rounded"></div> |
| 193 | + <div className="flex justify-between items-end"> |
| 194 | + <div className="w-6 h-6 rounded-full bg-cyan-500 text-[10px] text-white flex items-center justify-center font-bold">5</div> |
| 195 | + <div className="w-4 h-4 rounded-full bg-gray-200 dark:bg-zinc-700"></div> |
| 196 | + </div> |
| 197 | + </div> |
| 198 | + |
| 199 | + {/* Background dots grid */} |
| 200 | + <div className="absolute inset-0 bg-[radial-gradient(#e5e7eb_1px,transparent_1px)] dark:bg-[radial-gradient(#27272a_1px,transparent_1px)] [background-size:16px_16px] opacity-30"></div> |
| 201 | + </div> |
| 202 | + ); |
| 203 | +} |
| 204 | + |
| 205 | +export function ScalesAnimation() { |
| 206 | + const [scaleIdx, setScaleIdx] = useState(0); |
| 207 | + |
| 208 | + useEffect(() => { |
| 209 | + let isMounted = true; |
| 210 | + const play = async () => { |
| 211 | + while (isMounted) { |
| 212 | + await new Promise((r) => setTimeout(r, 2000)); |
| 213 | + if (!isMounted) break; |
| 214 | + setScaleIdx((prev) => (prev + 1) % 3); |
| 215 | + } |
| 216 | + }; |
| 217 | + play(); |
| 218 | + return () => { |
| 219 | + isMounted = false; |
| 220 | + }; |
| 221 | + }, []); |
| 222 | + |
| 223 | + const scales = [ |
| 224 | + ["1", "2", "3", "5", "8"], |
| 225 | + ["XS", "S", "M", "L", "XL"], |
| 226 | + ["1", "2", "4", "8", "16"], |
| 227 | + ]; |
| 228 | + |
| 229 | + return ( |
| 230 | + <div className="absolute inset-0 flex items-center justify-center sm:items-end sm:justify-end pr-0 sm:pr-8 pb-0 sm:pb-8 pointer-events-none"> |
| 231 | + <div className="absolute right-0 bottom-0 w-48 h-48 bg-orange-500/5 rounded-full blur-3xl"></div> |
| 232 | + <div className="flex gap-2 z-10 opacity-90"> |
| 233 | + {scales[scaleIdx].map((val, i) => ( |
| 234 | + <div |
| 235 | + key={i + val} // Key changes to force animation |
| 236 | + className="w-10 h-14 bg-white dark:bg-zinc-800 rounded-lg flex items-center justify-center font-bold text-gray-700 dark:text-gray-300 border border-gray-200 dark:border-zinc-700 shadow-sm animate-in zoom-in fade-in duration-300" |
| 237 | + style={{ animationDelay: `${i * 50}ms` }} |
| 238 | + > |
| 239 | + {val} |
| 240 | + </div> |
| 241 | + ))} |
| 242 | + </div> |
| 243 | + </div> |
| 244 | + ); |
| 245 | +} |
| 246 | + |
| 247 | +export function PlayerManagementAnimation() { |
| 248 | + const [avatars, setAvatars] = useState<number[]>([]); |
| 249 | + |
| 250 | + useEffect(() => { |
| 251 | + let isMounted = true; |
| 252 | + const play = async () => { |
| 253 | + while (isMounted) { |
| 254 | + setAvatars([]); |
| 255 | + await new Promise((r) => setTimeout(r, 1000)); |
| 256 | + if (!isMounted) break; |
| 257 | + |
| 258 | + setAvatars([1]); |
| 259 | + await new Promise((r) => setTimeout(r, 500)); |
| 260 | + if (!isMounted) break; |
| 261 | + |
| 262 | + setAvatars([1, 2]); |
| 263 | + await new Promise((r) => setTimeout(r, 700)); |
| 264 | + if (!isMounted) break; |
| 265 | + |
| 266 | + setAvatars([1, 2, 3]); |
| 267 | + await new Promise((r) => setTimeout(r, 2500)); |
| 268 | + } |
| 269 | + }; |
| 270 | + play(); |
| 271 | + return () => { |
| 272 | + isMounted = false; |
| 273 | + }; |
| 274 | + }, []); |
| 275 | + |
| 276 | + const colors = [ |
| 277 | + "bg-indigo-500 text-indigo-50", |
| 278 | + "bg-violet-500 text-violet-50", |
| 279 | + "bg-purple-500 text-purple-50", |
| 280 | + ]; |
| 281 | + const initials = ["JD", "AB", "RW"]; |
| 282 | + |
| 283 | + return ( |
| 284 | + <div className="absolute inset-0 flex items-end justify-center sm:justify-end pr-0 sm:pr-10 pb-4 sm:pb-8 pointer-events-none overflow-hidden"> |
| 285 | + <div className="absolute right-0 bottom-0 w-48 h-48 bg-indigo-500/5 rounded-full blur-3xl"></div> |
| 286 | + <div className="flex -space-x-4 z-10"> |
| 287 | + {avatars.map((a, i) => ( |
| 288 | + <div |
| 289 | + key={a} |
| 290 | + className={`w-14 h-14 rounded-full ${colors[i]} border-4 border-white dark:border-zinc-900 flex items-center justify-center shadow-lg animate-in fade-in zoom-in slide-in-from-right-4 duration-300`} |
| 291 | + style={{ zIndex: 10 - a }} |
| 292 | + > |
| 293 | + <span className="text-sm font-bold">{initials[i]}</span> |
| 294 | + </div> |
| 295 | + ))} |
| 296 | + </div> |
| 297 | + </div> |
| 298 | + ); |
| 299 | +} |
| 300 | + |
| 301 | +export function IssuesAnimation() { |
| 302 | + const [active, setActive] = useState(0); |
| 303 | + |
| 304 | + useEffect(() => { |
| 305 | + let isMounted = true; |
| 306 | + const play = async () => { |
| 307 | + while (isMounted) { |
| 308 | + await new Promise((r) => setTimeout(r, 1500)); |
| 309 | + if (!isMounted) break; |
| 310 | + setActive((prev) => (prev + 1) % 3); |
| 311 | + } |
| 312 | + }; |
| 313 | + play(); |
| 314 | + return () => { |
| 315 | + isMounted = false; |
| 316 | + }; |
| 317 | + }, []); |
| 318 | + |
| 319 | + const issues = ["PROJ-123", "PROJ-124", "PROJ-125"]; |
| 320 | + |
| 321 | + return ( |
| 322 | + <div className="absolute inset-0 flex items-end justify-center sm:justify-end pr-0 sm:pr-8 pb-4 sm:pb-8 pointer-events-none"> |
| 323 | + <div className="absolute right-0 bottom-0 w-48 h-48 bg-sky-500/5 rounded-full blur-3xl"></div> |
| 324 | + <div className="flex flex-col gap-2 z-10 w-48"> |
| 325 | + {issues.map((issue, i) => ( |
| 326 | + <div |
| 327 | + key={issue} |
| 328 | + className={`h-10 rounded-lg flex items-center px-3 gap-3 transition-all duration-500 border shadow-sm |
| 329 | + ${ |
| 330 | + active === i |
| 331 | + ? "bg-white dark:bg-zinc-800 border-sky-200 dark:border-sky-900/50 scale-105" |
| 332 | + : "bg-white/60 dark:bg-zinc-800/60 border-gray-100 dark:border-zinc-800/50 scale-100 opacity-60" |
| 333 | + } |
| 334 | + `} |
| 335 | + > |
| 336 | + <div className={`w-4 h-4 rounded-full border flex items-center justify-center ${active === i ? "border-sky-500 bg-sky-500 text-white" : "border-gray-300 dark:border-zinc-600"}`}> |
| 337 | + {active === i && <Check className="w-3 h-3" />} |
| 338 | + </div> |
| 339 | + <div className="h-2 w-16 bg-gray-200 dark:bg-zinc-700 rounded"></div> |
| 340 | + </div> |
| 341 | + ))} |
| 342 | + </div> |
| 343 | + </div> |
| 344 | + ); |
| 345 | +} |
| 346 | + |
| 347 | +export function AutoCompleteAnimation() { |
| 348 | + const [count, setCount] = useState(3); |
| 349 | + const [revealed, setRevealed] = useState(false); |
| 350 | + |
| 351 | + useEffect(() => { |
| 352 | + let isMounted = true; |
| 353 | + const play = async () => { |
| 354 | + while (isMounted) { |
| 355 | + setCount(3); |
| 356 | + setRevealed(false); |
| 357 | + await new Promise((r) => setTimeout(r, 1000)); |
| 358 | + if (!isMounted) break; |
| 359 | + |
| 360 | + setCount(2); |
| 361 | + await new Promise((r) => setTimeout(r, 1000)); |
| 362 | + if (!isMounted) break; |
| 363 | + |
| 364 | + setCount(1); |
| 365 | + await new Promise((r) => setTimeout(r, 1000)); |
| 366 | + if (!isMounted) break; |
| 367 | + |
| 368 | + setRevealed(true); |
| 369 | + await new Promise((r) => setTimeout(r, 2500)); |
| 370 | + } |
| 371 | + }; |
| 372 | + play(); |
| 373 | + return () => { |
| 374 | + isMounted = false; |
| 375 | + }; |
| 376 | + }, []); |
| 377 | + |
| 378 | + return ( |
| 379 | + <div className="absolute inset-0 flex items-end justify-center sm:justify-end pr-0 sm:pr-8 pb-4 sm:pb-8 pointer-events-none"> |
| 380 | + <div className="absolute right-0 bottom-0 w-48 h-48 bg-rose-500/5 rounded-full blur-3xl"></div> |
| 381 | + <div className="flex flex-col items-center gap-4 z-10"> |
| 382 | + {!revealed ? ( |
| 383 | + <div className="w-16 h-16 rounded-full bg-white dark:bg-zinc-800 border-4 border-rose-100 dark:border-rose-900/30 flex items-center justify-center shadow-lg animate-in zoom-in duration-300"> |
| 384 | + <span className="text-3xl font-bold text-rose-500">{count}</span> |
| 385 | + </div> |
| 386 | + ) : ( |
| 387 | + <div className="flex gap-2 animate-in slide-in-from-bottom-4 fade-in duration-500"> |
| 388 | + {[5, 5, 8].map((v, i) => ( |
| 389 | + <div key={i} className="w-10 h-14 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg flex items-center justify-center font-bold text-lg text-gray-900 dark:text-white shadow-md"> |
| 390 | + {v} |
| 391 | + </div> |
| 392 | + ))} |
| 393 | + </div> |
| 394 | + )} |
| 395 | + </div> |
| 396 | + </div> |
| 397 | + ); |
| 398 | +} |
0 commit comments