From 71972afb8792ad5f6648ae69a8c61a15ff73ac77 Mon Sep 17 00:00:00 2001 From: test Date: Sun, 30 Aug 2026 09:28:07 +0200 Subject: [PATCH] Make profile formatting hydration-safe --- .../achievements/AchievementCard.jsx | 5 +++-- .../profile/CreatorJourneySection.jsx | 21 ++++++++---------- .../profile/collections/CollectionCard.jsx | 18 +++++---------- .../js/components/profile/tabs/TabAbout.jsx | 22 +++++++++---------- .../components/profile/tabs/TabFollowers.jsx | 12 +++------- .../js/components/profile/tabs/TabStats.jsx | 7 +++--- .../worlds/ProfileWorldHistoryCard.jsx | 8 +++---- .../worlds/ProfileWorldHistorySummary.jsx | 8 +++---- .../js/components/social/FollowButton.jsx | 3 ++- 9 files changed, 44 insertions(+), 60 deletions(-) diff --git a/resources/js/components/achievements/AchievementCard.jsx b/resources/js/components/achievements/AchievementCard.jsx index b0f8fa7a..06ff974b 100644 --- a/resources/js/components/achievements/AchievementCard.jsx +++ b/resources/js/components/achievements/AchievementCard.jsx @@ -1,4 +1,5 @@ import React from 'react' +import { formatProfileDate } from '../../lib/profileFormatters' import AchievementBadge from './AchievementBadge' function cx(...parts) { @@ -46,9 +47,9 @@ export default function AchievementCard({ achievement, className = '' }) { {achievement?.unlocked_at ? (

- Unlocked {new Date(achievement.unlocked_at).toLocaleDateString()} + Unlocked {formatProfileDate(achievement.unlocked_at, { year: 'numeric', month: 'short', day: 'numeric' })}

) : null} ) -} \ No newline at end of file +} diff --git a/resources/js/components/profile/CreatorJourneySection.jsx b/resources/js/components/profile/CreatorJourneySection.jsx index 9969a8d4..13df9282 100644 --- a/resources/js/components/profile/CreatorJourneySection.jsx +++ b/resources/js/components/profile/CreatorJourneySection.jsx @@ -1,14 +1,11 @@ import React from 'react' +import { formatProfileDate, formatProfileNumber } from '../../lib/profileFormatters' function formatDate(value) { if (!value) return null try { - return new Intl.DateTimeFormat('en', { - month: 'short', - day: 'numeric', - year: 'numeric', - }).format(new Date(value)) + return formatProfileDate(value, { month: 'short', day: 'numeric', year: 'numeric' }) } catch { return null } @@ -17,7 +14,7 @@ function formatDate(value) { function formatYear(value) { if (!value) return null try { - return new Intl.DateTimeFormat('en', { year: 'numeric' }).format(new Date(value)) + return formatProfileDate(value, { year: 'numeric' }) } catch { return null } @@ -268,8 +265,8 @@ function YearlyProductivityChart({ recaps }) {
{uploads} upload{uploads !== 1 ? 's' : ''} - {(item.metrics?.views ?? 0) > 0 ? ` · ${Number(item.metrics.views).toLocaleString()} views` : ''} - {(item.metrics?.downloads ?? 0) > 0 ? ` · ${Number(item.metrics.downloads).toLocaleString()} dl` : ''} + {(item.metrics?.views ?? 0) > 0 ? ` · ${formatProfileNumber(item.metrics.views)} views` : ''} + {(item.metrics?.downloads ?? 0) > 0 ? ` · ${formatProfileNumber(item.metrics.downloads)} dl` : ''}
@@ -292,8 +289,8 @@ function YearlyProductivityChart({ recaps }) { {/* Summary footer */}
{sorted.length} active years - {sorted.reduce((s, r) => s + r.metrics.uploads_count, 0).toLocaleString()} total uploads - {Number(sorted.reduce((s, r) => s + (r.metrics.views ?? 0), 0)).toLocaleString()} total views + {formatProfileNumber(sorted.reduce((s, r) => s + r.metrics.uploads_count, 0))} total uploads + {formatProfileNumber(sorted.reduce((s, r) => s + (r.metrics.views ?? 0), 0))} total views
) @@ -550,8 +547,8 @@ export default function CreatorJourneySection({ journey, username }) {

{item.summary}

- - + + {(item.metrics?.featured_count ?? 0) > 0 && ( )} diff --git a/resources/js/components/profile/collections/CollectionCard.jsx b/resources/js/components/profile/collections/CollectionCard.jsx index a85bd292..774bfc0d 100644 --- a/resources/js/components/profile/collections/CollectionCard.jsx +++ b/resources/js/components/profile/collections/CollectionCard.jsx @@ -1,5 +1,6 @@ import React from 'react' import CollectionVisibilityBadge from './CollectionVisibilityBadge' +import { formatProfileDate, formatProfileNumber } from '../../../lib/profileFormatters' async function requestJson(url, { method = 'GET', body } = {}) { const response = await fetch(url, { @@ -26,14 +27,7 @@ async function requestJson(url, { method = 'GET', body } = {}) { function formatUpdated(value) { if (!value) return 'Updated recently' - const date = new Date(value) - if (Number.isNaN(date.getTime())) return 'Updated recently' - - return new Intl.DateTimeFormat(undefined, { - month: 'short', - day: 'numeric', - year: 'numeric', - }).format(date) + return formatProfileDate(value, { month: 'short', day: 'numeric', year: 'numeric' }) || 'Updated recently' } function StatPill({ icon, label, value }) { @@ -178,7 +172,7 @@ export default function CollectionCard({ collection, isOwner, onDelete, onToggle {collection?.subtitle ?

{collection.subtitle}

: null} {collection?.owner?.name ?

Curated by {collection.owner.name}{collection?.owner?.username ? ` • @${collection.owner.username}` : ''}

: null}

- {(collection?.artworks_count ?? 0).toLocaleString()} artworks + {formatProfileNumber(collection?.artworks_count ?? 0)} artworks

@@ -190,9 +184,9 @@ export default function CollectionCard({ collection, isOwner, onDelete, onToggle ) : null}
- - - {collection?.collaborators_count > 1 ? : null} + + + {collection?.collaborators_count > 1 ? : null}
diff --git a/resources/js/components/profile/tabs/TabAbout.jsx b/resources/js/components/profile/tabs/TabAbout.jsx index 2e91c8f9..feede062 100644 --- a/resources/js/components/profile/tabs/TabAbout.jsx +++ b/resources/js/components/profile/tabs/TabAbout.jsx @@ -2,6 +2,7 @@ import React from 'react' import { usePage } from '@inertiajs/react' import CreatorJourneySection from '../CreatorJourneySection' import { shinyFlagUrl } from '../../../utils/flagUrl' +import { formatProfileDate, formatProfileNumber } from '../../../lib/profileFormatters' const SOCIAL_ICONS = { twitter: { icon: 'fa-brands fa-x-twitter', label: 'X / Twitter', hoverClass: 'hover:border-slate-300/30 hover:text-slate-100 hover:bg-white/[0.08]' }, @@ -66,7 +67,7 @@ function getContentTypeIcon(label) { } function formatNumber(value) { - return Number(value ?? 0).toLocaleString() + return formatProfileNumber(value) } function formatRelativeDate(value) { @@ -79,7 +80,7 @@ function formatRelativeDate(value) { const now = new Date() const diffSeconds = Math.round((date.getTime() - now.getTime()) / 1000) const absSeconds = Math.abs(diffSeconds) - const formatter = new Intl.RelativeTimeFormat('en', { numeric: 'auto' }) + const formatter = new Intl.RelativeTimeFormat('en-US', { numeric: 'auto' }) if (absSeconds < 3600) { return formatter.format(Math.round(diffSeconds / 60), 'minute') @@ -110,7 +111,7 @@ function formatShortDate(value) { const date = new Date(value) if (Number.isNaN(date.getTime())) return null - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + return formatProfileDate(date, { month: 'short', day: 'numeric', year: 'numeric' }) } catch { return null } @@ -131,7 +132,7 @@ function formatContributionDate(value) { const date = new Date(value) if (Number.isNaN(date.getTime())) return null - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + return formatProfileDate(date, { month: 'short', day: 'numeric', year: 'numeric' }) } catch { return null } @@ -237,14 +238,13 @@ export default function TabAbout({ user, profile, stats, achievements, worldRewa const flagUrl = shinyFlagUrl(profile?.country_code, props?.cdn?.files_url) const joinDate = user.created_at - ? new Date(user.created_at).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) + ? formatProfileDate(user.created_at, { month: 'long', day: 'numeric', year: 'numeric' }) : null const lastVisit = user.last_visit_at ? (() => { try { - const d = new Date(user.last_visit_at) - return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + return formatProfileDate(user.last_visit_at, { month: 'short', day: 'numeric', year: 'numeric' }) } catch { return null } })() : null @@ -254,7 +254,7 @@ export default function TabAbout({ user, profile, stats, achievements, worldRewa const birthDate = profile?.birthdate ? (() => { try { - return new Date(profile.birthdate).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' }) + return formatProfileDate(profile.birthdate, { month: 'long', day: 'numeric', year: 'numeric' }) } catch { return null } })() : null @@ -381,9 +381,9 @@ export default function TabAbout({ user, profile, stats, achievements, worldRewa {entry.group?.headline ?

{truncateText(entry.group.headline, 100)}

: null} {entry.summary ?

{entry.summary}

: null}
- {Number(entry.counts?.credited_artworks || 0).toLocaleString()} credited artworks - {Number(entry.counts?.releases || 0).toLocaleString()} releases - {Number(entry.counts?.projects || 0).toLocaleString()} projects + {formatProfileNumber(entry.counts?.credited_artworks || 0)} credited artworks + {formatProfileNumber(entry.counts?.releases || 0)} releases + {formatProfileNumber(entry.counts?.projects || 0)} projects {entry.joined_at ? Joined {formatContributionDate(entry.joined_at)} : null}
{Array.isArray(entry.role_labels) && entry.role_labels.length > 0 ? ( diff --git a/resources/js/components/profile/tabs/TabFollowers.jsx b/resources/js/components/profile/tabs/TabFollowers.jsx index 2a57b30c..f1595508 100644 --- a/resources/js/components/profile/tabs/TabFollowers.jsx +++ b/resources/js/components/profile/tabs/TabFollowers.jsx @@ -1,17 +1,11 @@ import React from 'react' +import { formatProfileDate, formatProfileNumber } from '../../../lib/profileFormatters' function formatFollowedAt(value) { if (!value) return null try { - const date = new Date(value) - if (Number.isNaN(date.getTime())) return null - - return date.toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', - }) + return formatProfileDate(value, { year: 'numeric', month: 'short', day: 'numeric' }) } catch { return null } @@ -36,7 +30,7 @@ export default function TabFollowers({ listing, username, followerCount }) { Followers - {total.toLocaleString()} + {formatProfileNumber(total)} diff --git a/resources/js/components/profile/tabs/TabStats.jsx b/resources/js/components/profile/tabs/TabStats.jsx index c3d722d4..2d324b82 100644 --- a/resources/js/components/profile/tabs/TabStats.jsx +++ b/resources/js/components/profile/tabs/TabStats.jsx @@ -1,4 +1,5 @@ import React from 'react' +import { formatProfileNumber } from '../../../lib/profileFormatters' function KpiCard({ icon, label, value, color = 'text-sky-400' }) { return ( @@ -7,7 +8,7 @@ function KpiCard({ icon, label, value, color = 'text-sky-400' }) {
-

{Number(value ?? 0).toLocaleString()}

+

{formatProfileNumber(value)}

{label}

@@ -71,7 +72,7 @@ export default function TabStats({ stats, followerCount, followAnalytics }) {
Weighted Score
-
{Number(medalTotals?.score_total ?? 0).toLocaleString()}
+
{formatProfileNumber(medalTotals?.score_total ?? 0)}
@@ -83,7 +84,7 @@ export default function TabStats({ stats, followerCount, followAnalytics }) { ].map((item) => (
{item.label}
-
{Number(item.value).toLocaleString()}
+
{formatProfileNumber(item.value)}
))}
diff --git a/resources/js/components/profile/worlds/ProfileWorldHistoryCard.jsx b/resources/js/components/profile/worlds/ProfileWorldHistoryCard.jsx index 2a0ea65c..17876235 100644 --- a/resources/js/components/profile/worlds/ProfileWorldHistoryCard.jsx +++ b/resources/js/components/profile/worlds/ProfileWorldHistoryCard.jsx @@ -1,14 +1,12 @@ import React from 'react' +import { formatProfileDate } from '../../../lib/profileFormatters' import ProfileWorldRecognitionBadge from './ProfileWorldRecognitionBadge' function formatDate(value) { if (!value) return null try { - const date = new Date(value) - if (Number.isNaN(date.getTime())) return null - - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + return formatProfileDate(value, { month: 'short', day: 'numeric', year: 'numeric' }) } catch { return null } @@ -119,4 +117,4 @@ export default function ProfileWorldHistoryCard({ entry }) { ) -} \ No newline at end of file +} diff --git a/resources/js/components/profile/worlds/ProfileWorldHistorySummary.jsx b/resources/js/components/profile/worlds/ProfileWorldHistorySummary.jsx index 516da9ba..80e3712e 100644 --- a/resources/js/components/profile/worlds/ProfileWorldHistorySummary.jsx +++ b/resources/js/components/profile/worlds/ProfileWorldHistorySummary.jsx @@ -1,4 +1,5 @@ import React from 'react' +import { formatProfileDate } from '../../../lib/profileFormatters' import ProfileWorldStatsRow from './ProfileWorldStatsRow' import ProfileWorldRecognitionBadge from './ProfileWorldRecognitionBadge' @@ -6,10 +7,7 @@ function formatDate(value) { if (!value) return null try { - const date = new Date(value) - if (Number.isNaN(date.getTime())) return null - - return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + return formatProfileDate(value, { month: 'short', day: 'numeric', year: 'numeric' }) } catch { return null } @@ -51,4 +49,4 @@ export default function ProfileWorldHistorySummary({ history }) { ) : null} ) -} \ No newline at end of file +} diff --git a/resources/js/components/social/FollowButton.jsx b/resources/js/components/social/FollowButton.jsx index 6a6f8d40..ab2f6680 100644 --- a/resources/js/components/social/FollowButton.jsx +++ b/resources/js/components/social/FollowButton.jsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' import NovaConfirmDialog from '../ui/NovaConfirmDialog' +import { formatProfileNumber } from '../../lib/profileFormatters' export default function FollowButton({ username, @@ -79,7 +80,7 @@ export default function FollowButton({ > {following ? (hovered ? 'Unfollow' : 'Following') : 'Follow'} - {showCount ? {count.toLocaleString()} : null} + {showCount ? {formatProfileNumber(count)} : null}