Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e98d5ff
feat: add timezone column to users table
eswan18 Feb 6, 2026
041e571
feat: add timezone type to users table interface
eswan18 Feb 6, 2026
dcafeba
feat: allow timezone field in user updates
eswan18 Feb 6, 2026
b970940
feat: add timezone constants and types
eswan18 Feb 6, 2026
f9c2565
feat: add timezone parameter to date formatting functions
eswan18 Feb 6, 2026
84f6bb8
feat: add timezone settings component
eswan18 Feb 6, 2026
69ab027
feat: add timezone settings to account page
eswan18 Feb 6, 2026
69c08f3
feat: add useUserTimezone hook
eswan18 Feb 6, 2026
d0b1922
feat: use user timezone in upcoming deadlines component
eswan18 Feb 6, 2026
e5a8ef1
feat: use user timezone in competition prop view
eswan18 Feb 6, 2026
add728e
feat: use user timezone in resolved prop card
eswan18 Feb 6, 2026
c15c511
feat: use user timezone in user detail card
eswan18 Feb 6, 2026
0e0303a
feat: use user timezone in competitions page
eswan18 Feb 6, 2026
da7086c
feat: use user timezone in competition row component
eswan18 Feb 6, 2026
1fb3d1b
feat: use user timezone in new-prop-form.tsx
eswan18 Feb 6, 2026
b10e30f
feat: use user timezone in competition-start-end.tsx
eswan18 Feb 6, 2026
fbda4f6
refactor: use browser timezone detection instead of saved preference
eswan18 Feb 6, 2026
d577f90
refactor: inline DEFAULT_TIMEZONE in time-utils
eswan18 Feb 6, 2026
26b9cac
refactor: remove timezones.ts constants file
eswan18 Feb 6, 2026
f6dae17
refactor: remove timezone settings UI
eswan18 Feb 6, 2026
e07b43d
refactor: remove timezone DB column and migration
eswan18 Feb 6, 2026
22630c2
fix: remove stale user.timezone reference in competitions page
eswan18 Feb 6, 2026
c49b36b
fix: clean up server component timezone handling in competitions page
eswan18 Feb 6, 2026
e3a30c2
refactor: use formatDate instead of formatInTimeZone in competition-s…
eswan18 Feb 6, 2026
63e2a60
feat: add LocalDate component for timezone-aware dates in server comp…
eswan18 Feb 6, 2026
cf95dfb
refactor: rename useUserTimezone to getBrowserTimezone; revert resolv…
eswan18 Feb 6, 2026
c7815e8
fix: suppress hydration warning in LocalDate component
eswan18 Feb 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions app/admin/competitions/competition-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
import { CreateEditCompetitionForm } from "@/components/forms/create-edit-competition-form";
import { useState } from "react";
import { formatDate, formatDateTime } from "@/lib/time-utils";
import { getBrowserTimezone } from "@/hooks/getBrowserTimezone";
import { CompetitionStatusBadge } from "./competition-status-badge";
import { getCompetitionStatusFromObject } from "@/lib/competition-status";

