Make profile formatting hydration-safe
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { formatProfileDate } from '../../lib/profileFormatters'
|
||||||
import AchievementBadge from './AchievementBadge'
|
import AchievementBadge from './AchievementBadge'
|
||||||
|
|
||||||
function cx(...parts) {
|
function cx(...parts) {
|
||||||
@@ -46,9 +47,9 @@ export default function AchievementCard({ achievement, className = '' }) {
|
|||||||
|
|
||||||
{achievement?.unlocked_at ? (
|
{achievement?.unlocked_at ? (
|
||||||
<p className="mt-3 text-xs text-slate-500">
|
<p className="mt-3 text-xs text-slate-500">
|
||||||
Unlocked {new Date(achievement.unlocked_at).toLocaleDateString()}
|
Unlocked {formatProfileDate(achievement.unlocked_at, { year: 'numeric', month: 'short', day: 'numeric' })}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
</article>
|
</article>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { formatProfileDate, formatProfileNumber } from '../../lib/profileFormatters'
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) return null
|
if (!value) return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return new Intl.DateTimeFormat('en', {
|
return formatProfileDate(value, { month: 'short', day: 'numeric', year: 'numeric' })
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
}).format(new Date(value))
|
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -17,7 +14,7 @@ function formatDate(value) {
|
|||||||
function formatYear(value) {
|
function formatYear(value) {
|
||||||
if (!value) return null
|
if (!value) return null
|
||||||
try {
|
try {
|
||||||
return new Intl.DateTimeFormat('en', { year: 'numeric' }).format(new Date(value))
|
return formatProfileDate(value, { year: 'numeric' })
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -268,8 +265,8 @@ function YearlyProductivityChart({ recaps }) {
|
|||||||
<div className="absolute inset-y-0 left-0 flex w-full items-center px-3 opacity-0 transition-opacity group-hover:opacity-100">
|
<div className="absolute inset-y-0 left-0 flex w-full items-center px-3 opacity-0 transition-opacity group-hover:opacity-100">
|
||||||
<span className="text-[11px] font-semibold text-white drop-shadow-sm">
|
<span className="text-[11px] font-semibold text-white drop-shadow-sm">
|
||||||
{uploads} upload{uploads !== 1 ? 's' : ''}
|
{uploads} upload{uploads !== 1 ? 's' : ''}
|
||||||
{(item.metrics?.views ?? 0) > 0 ? ` · ${Number(item.metrics.views).toLocaleString()} views` : ''}
|
{(item.metrics?.views ?? 0) > 0 ? ` · ${formatProfileNumber(item.metrics.views)} views` : ''}
|
||||||
{(item.metrics?.downloads ?? 0) > 0 ? ` · ${Number(item.metrics.downloads).toLocaleString()} dl` : ''}
|
{(item.metrics?.downloads ?? 0) > 0 ? ` · ${formatProfileNumber(item.metrics.downloads)} dl` : ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -292,8 +289,8 @@ function YearlyProductivityChart({ recaps }) {
|
|||||||
{/* Summary footer */}
|
{/* Summary footer */}
|
||||||
<div className="mt-5 flex flex-wrap gap-4 border-t border-white/5 pt-4 text-[12px] text-slate-400">
|
<div className="mt-5 flex flex-wrap gap-4 border-t border-white/5 pt-4 text-[12px] text-slate-400">
|
||||||
<span><span className="font-semibold text-white">{sorted.length}</span> active years</span>
|
<span><span className="font-semibold text-white">{sorted.length}</span> active years</span>
|
||||||
<span><span className="font-semibold text-white">{sorted.reduce((s, r) => s + r.metrics.uploads_count, 0).toLocaleString()}</span> total uploads</span>
|
<span><span className="font-semibold text-white">{formatProfileNumber(sorted.reduce((s, r) => s + r.metrics.uploads_count, 0))}</span> total uploads</span>
|
||||||
<span><span className="font-semibold text-white">{Number(sorted.reduce((s, r) => s + (r.metrics.views ?? 0), 0)).toLocaleString()}</span> total views</span>
|
<span><span className="font-semibold text-white">{formatProfileNumber(sorted.reduce((s, r) => s + (r.metrics.views ?? 0), 0))}</span> total views</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -550,8 +547,8 @@ export default function CreatorJourneySection({ journey, username }) {
|
|||||||
<p className="mt-3 text-sm leading-relaxed text-slate-300">{item.summary}</p>
|
<p className="mt-3 text-sm leading-relaxed text-slate-300">{item.summary}</p>
|
||||||
|
|
||||||
<div className="mt-4 grid gap-2 sm:grid-cols-2">
|
<div className="mt-4 grid gap-2 sm:grid-cols-2">
|
||||||
<StatPill label="Views" value={(item.metrics?.views ?? 0).toLocaleString()} />
|
<StatPill label="Views" value={formatProfileNumber(item.metrics?.views ?? 0)} />
|
||||||
<StatPill label="Downloads" value={(item.metrics?.downloads ?? 0).toLocaleString()} />
|
<StatPill label="Downloads" value={formatProfileNumber(item.metrics?.downloads ?? 0)} />
|
||||||
{(item.metrics?.featured_count ?? 0) > 0 && (
|
{(item.metrics?.featured_count ?? 0) > 0 && (
|
||||||
<StatPill label="Featured" value={item.metrics.featured_count} />
|
<StatPill label="Featured" value={item.metrics.featured_count} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CollectionVisibilityBadge from './CollectionVisibilityBadge'
|
import CollectionVisibilityBadge from './CollectionVisibilityBadge'
|
||||||
|
import { formatProfileDate, formatProfileNumber } from '../../../lib/profileFormatters'
|
||||||
|
|
||||||
async function requestJson(url, { method = 'GET', body } = {}) {
|
async function requestJson(url, { method = 'GET', body } = {}) {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
@@ -26,14 +27,7 @@ async function requestJson(url, { method = 'GET', body } = {}) {
|
|||||||
function formatUpdated(value) {
|
function formatUpdated(value) {
|
||||||
if (!value) return 'Updated recently'
|
if (!value) return 'Updated recently'
|
||||||
|
|
||||||
const date = new Date(value)
|
return formatProfileDate(value, { month: 'short', day: 'numeric', year: 'numeric' }) || 'Updated recently'
|
||||||
if (Number.isNaN(date.getTime())) return 'Updated recently'
|
|
||||||
|
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
year: 'numeric',
|
|
||||||
}).format(date)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function StatPill({ icon, label, value }) {
|
function StatPill({ icon, label, value }) {
|
||||||
@@ -178,7 +172,7 @@ export default function CollectionCard({ collection, isOwner, onDelete, onToggle
|
|||||||
{collection?.subtitle ? <p className="mt-1 truncate text-sm text-slate-300">{collection.subtitle}</p> : null}
|
{collection?.subtitle ? <p className="mt-1 truncate text-sm text-slate-300">{collection.subtitle}</p> : null}
|
||||||
{collection?.owner?.name ? <p className="mt-1 truncate text-sm text-slate-400">Curated by {collection.owner.name}{collection?.owner?.username ? ` • @${collection.owner.username}` : ''}</p> : null}
|
{collection?.owner?.name ? <p className="mt-1 truncate text-sm text-slate-400">Curated by {collection.owner.name}{collection?.owner?.username ? ` • @${collection.owner.username}` : ''}</p> : null}
|
||||||
<p className="mt-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">
|
<p className="mt-1 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-400">
|
||||||
{(collection?.artworks_count ?? 0).toLocaleString()} artworks
|
{formatProfileNumber(collection?.artworks_count ?? 0)} artworks
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,9 +184,9 @@ export default function CollectionCard({ collection, isOwner, onDelete, onToggle
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="mt-4 flex flex-wrap gap-2">
|
<div className="mt-4 flex flex-wrap gap-2">
|
||||||
<StatPill icon="fa-heart" label="likes" value={(collection?.likes_count ?? 0).toLocaleString()} />
|
<StatPill icon="fa-heart" label="likes" value={formatProfileNumber(collection?.likes_count ?? 0)} />
|
||||||
<StatPill icon="fa-bell" label="followers" value={(collection?.followers_count ?? 0).toLocaleString()} />
|
<StatPill icon="fa-bell" label="followers" value={formatProfileNumber(collection?.followers_count ?? 0)} />
|
||||||
{collection?.collaborators_count > 1 ? <StatPill icon="fa-user-group" label="curators" value={(collection?.collaborators_count ?? 0).toLocaleString()} /> : null}
|
{collection?.collaborators_count > 1 ? <StatPill icon="fa-user-group" label="curators" value={formatProfileNumber(collection?.collaborators_count ?? 0)} /> : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 flex items-center justify-between gap-3 text-xs text-slate-400">
|
<div className="mt-4 flex items-center justify-between gap-3 text-xs text-slate-400">
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import React from 'react'
|
|||||||
import { usePage } from '@inertiajs/react'
|
import { usePage } from '@inertiajs/react'
|
||||||
import CreatorJourneySection from '../CreatorJourneySection'
|
import CreatorJourneySection from '../CreatorJourneySection'
|
||||||
import { shinyFlagUrl } from '../../../utils/flagUrl'
|
import { shinyFlagUrl } from '../../../utils/flagUrl'
|
||||||
|
import { formatProfileDate, formatProfileNumber } from '../../../lib/profileFormatters'
|
||||||
|
|
||||||
const SOCIAL_ICONS = {
|
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]' },
|
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) {
|
function formatNumber(value) {
|
||||||
return Number(value ?? 0).toLocaleString()
|
return formatProfileNumber(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRelativeDate(value) {
|
function formatRelativeDate(value) {
|
||||||
@@ -79,7 +80,7 @@ function formatRelativeDate(value) {
|
|||||||
const now = new Date()
|
const now = new Date()
|
||||||
const diffSeconds = Math.round((date.getTime() - now.getTime()) / 1000)
|
const diffSeconds = Math.round((date.getTime() - now.getTime()) / 1000)
|
||||||
const absSeconds = Math.abs(diffSeconds)
|
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) {
|
if (absSeconds < 3600) {
|
||||||
return formatter.format(Math.round(diffSeconds / 60), 'minute')
|
return formatter.format(Math.round(diffSeconds / 60), 'minute')
|
||||||
@@ -110,7 +111,7 @@ function formatShortDate(value) {
|
|||||||
const date = new Date(value)
|
const date = new Date(value)
|
||||||
if (Number.isNaN(date.getTime())) return null
|
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 {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -131,7 +132,7 @@ function formatContributionDate(value) {
|
|||||||
const date = new Date(value)
|
const date = new Date(value)
|
||||||
if (Number.isNaN(date.getTime())) return null
|
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 {
|
} catch {
|
||||||
return null
|
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 flagUrl = shinyFlagUrl(profile?.country_code, props?.cdn?.files_url)
|
||||||
|
|
||||||
const joinDate = user.created_at
|
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
|
: null
|
||||||
|
|
||||||
const lastVisit = user.last_visit_at
|
const lastVisit = user.last_visit_at
|
||||||
? (() => {
|
? (() => {
|
||||||
try {
|
try {
|
||||||
const d = new Date(user.last_visit_at)
|
return formatProfileDate(user.last_visit_at, { month: 'short', day: 'numeric', year: 'numeric' })
|
||||||
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
||||||
} catch { return null }
|
} catch { return null }
|
||||||
})()
|
})()
|
||||||
: null
|
: null
|
||||||
@@ -254,7 +254,7 @@ export default function TabAbout({ user, profile, stats, achievements, worldRewa
|
|||||||
const birthDate = profile?.birthdate
|
const birthDate = profile?.birthdate
|
||||||
? (() => {
|
? (() => {
|
||||||
try {
|
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 }
|
} catch { return null }
|
||||||
})()
|
})()
|
||||||
: null
|
: null
|
||||||
@@ -381,9 +381,9 @@ export default function TabAbout({ user, profile, stats, achievements, worldRewa
|
|||||||
{entry.group?.headline ? <p className="mt-1 text-sm text-slate-400">{truncateText(entry.group.headline, 100)}</p> : null}
|
{entry.group?.headline ? <p className="mt-1 text-sm text-slate-400">{truncateText(entry.group.headline, 100)}</p> : null}
|
||||||
{entry.summary ? <p className="mt-3 text-sm text-slate-300">{entry.summary}</p> : null}
|
{entry.summary ? <p className="mt-3 text-sm text-slate-300">{entry.summary}</p> : null}
|
||||||
<div className="mt-3 flex flex-wrap gap-3 text-xs text-slate-500">
|
<div className="mt-3 flex flex-wrap gap-3 text-xs text-slate-500">
|
||||||
<span>{Number(entry.counts?.credited_artworks || 0).toLocaleString()} credited artworks</span>
|
<span>{formatProfileNumber(entry.counts?.credited_artworks || 0)} credited artworks</span>
|
||||||
<span>{Number(entry.counts?.releases || 0).toLocaleString()} releases</span>
|
<span>{formatProfileNumber(entry.counts?.releases || 0)} releases</span>
|
||||||
<span>{Number(entry.counts?.projects || 0).toLocaleString()} projects</span>
|
<span>{formatProfileNumber(entry.counts?.projects || 0)} projects</span>
|
||||||
{entry.joined_at ? <span>Joined {formatContributionDate(entry.joined_at)}</span> : null}
|
{entry.joined_at ? <span>Joined {formatContributionDate(entry.joined_at)}</span> : null}
|
||||||
</div>
|
</div>
|
||||||
{Array.isArray(entry.role_labels) && entry.role_labels.length > 0 ? (
|
{Array.isArray(entry.role_labels) && entry.role_labels.length > 0 ? (
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { formatProfileDate, formatProfileNumber } from '../../../lib/profileFormatters'
|
||||||
|
|
||||||
function formatFollowedAt(value) {
|
function formatFollowedAt(value) {
|
||||||
if (!value) return null
|
if (!value) return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const date = new Date(value)
|
return formatProfileDate(value, { year: 'numeric', month: 'short', day: 'numeric' })
|
||||||
if (Number.isNaN(date.getTime())) return null
|
|
||||||
|
|
||||||
return date.toLocaleDateString(undefined, {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
})
|
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -36,7 +30,7 @@ export default function TabFollowers({ listing, username, followerCount }) {
|
|||||||
<i className="fa-solid fa-users text-sky-400 fa-fw" />
|
<i className="fa-solid fa-users text-sky-400 fa-fw" />
|
||||||
Followers
|
Followers
|
||||||
<span className="rounded bg-white/5 px-1.5 py-0.5 text-[10px] font-medium normal-case tracking-normal text-slate-400">
|
<span className="rounded bg-white/5 px-1.5 py-0.5 text-[10px] font-medium normal-case tracking-normal text-slate-400">
|
||||||
{total.toLocaleString()}
|
{formatProfileNumber(total)}
|
||||||
</span>
|
</span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { formatProfileNumber } from '../../../lib/profileFormatters'
|
||||||
|
|
||||||
function KpiCard({ icon, label, value, color = 'text-sky-400' }) {
|
function KpiCard({ icon, label, value, color = 'text-sky-400' }) {
|
||||||
return (
|
return (
|
||||||
@@ -7,7 +8,7 @@ function KpiCard({ icon, label, value, color = 'text-sky-400' }) {
|
|||||||
<i className={`fa-solid ${icon} text-xl`} />
|
<i className={`fa-solid ${icon} text-xl`} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-2xl font-bold text-white tabular-nums">{Number(value ?? 0).toLocaleString()}</p>
|
<p className="text-2xl font-bold text-white tabular-nums">{formatProfileNumber(value)}</p>
|
||||||
<p className="text-xs text-slate-500 mt-0.5 uppercase tracking-wider">{label}</p>
|
<p className="text-xs text-slate-500 mt-0.5 uppercase tracking-wider">{label}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,7 +72,7 @@ export default function TabStats({ stats, followerCount, followAnalytics }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-xs uppercase tracking-widest text-slate-500">Weighted Score</div>
|
<div className="text-xs uppercase tracking-widest text-slate-500">Weighted Score</div>
|
||||||
<div className="mt-1 text-2xl font-bold text-white tabular-nums">{Number(medalTotals?.score_total ?? 0).toLocaleString()}</div>
|
<div className="mt-1 text-2xl font-bold text-white tabular-nums">{formatProfileNumber(medalTotals?.score_total ?? 0)}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-4 grid grid-cols-2 gap-3 md:grid-cols-4">
|
<div className="mt-4 grid grid-cols-2 gap-3 md:grid-cols-4">
|
||||||
@@ -83,7 +84,7 @@ export default function TabStats({ stats, followerCount, followAnalytics }) {
|
|||||||
].map((item) => (
|
].map((item) => (
|
||||||
<div key={item.label} className="rounded-2xl border border-white/10 bg-black/15 px-4 py-4">
|
<div key={item.label} className="rounded-2xl border border-white/10 bg-black/15 px-4 py-4">
|
||||||
<div className="text-[11px] uppercase tracking-widest text-slate-500">{item.label}</div>
|
<div className="text-[11px] uppercase tracking-widest text-slate-500">{item.label}</div>
|
||||||
<div className={`mt-2 text-2xl font-semibold tabular-nums ${item.color}`}>{Number(item.value).toLocaleString()}</div>
|
<div className={`mt-2 text-2xl font-semibold tabular-nums ${item.color}`}>{formatProfileNumber(item.value)}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { formatProfileDate } from '../../../lib/profileFormatters'
|
||||||
import ProfileWorldRecognitionBadge from './ProfileWorldRecognitionBadge'
|
import ProfileWorldRecognitionBadge from './ProfileWorldRecognitionBadge'
|
||||||
|
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (!value) return null
|
if (!value) return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const date = new Date(value)
|
return formatProfileDate(value, { month: 'short', day: 'numeric', year: 'numeric' })
|
||||||
if (Number.isNaN(date.getTime())) return null
|
|
||||||
|
|
||||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -119,4 +117,4 @@ export default function ProfileWorldHistoryCard({ entry }) {
|
|||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import { formatProfileDate } from '../../../lib/profileFormatters'
|
||||||
import ProfileWorldStatsRow from './ProfileWorldStatsRow'
|
import ProfileWorldStatsRow from './ProfileWorldStatsRow'
|
||||||
import ProfileWorldRecognitionBadge from './ProfileWorldRecognitionBadge'
|
import ProfileWorldRecognitionBadge from './ProfileWorldRecognitionBadge'
|
||||||
|
|
||||||
@@ -6,10 +7,7 @@ function formatDate(value) {
|
|||||||
if (!value) return null
|
if (!value) return null
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const date = new Date(value)
|
return formatProfileDate(value, { month: 'short', day: 'numeric', year: 'numeric' })
|
||||||
if (Number.isNaN(date.getTime())) return null
|
|
||||||
|
|
||||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
|
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -51,4 +49,4 @@ export default function ProfileWorldHistorySummary({ history }) {
|
|||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import NovaConfirmDialog from '../ui/NovaConfirmDialog'
|
import NovaConfirmDialog from '../ui/NovaConfirmDialog'
|
||||||
|
import { formatProfileNumber } from '../../lib/profileFormatters'
|
||||||
|
|
||||||
export default function FollowButton({
|
export default function FollowButton({
|
||||||
username,
|
username,
|
||||||
@@ -79,7 +80,7 @@ export default function FollowButton({
|
|||||||
>
|
>
|
||||||
<i className={`fa-solid fa-fw ${loading ? 'fa-circle-notch fa-spin' : following ? (hovered ? 'fa-user-minus' : 'fa-user-check') : 'fa-user-plus'}`} />
|
<i className={`fa-solid fa-fw ${loading ? 'fa-circle-notch fa-spin' : following ? (hovered ? 'fa-user-minus' : 'fa-user-check') : 'fa-user-plus'}`} />
|
||||||
<span>{following ? (hovered ? 'Unfollow' : 'Following') : 'Follow'}</span>
|
<span>{following ? (hovered ? 'Unfollow' : 'Following') : 'Follow'}</span>
|
||||||
{showCount ? <span className="text-xs opacity-70">{count.toLocaleString()}</span> : null}
|
{showCount ? <span className="text-xs opacity-70">{formatProfileNumber(count)}</span> : null}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<NovaConfirmDialog
|
<NovaConfirmDialog
|
||||||
|
|||||||
Reference in New Issue
Block a user