Last active
May 17, 2023 13:16
-
-
Save mikoloism/3a08208487c5139cd94ceedaa03139e2 to your computer and use it in GitHub Desktop.
Countdown OTP Form by Zustand.js (persistent)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { create, type SetState } from '@core/module/store'; | |
import { | |
COUNTDOWN_DEFAULT_DURATION, | |
COUNTDOWN_DEFAULT_STATE, | |
COUNTDOWN_STORAGE_KEY, | |
} from './countdown.constants'; | |
import type { | |
CountDownAction, | |
CountDownState, | |
CountDownStore, | |
} from './countdown.interface'; | |
import { assign, interval, storage } from './countdown.utils'; | |
import { formatDuration } from './format.utils'; | |
const countDownStore = create<CountDownStore>(CountDownStoreCreator); | |
function CountDownStoreCreator(set: SetState<CountDownState>) { | |
let startTime = COUNTDOWN_DEFAULT_DURATION; | |
const jsonInitial = storage.getJson<CountDownState>(COUNTDOWN_STORAGE_KEY); | |
if (jsonInitial !== null) { | |
startTime = jsonInitial.count; | |
} | |
const state = assign<CountDownState>(COUNTDOWN_DEFAULT_STATE, { | |
count: startTime, | |
}); | |
const action: CountDownAction = { | |
start() { | |
if (interval.isRunning()) return; | |
const shouldResetBeforeStart = | |
storage.hasKey(COUNTDOWN_STORAGE_KEY) !== true; | |
if (shouldResetBeforeStart) { | |
action.reset(); | |
} | |
interval.execute(() => set(loop)); | |
function loop(store: CountDownState) { | |
let count: number = store.count; | |
const jsonData = storage.getJson<CountDownState>( | |
COUNTDOWN_STORAGE_KEY, | |
); | |
if (jsonData !== null) { | |
count = jsonData.count <= 1 ? count : jsonData.count; | |
} | |
const shouldStop: boolean = count - 1 <= 0; | |
if (shouldStop) { | |
const state = assign<CountDownState>( | |
COUNTDOWN_DEFAULT_STATE, | |
{ isCounted: true }, | |
); | |
action.stop(); | |
storage.setJson(COUNTDOWN_STORAGE_KEY, state); | |
return state; | |
} else { | |
count = count - 1; | |
const formatted = formatDuration(count); | |
const state = assign<CountDownState>(formatted, { | |
isCounted: false, | |
count, | |
}); | |
storage.setJson(COUNTDOWN_STORAGE_KEY, state); | |
return state; | |
} | |
} | |
}, | |
stop() { | |
storage.removeKey(COUNTDOWN_STORAGE_KEY); | |
interval.clear(); | |
}, | |
reset() { | |
const count = COUNTDOWN_DEFAULT_DURATION; | |
const formatted = formatDuration(count); | |
const state = assign<CountDownState>(formatted, { | |
isCounted: false, | |
count, | |
}); | |
storage.setJson(COUNTDOWN_STORAGE_KEY, state); | |
set(() => state); | |
}, | |
}; | |
const store = assign<CountDownStore>(state, action); | |
return store; | |
} | |
export { | |
countDownStore, | |
countDownStore as useCountDownStore, | |
countDownStore as useCountDown, | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface CountDownFormat { | |
hour: string; | |
minute: string; | |
second: string; | |
} | |
export interface CountDownState extends CountDownFormat { | |
isCounted: boolean; | |
count: number; | |
} | |
export interface CountDownAction { | |
start(): void; | |
stop(): void; | |
reset(): void; | |
} | |
export interface CountDownStore extends CountDownState, CountDownAction {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { CountDownState } from './countdown.interface'; | |
// 5 MIN in SEC | |
export const COUNTDOWN_DEFAULT_DURATION: number = 300; | |
export const COUNTDOWN_DEFAULT_STATE: CountDownState = { | |
count: COUNTDOWN_DEFAULT_DURATION, | |
isCounted: false, | |
hour: '00', | |
minute: '00', | |
second: '00', | |
}; | |
export const COUNTDOWN_STORAGE_KEY: string = 'x-countdown'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export class storage { | |
public static canUse(): boolean { | |
return typeof localStorage !== 'undefined'; | |
} | |
public static hasKey(key: string): boolean { | |
return storage.canUse() && localStorage.getItem(key) !== null; | |
} | |
public static getJson<T>(key: string): T | null { | |
if (storage.canUse()) { | |
const raw = localStorage.getItem(key); | |
if (raw !== null) { | |
return JSON.parse(raw); | |
} | |
} | |
return null; | |
} | |
public static setJson<T>(key: string, json: T): void { | |
if (storage.canUse()) { | |
const raw = JSON.stringify(json); | |
localStorage.setItem(key, raw); | |
} | |
} | |
public static removeKey(key: string): void { | |
if (storage.canUse()) { | |
localStorage.removeItem(key); | |
} | |
} | |
} | |
export class interval { | |
private static id: any | null = null; | |
public static isRunning(): boolean { | |
return interval.id != null; | |
} | |
public static execute(callback: Function, duration: number = 1000): void { | |
interval.id = setInterval(callback, duration); | |
} | |
public static clear(): void { | |
clearInterval(interval.id); | |
interval.id = null; | |
} | |
} | |
export function assign<T extends any = any>(...sources: any[]): T { | |
return Object.assign({}, ...sources); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export * from './countdown.constants'; | |
export type { | |
CountDownAction, | |
CountDownState, | |
CountDownStore, | |
} from './countdown.interface'; | |
export { | |
countDownStore, | |
useCountDown, | |
useCountDownStore, | |
} from './countdown.store'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { CountDownFormat } from './countdown.interface'; | |
export function formatPadding(value: number): string { | |
const str_value = value.toString(); | |
return value < 10 ? '0'.concat(str_value) : str_value; | |
} | |
export function formatDuration(duration: number): CountDownFormat { | |
const minute = Math.floor(duration / 60); | |
const hour = Math.floor(minute / 3600); | |
const second = Math.floor(duration % 60); | |
const str_hour = formatPadding(hour); | |
const str_minute = formatPadding(minute); | |
const str_second = formatPadding(second); | |
return { | |
hour: str_hour, | |
minute: str_minute, | |
second: str_second, | |
}; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type SetState<T> = ( | |
partial: T | Partial<T> | ((state: T) => T | Partial<T>), | |
replace?: boolean | undefined, | |
) => void; | |
export { create } from 'zustand'; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useEffect } from 'react'; | |
import { useCountDownStore } from '@core/countdown'; | |
export default OTPForm(){ | |
const countdown = useCountDownStore(); | |
useEffect(() => { | |
countdown.start(); | |
}, []); | |
return ( | |
<div> | |
<button onClick={() => countdown.stop()}>stop and reset</button> | |
<button onClick={() => countdown.start()}>start</button> | |
<span>{ countdown.isCounted ? 'finished' : 'under counting' }</span> | |
<p> | |
<span>{countdown.minute}</span> : <span>{countdown.seconds}</span> | |
</p> | |
</div> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment