rooks
Version:
Collection of awesome react hooks
562 lines (556 loc) • 14.8 kB
JavaScript
// src/hooks/useTemporalAge.ts
import { useEffect as useEffect2, useMemo, useSyncExternalStore } from "react";
// src/hooks/useLoadTemporal.ts
import { useEffect, useState } from "react";
var cached = null;
var loading = null;
function getTemporalApi() {
const native = globalThis.Temporal;
if (native) {
return native;
}
return cached;
}
function useLoadTemporal() {
const [api, setApi] = useState(getTemporalApi);
useEffect(() => {
if (api) return;
if (!loading) {
loading = import("@js-temporal/polyfill").then((mod) => {
cached = mod.Temporal;
});
}
loading.then(() => {
setApi(getTemporalApi());
});
}, [api]);
return api;
}
// src/hooks/useTemporalAge.ts
function getCurrentInstant() {
return getTemporalApi().Instant.fromEpochMilliseconds(Date.now());
}
function getCurrentTimeZoneId() {
return getTemporalApi().Now.timeZoneId();
}
function resolveDate(date) {
const T = getTemporalApi();
if (date instanceof T.PlainDate) {
return date;
}
return T.PlainDate.from(date);
}
function computeDayBoundaryDelay(instant, timeZone) {
const nextDayStart = instant.toZonedDateTimeISO(timeZone).add({ days: 1 }).startOfDay().toInstant();
return Number(nextDayStart.epochMilliseconds - instant.epochMilliseconds);
}
var AgeStore = class {
constructor(timeZone) {
this.timeZone = timeZone;
this.snapshot = { instant: getCurrentInstant() };
}
snapshot;
listeners = /* @__PURE__ */ new Set();
timer = null;
subscribe = (listener) => {
this.listeners.add(listener);
if (this.listeners.size === 1) {
this.start();
}
return () => {
this.listeners.delete(listener);
if (this.listeners.size === 0) {
this.stop();
}
};
};
getSnapshot = () => {
return this.snapshot;
};
dispose = () => {
this.stop();
this.listeners.clear();
};
emit() {
this.listeners.forEach((listener) => {
listener();
});
}
start() {
if (this.timer !== null) {
return;
}
const instant = this.snapshot?.instant ?? getCurrentInstant();
const delay = computeDayBoundaryDelay(instant, this.timeZone);
this.timer = setTimeout(this.tick, delay);
}
stop() {
if (this.timer !== null) {
clearTimeout(this.timer);
this.timer = null;
}
}
tick = () => {
const now = getCurrentInstant();
this.snapshot = { instant: now };
this.emit();
const delay = computeDayBoundaryDelay(now, this.timeZone);
this.timer = setTimeout(this.tick, delay);
};
};
function deriveAge(snapshot, birthDate, timeZone) {
if (snapshot === null) {
return null;
}
const today = snapshot.instant.toZonedDateTimeISO(timeZone).toPlainDate();
const duration = birthDate.until(today, {
largestUnit: "year"
});
return {
duration,
years: duration.years,
months: duration.months,
days: duration.days
};
}
function getServerSnapshot() {
return null;
}
var noopSubscribe = () => () => {
};
var noopGetSnapshot = () => null;
function useTemporalAge(options) {
const temporalApi = useLoadTemporal();
const { date, timeZone } = options;
const resolvedTimeZone = useMemo(
() => temporalApi ? timeZone ?? getCurrentTimeZoneId() : "UTC",
[timeZone, temporalApi]
);
const birthDate = useMemo(
() => temporalApi ? resolveDate(date) : null,
[date, temporalApi]
);
const store = useMemo(() => {
if (!temporalApi) return null;
return new AgeStore(resolvedTimeZone);
}, [resolvedTimeZone, temporalApi]);
useEffect2(() => {
return () => {
store?.dispose();
};
}, [store]);
const snapshot = useSyncExternalStore(
store?.subscribe ?? noopSubscribe,
store?.getSnapshot ?? noopGetSnapshot,
getServerSnapshot
);
return useMemo(() => {
if (!birthDate) return null;
return deriveAge(snapshot, birthDate, resolvedTimeZone);
}, [snapshot, birthDate, resolvedTimeZone]);
}
// src/hooks/useTemporalCountdown.ts
import { useEffect as useEffect3, useMemo as useMemo2, useSyncExternalStore as useSyncExternalStore2 } from "react";
function getCurrentInstant2() {
return getTemporalApi().Instant.fromEpochMilliseconds(Date.now());
}
function resolveTarget(target) {
const T = getTemporalApi();
if (target instanceof T.Instant) {
return target;
}
if (typeof target === "number") {
return T.Instant.fromEpochMilliseconds(target);
}
return T.Instant.from(target);
}
function computeDelay(instant, precision) {
switch (precision) {
case "second": {
const remainder = instant.epochMilliseconds % 1e3;
return remainder === 0 ? 1e3 : 1e3 - remainder;
}
case "minute": {
const remainder = instant.epochMilliseconds % 6e4;
return remainder === 0 ? 6e4 : 6e4 - remainder;
}
}
}
var CountdownStore = class {
constructor(targetInstant, precision) {
this.targetInstant = targetInstant;
this.precision = precision;
this.snapshot = { instant: getCurrentInstant2() };
}
snapshot;
listeners = /* @__PURE__ */ new Set();
timer = null;
subscribe = (listener) => {
this.listeners.add(listener);
if (this.listeners.size === 1) {
this.start();
}
return () => {
this.listeners.delete(listener);
if (this.listeners.size === 0) {
this.stop();
}
};
};
getSnapshot = () => {
return this.snapshot;
};
dispose = () => {
this.stop();
this.listeners.clear();
};
emit() {
this.listeners.forEach((listener) => {
listener();
});
}
start() {
if (this.timer !== null) {
return;
}
const T = getTemporalApi();
const now = this.snapshot?.instant ?? getCurrentInstant2();
if (T.Instant.compare(now, this.targetInstant) >= 0) {
return;
}
const delay = computeDelay(now, this.precision);
this.timer = setTimeout(this.tick, delay);
}
stop() {
if (this.timer !== null) {
clearTimeout(this.timer);
this.timer = null;
}
}
tick = () => {
const T = getTemporalApi();
const now = getCurrentInstant2();
this.snapshot = { instant: now };
this.emit();
if (T.Instant.compare(now, this.targetInstant) >= 0) {
this.stop();
return;
}
const delay = computeDelay(now, this.precision);
this.timer = setTimeout(this.tick, delay);
};
};
function deriveResult(snapshot, targetInstant) {
if (snapshot === null) {
return null;
}
const T = getTemporalApi();
const comparison = T.Instant.compare(snapshot.instant, targetInstant);
if (comparison >= 0) {
return {
remaining: new T.Duration(),
done: true
};
}
const remaining = snapshot.instant.until(targetInstant);
return {
remaining,
done: false
};
}
function getServerSnapshot2() {
return null;
}
var noopSubscribe2 = () => () => {
};
var noopGetSnapshot2 = () => null;
function useTemporalCountdown(options) {
const temporalApi = useLoadTemporal();
const { target, precision = "second" } = options;
const targetInstant = useMemo2(
() => temporalApi ? resolveTarget(target) : null,
[target, temporalApi]
);
const store = useMemo2(() => {
if (!temporalApi || !targetInstant) return null;
return new CountdownStore(targetInstant, precision);
}, [targetInstant, precision, temporalApi]);
useEffect3(() => {
return () => {
store?.dispose();
};
}, [store]);
const snapshot = useSyncExternalStore2(
store?.subscribe ?? noopSubscribe2,
store?.getSnapshot ?? noopGetSnapshot2,
getServerSnapshot2
);
return useMemo2(() => {
if (!targetInstant) return null;
return deriveResult(snapshot, targetInstant);
}, [snapshot, targetInstant]);
}
// src/hooks/useTemporalElapsed.ts
import { useEffect as useEffect4, useMemo as useMemo3, useSyncExternalStore as useSyncExternalStore3 } from "react";
function getCurrentInstant3() {
return getTemporalApi().Instant.fromEpochMilliseconds(Date.now());
}
function resolveSince(since) {
const T = getTemporalApi();
if (since === void 0) {
return getCurrentInstant3();
}
if (since instanceof T.Instant) {
return since;
}
if (typeof since === "number") {
return T.Instant.fromEpochMilliseconds(since);
}
return T.Instant.from(since);
}
function computeDelay2(instant, precision) {
switch (precision) {
case "second": {
const remainder = instant.epochMilliseconds % 1e3;
return remainder === 0 ? 1e3 : 1e3 - remainder;
}
case "minute": {
const remainder = instant.epochMilliseconds % 6e4;
return remainder === 0 ? 6e4 : 6e4 - remainder;
}
}
}
var ElapsedStore = class {
constructor(precision) {
this.precision = precision;
this.snapshot = { instant: getCurrentInstant3() };
}
snapshot;
listeners = /* @__PURE__ */ new Set();
timer = null;
subscribe = (listener) => {
this.listeners.add(listener);
if (this.listeners.size === 1) {
this.start();
}
return () => {
this.listeners.delete(listener);
if (this.listeners.size === 0) {
this.stop();
}
};
};
getSnapshot = () => {
return this.snapshot;
};
dispose = () => {
this.stop();
this.listeners.clear();
};
emit() {
this.listeners.forEach((listener) => {
listener();
});
}
start() {
if (this.timer !== null) {
return;
}
const now = this.snapshot?.instant ?? getCurrentInstant3();
const delay = computeDelay2(now, this.precision);
this.timer = setTimeout(this.tick, delay);
}
stop() {
if (this.timer !== null) {
clearTimeout(this.timer);
this.timer = null;
}
}
tick = () => {
const now = getCurrentInstant3();
this.snapshot = { instant: now };
this.emit();
const delay = computeDelay2(now, this.precision);
this.timer = setTimeout(this.tick, delay);
};
};
function deriveElapsed(snapshot, sinceInstant) {
if (snapshot === null) {
return null;
}
const T = getTemporalApi();
const comparison = T.Instant.compare(snapshot.instant, sinceInstant);
if (comparison <= 0) {
return new T.Duration();
}
return sinceInstant.until(snapshot.instant);
}
function getServerSnapshot3() {
return null;
}
var noopSubscribe3 = () => () => {
};
var noopGetSnapshot3 = () => null;
function useTemporalElapsed(options = {}) {
const temporalApi = useLoadTemporal();
const { since, precision = "second" } = options;
const sinceInstant = useMemo3(
() => temporalApi ? resolveSince(since) : null,
[since, temporalApi]
);
const store = useMemo3(() => {
if (!temporalApi) return null;
return new ElapsedStore(precision);
}, [precision, temporalApi]);
useEffect4(() => {
return () => {
store?.dispose();
};
}, [store]);
const snapshot = useSyncExternalStore3(
store?.subscribe ?? noopSubscribe3,
store?.getSnapshot ?? noopGetSnapshot3,
getServerSnapshot3
);
return useMemo3(() => {
if (!sinceInstant) return null;
return deriveElapsed(snapshot, sinceInstant);
}, [snapshot, sinceInstant]);
}
// src/hooks/useTemporalNow.ts
import { useEffect as useEffect5, useMemo as useMemo4, useSyncExternalStore as useSyncExternalStore4 } from "react";
var ClockStore = class {
constructor(precision, timeZone) {
this.precision = precision;
this.timeZone = timeZone;
this.snapshot = { instant: getCurrentInstant4() };
}
snapshot;
listeners = /* @__PURE__ */ new Set();
timer = null;
subscribe = (listener) => {
this.listeners.add(listener);
if (this.listeners.size === 1) {
this.start();
}
return () => {
this.listeners.delete(listener);
if (this.listeners.size === 0) {
this.stop();
}
};
};
getSnapshot = () => {
return this.snapshot;
};
dispose = () => {
this.stop();
this.listeners.clear();
};
emit() {
this.listeners.forEach((listener) => {
listener();
});
}
start() {
if (this.timer !== null) {
return;
}
const instant = this.snapshot?.instant ?? getCurrentInstant4();
const delay = computeDelayFromInstant(
instant,
this.precision,
this.timeZone
);
this.timer = setTimeout(this.tick, delay);
}
stop() {
if (this.timer !== null) {
clearTimeout(this.timer);
this.timer = null;
}
}
tick = () => {
const now = getCurrentInstant4();
this.snapshot = { instant: now };
this.emit();
const delay = computeDelayFromInstant(now, this.precision, this.timeZone);
this.timer = setTimeout(this.tick, delay);
};
};
function getCurrentInstant4() {
return getTemporalApi().Instant.fromEpochMilliseconds(Date.now());
}
function getCurrentTimeZoneId2() {
return getTemporalApi().Now.timeZoneId();
}
function computeDelayFromInstant(instant, precision, timeZone) {
switch (precision) {
case "second": {
const remainder = instant.epochMilliseconds % 1e3;
return remainder === 0 ? 1e3 : 1e3 - remainder;
}
case "minute": {
const remainder = instant.epochMilliseconds % 6e4;
return remainder === 0 ? 6e4 : 6e4 - remainder;
}
case "day": {
const zone = timeZone ?? getCurrentTimeZoneId2();
const nextDayStart = instant.toZonedDateTimeISO(zone).add({ days: 1 }).startOfDay().toInstant();
return Number(nextDayStart.epochMilliseconds - instant.epochMilliseconds);
}
}
}
function deriveValue(snapshot, kind, timeZone) {
if (snapshot === null) {
return null;
}
if (kind === "instant") {
return snapshot.instant;
}
const zone = timeZone ?? getCurrentTimeZoneId2();
const zonedDateTime = snapshot.instant.toZonedDateTimeISO(zone);
switch (kind) {
case "zoned":
return zonedDateTime;
case "plainDateTime":
return zonedDateTime.toPlainDateTime();
case "plainDate":
return zonedDateTime.toPlainDate();
case "instant":
return snapshot.instant;
}
}
function getServerSnapshot4() {
return null;
}
var noopSubscribe4 = () => () => {
};
var noopGetSnapshot4 = () => null;
function useTemporalNow(options = {}) {
const temporalApi = useLoadTemporal();
const { kind = "instant", precision = "second", timeZone } = options;
const store = useMemo4(() => {
if (!temporalApi) return null;
return new ClockStore(precision, timeZone);
}, [precision, timeZone, temporalApi]);
useEffect5(() => {
return () => {
store?.dispose();
};
}, [store]);
const snapshot = useSyncExternalStore4(
store?.subscribe ?? noopSubscribe4,
store?.getSnapshot ?? noopGetSnapshot4,
getServerSnapshot4
);
return useMemo4(() => {
return deriveValue(snapshot, kind, timeZone);
}, [snapshot, kind, timeZone]);
}
export {
useTemporalAge,
useTemporalCountdown,
useTemporalElapsed,
useTemporalNow
};