Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 2 additions & 1 deletion client/src/components/ApiKeysPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from "react";
import { formatDate } from "@/lib/date-format";
import { useAuth } from "@/hooks/use-auth";
import { useApiKeys, useCreateApiKey, useRevokeApiKey } from "@/hooks/use-api-keys";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
Expand Down Expand Up @@ -137,7 +138,7 @@ function ApiKeysTable() {
<code>{key.keyPrefix}...</code>
{key.lastUsedAt && (
<> &middot; Last used{" "}
{new Date(key.lastUsedAt).toLocaleDateString()}
{formatDate(key.lastUsedAt)}
</>
)}
{!key.lastUsedAt && <> &middot; Never used</>}
Expand Down
3 changes: 2 additions & 1 deletion client/src/components/DeliveryLog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from "react";
import { formatDateTime } from "@/lib/date-format";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -75,7 +76,7 @@ export function DeliveryLog({ monitorId }: DeliveryLogProps) {
return (
<TableRow key={entry.id}>
<TableCell className="text-sm text-muted-foreground whitespace-nowrap">
{new Date(entry.createdAt).toLocaleString()}
{formatDateTime(entry.createdAt)}
</TableCell>
<TableCell>
<div className="flex items-center gap-1.5">
Expand Down
16 changes: 16 additions & 0 deletions client/src/lib/date-format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { format } from "date-fns";

/** "Mar 6, 2026" */
export function formatDate(date: Date | string): string {
return format(new Date(date), "MMM d, yyyy");
}
Comment on lines +4 to +6
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Consider adding null/undefined guard to prevent silent failures.

new Date(null) returns Unix epoch (Jan 1, 1970), and new Date(undefined) returns Invalid Date. Since callers may pass nullable timestamps (e.g., lastUsedAt in ApiKeysPanel), a defensive check would prevent unexpected output if the null guard is accidentally removed at call sites.

 export function formatDate(date: Date | string): string {
+  if (date == null) {
+    throw new Error("formatDate received null or undefined");
+  }
   return format(new Date(date), "MMM d, yyyy");
 }

Alternatively, update the type signature to Date | string | null | undefined and return a fallback like "—".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@client/src/lib/date-format.ts` around lines 4 - 6, The formatDate function
currently calls new Date(date) which yields epoch or Invalid Date for
null/undefined; update formatDate to accept Date | string | null | undefined (or
keep signature and add runtime guards) and return a safe fallback like "—" when
date is null/undefined or when new Date(date) is invalid; specifically, in
function formatDate check for date == null, parse to a Date object, verify
!isNaN(parsed.getTime()), and only then call format(parsed, "MMM d, yyyy"),
otherwise return the fallback.


/** "1557" */
export function formatTime(date: Date | string): string {
return format(new Date(date), "HHmm");
}

/** "Mar 6, 2026 1557" */
export function formatDateTime(date: Date | string): string {
return format(new Date(date), "MMM d, yyyy HHmm");
}
8 changes: 2 additions & 6 deletions client/src/pages/AdminCampaignDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { formatDateTime as formatDateTimeStd } from "@/lib/date-format";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -60,12 +61,7 @@ const recipientStatusConfig: Record<string, { variant: "default" | "secondary" |

function formatDateTime(dateStr: string | null | undefined): string {
if (!dateStr) return "-";
return new Date(dateStr).toLocaleString("en-US", {
month: "short",
day: "numeric",
hour: "numeric",
minute: "2-digit",
});
return formatDateTimeStd(dateStr);
}

function formatRate(count: number, total: number): string {
Expand Down
7 changes: 2 additions & 5 deletions client/src/pages/AdminCampaigns.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState } from "react";
import { formatDate as formatDateStd } from "@/lib/date-format";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -72,11 +73,7 @@ const statusConfig: Record<string, { variant: "default" | "secondary" | "outline

function formatDate(dateStr: string | null | undefined): string {
if (!dateStr) return "-";
return new Date(dateStr).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
});
return formatDateStd(dateStr);
}

function formatRate(count: number, total: number): string {
Expand Down
12 changes: 4 additions & 8 deletions client/src/pages/AdminErrors.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useState, useRef, useCallback, useEffect } from "react";
import { formatDate, formatDateTime } from "@/lib/date-format";
import { useQuery, useMutation } from "@tanstack/react-query";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
Expand Down Expand Up @@ -327,8 +328,7 @@ export default function AdminErrors() {
: selectedIds.size > 0 && !allVisibleSelected;

const formatTimestamp = (ts: string) => {
const d = new Date(ts);
return d.toLocaleString();
return formatDateTime(ts);
};

const getTierBadgeClass = (tier: string) => {
Expand All @@ -351,7 +351,7 @@ export default function AdminErrors() {
if (diffHours < 24) return `${diffHours}h ago`;
const diffDays = Math.floor(diffHours / 24);
if (diffDays < 30) return `${diffDays}d ago`;
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
return formatDate(date);
};

return (
Expand Down Expand Up @@ -476,11 +476,7 @@ export default function AdminErrors() {
</TableCell>
<TableCell className="text-right text-xs text-muted-foreground">
{u.created_at
? new Date(u.created_at).toLocaleDateString("en-US", {
month: "short",
day: "numeric",
year: "numeric",
})
? formatDate(u.created_at)
: "\u2014"}
</TableCell>
</TableRow>
Expand Down
7 changes: 2 additions & 5 deletions client/src/pages/Blog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Link } from "wouter";
import { formatDate } from "@/lib/date-format";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { ArrowRight } from "lucide-react";
Expand Down Expand Up @@ -58,11 +59,7 @@ export default function Blog() {
<div className="flex items-center gap-2 mb-2">
<Badge variant="secondary">{post.category}</Badge>
<span className="text-sm text-muted-foreground">
{new Date(post.date).toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric"
})}
{formatDate(post.date)}
</span>
</div>
<CardTitle className="text-xl md:text-2xl">{post.title}</CardTitle>
Expand Down
3 changes: 2 additions & 1 deletion client/src/pages/BlogComparison.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect } from "react";
import { formatDate } from "@/lib/date-format";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ArrowRight, Check, X, Minus } from "lucide-react";
Expand Down Expand Up @@ -115,7 +116,7 @@ export default function BlogComparison() {
FetchTheChange vs Distill, Visualping, Hexowatch (and others): Which Website Change Monitor Should You Use?
</h1>
<p className="text-muted-foreground">
By {AUTHOR} · Published {new Date(PUBLISH_DATE).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}
By {AUTHOR} · Published {formatDate(PUBLISH_DATE)}
</p>
</header>

Expand Down
3 changes: 2 additions & 1 deletion client/src/pages/BlogPriceMonitoring.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect } from "react";
import { formatDate } from "@/lib/date-format";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ArrowRight } from "lucide-react";
Expand Down Expand Up @@ -102,7 +103,7 @@ export default function BlogPriceMonitoring() {
How to Monitor Competitor Prices Without Getting Blocked (2026 Guide)
</h1>
<p className="text-muted-foreground">
By {AUTHOR} · Published {new Date(PUBLISH_DATE).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}
By {AUTHOR} · Published {formatDate(PUBLISH_DATE)}
</p>
</header>

Expand Down
3 changes: 2 additions & 1 deletion client/src/pages/BlogSelectorBreakage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect } from "react";
import { formatDate } from "@/lib/date-format";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ArrowRight } from "lucide-react";
Expand Down Expand Up @@ -102,7 +103,7 @@ export default function BlogSelectorBreakage() {
CSS Selectors Keep Breaking? Why It Happens and How to Fix It
</h1>
<p className="text-muted-foreground">
By {AUTHOR} · Published {new Date(PUBLISH_DATE).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}
By {AUTHOR} · Published {formatDate(PUBLISH_DATE)}
</p>
</header>

Expand Down
3 changes: 2 additions & 1 deletion client/src/pages/BlogWhyMonitorsFail.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { useEffect } from "react";
import { formatDate } from "@/lib/date-format";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { ArrowRight, CheckCircle2 } from "lucide-react";
Expand Down Expand Up @@ -94,7 +95,7 @@ export default function BlogWhyMonitorsFail() {
Why Website Change Monitors Fail Silently on JavaScript-Heavy Sites
</h1>
<p className="text-muted-foreground">
By {AUTHOR} · Published {new Date(PUBLISH_DATE).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}
By {AUTHOR} · Published {formatDate(PUBLISH_DATE)}
</p>
</header>

Expand Down
8 changes: 4 additions & 4 deletions client/src/pages/MonitorDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useRoute, Link } from "wouter";
import { format } from "date-fns";
import { formatDateTime } from "@/lib/date-format";
import { useAuth } from "@/hooks/use-auth";
import { useMonitor, useMonitorHistory, useCheckMonitor, useDeleteMonitor, useUpdateMonitor } from "@/hooks/use-monitors";
import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -358,7 +358,7 @@ export default function MonitorDetails() {
<p className="text-sm text-muted-foreground">Last Checked</p>
<p className="font-medium">
{monitor.lastChecked
? format(new Date(monitor.lastChecked), "PPp")
? formatDateTime(monitor.lastChecked)
: "Never checked"}
</p>
</div>
Expand All @@ -371,7 +371,7 @@ export default function MonitorDetails() {
<p className="text-sm text-muted-foreground">Last Change Detected</p>
<p className="font-medium">
{monitor.lastChanged
? format(new Date(monitor.lastChanged), "PPp")
? formatDateTime(monitor.lastChanged)
: "No changes detected"}
</p>
</div>
Expand Down Expand Up @@ -473,7 +473,7 @@ export default function MonitorDetails() {
{history.map((change) => (
<TableRow key={change.id}>
<TableCell className="font-medium whitespace-nowrap">
{format(new Date(change.detectedAt), "MMM d, yyyy HH:mm")}
{formatDateTime(change.detectedAt)}
</TableCell>
<TableCell className="font-mono text-xs text-muted-foreground max-w-[300px] truncate" title={change.oldValue || ""}>
{change.oldValue || <span className="italic">null</span>}
Expand Down
3 changes: 2 additions & 1 deletion server/services/browserlessTracker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { format } from "date-fns";
import { db } from "../db";
import { browserlessUsage, errorLogs } from "@shared/schema";
import { BROWSERLESS_CAPS, users, type UserTier } from "@shared/models/auth";
Expand All @@ -15,7 +16,7 @@ function getMonthEnd(): Date {

export function getMonthResetDate(): string {
const end = getMonthEnd();
return end.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
return format(end, "MMM d, yyyy");
}

export class BrowserlessUsageTracker {
Expand Down
5 changes: 3 additions & 2 deletions server/services/email.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Resend } from "resend";
import { format } from "date-fns";
import { type Monitor, type MonitorChange } from "@shared/schema";
import { authStorage } from "../replit_integrations/auth/storage";
import { type UserTier } from "@shared/models/auth";
Expand Down Expand Up @@ -258,12 +259,12 @@ export async function sendDigestEmail(monitor: Monitor, changes: MonitorChange[]
const recipientEmail = emailOverride || user.notificationEmail || user.email;

const changesTextList = changes.map((c, i) => {
const dateStr = new Date(c.detectedAt).toLocaleString("en-US");
const dateStr = format(new Date(c.detectedAt), "MMM d, yyyy HHmm");
return ` ${i + 1}. [${dateStr}]\n Old: ${sanitizePlainText(c.oldValue)}\n New: ${sanitizePlainText(c.newValue)}`;
}).join("\n\n");

const changesHtmlList = changes.map((c) => {
const dateStr = new Date(c.detectedAt).toLocaleString("en-US");
const dateStr = format(new Date(c.detectedAt), "MMM d, yyyy HHmm");
Comment on lines +262 to +267
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Date formatting changes are safe and preserve XSS protections.

The dateStr values flow correctly through sanitizePlainText (line 263) and escapeHtml (line 270) respectively, maintaining the existing security posture.

Minor maintainability note: The format string "MMM d, yyyy HHmm" is duplicated between this server file and the new client/src/lib/date-format.ts utility. If the format ever needs to change, both locations must be updated. Consider extracting a shared constant to a @shared/ module that both client and server can import.
,

♻️ Optional: Extract shared format constant

Create a shared constant (e.g., in @shared/constants.ts):

export const DATE_TIME_FORMAT = "MMM d, yyyy HHmm";

Then import and use in both locations:

-import { format } from "date-fns";
+import { format } from "date-fns";
+import { DATE_TIME_FORMAT } from "@shared/constants";
-const dateStr = format(new Date(c.detectedAt), "MMM d, yyyy HHmm");
+const dateStr = format(new Date(c.detectedAt), DATE_TIME_FORMAT);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@server/services/email.ts` around lines 262 - 267, The date format string "MMM
d, yyyy HHmm" is duplicated; extract it into a shared constant (e.g.,
DATE_TIME_FORMAT) in a shared module (for example `@shared/constants.ts`) and
replace the literal in server/services/email.ts where dateStr is created (the
format(...) calls that assign dateStr for plain-text and HTML lists) and in the
client utility (client/src/lib/date-format.ts) to import and use
DATE_TIME_FORMAT instead so future changes only require updating one constant.

return `
<tr>
<td style="padding: 8px; border: 1px solid #ddd;">${escapeHtml(dateStr)}</td>
Expand Down
5 changes: 3 additions & 2 deletions server/services/resendTracker.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { format, parseISO } from "date-fns";
import { db } from "../db";
import { resendUsage, errorLogs } from "@shared/schema";
import { RESEND_CAPS } from "@shared/models/auth";
Expand All @@ -20,7 +21,7 @@ function getMonthEnd(): Date {

export function getResendResetDate(): string {
const end = getMonthEnd();
return end.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" });
return format(end, "MMM d, yyyy");
}

export class ResendUsageTracker {
Expand Down Expand Up @@ -165,7 +166,7 @@ export class ResendUsageTracker {
`);

return (result.rows as any[]).map(r => ({
date: new Date(r.date).toLocaleDateString("en-US", { month: "short", day: "numeric" }),
date: format(parseISO(r.date), "MMM d, yyyy"),
count: Number(r.count),
}));
}
Expand Down
Loading