@@ -110,49 +110,69 @@ export function AnalyticsAnimation() {
110110 ) ;
111111}
112112
113- export function TimerAnimation ( ) {
114- const [ time , setTime ] = useState ( 60 ) ;
113+ export function JiraIntegrationAnimation ( ) {
114+ const [ phase , setPhase ] = useState < "idle" | "import" | "sync" | "done" > ( "idle" ) ;
115115
116116 useEffect ( ( ) => {
117117 let isMounted = true ;
118- let interval : NodeJS . Timeout ;
119-
120118 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
119+ while ( isMounted ) {
120+ setPhase ( "idle" ) ;
121+ await new Promise ( ( r ) => setTimeout ( r , 1000 ) ) ;
122+ if ( ! isMounted ) break ;
123+
124+ setPhase ( "import" ) ;
125+ await new Promise ( ( r ) => setTimeout ( r , 1200 ) ) ;
126+ if ( ! isMounted ) break ;
127+
128+ setPhase ( "sync" ) ;
129+ await new Promise ( ( r ) => setTimeout ( r , 1200 ) ) ;
130+ if ( ! isMounted ) break ;
131+
132+ setPhase ( "done" ) ;
133+ await new Promise ( ( r ) => setTimeout ( r , 2000 ) ) ;
134+ }
126135 } ;
127-
128136 play ( ) ;
129- return ( ) => {
130- isMounted = false ;
131- clearInterval ( interval ) ;
132- } ;
137+ return ( ) => { isMounted = false ; } ;
133138 } , [ ] ) ;
134139
135140 return (
136141 < 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 >
142+ < div className = "absolute left-1/2 -translate-x-1/2 bottom-0 w-48 h-48 bg-blue-500/5 rounded-full blur-3xl" > </ div >
143+ < div className = "flex items-center gap-4 sm:gap-6 z-10" >
144+ { /* Jira stack */ }
145+ < div className = "flex flex-col gap-1.5" >
146+ < div className = "w-20 h-7 sm:w-24 sm:h-8 rounded-lg bg-blue-500/10 dark:bg-blue-500/20 border border-blue-200 dark:border-blue-800/50 flex items-center px-2 gap-1.5" >
147+ < div className = "w-3 h-3 rounded-sm bg-blue-500" > </ div >
148+ < div className = "h-1.5 w-10 bg-blue-200 dark:bg-blue-800 rounded" > </ div >
149+ </ div >
150+ < div className = "w-20 h-7 sm:w-24 sm:h-8 rounded-lg bg-blue-500/10 dark:bg-blue-500/20 border border-blue-200 dark:border-blue-800/50 flex items-center px-2 gap-1.5" >
151+ < div className = "w-3 h-3 rounded-sm bg-blue-500" > </ div >
152+ < div className = "h-1.5 w-8 bg-blue-200 dark:bg-blue-800 rounded" > </ div >
153+ </ div >
154+ </ div >
155+
156+ { /* Arrow / sync area */ }
157+ < div className = "flex flex-col items-center gap-1" >
158+ < div className = { `text-xs font-bold transition-all duration-500 ${ phase === "import" ? "text-blue-500 opacity-100 translate-x-2" : phase === "sync" ? "text-indigo-500 opacity-100 -translate-x-2" : "opacity-0" } ` } >
159+ { phase === "import" ? "→" : "←" }
160+ </ div >
161+ < div className = { `w-8 h-0.5 rounded transition-all duration-500 ${ phase === "import" ? "bg-blue-400 scale-x-100" : phase === "sync" ? "bg-indigo-400 scale-x-100" : "bg-gray-200 dark:bg-zinc-700 scale-x-50" } ` } > </ div >
162+ </ div >
163+
164+ { /* AgileKit stack */ }
165+ < div className = "flex flex-col gap-1.5" >
166+ < div className = { `w-20 h-7 sm:w-24 sm:h-8 rounded-lg border flex items-center px-2 gap-1.5 transition-all duration-500 ${ phase === "done" ? "bg-green-50 dark:bg-green-500/10 border-green-200 dark:border-green-800/50" : "bg-white dark:bg-zinc-800 border-gray-200 dark:border-zinc-700" } ` } >
167+ < div className = "h-1.5 w-10 bg-gray-200 dark:bg-zinc-700 rounded" > </ div >
168+ { phase === "done" && < Check className = "w-3 h-3 text-green-500" /> }
169+ </ div >
170+ < div className = { `w-20 h-7 sm:w-24 sm:h-8 rounded-lg border flex items-center px-2 gap-1.5 transition-all duration-500 ${ phase === "sync" || phase === "done" ? "bg-indigo-50 dark:bg-indigo-500/10 border-indigo-200 dark:border-indigo-800/50" : "bg-white dark:bg-zinc-800 border-gray-200 dark:border-zinc-700" } ` } >
171+ { ( phase === "sync" || phase === "done" ) && < span className = "text-[10px] font-bold text-indigo-500" > 5 SP</ span > }
172+ < div className = "h-1.5 w-8 bg-gray-200 dark:bg-zinc-700 rounded" > </ div >
173+ </ div >
174+ </ div >
175+ </ div >
156176 </ div >
157177 ) ;
158178}
@@ -244,55 +264,60 @@ export function ScalesAnimation() {
244264 ) ;
245265}
246266
247- export function PlayerManagementAnimation ( ) {
248- const [ avatars , setAvatars ] = useState < number [ ] > ( [ ] ) ;
267+ export function TimeToConsensusAnimation ( ) {
268+ const [ seconds , setSeconds ] = useState ( 0 ) ;
269+ const [ dots , setDots ] = useState < boolean [ ] > ( [ false , false , false , false ] ) ;
270+ const [ consensus , setConsensus ] = useState ( false ) ;
249271
250272 useEffect ( ( ) => {
251273 let isMounted = true ;
252274 const play = async ( ) => {
253275 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 ) ) ;
276+ setSeconds ( 0 ) ;
277+ setDots ( [ false , false , false , false ] ) ;
278+ setConsensus ( false ) ;
279+ await new Promise ( ( r ) => setTimeout ( r , 800 ) ) ;
260280 if ( ! isMounted ) break ;
261281
262- setAvatars ( [ 1 , 2 ] ) ;
263- await new Promise ( ( r ) => setTimeout ( r , 700 ) ) ;
282+ for ( let s = 1 ; s <= 12 ; s ++ ) {
283+ if ( ! isMounted ) break ;
284+ setSeconds ( s ) ;
285+ if ( s === 3 ) setDots ( [ true , false , false , false ] ) ;
286+ if ( s === 6 ) setDots ( [ true , true , false , false ] ) ;
287+ if ( s === 8 ) setDots ( [ true , true , true , false ] ) ;
288+ if ( s === 11 ) setDots ( [ true , true , true , true ] ) ;
289+ await new Promise ( ( r ) => setTimeout ( r , 150 ) ) ;
290+ }
264291 if ( ! isMounted ) break ;
265292
266- setAvatars ( [ 1 , 2 , 3 ] ) ;
293+ setConsensus ( true ) ;
267294 await new Promise ( ( r ) => setTimeout ( r , 2500 ) ) ;
268295 }
269296 } ;
270297 play ( ) ;
271- return ( ) => {
272- isMounted = false ;
273- } ;
298+ return ( ) => { isMounted = false ; } ;
274299 } , [ ] ) ;
275300
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" ] ;
301+ const formatTime = ( s : number ) => `0:${ s . toString ( ) . padStart ( 2 , "0" ) } ` ;
282302
283303 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- ) ) }
304+ < div className = "absolute inset-0 flex items-end justify-center pb-4 sm:pb-8 pointer-events-none" >
305+ < div className = "absolute left-1/2 -translate-x-1/2 bottom-0 w-48 h-48 bg-rose-500/5 rounded-full blur-3xl" > </ div >
306+ < div className = "flex flex-col items-center gap-3 z-10" >
307+ < span className = { `text-3xl sm:text-4xl font-bold font-mono transition-colors duration-300 ${ consensus ? "text-green-500" : "text-rose-500" } ` } >
308+ { formatTime ( seconds ) }
309+ </ span >
310+ < div className = "flex gap-2" >
311+ { dots . map ( ( filled , i ) => (
312+ < div
313+ key = { i }
314+ className = { `w-3 h-3 rounded-full transition-all duration-300 ${ filled ? ( consensus ? "bg-green-500 scale-110" : "bg-rose-400" ) : "bg-gray-200 dark:bg-zinc-700" } ` }
315+ > </ div >
316+ ) ) }
317+ </ div >
318+ < span className = { `text-xs font-bold uppercase tracking-widest transition-all duration-500 ${ consensus ? "text-green-500 opacity-100 translate-y-0" : "opacity-0 translate-y-2" } ` } >
319+ Consensus
320+ </ span >
296321 </ div >
297322 </ div >
298323 ) ;
@@ -344,54 +369,73 @@ export function IssuesAnimation() {
344369 ) ;
345370}
346371
347- export function AutoCompleteAnimation ( ) {
348- const [ count , setCount ] = useState ( 3 ) ;
349- const [ revealed , setRevealed ] = useState ( false ) ;
372+ export function VoterAlignmentAnimation ( ) {
373+ const [ phase , setPhase ] = useState < "scattered" | "converging" | "aligned" > ( "scattered" ) ;
350374
351375 useEffect ( ( ) => {
352376 let isMounted = true ;
353377 const play = async ( ) => {
354378 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 ) ) ;
379+ setPhase ( "scattered" ) ;
380+ await new Promise ( ( r ) => setTimeout ( r , 1200 ) ) ;
362381 if ( ! isMounted ) break ;
363382
364- setCount ( 1 ) ;
383+ setPhase ( "converging" ) ;
365384 await new Promise ( ( r ) => setTimeout ( r , 1000 ) ) ;
366385 if ( ! isMounted ) break ;
367386
368- setRevealed ( true ) ;
387+ setPhase ( "aligned" ) ;
369388 await new Promise ( ( r ) => setTimeout ( r , 2500 ) ) ;
370389 }
371390 } ;
372391 play ( ) ;
373- return ( ) => {
374- isMounted = false ;
375- } ;
392+ return ( ) => { isMounted = false ; } ;
376393 } , [ ] ) ;
377394
395+ const scattered = [
396+ { top : "12%" , left : "10%" } ,
397+ { top : "8%" , left : "78%" } ,
398+ { top : "70%" , left : "5%" } ,
399+ { top : "75%" , left : "85%" } ,
400+ { top : "40%" , left : "90%" } ,
401+ { top : "65%" , left : "45%" } ,
402+ ] ;
403+
404+ const center = { top : "42%" , left : "46%" } ;
405+
406+ const getPos = ( i : number ) => {
407+ if ( phase === "aligned" ) return center ;
408+ if ( phase === "converging" ) {
409+ const s = scattered [ i ] ;
410+ return {
411+ top : `${ ( parseFloat ( s . top ) + parseFloat ( center . top ) ) / 2 } %` ,
412+ left : `${ ( parseFloat ( s . left ) + parseFloat ( center . left ) ) / 2 } %` ,
413+ } ;
414+ }
415+ return scattered [ i ] ;
416+ } ;
417+
378418 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- ) }
419+ < div className = "absolute inset-0 flex items-end justify-center pb-4 sm:pb-8 pointer-events-none" >
420+ < 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 >
421+ < div className = "relative w-36 h-28 sm:w-44 sm:h-36 z-10" >
422+ { /* Background grid */ }
423+ < div className = "absolute inset-0 grid grid-cols-4 grid-rows-4 gap-px opacity-20" >
424+ { Array . from ( { length : 16 } ) . map ( ( _ , i ) => (
425+ < div key = { i } className = "border border-gray-300 dark:border-zinc-700 rounded-sm" > </ div >
426+ ) ) }
427+ </ div >
428+ { /* Dots */ }
429+ { scattered . map ( ( _ , i ) => {
430+ const pos = getPos ( i ) ;
431+ return (
432+ < div
433+ key = { i }
434+ className = { `absolute w-3 h-3 rounded-full transition-all duration-700 ease-in-out ${ phase === "aligned" ? "bg-green-500 scale-125" : "bg-rose-400" } ` }
435+ style = { { top : pos . top , left : pos . left } }
436+ > </ div >
437+ ) ;
438+ } ) }
395439 </ div >
396440 </ div >
397441 ) ;
0 commit comments