Expand All @@ -32,6 +33,7 @@ export default function CompetitionRow({
nResolvedProps: number;
}) {
const [open, setOpen] = useState(false);
const timezone = getBrowserTimezone();

const status = getCompetitionStatusFromObject(competition);

Expand Down Expand Up @@ -84,22 +86,22 @@ export default function CompetitionRow({
competition.forecasts_open_date && (
<>
Forecasts open{" "}
{formatDate(competition.forecasts_open_date)}
{formatDate(competition.forecasts_open_date, timezone)}
</>
)}
{status === "forecasts-open" &&
competition.forecasts_close_date && (
<>
Forecasts close{" "}
{formatDate(competition.forecasts_close_date)}
{formatDate(competition.forecasts_close_date, timezone)}
</>
)}
{status === "forecasts-closed" &&
competition.end_date && (
<>Ends {formatDate(competition.end_date)}</>
<>Ends {formatDate(competition.end_date, timezone)}</>
)}
{status === "ended" && competition.end_date && (
<>Ended {formatDate(competition.end_date)}</>
<>Ended {formatDate(competition.end_date, timezone)}</>
)}
{status === "private" && (
<>Uses per-prop deadlines</>
Expand All @@ -114,17 +116,17 @@ export default function CompetitionRow({
{competition.forecasts_open_date && (
<p>
Forecasts Open:{" "}
{formatDateTime(competition.forecasts_open_date)}
{formatDateTime(competition.forecasts_open_date, timezone)}
</p>
)}
{competition.forecasts_close_date && (
<p>
Forecasts Close:{" "}
{formatDateTime(competition.forecasts_close_date)}
{formatDateTime(competition.forecasts_close_date, timezone)}
</p>
)}
{competition.end_date && (
<p>Ends: {formatDateTime(competition.end_date)}</p>
<p>Ends: {formatDateTime(competition.end_date, timezone)}</p>
)}
</div>
)}
Expand Down
13 changes: 6 additions & 7 deletions app/admin/users/[userId]/user-detail-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import {
DialogTrigger,
} from "@/components/ui/dialog";
import { Copy, Shield, User, UserCheck, UserX } from "lucide-react";
import { format } from "date-fns";
import { setUserActive } from "@/lib/db_actions/users";
import { getBrowserTimezone } from "@/hooks/getBrowserTimezone";
import { formatDate, formatDateTime } from "@/lib/time-utils";
import { startImpersonation } from "@/lib/auth/impersonation";
import { handleServerActionResult } from "@/lib/server-action-helpers";
import { toast } from "@/hooks/use-toast";
Expand All @@ -32,6 +33,7 @@ export default function UserDetailCard({ user }: UserDetailCardProps) {
const [isImpersonateDialogOpen, setIsImpersonateDialogOpen] = useState(false);
const [isLoading, setIsLoading] = useState(false);
const router = useRouter();
const timezone = getBrowserTimezone();
const isActive = user.deactivated_at === null;
const canImpersonate = !user.is_admin && isActive;

Expand Down Expand Up @@ -191,7 +193,7 @@ export default function UserDetailCard({ user }: UserDetailCardProps) {
Created
</span>
<span className="text-sm">
{format(new Date(user.created_at), "MMM d, yyyy")}
{formatDate(new Date(user.created_at), timezone)}
</span>
</div>

Expand All @@ -200,7 +202,7 @@ export default function UserDetailCard({ user }: UserDetailCardProps) {
Updated
</span>
<span className="text-sm">
{format(new Date(user.updated_at), "MMM d, yyyy")}
{formatDate(new Date(user.updated_at), timezone)}
</span>
</div>

Expand All @@ -210,10 +212,7 @@ export default function UserDetailCard({ user }: UserDetailCardProps) {
Deactivated
</span>
<span className="text-sm">
{format(
new Date(user.deactivated_at),
"MMM d, yyyy 'at' h:mm a",
)}
{formatDateTime(new Date(user.deactivated_at), timezone)}
</span>
</div>
)}
Expand Down
40 changes: 10 additions & 30 deletions app/competitions/[competitionId]/competition-start-end.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
"use client";

import { Competition } from "@/types/db_types";
import { formatInTimeZone } from "date-fns-tz";
import { getCompetitionStatusFromObject } from "@/lib/competition-status";

const DATE_FORMAT = "MMM d, yyyy";
import { getBrowserTimezone } from "@/hooks/getBrowserTimezone";
import { formatDate } from "@/lib/time-utils";

export default function CompetitionStartEnd({
competition,
}: {
competition: Competition;
}) {
const timezone = getBrowserTimezone();
const status = getCompetitionStatusFromObject(competition);
return (
<div className="flex flex-row flex-wrap items-center justify-start gap-x-4 sm:gap-x-8 gap-y-2 mb-4 text-sm">
Expand All @@ -20,19 +20,11 @@ export default function CompetitionStartEnd({
<>
<p>
<span className="text-muted-foreground">Forecasts open</span>{" "}
{formatInTimeZone(
competition.forecasts_open_date,
"UTC",
DATE_FORMAT,
)}
{formatDate(competition.forecasts_open_date, timezone)}
</p>
<p>
<span className="text-muted-foreground">Forecasts close</span>{" "}
{formatInTimeZone(
competition.forecasts_close_date,
"UTC",
DATE_FORMAT,
)}
{formatDate(competition.forecasts_close_date, timezone)}
</p>
</>
)}
Expand All @@ -42,19 +34,11 @@ export default function CompetitionStartEnd({
<>
<p>
<span className="text-muted-foreground">Forecasts opened</span>{" "}
{formatInTimeZone(
competition.forecasts_open_date,
"UTC",
DATE_FORMAT,
)}
{formatDate(competition.forecasts_open_date, timezone)}
</p>
<p>
<span className="text-muted-foreground">Forecasts close</span>{" "}
{formatInTimeZone(
competition.forecasts_close_date,
"UTC",
DATE_FORMAT,
)}
{formatDate(competition.forecasts_close_date, timezone)}
</p>
</>
)}
Expand All @@ -64,22 +48,18 @@ export default function CompetitionStartEnd({
<>
<p>
<span className="text-muted-foreground">Forecasts closed</span>{" "}
{formatInTimeZone(
competition.forecasts_close_date,
"UTC",
DATE_FORMAT,
)}
{formatDate(competition.forecasts_close_date, timezone)}
</p>
<p>
<span className="text-muted-foreground">Competition Ends</span>{" "}
{formatInTimeZone(competition.end_date, "UTC", DATE_FORMAT)}
{formatDate(competition.end_date, timezone)}
</p>
</>
)}
{status === "ended" && competition.end_date && (
<p>
<span className="text-muted-foreground">Ended</span>{" "}
{formatInTimeZone(competition.end_date, "UTC", DATE_FORMAT)}
{formatDate(competition.end_date, timezone)}
</p>
)}
{status === "private" && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
import { MarkdownRenderer } from "@/components/markdown";
import { useCurrentUser } from "@/hooks/useCurrentUser";
import { getBrowserTimezone } from "@/hooks/getBrowserTimezone";
import { createForecast, updateForecast } from "@/lib/db_actions";
import { useServerAction } from "@/hooks/use-server-action";
import { Spinner } from "@/components/ui/spinner";
import { PropEditDialog } from "@/components/dialogs/prop-edit-dialog";
import { formatDateTime } from "@/lib/time-utils";

interface CompetitionPropViewProps {
prop: PropWithUserForecast;
Expand Down Expand Up @@ -68,17 +70,10 @@ const getProbColor = (prob: number | null) => {
};
};

function formatDate(date: Date | string | null): string {
function formatPropDate(date: Date | string | null, timezone: string): string {
if (!date) return "No deadline";
const d = typeof date === "string" ? new Date(date) : date;
return d.toLocaleDateString("en-US", {
weekday: "short",
month: "short",
day: "numeric",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
return formatDateTime(d, timezone);
}

function getRelativeDeadline(date: Date | string | null): string | null {
Expand All @@ -104,6 +99,7 @@ export function CompetitionPropView({
}: CompetitionPropViewProps) {
const router = useRouter();
const { user } = useCurrentUser();
const timezone = getBrowserTimezone();
const [localForecast, setLocalForecast] = useState<number | null>(
prop.user_forecast,
);
Expand Down Expand Up @@ -235,7 +231,7 @@ export function CompetitionPropView({
<div className="flex flex-wrap items-center gap-4 mt-4 text-sm">
<div className="flex items-center gap-2 text-muted-foreground">
<CalendarClock className="h-4 w-4" />
<span>Forecasts due: {formatDate(prop.prop_forecasts_due_date)}</span>
<span>Forecasts due: {formatPropDate(prop.prop_forecasts_due_date, timezone)}</span>
{relativeDeadline && (
<Badge
variant={relativeDeadline === "Closed" ? "destructive" : "outline"}
Expand All @@ -248,7 +244,7 @@ export function CompetitionPropView({
{prop.prop_resolution_due_date && (
<div className="flex items-center gap-2 text-muted-foreground">
<Calendar className="h-4 w-4" />
<span>Resolves: {formatDate(prop.prop_resolution_due_date)}</span>
<span>Resolves: {formatPropDate(prop.prop_resolution_due_date, timezone)}</span>
</div>
)}
</div>
Expand Down
5 changes: 4 additions & 1 deletion app/competitions/[competitionId]/props/new/new-prop-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@
import { DateTimePicker } from "@/components/ui/date-time-picker";
import { Spinner } from "@/components/ui/spinner";
import { useServerAction } from "@/hooks/use-server-action";
import { getBrowserTimezone } from "@/hooks/getBrowserTimezone";
import { createProp } from "@/lib/db_actions";
import { formatDate } from "@/lib/time-utils";
import type { Category } from "@/types/db_types";

const formSchema = z
Expand Down Expand Up @@ -95,11 +97,12 @@

export function NewPropForm({
competitionId,
competitionName,

Check warning on line 100 in app/competitions/[competitionId]/props/new/new-prop-form.tsx

View workflow job for this annotation

GitHub Actions / PR Checks

'competitionName' is defined but never used
categories,
userId,
}: NewPropFormProps) {
const router = useRouter();
const timezone = getBrowserTimezone();
const [showPreview, setShowPreview] = useState(false);

const form = useForm<FormValues>({
Expand Down Expand Up @@ -346,7 +349,7 @@
{watchedForecastsDueDate && (
<p className="text-xs text-muted-foreground">
Forecasts due:{" "}
{watchedForecastsDueDate.toLocaleDateString()}
{formatDate(watchedForecastsDueDate, timezone)}
</p>
)}
</div>
Expand Down
10 changes: 5 additions & 5 deletions app/competitions/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
import { Button } from "@/components/ui/button";
import { Trophy, BarChart3, List } from "lucide-react";
import { CompetitionStatusBadge } from "@/app/admin/competitions/competition-status-badge";
import { formatDate } from "@/lib/time-utils";
import { LocalDate } from "@/components/local-date";
import { getCompetitionStatusFromObject } from "@/lib/competition-status";

export default async function CompetitionsPage() {
Expand Down Expand Up @@ -74,13 +74,13 @@ export default async function CompetitionsPage() {
<span className="font-medium">
Forecasts due:
</span>{" "}
{formatDate(competition.forecasts_close_date)}
<LocalDate date={competition.forecasts_close_date} />
</p>
)}
{competition.end_date && (
<p>
<span className="font-medium">Ends:</span>{" "}
{formatDate(competition.end_date)}
<LocalDate date={competition.end_date} />
</p>
)}
</>
Expand Down Expand Up @@ -133,13 +133,13 @@ export default async function CompetitionsPage() {
<span className="font-medium">
Forecasts due:
</span>{" "}
{formatDate(competition.forecasts_close_date)}
<LocalDate date={competition.forecasts_close_date} />
</p>
)}
{competition.end_date && (
<p>
<span className="font-medium">Ends:</span>{" "}
{formatDate(competition.end_date)}
<LocalDate date={competition.end_date} />
</p>
)}
</>
Expand Down
18 changes: 10 additions & 8 deletions components/competition-dashboard/upcoming-deadlines.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,21 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
import type { UpcomingDeadline } from "@/lib/db_actions/competition-stats";
import { getBrowserTimezone } from "@/hooks/getBrowserTimezone";
import { formatDate } from "@/lib/time-utils";

interface DeadlineDisplay {
text: string;
relative: string | null;
urgent: boolean;
}

function formatDeadline(date: Date): DeadlineDisplay {
function formatDeadline(date: Date, timezone: string): DeadlineDisplay {
const now = new Date();
const diffMs = date.getTime() - now.getTime();
const diffDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));

const formatted = date.toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: date.getFullYear() !== now.getFullYear() ? "numeric" : undefined,
});
const formatted = formatDate(date, timezone);

if (diffDays <= 0) {
return { text: formatted, relative: "Overdue", urgent: true };
Expand Down Expand Up @@ -73,10 +71,11 @@ function getProbColor(prob: number | null): { bg: string; text: string } {
interface UpcomingPropRowProps {
prop: UpcomingDeadline;
competitionId: number;
timezone: string;
}

function UpcomingPropRow({ prop, competitionId }: UpcomingPropRowProps) {
const deadline = formatDeadline(prop.deadline);
function UpcomingPropRow({ prop, competitionId, timezone }: UpcomingPropRowProps) {
const deadline = formatDeadline(prop.deadline, timezone);
const colors = getProbColor(prop.userForecast);
const percent =
prop.userForecast !== null ? Math.round(prop.userForecast * 100) : null;
Expand Down Expand Up @@ -140,6 +139,8 @@ export function UpcomingDeadlines({
competitionId,
onViewAll,
}: UpcomingDeadlinesProps) {
const timezone = getBrowserTimezone();

if (deadlines.length === 0) {
return (
<div className="bg-card border border-border rounded-lg p-5">
Expand Down Expand Up @@ -173,6 +174,7 @@ export function UpcomingDeadlines({
key={prop.propId}
prop={prop}
competitionId={competitionId}
timezone={timezone}
/>
))}
</div>
Expand Down
Loading
Loading