mirror of
https://github.com/lukevella/rallly.git
synced 2025-08-06 09:59:00 +02:00
✨ Updated sidebar layout (#1661)
This commit is contained in:
parent
8c0814b92b
commit
72ca1d4c38
104 changed files with 3268 additions and 1331 deletions
|
@ -0,0 +1,35 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
function cmdKey(e: KeyboardEvent) {
|
||||
if (e.metaKey || e.ctrlKey) {
|
||||
return e.key;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function CommandGlobalShortcut({ trigger }: { trigger: () => void }) {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
switch (cmdKey(e)) {
|
||||
case "k":
|
||||
e.preventDefault();
|
||||
trigger();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
};
|
||||
}, [router, trigger]);
|
||||
|
||||
// This component doesn't render anything
|
||||
return null;
|
||||
}
|
115
apps/web/src/features/navigation/command-menu/command-menu.tsx
Normal file
115
apps/web/src/features/navigation/command-menu/command-menu.tsx
Normal file
|
@ -0,0 +1,115 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@rallly/ui/command";
|
||||
import { DialogDescription, DialogTitle, useDialog } from "@rallly/ui/dialog";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import {
|
||||
BillingPageIcon,
|
||||
EventPageIcon,
|
||||
HomePageIcon,
|
||||
PageIcon,
|
||||
PollPageIcon,
|
||||
PreferencesPageIcon,
|
||||
ProfilePageIcon,
|
||||
} from "@/app/components/page-icons";
|
||||
import { Trans } from "@/components/trans";
|
||||
|
||||
import { CommandGlobalShortcut } from "./command-global-shortcut";
|
||||
|
||||
export function CommandMenu() {
|
||||
const router = useRouter();
|
||||
const { trigger, dialogProps, dismiss } = useDialog();
|
||||
|
||||
const handleSelect = (route: string) => {
|
||||
router.push(route);
|
||||
dismiss();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CommandGlobalShortcut trigger={trigger} />
|
||||
|
||||
{/* <Button variant="ghost" onClick={trigger}>
|
||||
<Icon>
|
||||
<SearchIcon />
|
||||
</Icon>
|
||||
<Trans i18nKey="search" defaults="Search" />
|
||||
<CommandShortcutSymbol symbol="K" />
|
||||
</Button> */}
|
||||
<CommandDialog {...dialogProps}>
|
||||
<DialogTitle className="sr-only">
|
||||
<Trans i18nKey="commandMenu" defaults="Command Menu" />
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
<Trans i18nKey="commandMenuDescription" defaults="Select a command" />
|
||||
</DialogDescription>
|
||||
<CommandInput
|
||||
autoFocus={true}
|
||||
placeholder="Type a command or search..."
|
||||
/>
|
||||
<CommandList className="max-h-max">
|
||||
<CommandEmpty>
|
||||
<span>
|
||||
<Trans i18nKey="commandMenuNoResults" defaults="No results" />
|
||||
</span>
|
||||
</CommandEmpty>
|
||||
<CommandGroup heading={<Trans i18nKey="polls" defaults="Actions" />}>
|
||||
<CommandItem onSelect={() => handleSelect("/new")}>
|
||||
<PageIcon>
|
||||
<PlusIcon />
|
||||
</PageIcon>
|
||||
<Trans i18nKey="create" defaults="Create" />
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
<CommandGroup heading="Navigation">
|
||||
<CommandItem onSelect={() => handleSelect("/")}>
|
||||
<HomePageIcon />
|
||||
<Trans i18nKey="home" defaults="Home" />
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => handleSelect("/polls")}>
|
||||
<PollPageIcon />
|
||||
<Trans i18nKey="polls" defaults="Polls" />
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => handleSelect("/events")}>
|
||||
<EventPageIcon />
|
||||
<Trans i18nKey="events" defaults="Events" />
|
||||
</CommandItem>
|
||||
{/* <CommandItem onSelect={() => handleSelect("/teams")}>
|
||||
<TeamsPageIcon />
|
||||
<Trans i18nKey="teams" defaults="Teams" />
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => handleSelect("/spaces")}>
|
||||
<SpacesPageIcon />
|
||||
<Trans i18nKey="spaces" defaults="Spaces" />
|
||||
</CommandItem> */}
|
||||
</CommandGroup>
|
||||
<CommandGroup
|
||||
heading={<Trans i18nKey="account" defaults="Account" />}
|
||||
>
|
||||
<CommandItem onSelect={() => handleSelect("/settings/profile")}>
|
||||
<ProfilePageIcon />
|
||||
<Trans i18nKey="profile" defaults="Profile" />
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => handleSelect("/settings/preferences")}>
|
||||
<PreferencesPageIcon />
|
||||
<Trans i18nKey="preferences" defaults="Preferences" />
|
||||
</CommandItem>
|
||||
<CommandItem onSelect={() => handleSelect("/settings/billing")}>
|
||||
<BillingPageIcon />
|
||||
<Trans i18nKey="billing" defaults="Billing" />
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</CommandDialog>
|
||||
</>
|
||||
);
|
||||
}
|
1
apps/web/src/features/navigation/command-menu/index.ts
Normal file
1
apps/web/src/features/navigation/command-menu/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export * from "./command-menu";
|
130
apps/web/src/features/poll-selection/context.tsx
Normal file
130
apps/web/src/features/poll-selection/context.tsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { createContext, useCallback, useMemo, useState } from "react";
|
||||
|
||||
import { useRequiredContext } from "@/components/use-required-context";
|
||||
|
||||
import { PollSelectionActionBar } from "./poll-selection-action-bar";
|
||||
|
||||
type RowSelectionState = Record<string, boolean>;
|
||||
|
||||
type PollSelectionContextType = {
|
||||
selectedPolls: RowSelectionState;
|
||||
setSelectedPolls: (selection: RowSelectionState) => void;
|
||||
selectPolls: (pollIds: string[]) => void;
|
||||
unselectPolls: (pollIds: string[]) => void;
|
||||
togglePollSelection: (pollId: string) => void;
|
||||
clearSelection: () => void;
|
||||
isSelected: (pollId: string) => boolean;
|
||||
getSelectedPollIds: () => string[];
|
||||
selectedCount: number;
|
||||
};
|
||||
|
||||
const PollSelectionContext = createContext<PollSelectionContextType | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
type PollSelectionProviderProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export const PollSelectionProvider = ({
|
||||
children,
|
||||
}: PollSelectionProviderProps) => {
|
||||
const [selectedPolls, setSelectedPolls] = useState<RowSelectionState>({});
|
||||
|
||||
const selectPolls = useCallback((pollIds: string[]) => {
|
||||
setSelectedPolls((prev) => {
|
||||
const newSelection = { ...prev };
|
||||
pollIds.forEach((id) => {
|
||||
newSelection[id] = true;
|
||||
});
|
||||
return newSelection;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const unselectPolls = useCallback(
|
||||
(pollIds: string[]) =>
|
||||
setSelectedPolls((prev) => {
|
||||
const newSelection = { ...prev };
|
||||
pollIds.forEach((id) => {
|
||||
delete newSelection[id];
|
||||
});
|
||||
return newSelection;
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const togglePollSelection = useCallback(
|
||||
(pollId: string) =>
|
||||
setSelectedPolls((prev) => {
|
||||
const newSelection = { ...prev };
|
||||
if (newSelection[pollId]) {
|
||||
delete newSelection[pollId];
|
||||
} else {
|
||||
newSelection[pollId] = true;
|
||||
}
|
||||
return newSelection;
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const clearSelection = useCallback(() => setSelectedPolls({}), []);
|
||||
|
||||
const isSelected = useCallback(
|
||||
(pollId: string) => Boolean(selectedPolls[pollId]),
|
||||
[selectedPolls],
|
||||
);
|
||||
|
||||
const getSelectedPollIds = useCallback(
|
||||
() => Object.keys(selectedPolls),
|
||||
[selectedPolls],
|
||||
);
|
||||
|
||||
const selectedCount = useMemo(
|
||||
() => Object.keys(selectedPolls).length,
|
||||
[selectedPolls],
|
||||
);
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
selectedPolls,
|
||||
setSelectedPolls,
|
||||
selectPolls,
|
||||
unselectPolls,
|
||||
togglePollSelection,
|
||||
clearSelection,
|
||||
isSelected,
|
||||
getSelectedPollIds,
|
||||
selectedCount,
|
||||
}),
|
||||
[
|
||||
selectedPolls,
|
||||
setSelectedPolls,
|
||||
selectPolls,
|
||||
unselectPolls,
|
||||
togglePollSelection,
|
||||
clearSelection,
|
||||
isSelected,
|
||||
getSelectedPollIds,
|
||||
selectedCount,
|
||||
],
|
||||
);
|
||||
|
||||
return (
|
||||
<PollSelectionContext.Provider value={value}>
|
||||
{children}
|
||||
<PollSelectionActionBar />
|
||||
</PollSelectionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const usePollSelection = () => {
|
||||
const context = useRequiredContext(
|
||||
PollSelectionContext,
|
||||
"usePollSelection must be used within a PollSelectionProvider",
|
||||
);
|
||||
|
||||
return context;
|
||||
};
|
|
@ -0,0 +1,142 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
ActionBarContainer,
|
||||
ActionBarContent,
|
||||
ActionBarGroup,
|
||||
ActionBarPortal,
|
||||
} from "@rallly/ui/action-bar";
|
||||
import { Button } from "@rallly/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@rallly/ui/dialog";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { TrashIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
|
||||
import { deletePolls } from "@/app/[locale]/(admin)/polls/actions";
|
||||
import { Trans } from "@/components/trans";
|
||||
|
||||
import { usePollSelection } from "./context";
|
||||
|
||||
const MActionBar = motion(ActionBarContainer);
|
||||
|
||||
export function PollSelectionActionBar() {
|
||||
const { selectedCount, clearSelection, getSelectedPollIds } =
|
||||
usePollSelection();
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = React.useState(false);
|
||||
const [isDeleting, setIsDeleting] = React.useState(false);
|
||||
|
||||
const handleDelete = async () => {
|
||||
const selectedPollIds = getSelectedPollIds();
|
||||
if (selectedPollIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
try {
|
||||
const result = await deletePolls(selectedPollIds);
|
||||
|
||||
if (result.success) {
|
||||
setIsDeleteDialogOpen(false);
|
||||
clearSelection();
|
||||
} else {
|
||||
// Handle error case
|
||||
console.error("Failed to delete polls:", result.error);
|
||||
}
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ActionBarPortal>
|
||||
<AnimatePresence>
|
||||
{selectedCount > 0 && (
|
||||
<MActionBar
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 20 }}
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
mass: 0.5,
|
||||
}}
|
||||
>
|
||||
<ActionBarContent>
|
||||
<span className="text-sm font-medium">
|
||||
<Trans
|
||||
i18nKey="selectedPolls"
|
||||
defaults="{count} {count, plural, one {poll} other {polls}} selected"
|
||||
values={{ count: selectedCount }}
|
||||
/>
|
||||
</span>
|
||||
</ActionBarContent>
|
||||
<ActionBarGroup>
|
||||
<Button
|
||||
variant="actionBar"
|
||||
onClick={clearSelection}
|
||||
className="text-action-bar-foreground"
|
||||
>
|
||||
<Trans i18nKey="unselectAll" defaults="Unselect All" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => setIsDeleteDialogOpen(true)}
|
||||
>
|
||||
<TrashIcon className="size-4" />
|
||||
<Trans i18nKey="delete" defaults="Delete" />
|
||||
</Button>
|
||||
</ActionBarGroup>
|
||||
</MActionBar>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Delete Polls Dialog */}
|
||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<DialogContent size="sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<Trans i18nKey="deletePolls" defaults="Delete Polls" />
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<p className="text-sm">
|
||||
{selectedCount === 1 ? (
|
||||
<Trans
|
||||
i18nKey="deletePollDescription"
|
||||
defaults="Are you sure you want to delete this poll? This action cannot be undone."
|
||||
/>
|
||||
) : (
|
||||
<Trans
|
||||
i18nKey="deletePollsDescription"
|
||||
defaults="Are you sure you want to delete these {count} polls? This action cannot be undone."
|
||||
values={{ count: selectedCount }}
|
||||
/>
|
||||
)}
|
||||
</p>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setIsDeleteDialogOpen(false);
|
||||
}}
|
||||
>
|
||||
<Trans i18nKey="cancel" defaults="Cancel" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={handleDelete}
|
||||
loading={isDeleting}
|
||||
>
|
||||
<Trans i18nKey="delete" defaults="Delete" />
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</ActionBarPortal>
|
||||
);
|
||||
}
|
3
apps/web/src/features/timezone/index.ts
Normal file
3
apps/web/src/features/timezone/index.ts
Normal file
|
@ -0,0 +1,3 @@
|
|||
export * from "./timezone-context";
|
||||
export * from "./timezone-display";
|
||||
export * from "./timezone-utils";
|
104
apps/web/src/features/timezone/timezone-context.tsx
Normal file
104
apps/web/src/features/timezone/timezone-context.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
"use client";
|
||||
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
import { createContext, useContext, useEffect, useState } from "react";
|
||||
|
||||
// Initialize dayjs plugins
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
// Default to browser timezone if not specified
|
||||
const getBrowserTimezone = () => {
|
||||
if (typeof window !== "undefined") {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
return "UTC"; // Default to UTC for server-side rendering
|
||||
};
|
||||
|
||||
type TimezoneContextType = {
|
||||
timezone: string;
|
||||
setTimezone: (timezone: string) => void;
|
||||
formatDate: (date: string | Date | dayjs.Dayjs, format?: string) => string;
|
||||
formatTime: (date: string | Date | dayjs.Dayjs, format?: string) => string;
|
||||
formatDateTime: (
|
||||
date: string | Date | dayjs.Dayjs,
|
||||
format?: string,
|
||||
) => string;
|
||||
};
|
||||
|
||||
const TimezoneContext = createContext<TimezoneContextType | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
export const TimezoneProvider = ({
|
||||
initialTimezone,
|
||||
children,
|
||||
}: {
|
||||
initialTimezone?: string;
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
// Initialize with browser timezone, but allow user preference to override
|
||||
const [timezone, setTimezone] = useState<string>(() => {
|
||||
if (initialTimezone) {
|
||||
return initialTimezone;
|
||||
}
|
||||
// Try to get from localStorage first (user preference)
|
||||
if (typeof window !== "undefined") {
|
||||
const savedTimezone = localStorage.getItem("userTimezone");
|
||||
if (savedTimezone) {
|
||||
return savedTimezone;
|
||||
}
|
||||
}
|
||||
return getBrowserTimezone();
|
||||
});
|
||||
|
||||
// Save timezone preference to localStorage when it changes
|
||||
useEffect(() => {
|
||||
if (typeof window !== "undefined") {
|
||||
localStorage.setItem("userTimezone", timezone);
|
||||
}
|
||||
}, [timezone]);
|
||||
|
||||
// Format functions that automatically use the current timezone
|
||||
const formatDate = (
|
||||
date: string | Date | dayjs.Dayjs,
|
||||
format = "YYYY-MM-DD",
|
||||
) => {
|
||||
return dayjs(date).tz(timezone).format(format);
|
||||
};
|
||||
|
||||
const formatTime = (date: string | Date | dayjs.Dayjs, format = "HH:mm") => {
|
||||
return dayjs(date).tz(timezone).format(format);
|
||||
};
|
||||
|
||||
const formatDateTime = (
|
||||
date: string | Date | dayjs.Dayjs,
|
||||
format = "YYYY-MM-DD HH:mm",
|
||||
) => {
|
||||
return dayjs(date).tz(timezone).format(format);
|
||||
};
|
||||
|
||||
const value = {
|
||||
timezone,
|
||||
setTimezone,
|
||||
formatDate,
|
||||
formatTime,
|
||||
formatDateTime,
|
||||
};
|
||||
|
||||
return (
|
||||
<TimezoneContext.Provider value={value}>
|
||||
{children}
|
||||
</TimezoneContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useTimezone = () => {
|
||||
const context = useContext(TimezoneContext);
|
||||
if (context === undefined) {
|
||||
throw new Error("useTimezone must be used within a TimezoneProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
31
apps/web/src/features/timezone/timezone-display.tsx
Normal file
31
apps/web/src/features/timezone/timezone-display.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
"use client";
|
||||
|
||||
import type dayjs from "dayjs";
|
||||
|
||||
import { useTimezone } from "./timezone-context";
|
||||
|
||||
type DateDisplayProps = {
|
||||
date: string | Date | dayjs.Dayjs;
|
||||
format?: string;
|
||||
};
|
||||
|
||||
export function DateDisplay({ date, format = "LL" }: DateDisplayProps) {
|
||||
const { formatDate } = useTimezone();
|
||||
return <span>{formatDate(date, format)}</span>;
|
||||
}
|
||||
|
||||
export function TimeDisplay({ date, format = "HH:mm" }: DateDisplayProps) {
|
||||
const { formatTime } = useTimezone();
|
||||
return <span>{formatTime(date, format)}</span>;
|
||||
}
|
||||
|
||||
export function DateTimeDisplay({ date, format = "LL, LT" }: DateDisplayProps) {
|
||||
const { formatDateTime } = useTimezone();
|
||||
return <span>{formatDateTime(date, format)}</span>;
|
||||
}
|
||||
|
||||
// Component to display the current timezone
|
||||
export function CurrentTimezone() {
|
||||
const { timezone } = useTimezone();
|
||||
return <span>{timezone}</span>;
|
||||
}
|
110
apps/web/src/features/timezone/timezone-utils.ts
Normal file
110
apps/web/src/features/timezone/timezone-utils.ts
Normal file
|
@ -0,0 +1,110 @@
|
|||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import utc from "dayjs/plugin/utc";
|
||||
|
||||
// Initialize dayjs plugins
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
|
||||
/**
|
||||
* Get the browser's timezone
|
||||
*/
|
||||
export const getBrowserTimezone = (): string => {
|
||||
if (typeof window !== "undefined") {
|
||||
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
}
|
||||
return "UTC";
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a list of all available timezones
|
||||
*/
|
||||
export const getAllTimezones = (): string[] => {
|
||||
// This is a simplified list - in a real implementation, you might want to use a more comprehensive list
|
||||
return [
|
||||
"UTC",
|
||||
"Africa/Cairo",
|
||||
"Africa/Johannesburg",
|
||||
"Africa/Lagos",
|
||||
"America/Anchorage",
|
||||
"America/Bogota",
|
||||
"America/Chicago",
|
||||
"America/Denver",
|
||||
"America/Los_Angeles",
|
||||
"America/Mexico_City",
|
||||
"America/New_York",
|
||||
"America/Phoenix",
|
||||
"America/Sao_Paulo",
|
||||
"America/Toronto",
|
||||
"Asia/Bangkok",
|
||||
"Asia/Dubai",
|
||||
"Asia/Hong_Kong",
|
||||
"Asia/Jakarta",
|
||||
"Asia/Kolkata",
|
||||
"Asia/Seoul",
|
||||
"Asia/Shanghai",
|
||||
"Asia/Singapore",
|
||||
"Asia/Tokyo",
|
||||
"Australia/Melbourne",
|
||||
"Australia/Perth",
|
||||
"Australia/Sydney",
|
||||
"Europe/Amsterdam",
|
||||
"Europe/Berlin",
|
||||
"Europe/Istanbul",
|
||||
"Europe/London",
|
||||
"Europe/Madrid",
|
||||
"Europe/Moscow",
|
||||
"Europe/Paris",
|
||||
"Europe/Rome",
|
||||
"Pacific/Auckland",
|
||||
"Pacific/Honolulu",
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert a date from one timezone to another
|
||||
*/
|
||||
export const convertTimezone = (
|
||||
date: string | Date | dayjs.Dayjs,
|
||||
fromTimezone: string,
|
||||
toTimezone: string,
|
||||
): dayjs.Dayjs => {
|
||||
return dayjs(date).tz(fromTimezone).tz(toTimezone);
|
||||
};
|
||||
|
||||
/**
|
||||
* Format a date for display with timezone
|
||||
*/
|
||||
export const formatWithTimezone = (
|
||||
date: string | Date | dayjs.Dayjs,
|
||||
timezone: string,
|
||||
format: string,
|
||||
): string => {
|
||||
return dayjs(date).tz(timezone).format(format);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the timezone offset as a string (e.g., "UTC+1:00")
|
||||
*/
|
||||
export const getTimezoneOffset = (timezone: string): string => {
|
||||
const offset = dayjs().tz(timezone).format("Z");
|
||||
return `UTC${offset}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Group timezones by offset
|
||||
*/
|
||||
export const groupTimezonesByOffset = (): Record<string, string[]> => {
|
||||
const timezones = getAllTimezones();
|
||||
const grouped: Record<string, string[]> = {};
|
||||
|
||||
timezones.forEach((tz) => {
|
||||
const offset = dayjs().tz(tz).format("Z");
|
||||
if (!grouped[offset]) {
|
||||
grouped[offset] = [];
|
||||
}
|
||||
grouped[offset].push(tz);
|
||||
});
|
||||
|
||||
return grouped;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue