mirror of
https://github.com/mroxso/zelo-news.git
synced 2026-06-05 10:01:22 +02:00
feat: add date validation and safe ISO string conversion for improved date handling
This commit is contained in:
@@ -7,6 +7,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Calendar } from 'lucide-react';
|
||||
import { useAuthor } from '@/hooks/useAuthor';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { isValidDate, toISOStringSafe } from '@/lib/date';
|
||||
|
||||
interface ArticlePreviewProps {
|
||||
post: NostrEvent;
|
||||
@@ -48,6 +49,8 @@ export function ArticlePreview({ post, variant = 'default', showAuthor = true }:
|
||||
? { month: 'short', day: 'numeric', year: 'numeric' }
|
||||
: { year: 'numeric', month: 'long', day: 'numeric' };
|
||||
|
||||
const valid = isValidDate(date);
|
||||
|
||||
return (
|
||||
<Link to={`/${naddr}`}>
|
||||
<Card className="overflow-hidden hover:shadow-lg transition-shadow h-full flex flex-col">
|
||||
@@ -71,12 +74,14 @@ export function ArticlePreview({ post, variant = 'default', showAuthor = true }:
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className={`flex items-center gap-2 text-xs text-muted-foreground ${showAuthor || hashtags.length > 0 ? 'mb-3' : ''}`}>
|
||||
<Calendar className="h-3 w-3" />
|
||||
<time dateTime={date.toISOString()}>
|
||||
{date.toLocaleDateString('en-US', dateFormat as Intl.DateTimeFormatOptions)}
|
||||
</time>
|
||||
</div>
|
||||
{valid && (
|
||||
<div className={`flex items-center gap-2 text-xs text-muted-foreground ${showAuthor || hashtags.length > 0 ? 'mb-3' : ''}`}>
|
||||
<Calendar className="h-3 w-3" />
|
||||
<time dateTime={toISOStringSafe(date)}>
|
||||
{date.toLocaleDateString('en-US', dateFormat as Intl.DateTimeFormatOptions)}
|
||||
</time>
|
||||
</div>
|
||||
)}
|
||||
{showAuthor && (
|
||||
<div className={`flex items-center gap-2 ${hashtags.length > 0 ? 'mb-3' : ''}`}>
|
||||
<Avatar className="h-6 w-6">
|
||||
|
||||
@@ -17,6 +17,7 @@ import { Separator } from '@/components/ui/separator';
|
||||
import { Calendar, Heart, Edit, ArrowLeft, Share2, Check, Code } from 'lucide-react';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { calculateReadingTime } from '@/lib/calculateReadingTime';
|
||||
import { isValidDate, toISOStringSafe } from '@/lib/date';
|
||||
import { useToast } from '@/hooks/useToast';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
@@ -108,6 +109,8 @@ export function ArticleView({ post }: ArticleViewProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const validDate = isValidDate(date);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
<ArticleProgressBar />
|
||||
@@ -144,16 +147,18 @@ export function ArticleView({ post }: ArticleViewProps) {
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-semibold">{displayName}</div>
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<time dateTime={date.toISOString()}>
|
||||
{date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
{validDate && (
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<time dateTime={toISOStringSafe(date)}>
|
||||
{date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
|
||||
13
src/lib/date.ts
Normal file
13
src/lib/date.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export function isValidDate(date: Date | null | undefined): boolean {
|
||||
if (!date) return false;
|
||||
return !isNaN(date.getTime());
|
||||
}
|
||||
|
||||
export function toISOStringSafe(date: Date | null | undefined): string {
|
||||
if (!isValidDate(date as Date)) return "";
|
||||
try {
|
||||
return (date as Date).toISOString();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { ArticleView } from '@/components/ArticleView';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import NotFound from '@/pages/NotFound';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import { toISOStringSafe } from '@/lib/date';
|
||||
|
||||
export default function ArticlePage() {
|
||||
const { nip19: naddr } = useParams<{ nip19: string }>();
|
||||
@@ -71,11 +72,16 @@ export default function ArticlePage() {
|
||||
ogImage: image || `${siteUrl}/icon-512.png`,
|
||||
ogSiteName: 'zelo.news',
|
||||
// Article-specific OG tags
|
||||
...(post && isValidNaddr && {
|
||||
articlePublishedTime: date.toISOString(),
|
||||
articleAuthor: [authorName],
|
||||
...(hashtags.length > 0 && { articleTag: hashtags }),
|
||||
}),
|
||||
...(post && isValidNaddr && (() => {
|
||||
const iso = toISOStringSafe(date);
|
||||
return iso
|
||||
? {
|
||||
articlePublishedTime: iso,
|
||||
articleAuthor: [authorName],
|
||||
...(hashtags.length > 0 && { articleTag: hashtags }),
|
||||
}
|
||||
: {};
|
||||
})()),
|
||||
// Twitter Card tags
|
||||
twitterCard: 'summary_large_image',
|
||||
twitterTitle: title,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ArrowLeft, Calendar, Hash } from 'lucide-react';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import NotFound from './NotFound';
|
||||
import { isValidDate, toISOStringSafe } from '@/lib/date';
|
||||
|
||||
interface EventPageProps {
|
||||
eventId: string;
|
||||
@@ -118,6 +119,7 @@ export function EventPage({ eventId, relayHints, authorPubkey, kind }: EventPage
|
||||
}
|
||||
|
||||
const date = new Date(event.created_at * 1000);
|
||||
const validDate = isValidDate(date);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
@@ -141,19 +143,21 @@ export function EventPage({ eventId, relayHints, authorPubkey, kind }: EventPage
|
||||
<Hash className="h-3 w-3" />
|
||||
Kind {event.kind}
|
||||
</Badge>
|
||||
<time
|
||||
dateTime={date.toISOString()}
|
||||
className="text-sm text-muted-foreground flex items-center gap-1"
|
||||
>
|
||||
<Calendar className="h-3 w-3" />
|
||||
{date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</time>
|
||||
{validDate && (
|
||||
<time
|
||||
dateTime={toISOStringSafe(date)}
|
||||
className="text-sm text-muted-foreground flex items-center gap-1"
|
||||
>
|
||||
<Calendar className="h-3 w-3" />
|
||||
{date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Link
|
||||
|
||||
@@ -18,6 +18,7 @@ import { Heart, MessageCircle, ArrowLeft } from 'lucide-react';
|
||||
import { genUserName } from '@/lib/genUserName';
|
||||
import type { NostrEvent } from '@nostrify/nostrify';
|
||||
import NotFound from './NotFound';
|
||||
import { isValidDate, toISOStringSafe } from '@/lib/date';
|
||||
|
||||
interface NotePageProps {
|
||||
eventId: string;
|
||||
@@ -117,6 +118,7 @@ export function NotePage({ eventId }: NotePageProps) {
|
||||
}
|
||||
|
||||
const date = new Date(note.created_at * 1000);
|
||||
const validDate = isValidDate(date);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen">
|
||||
@@ -145,18 +147,20 @@ export function NotePage({ eventId }: NotePageProps) {
|
||||
</Avatar>
|
||||
<div>
|
||||
<div className="font-semibold">{displayName}</div>
|
||||
<time
|
||||
dateTime={date.toISOString()}
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</time>
|
||||
{validDate && (
|
||||
<time
|
||||
dateTime={toISOStringSafe(date)}
|
||||
className="text-sm text-muted-foreground"
|
||||
>
|
||||
{date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})}
|
||||
</time>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
|
||||
Reference in New Issue
Block a user