Repair: copy legacy joinDate into new user's created_at when creating users from legacy wallz

This commit is contained in:
2026-03-22 09:13:39 +01:00
parent e8b5edf5d2
commit 2608be7420
80 changed files with 3991 additions and 723 deletions

View File

@@ -10,6 +10,100 @@ const SOCIAL_ICONS = {
website: { icon: 'fa-solid fa-link', label: 'Website' },
}
function formatNumber(value) {
return Number(value ?? 0).toLocaleString()
}
function formatRelativeDate(value) {
if (!value) return null
try {
const date = new Date(value)
if (Number.isNaN(date.getTime())) return null
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' })
if (absSeconds < 3600) {
return formatter.format(Math.round(diffSeconds / 60), 'minute')
}
if (absSeconds < 86400) {
return formatter.format(Math.round(diffSeconds / 3600), 'hour')
}
if (absSeconds < 604800) {
return formatter.format(Math.round(diffSeconds / 86400), 'day')
}
if (absSeconds < 2629800) {
return formatter.format(Math.round(diffSeconds / 604800), 'week')
}
return formatter.format(Math.round(diffSeconds / 2629800), 'month')
} catch {
return null
}
}
function formatShortDate(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' })
} catch {
return null
}
}
function truncateText(value, maxLength = 140) {
const text = String(value ?? '').trim()
if (!text) return ''
if (text.length <= maxLength) return text
return `${text.slice(0, maxLength).trimEnd()}...`
}
function buildInterestGroups(artworks = []) {
const categoryMap = new Map()
const contentTypeMap = new Map()
artworks.forEach((artwork) => {
const categoryKey = String(artwork?.category_slug || artwork?.category || '').trim().toLowerCase()
const categoryLabel = String(artwork?.category || '').trim()
const contentTypeKey = String(artwork?.content_type_slug || artwork?.content_type || '').trim().toLowerCase()
const contentTypeLabel = String(artwork?.content_type || '').trim()
if (categoryKey && categoryLabel) {
categoryMap.set(categoryKey, {
label: categoryLabel,
count: (categoryMap.get(categoryKey)?.count ?? 0) + 1,
})
}
if (contentTypeKey && contentTypeLabel) {
contentTypeMap.set(contentTypeKey, {
label: contentTypeLabel,
count: (contentTypeMap.get(contentTypeKey)?.count ?? 0) + 1,
})
}
})
const toSortedList = (source) => Array.from(source.values())
.sort((left, right) => right.count - left.count || left.label.localeCompare(right.label))
.slice(0, 5)
return {
categories: toSortedList(categoryMap),
contentTypes: toSortedList(contentTypeMap),
}
}
function InfoRow({ icon, label, children }) {
return (
<div className="flex items-start gap-3 py-2.5 border-b border-white/5 last:border-0">
@@ -22,11 +116,47 @@ function InfoRow({ icon, label, children }) {
)
}
function StatCard({ icon, label, value, tone = 'sky' }) {
const tones = {
sky: 'text-sky-300 bg-sky-400/10 border-sky-300/15',
amber: 'text-amber-200 bg-amber-300/10 border-amber-300/15',
emerald: 'text-emerald-200 bg-emerald-400/10 border-emerald-300/15',
violet: 'text-violet-200 bg-violet-400/10 border-violet-300/15',
}
return (
<div className="rounded-[24px] border border-white/10 bg-white/[0.04] p-4 shadow-[0_18px_44px_rgba(2,6,23,0.18)]">
<div className={`inline-flex h-11 w-11 items-center justify-center rounded-2xl border ${tones[tone] || tones.sky}`}>
<i className={`fa-solid ${icon}`} />
</div>
<div className="mt-4 text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">{label}</div>
<div className="mt-1 text-2xl font-semibold tracking-tight text-white">{value}</div>
</div>
)
}
function SectionCard({ icon, eyebrow, title, children, className = '' }) {
return (
<section className={`rounded-[28px] border border-white/10 bg-white/[0.04] p-5 shadow-[0_20px_52px_rgba(2,6,23,0.18)] md:p-6 ${className}`.trim()}>
<div className="flex items-start gap-3">
<div className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl border border-white/10 bg-white/[0.05] text-sky-300">
<i className={`${icon} text-base`} />
</div>
<div className="min-w-0">
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-slate-500">{eyebrow}</p>
<h2 className="mt-1 text-xl font-semibold tracking-[-0.02em] text-white md:text-2xl">{title}</h2>
</div>
</div>
<div className="mt-5">{children}</div>
</section>
)
}
/**
* TabAbout
* Bio, social links, metadata - replaces old sidebar profile card.
*/
export default function TabAbout({ user, profile, socialLinks, countryName, followerCount }) {
export default function TabAbout({ user, profile, stats, achievements, artworks, creatorStories, profileComments, socialLinks, countryName, followerCount, recentFollowers, leaderboardRank }) {
const uname = user.username || user.name
const displayName = user.name || uname
const about = profile?.about
@@ -47,119 +177,344 @@ export default function TabAbout({ user, profile, socialLinks, countryName, foll
const genderMap = { M: 'Male', F: 'Female', X: 'Non-binary / N/A' }
const genderLabel = genderMap[profile?.gender?.toUpperCase()] ?? null
const birthDate = profile?.birthdate
? (() => {
try {
return new Date(profile.birthdate).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })
} catch { return null }
})()
: null
const lastSeenRelative = formatRelativeDate(user.last_visit_at)
const socialEntries = socialLinks
? Object.entries(socialLinks).filter(([, link]) => link?.url)
: []
const followers = recentFollowers ?? []
const recentAchievements = Array.isArray(achievements?.recent) ? achievements.recent : []
const stories = Array.isArray(creatorStories) ? creatorStories : []
const comments = Array.isArray(profileComments) ? profileComments : []
const interestGroups = buildInterestGroups(Array.isArray(artworks) ? artworks : [])
const summaryCards = [
{ icon: 'fa-user-group', label: 'Followers', value: formatNumber(followerCount), tone: 'sky' },
{ icon: 'fa-images', label: 'Uploads', value: formatNumber(stats?.uploads_count ?? 0), tone: 'violet' },
{ icon: 'fa-eye', label: 'Profile views', value: formatNumber(stats?.profile_views_count ?? 0), tone: 'emerald' },
{ icon: 'fa-trophy', label: 'Weekly rank', value: leaderboardRank?.rank ? `#${formatNumber(leaderboardRank.rank)}` : 'Unranked', tone: 'amber' },
]
return (
<div
id="tabpanel-about"
role="tabpanel"
aria-labelledby="tab-about"
className="pt-6 max-w-2xl"
className="mx-auto max-w-7xl px-4 pt-4 pb-10 md:px-6"
>
{/* Bio */}
{about ? (
<div className="bg-white/4 ring-1 ring-white/10 rounded-2xl p-5 mb-5 shadow-xl shadow-black/20 backdrop-blur">
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500 mb-3 flex items-center gap-2">
<i className="fa-solid fa-quote-left text-purple-400 fa-fw" />
About
</h2>
<p className="text-sm text-slate-300 leading-relaxed whitespace-pre-line">{about}</p>
</div>
) : (
<div className="bg-white/4 ring-1 ring-white/10 rounded-2xl p-5 mb-5 text-center text-slate-500 text-sm">
No bio yet.
</div>
)}
{/* Info card */}
<div className="bg-white/4 ring-1 ring-white/10 rounded-2xl p-5 mb-5 shadow-xl shadow-black/20">
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500 mb-3 flex items-center gap-2">
<i className="fa-solid fa-id-card text-sky-400 fa-fw" />
Profile Info
</h2>
<div className="divide-y divide-white/5">
{displayName && displayName !== uname && (
<InfoRow icon="fa-user" label="Display name">{displayName}</InfoRow>
)}
<InfoRow icon="fa-at" label="Username">
<span className="font-mono">@{uname}</span>
</InfoRow>
{genderLabel && (
<InfoRow icon="fa-venus-mars" label="Gender">{genderLabel}</InfoRow>
)}
{countryName && (
<InfoRow icon="fa-earth-americas" label="Country">
<span className="flex items-center gap-2">
{profile?.country_code && (
<img
src={`/gfx/flags/shiny/24/${encodeURIComponent(String(profile.country_code).toUpperCase())}.png`}
alt={countryName}
className="w-4 h-auto rounded-sm"
onError={(e) => { e.target.style.display = 'none' }}
/>
)}
{countryName}
</span>
</InfoRow>
)}
{website && (
<InfoRow icon="fa-link" label="Website">
<a
href={website.startsWith('http') ? website : `https://${website}`}
target="_blank"
rel="nofollow noopener noreferrer"
className="text-sky-400 hover:text-sky-300 hover:underline transition-colors"
>
{(() => {
try {
const url = website.startsWith('http') ? website : `https://${website}`
return new URL(url).hostname
} catch { return website }
})()}
</a>
</InfoRow>
)}
{joinDate && (
<InfoRow icon="fa-calendar-days" label="Member since">{joinDate}</InfoRow>
)}
{lastVisit && (
<InfoRow icon="fa-clock" label="Last seen">{lastVisit}</InfoRow>
)}
<InfoRow icon="fa-users" label="Followers">{Number(followerCount).toLocaleString()}</InfoRow>
</div>
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
{summaryCards.map((card) => (
<StatCard key={card.label} {...card} />
))}
</div>
{/* Social links */}
{socialEntries.length > 0 && (
<div className="bg-white/4 ring-1 ring-white/10 rounded-2xl p-5 shadow-xl shadow-black/20">
<h2 className="text-xs font-semibold uppercase tracking-widest text-slate-500 mb-3 flex items-center gap-2">
<i className="fa-solid fa-share-nodes text-sky-400 fa-fw" />
Social Links
</h2>
<div className="flex flex-wrap gap-2">
{socialEntries.map(([platform, link]) => {
const si = SOCIAL_ICONS[platform] ?? { icon: 'fa-solid fa-link', label: platform }
const href = link.url.startsWith('http') ? link.url : `https://${link.url}`
return (
<a
key={platform}
href={href}
target="_blank"
rel="nofollow noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 rounded-xl text-sm border border-white/10 text-slate-300 hover:text-white hover:bg-white/8 hover:border-sky-400/30 transition-all"
aria-label={si.label}
>
<i className={`${si.icon} fa-fw`} />
<span>{si.label}</span>
</a>
)
})}
</div>
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1.2fr)_380px]">
<div className="space-y-6">
<SectionCard icon="fa-solid fa-circle-info" eyebrow="Profile story" title={`About ${displayName}`} className="bg-[linear-gradient(135deg,rgba(56,189,248,0.08),rgba(255,255,255,0.04),rgba(249,115,22,0.05))]">
{about ? (
<p className="whitespace-pre-line text-[15px] leading-8 text-slate-200/90">{about}</p>
) : (
<div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.03] px-5 py-8 text-center text-sm text-slate-400">
This creator has not written a public bio yet.
</div>
)}
</SectionCard>
<SectionCard icon="fa-solid fa-address-card" eyebrow="Details" title="Profile information">
<div className="grid gap-3 md:grid-cols-2">
{displayName && displayName !== uname ? (
<InfoRow icon="fa-user" label="Display name">{displayName}</InfoRow>
) : null}
<InfoRow icon="fa-at" label="Username"><span className="font-mono">@{uname}</span></InfoRow>
{genderLabel ? <InfoRow icon="fa-venus-mars" label="Gender">{genderLabel}</InfoRow> : null}
{countryName ? (
<InfoRow icon="fa-earth-americas" label="Country">
<span className="flex items-center gap-2">
{profile?.country_code ? (
<img
src={`/gfx/flags/shiny/24/${encodeURIComponent(String(profile.country_code).toUpperCase())}.png`}
alt={countryName}
className="h-auto w-4 rounded-sm"
onError={(e) => { e.target.style.display = 'none' }}
/>
) : null}
{countryName}
</span>
</InfoRow>
) : null}
{website ? (
<InfoRow icon="fa-link" label="Website">
<a
href={website.startsWith('http') ? website : `https://${website}`}
target="_blank"
rel="nofollow noopener noreferrer"
className="text-sky-300 transition-colors hover:text-sky-200 hover:underline"
>
{(() => {
try {
const url = website.startsWith('http') ? website : `https://${website}`
return new URL(url).hostname
} catch { return website }
})()}
</a>
</InfoRow>
) : null}
{birthDate ? <InfoRow icon="fa-cake-candles" label="Birth date">{birthDate}</InfoRow> : null}
{joinDate ? <InfoRow icon="fa-calendar-days" label="Member since">{joinDate}</InfoRow> : null}
{lastVisit ? <InfoRow icon="fa-clock" label="Last seen">{lastSeenRelative ? `${lastSeenRelative} · ${lastVisit}` : lastVisit}</InfoRow> : null}
</div>
</SectionCard>
{followers.length > 0 ? (
<SectionCard icon="fa-solid fa-user-group" eyebrow="Community" title="Recent followers">
<div className="grid gap-3 sm:grid-cols-2">
{followers.slice(0, 6).map((follower) => (
<a
key={follower.id}
href={follower.profile_url ?? `/@${follower.username}`}
className="group flex items-center gap-3 rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-3 transition-colors hover:border-white/14 hover:bg-white/[0.06]"
>
<img
src={follower.avatar_url ?? '/images/avatar_default.webp'}
alt={follower.username}
className="h-11 w-11 rounded-2xl object-cover ring-1 ring-white/10 transition-all group-hover:ring-sky-400/30"
loading="lazy"
/>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-medium text-slate-200 group-hover:text-white">{follower.uname || follower.username}</div>
<div className="truncate text-xs text-slate-500">@{follower.username}</div>
</div>
</a>
))}
</div>
</SectionCard>
) : null}
{recentAchievements.length > 0 ? (
<SectionCard icon="fa-solid fa-trophy" eyebrow="Recent wins" title="Latest achievements">
<div className="grid gap-3 sm:grid-cols-2">
{recentAchievements.slice(0, 4).map((achievement) => (
<div
key={achievement.id}
className="rounded-2xl border border-white/8 bg-white/[0.03] px-4 py-4 transition-colors hover:bg-white/[0.05]"
>
<div className="flex items-start gap-3">
<div className="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl border border-amber-300/15 bg-amber-300/10 text-amber-100">
<i className={`fa-solid ${achievement.icon || 'fa-trophy'}`} />
</div>
<div className="min-w-0 flex-1">
<div className="truncate text-sm font-semibold text-white">{achievement.name}</div>
{achievement.description ? (
<div className="mt-1 line-clamp-2 text-sm leading-relaxed text-slate-400">{achievement.description}</div>
) : null}
<div className="mt-3 flex flex-wrap gap-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-300/75">
{achievement.unlocked_at ? (
<span className="rounded-full border border-white/10 bg-black/20 px-2.5 py-1">
{formatShortDate(achievement.unlocked_at) || 'Unlocked'}
</span>
) : null}
<span className="rounded-full border border-white/10 bg-black/20 px-2.5 py-1">
+{formatNumber(achievement.xp_reward ?? 0)} XP
</span>
</div>
</div>
</div>
</div>
))}
</div>
</SectionCard>
) : null}
{stories.length > 0 || comments.length > 0 ? (
<SectionCard icon="fa-solid fa-wave-square" eyebrow="Fresh from this creator" title="Recent activity">
<div className="grid gap-3 lg:grid-cols-2">
{stories.length > 0 ? (
<div className="rounded-[24px] border border-white/8 bg-white/[0.03] p-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Latest story</div>
<span className="rounded-full border border-sky-300/15 bg-sky-400/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-sky-100/80">
{formatShortDate(stories[0]?.published_at) || 'Published'}
</span>
</div>
<a
href={`/stories/${stories[0].slug}`}
className="mt-3 block text-lg font-semibold tracking-tight text-white transition-colors hover:text-sky-200"
>
{stories[0].title}
</a>
{stories[0].excerpt ? (
<p className="mt-2 text-sm leading-7 text-slate-400">
{truncateText(stories[0].excerpt, 180)}
</p>
) : null}
<div className="mt-4 flex flex-wrap gap-2 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-300/75">
{stories[0].reading_time ? (
<span className="rounded-full border border-white/10 bg-black/20 px-2.5 py-1">
{stories[0].reading_time} min read
</span>
) : null}
<span className="rounded-full border border-white/10 bg-black/20 px-2.5 py-1">
{formatNumber(stories[0].views ?? 0)} views
</span>
<span className="rounded-full border border-white/10 bg-black/20 px-2.5 py-1">
{formatNumber(stories[0].comments_count ?? 0)} comments
</span>
</div>
</div>
) : null}
{comments.length > 0 ? (
<div className="rounded-[24px] border border-white/8 bg-white/[0.03] p-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Latest guestbook comment</div>
<span className="rounded-full border border-amber-300/15 bg-amber-300/10 px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.14em] text-amber-100/80">
{formatRelativeDate(comments[0]?.created_at) || 'Recently'}
</span>
</div>
<div className="mt-3 flex items-start gap-3">
<img
src={comments[0].author_avatar || '/images/avatar_default.webp'}
alt={comments[0].author_name}
className="h-11 w-11 rounded-2xl object-cover ring-1 ring-white/10"
loading="lazy"
onError={(e) => { e.target.src = '/images/avatar_default.webp' }}
/>
<div className="min-w-0 flex-1">
<a
href={comments[0].author_profile_url}
className="text-sm font-semibold text-white transition-colors hover:text-sky-200"
>
{comments[0].author_name}
</a>
<p className="mt-2 text-sm leading-7 text-slate-400">
{truncateText(comments[0].body, 180)}
</p>
</div>
</div>
</div>
) : null}
</div>
</SectionCard>
) : null}
</div>
)}
<div className="space-y-6">
<SectionCard icon="fa-solid fa-sparkles" eyebrow="Creator snapshot" title="Profile snapshot" className="bg-[linear-gradient(180deg,rgba(15,23,42,0.72),rgba(2,6,23,0.5))]">
<div className="space-y-4">
<div className="rounded-[24px] border border-white/10 bg-white/[0.04] p-4">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Creator level</div>
<div className="mt-2 flex items-end justify-between gap-4">
<div>
<div className="text-3xl font-semibold tracking-tight text-white">Lv {formatNumber(user?.level ?? 1)}</div>
<div className="mt-1 text-sm text-slate-400">{user?.rank || 'Creator'}</div>
</div>
<div className="rounded-2xl border border-sky-300/15 bg-sky-400/10 px-3 py-2 text-right">
<div className="text-[10px] font-semibold uppercase tracking-[0.16em] text-sky-100/70">XP</div>
<div className="mt-1 text-lg font-semibold text-sky-100">{formatNumber(user?.xp ?? 0)}</div>
</div>
</div>
<div className="mt-4 h-2 overflow-hidden rounded-full bg-white/8">
<div className="h-full rounded-full bg-[linear-gradient(90deg,#38bdf8,#60a5fa,#f59e0b)]" style={{ width: `${Math.max(0, Math.min(100, Number(user?.progress_percent ?? 0)))}%` }} />
</div>
</div>
<div className="grid gap-3 sm:grid-cols-2">
<div className="rounded-2xl border border-white/10 bg-white/[0.04] p-4">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Weekly rank</div>
<div className="mt-2 text-2xl font-semibold tracking-tight text-white">{leaderboardRank?.rank ? `#${formatNumber(leaderboardRank.rank)}` : 'Not ranked'}</div>
{leaderboardRank?.score ? <div className="mt-1 text-sm text-slate-400">Score {formatNumber(leaderboardRank.score)}</div> : null}
</div>
<div className="rounded-2xl border border-white/10 bg-white/[0.04] p-4">
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Community size</div>
<div className="mt-2 text-2xl font-semibold tracking-tight text-white">{formatNumber(followerCount)}</div>
<div className="mt-1 text-sm text-slate-400">Followers</div>
</div>
</div>
</div>
</SectionCard>
<SectionCard icon="fa-solid fa-chart-simple" eyebrow="Highlights" title="Useful stats">
<div className="space-y-3">
<InfoRow icon="fa-images" label="Uploads">{formatNumber(stats?.uploads_count ?? 0)}</InfoRow>
<InfoRow icon="fa-eye" label="Artwork views received">{formatNumber(stats?.artwork_views_received_count ?? 0)}</InfoRow>
<InfoRow icon="fa-download" label="Downloads received">{formatNumber(stats?.downloads_received_count ?? 0)}</InfoRow>
<InfoRow icon="fa-heart" label="Favourites received">{formatNumber(stats?.favourites_received_count ?? 0)}</InfoRow>
<InfoRow icon="fa-comment" label="Comments received">{formatNumber(stats?.comments_received_count ?? 0)}</InfoRow>
</div>
</SectionCard>
{interestGroups.categories.length > 0 || interestGroups.contentTypes.length > 0 ? (
<SectionCard icon="fa-solid fa-layer-group" eyebrow="Creative focus" title="Favourite categories & formats">
<div className="space-y-5">
{interestGroups.categories.length > 0 ? (
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Top categories</div>
<div className="mt-3 flex flex-wrap gap-2.5">
{interestGroups.categories.map((category) => (
<span
key={category.label}
className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-3.5 py-2 text-sm text-slate-200"
>
<span>{category.label}</span>
<span className="rounded-full bg-black/20 px-2 py-0.5 text-[11px] font-semibold text-slate-400">{formatNumber(category.count)}</span>
</span>
))}
</div>
</div>
) : null}
{interestGroups.contentTypes.length > 0 ? (
<div>
<div className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Preferred formats</div>
<div className="mt-3 flex flex-wrap gap-2.5">
{interestGroups.contentTypes.map((contentType) => (
<span
key={contentType.label}
className="inline-flex items-center gap-2 rounded-2xl border border-sky-300/15 bg-sky-400/10 px-3.5 py-2 text-sm text-sky-100"
>
<span>{contentType.label}</span>
<span className="rounded-full bg-black/20 px-2 py-0.5 text-[11px] font-semibold text-sky-100/70">{formatNumber(contentType.count)}</span>
</span>
))}
</div>
</div>
) : null}
</div>
</SectionCard>
) : null}
{socialEntries.length > 0 ? (
<SectionCard icon="fa-solid fa-share-nodes" eyebrow="Links" title="Social links">
<div className="flex flex-wrap gap-2.5">
{socialEntries.map(([platform, link]) => {
const si = SOCIAL_ICONS[platform] ?? { icon: 'fa-solid fa-link', label: platform }
const href = link.url.startsWith('http') ? link.url : `https://${link.url}`
return (
<a
key={platform}
href={href}
target="_blank"
rel="nofollow noopener noreferrer"
className="inline-flex items-center gap-2 rounded-2xl border border-white/10 bg-white/[0.04] px-3.5 py-2.5 text-sm text-slate-300 transition-all hover:border-sky-400/30 hover:bg-white/[0.07] hover:text-white"
aria-label={si.label}
>
<i className={`${si.icon} fa-fw`} />
<span>{si.label}</span>
</a>
)
})}
</div>
</SectionCard>
) : null}
</div>
</div>
</div>
)
}