Skip to content

Instantly share code, notes, and snippets.

@mikoloism
Last active May 17, 2023 13:16
Show Gist options
  • Save mikoloism/3a08208487c5139cd94ceedaa03139e2 to your computer and use it in GitHub Desktop.
Save mikoloism/3a08208487c5139cd94ceedaa03139e2 to your computer and use it in GitHub Desktop.
Countdown OTP Form by Zustand.js (persistent)
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,
};
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 {}
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';
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);
}
export * from './countdown.constants';
export type {
CountDownAction,
CountDownState,
CountDownStore,
} from './countdown.interface';
export {
countDownStore,
useCountDown,
useCountDownStore,
} from './countdown.store';
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,
};
}
export type SetState<T> = (
partial: T | Partial<T> | ((state: T) => T | Partial<T>),
replace?: boolean | undefined,
) => void;
export { create } from 'zustand';
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