Skip to content

Commit

Permalink
feat: Add useTimeleft hook and utils to @w3ux/hooks (#143)
Browse files Browse the repository at this point in the history
  • Loading branch information
rossbulat authored Nov 12, 2024
1 parent a31932f commit 093b309
Show file tree
Hide file tree
Showing 11 changed files with 326 additions and 39 deletions.
8 changes: 7 additions & 1 deletion builder/src/builders/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,13 @@ export const build = async () => {
!(await generatePackageJson(
libDirectory,
`${libDirectory}/${PACKAGE_OUTPUT}`,
"gulp"
"gulp",
{
"./util": {
import: "./mjs/util.js",
require: "./cjs/util.js",
},
}
))
) {
throw `Failed to generate package.json file.`;
Expand Down
8 changes: 6 additions & 2 deletions library/hooks/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@w3ux/hooks-source",
"license": "GPL-3.0-only",
"version": "1.3.0",
"version": "1.3.1-beta.7",
"type": "module",
"scripts": {
"clear": "rm -rf node_modules dist tsconfig.tsbuildinfo",
Expand All @@ -12,12 +12,16 @@
},
"devDependencies": {
"@types/react": "^18",
"@w3ux/types": "^0.2.0",
"@w3ux/types": "0.2.1-beta.1",
"gulp": "^5.0.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-strip-comments": "^2.6.0",
"gulp-typescript": "^6.0.0-alpha.1",
"react": "^18",
"typescript": "^5.4.5"
},
"dependencies": {
"@w3ux/utils": "^1.1.1-beta.9",
"date-fns": "^4.1.0"
}
}
1 change: 1 addition & 0 deletions library/hooks/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ export * from "./useEffectIgnoreInitial";
export * from "./useOnResize";
export * from "./useOutsideAlerter";
export * from "./useSize";
export * from "./useTimeLeft";
14 changes: 14 additions & 0 deletions library/hooks/src/useTimeleft/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* @license Copyright 2024 w3ux authors & contributors
SPDX-License-Identifier: GPL-3.0-only */

import type { TimeleftDuration } from "@w3ux/types";

export const defaultDuration: TimeleftDuration = {
days: 0,
hours: 0,
minutes: 0,
seconds: 0,
lastMinute: false,
};

export const defaultRefreshInterval = 60;
121 changes: 121 additions & 0 deletions library/hooks/src/useTimeleft/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/* @license Copyright 2024 w3ux authors & contributors
SPDX-License-Identifier: GPL-3.0-only */

import { setStateWithRef } from "@w3ux/utils";
import { useEffect, useRef, useState } from "react";
import type {
TimeLeftAll,
TimeLeftRaw,
TimeleftDuration,
UseTimeleftProps,
} from "@w3ux/types";
import { getDuration } from "../util";

export const useTimeLeft = (props?: UseTimeleftProps) => {
const depsTimeleft = props?.depsTimeleft || [];
const depsFormat = props?.depsFormat || [];

// check whether timeleft is within a minute of finishing.
const inLastHour = () => {
const { days, hours } = getDuration(toRef.current);
return !days && !hours;
};

// get the amount of seconds left if timeleft is in the last minute.
const lastMinuteCountdown = () => {
const { seconds } = getDuration(toRef.current);
if (!inLastHour()) {
return 60;
}
return seconds;
};

// calculate resulting timeleft object from latest duration.
const getTimeleft = (c?: TimeleftDuration): TimeLeftAll => {
const { days, hours, minutes, seconds } = c || getDuration(toRef.current);
const raw: TimeLeftRaw = {
days,
hours,
minutes,
};
if (!days && !hours) {
raw.seconds = seconds;
}
return {
raw,
};
};

// the end time as a date.
const [to, setTo] = useState<Date | null>(null);
const toRef = useRef(to);

// resulting timeleft object to be returned.
const [timeleft, setTimeleft] = useState<TimeLeftAll>(getTimeleft());

// timeleft refresh intervals.
const [minInterval, setMinInterval] = useState<
ReturnType<typeof setInterval> | undefined
>(undefined);
const minIntervalRef = useRef(minInterval);

const [secInterval, setSecInterval] = useState<
ReturnType<typeof setInterval> | undefined
>(undefined);
const secIntervalRef = useRef(secInterval);

// refresh effects.
useEffect(() => {
setTimeleft(getTimeleft());
if (inLastHour()) {
// refresh timeleft every second.
if (!secIntervalRef.current) {
const interval = setInterval(() => {
if (!inLastHour()) {
clearInterval(secIntervalRef.current);
setStateWithRef(undefined, setSecInterval, secIntervalRef);
}
setTimeleft(getTimeleft());
}, 1000);

setStateWithRef(interval, setSecInterval, secIntervalRef);
}
}
// refresh timeleft every minute.
else if (!minIntervalRef.current) {
const interval = setInterval(() => {
if (inLastHour()) {
clearInterval(minIntervalRef.current);
setStateWithRef(undefined, setMinInterval, minIntervalRef);
}
setTimeleft(getTimeleft());
}, 60000);
setStateWithRef(interval, setMinInterval, minIntervalRef);
}
}, [to, inLastHour(), lastMinuteCountdown(), ...depsTimeleft]);

// re-render the timeleft upon formatting changes.
useEffect(() => {
setTimeleft(getTimeleft());
}, [...depsFormat]);

// clear intervals on unmount
useEffect(
() => () => {
clearInterval(minInterval);
clearInterval(secInterval);
},
[]
);

// Set the end time and calculate timeleft.
const setFromNow = (dateFrom: Date, dateTo: Date) => {
setTimeleft(getTimeleft(getDuration(dateFrom)));
setStateWithRef(dateTo, setTo, toRef);
};

return {
setFromNow,
timeleft,
};
};
75 changes: 75 additions & 0 deletions library/hooks/src/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* @license Copyright 2024 w3ux authors & contributors
SPDX-License-Identifier: GPL-3.0-only */

import { TimeleftDuration } from "@w3ux/types";
import { differenceInDays, getUnixTime, intervalToDuration } from "date-fns";
import { defaultDuration } from "./useTimeLeft/defaults";

// calculates the current timeleft duration.
export const getDuration = (toDate: Date | null): TimeleftDuration => {
if (!toDate) {
return defaultDuration;
}
if (getUnixTime(toDate) <= getUnixTime(new Date())) {
return defaultDuration;
}

toDate.setSeconds(toDate.getSeconds());
const d = intervalToDuration({
start: Date.now(),
end: toDate,
});

const days = differenceInDays(toDate, Date.now());
const hours = d?.hours || 0;
const minutes = d?.minutes || 0;
const seconds = d?.seconds || 0;
const lastHour = days === 0 && hours === 0;
const lastMinute = lastHour && minutes === 0;

return {
days,
hours,
minutes,
seconds,
lastMinute,
};
};

// Helper: Adds `seconds` to the current time and returns the resulting date.
export const secondsFromNow = (seconds: number): Date => {
const end = new Date();
end.setSeconds(end.getSeconds() + seconds);
return end;
};

// Helper: Calculates the duration between the current time and the provided date.
export const getDurationFromNow = (toDate: Date | null): TimeleftDuration => {
if (!toDate) {
return defaultDuration;
}
if (getUnixTime(toDate) <= getUnixTime(new Date())) {
return defaultDuration;
}

toDate.setSeconds(toDate.getSeconds());
const d = intervalToDuration({
start: Date.now(),
end: toDate,
});

const days = differenceInDays(toDate, Date.now());
const hours = d?.hours || 0;
const minutes = d?.minutes || 0;
const seconds = d?.seconds || 0;
const lastHour = days === 0 && hours === 0;
const lastMinute = lastHour && minutes === 0;

return {
days,
hours,
minutes,
seconds,
lastMinute,
};
};
2 changes: 1 addition & 1 deletion library/types/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@w3ux/types-source",
"license": "GPL-3.0-only",
"version": "0.2.0",
"version": "0.2.1-beta.1",
"type": "module",
"scripts": {
"clear": "rm -rf node_modules dist tsconfig.tsbuildinfo",
Expand Down
34 changes: 34 additions & 0 deletions library/types/src/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { CSSProperties, ReactNode } from "react";

// A general purpose sync status, tracking whether an item is not synced, in progress, or completed
// syncing. Useful for tracking async function progress, used as React refs, etc.
export type Sync = "synced" | "unsynced" | "syncing";

// A general purpose medium components are being displayed on.
export type DisplayFor = "default" | "modal" | "canvas" | "card";

// A generic type for basic React components. We assume the component may have children and styling
// applied to it.
export interface ComponentBase {
// passing react children.
children?: ReactNode;
// passing custom styling.
style?: CSSProperties;
}

// An extension of the ComponentBase type with an additional className property.
export type ComponentBaseWithClassName = ComponentBase & {
// passing a className string.
className?: string;
};

// A funtion with no arguments and no return value.
export type VoidFn = () => void;

// A JSON value: string, number, object, array, true, false, null.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyJson = any;

// A function definition.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyFunction = any;
36 changes: 2 additions & 34 deletions library/types/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,5 @@
/* @license Copyright 2024 w3ux authors & contributors
SPDX-License-Identifier: GPL-3.0-only */

import { CSSProperties, ReactNode } from "react";

// A general purpose sync status, tracking whether an item is not synced, in progress, or completed
// syncing. Useful for tracking async function progress, used as React refs, etc.
export type Sync = "synced" | "unsynced" | "syncing";

// A general purpose medium components are being displayed on.
export type DisplayFor = "default" | "modal" | "canvas" | "card";

// A generic type for basic React components. We assume the component may have children and styling
// applied to it.
export interface ComponentBase {
// passing react children.
children?: ReactNode;
// passing custom styling.
style?: CSSProperties;
}

// An extension of the ComponentBase type with an additional className property.
export type ComponentBaseWithClassName = ComponentBase & {
// passing a className string.
className?: string;
};

// A funtion with no arguments and no return value.
export type VoidFn = () => void;

// A JSON value: string, number, object, array, true, false, null.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyJson = any;

// A function definition.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyFunction = any;
export * from "./common";
export * from "./time";
39 changes: 39 additions & 0 deletions library/types/src/time.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* @license Copyright 2024 w3ux authors & contributors
SPDX-License-Identifier: GPL-3.0-only */

export interface UseTimeleftProps {
// Dependencies to trigger re-calculation of timeleft.
depsTimeleft: unknown[];
// Dependencies to trigger re-render of timeleft, e.g. if language switching occurs.
depsFormat: unknown[];
}

export interface TimeleftDuration {
days: number;
hours: number;
minutes: number;
seconds: number;
lastMinute: boolean;
}

export interface TimeLeftRaw {
days: number;
hours: number;
minutes: number;
seconds?: number;
}

export interface TimeLeftFormatted {
days: [number, string];
hours: [number, string];
minutes: [number, string];
seconds?: [number, string];
}

export interface TimeLeftAll {
raw: TimeLeftRaw;
}

export interface TimeleftHookProps {
refreshInterval: number;
}
Loading

0 comments on commit 093b309

Please sign in to comment.