mirror of
https://github.com/Xevion/rdap.git
synced 2025-12-06 01:16:00 -06:00
feat: add global date format preferences with timezone support
Implements a synchronized date format toggle across all timestamps with persistent user preferences. Timestamps now support three format views (relative, absolute, ISO) with tooltips and copy functionality. Adds automatic timezone detection and display.
This commit is contained in:
@@ -27,10 +27,13 @@
|
|||||||
"@fontsource/ibm-plex-mono": "^5.2.7",
|
"@fontsource/ibm-plex-mono": "^5.2.7",
|
||||||
"@mantine/hooks": "^8.3.5",
|
"@mantine/hooks": "^8.3.5",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@radix-ui/themes": "^3.2.1",
|
"@radix-ui/themes": "^3.2.1",
|
||||||
"@swc/helpers": "^0.5.11",
|
"@swc/helpers": "^0.5.11",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
"date-fns-tz": "^3.2.0",
|
||||||
|
"lucide-react": "^0.546.0",
|
||||||
"next": "^15.5.6",
|
"next": "^15.5.6",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"react": "19.2.0",
|
"react": "19.2.0",
|
||||||
|
|||||||
27
pnpm-lock.yaml
generated
27
pnpm-lock.yaml
generated
@@ -20,6 +20,9 @@ importers:
|
|||||||
'@radix-ui/react-icons':
|
'@radix-ui/react-icons':
|
||||||
specifier: ^1.3.2
|
specifier: ^1.3.2
|
||||||
version: 1.3.2(react@19.2.0)
|
version: 1.3.2(react@19.2.0)
|
||||||
|
'@radix-ui/react-tooltip':
|
||||||
|
specifier: ^1.2.8
|
||||||
|
version: 1.2.8(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
'@radix-ui/themes':
|
'@radix-ui/themes':
|
||||||
specifier: ^3.2.1
|
specifier: ^3.2.1
|
||||||
version: 3.2.1(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
version: 3.2.1(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||||
@@ -32,6 +35,12 @@ importers:
|
|||||||
date-fns:
|
date-fns:
|
||||||
specifier: ^4.1.0
|
specifier: ^4.1.0
|
||||||
version: 4.1.0
|
version: 4.1.0
|
||||||
|
date-fns-tz:
|
||||||
|
specifier: ^3.2.0
|
||||||
|
version: 3.2.0(date-fns@4.1.0)
|
||||||
|
lucide-react:
|
||||||
|
specifier: ^0.546.0
|
||||||
|
version: 0.546.0(react@19.2.0)
|
||||||
next:
|
next:
|
||||||
specifier: ^15.5.6
|
specifier: ^15.5.6
|
||||||
version: 15.5.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2)
|
version: 15.5.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(sass@1.93.2)
|
||||||
@@ -2324,6 +2333,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
|
|
||||||
|
date-fns-tz@3.2.0:
|
||||||
|
resolution: {integrity: sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==}
|
||||||
|
peerDependencies:
|
||||||
|
date-fns: ^3.0.0 || ^4.0.0
|
||||||
|
|
||||||
date-fns@4.1.0:
|
date-fns@4.1.0:
|
||||||
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==}
|
||||||
|
|
||||||
@@ -3161,6 +3175,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
lucide-react@0.546.0:
|
||||||
|
resolution: {integrity: sha512-Z94u6fKT43lKeYHiVyvyR8fT7pwCzDu7RyMPpTvh054+xahSgj4HFQ+NmflvzdXsoAjYGdCguGaFKYuvq0ThCQ==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
lz-string@1.5.0:
|
lz-string@1.5.0:
|
||||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -6233,6 +6252,10 @@ snapshots:
|
|||||||
es-errors: 1.3.0
|
es-errors: 1.3.0
|
||||||
is-data-view: 1.0.2
|
is-data-view: 1.0.2
|
||||||
|
|
||||||
|
date-fns-tz@3.2.0(date-fns@4.1.0):
|
||||||
|
dependencies:
|
||||||
|
date-fns: 4.1.0
|
||||||
|
|
||||||
date-fns@4.1.0: {}
|
date-fns@4.1.0: {}
|
||||||
|
|
||||||
debug@3.2.7:
|
debug@3.2.7:
|
||||||
@@ -7194,6 +7217,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 4.0.0
|
js-tokens: 4.0.0
|
||||||
|
|
||||||
|
lucide-react@0.546.0(react@19.2.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.0
|
||||||
|
|
||||||
lz-string@1.5.0: {}
|
lz-string@1.5.0: {}
|
||||||
|
|
||||||
magic-string@0.30.19:
|
magic-string@0.30.19:
|
||||||
|
|||||||
@@ -1,38 +1,74 @@
|
|||||||
import type { FunctionComponent } from "react";
|
import type { FunctionComponent } from "react";
|
||||||
import { useBoolean } from "usehooks-ts";
|
import { useMemo } from "react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
|
import { formatInTimeZone } from "date-fns-tz";
|
||||||
import TimeAgo from "react-timeago";
|
import TimeAgo from "react-timeago";
|
||||||
import { Button, Text } from "@radix-ui/themes";
|
import { Box, Button, Tooltip, Text, Flex } from "@radix-ui/themes";
|
||||||
|
import { useDateFormat } from "@/contexts/DateFormatContext";
|
||||||
|
|
||||||
type DynamicDateProps = {
|
type DynamicDateProps = {
|
||||||
value: Date | number;
|
value: Date | number;
|
||||||
absoluteFormat?: string;
|
absoluteFormat?: string;
|
||||||
|
showTimezone?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component for a toggleable switch between the absolute & human-relative date.
|
* A timestamp component with global format preferences.
|
||||||
* @param value The date to be displayed, the Date value, or
|
* Features:
|
||||||
* @param absoluteFormat Optional - the date-fns format string to use for the absolute date rendering.
|
* - Toggle between relative and absolute time formats
|
||||||
|
* - Global state - clicking any timestamp toggles all timestamps on the page
|
||||||
|
* - Tooltip shows all formats (relative, absolute, ISO) with copy buttons
|
||||||
|
* - Timezone support with automatic detection
|
||||||
|
* - Minimal, clean design
|
||||||
*/
|
*/
|
||||||
const DynamicDate: FunctionComponent<DynamicDateProps> = ({ value, absoluteFormat }) => {
|
const DynamicDate: FunctionComponent<DynamicDateProps> = ({
|
||||||
const { value: showAbsolute, toggle: toggleFormat } = useBoolean(true);
|
value,
|
||||||
|
absoluteFormat,
|
||||||
|
showTimezone = true,
|
||||||
|
}) => {
|
||||||
|
const { format: dateFormat, toggleFormat } = useDateFormat();
|
||||||
|
|
||||||
|
// Get user timezone
|
||||||
|
const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||||
|
|
||||||
|
const date = useMemo(() => new Date(value), [value]);
|
||||||
|
|
||||||
|
// Format variants
|
||||||
|
const absoluteFormatString = absoluteFormat ?? "PPP 'at' p";
|
||||||
|
const absoluteWithTz = showTimezone
|
||||||
|
? formatInTimeZone(date, userTimezone, `${absoluteFormatString} (zzz)`)
|
||||||
|
: format(date, absoluteFormatString);
|
||||||
|
const isoString = date.toISOString();
|
||||||
|
|
||||||
|
// Get display value based on global format
|
||||||
|
const displayValue = dateFormat === "relative" ? <TimeAgo date={date} /> : absoluteWithTz;
|
||||||
|
|
||||||
const date = new Date(value);
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Tooltip
|
||||||
variant="ghost"
|
content={
|
||||||
size="1"
|
<Box style={{ minWidth: "280px" }} as="span">
|
||||||
onClick={toggleFormat}
|
<Flex align="center" justify="between" mb="2" as="span">
|
||||||
style={{ padding: 0, height: "auto" }}
|
<Text size="1">
|
||||||
|
<strong>Relative:</strong> <TimeAgo date={date} />
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex align="center" justify="between" mb="2" as="span">
|
||||||
|
<Text size="1">
|
||||||
|
<strong>Absolute:</strong> {absoluteWithTz}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
<Flex align="center" justify="between" as="span">
|
||||||
|
<Text size="1" style={{ wordBreak: "break-all" }}>
|
||||||
|
<strong>ISO:</strong> {isoString}
|
||||||
|
</Text>
|
||||||
|
</Flex>
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Text className="dashed" title={date.toISOString()} size="2">
|
<Button variant="ghost" size="1" onClick={toggleFormat} style={{ cursor: "pointer" }}>
|
||||||
{showAbsolute ? (
|
<Text size="2">{displayValue}</Text>
|
||||||
format(date, absoluteFormat ?? "LLL do, y HH:mm:ss xxx")
|
</Button>
|
||||||
) : (
|
</Tooltip>
|
||||||
<TimeAgo date={date} />
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
49
src/contexts/DateFormatContext.tsx
Normal file
49
src/contexts/DateFormatContext.tsx
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import type { FunctionComponent, ReactNode } from "react";
|
||||||
|
import { createContext, useContext, useState, useEffect, useCallback } from "react";
|
||||||
|
|
||||||
|
type DateFormat = "relative" | "absolute";
|
||||||
|
|
||||||
|
type DateFormatContextType = {
|
||||||
|
format: DateFormat;
|
||||||
|
toggleFormat: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DateFormatContext = createContext<DateFormatContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
const STORAGE_KEY = "global-date-format-preference";
|
||||||
|
|
||||||
|
export const DateFormatProvider: FunctionComponent<{ children: ReactNode }> = ({ children }) => {
|
||||||
|
const [format, setFormat] = useState<DateFormat>(() => {
|
||||||
|
// Initialize from localStorage on client side
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
|
return (saved as DateFormat) || "relative";
|
||||||
|
}
|
||||||
|
return "relative";
|
||||||
|
});
|
||||||
|
|
||||||
|
// Persist to localStorage whenever format changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
localStorage.setItem(STORAGE_KEY, format);
|
||||||
|
}
|
||||||
|
}, [format]);
|
||||||
|
|
||||||
|
const toggleFormat = useCallback(() => {
|
||||||
|
setFormat((current) => (current === "relative" ? "absolute" : "relative"));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DateFormatContext.Provider value={{ format, toggleFormat }}>
|
||||||
|
{children}
|
||||||
|
</DateFormatContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDateFormat = (): DateFormatContextType => {
|
||||||
|
const context = useContext(DateFormatContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error("useDateFormat must be used within a DateFormatProvider");
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -7,6 +7,7 @@ import "@fontsource/ibm-plex-mono/400.css";
|
|||||||
import "@radix-ui/themes/styles.css";
|
import "@radix-ui/themes/styles.css";
|
||||||
|
|
||||||
import "@/styles/globals.css";
|
import "@/styles/globals.css";
|
||||||
|
import { DateFormatProvider } from "@/contexts/DateFormatContext";
|
||||||
|
|
||||||
const MyApp: AppType = ({ Component, pageProps }) => {
|
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||||
return (
|
return (
|
||||||
@@ -17,7 +18,9 @@ const MyApp: AppType = ({ Component, pageProps }) => {
|
|||||||
scriptProps={{ "data-cfasync": "false" }}
|
scriptProps={{ "data-cfasync": "false" }}
|
||||||
>
|
>
|
||||||
<Theme accentColor="indigo" grayColor="slate" radius="medium" scaling="100%">
|
<Theme accentColor="indigo" grayColor="slate" radius="medium" scaling="100%">
|
||||||
<Component {...pageProps} />
|
<DateFormatProvider>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</DateFormatProvider>
|
||||||
</Theme>
|
</Theme>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user