update
This commit is contained in:
69
resources/js/components/leaderboard/LeaderboardItem.jsx
Normal file
69
resources/js/components/leaderboard/LeaderboardItem.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react'
|
||||
import LevelBadge from '../xp/LevelBadge'
|
||||
|
||||
const PODIUM_STYLES = {
|
||||
1: 'border-yellow-300/40 bg-[linear-gradient(180deg,rgba(250,204,21,0.18),rgba(15,23,42,0.84))]',
|
||||
2: 'border-slate-300/30 bg-[linear-gradient(180deg,rgba(226,232,240,0.16),rgba(15,23,42,0.84))]',
|
||||
3: 'border-amber-700/40 bg-[linear-gradient(180deg,rgba(180,83,9,0.22),rgba(15,23,42,0.84))]',
|
||||
}
|
||||
|
||||
function cx(...parts) {
|
||||
return parts.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
function formatScore(score) {
|
||||
return new Intl.NumberFormat().format(Math.round(Number(score || 0)))
|
||||
}
|
||||
|
||||
export default function LeaderboardItem({ item, type, highlight = false }) {
|
||||
const entity = item?.entity || {}
|
||||
const rank = Number(item?.rank || 0)
|
||||
const tone = highlight ? PODIUM_STYLES[rank] || PODIUM_STYLES[3] : 'border-white/10 bg-white/[0.03]'
|
||||
const image = entity.avatar || entity.image || null
|
||||
|
||||
return (
|
||||
<article className={cx('rounded-3xl border p-4 shadow-lg transition', tone)}>
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={cx('flex shrink-0 items-center justify-center rounded-2xl border font-black', highlight ? 'h-14 w-14 text-xl' : 'h-11 w-11 text-base', 'border-white/10 bg-slate-950/70 text-white')}>
|
||||
#{rank}
|
||||
</div>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<a href={entity.url || '#'} className="block text-lg font-semibold text-white hover:text-sky-300 transition">
|
||||
{entity.name || 'Unknown'}
|
||||
</a>
|
||||
{entity.creator_name ? (
|
||||
<a href={entity.creator_url || '#'} className="mt-1 block text-sm text-slate-400 hover:text-sky-300 transition">
|
||||
by {entity.creator_name}
|
||||
</a>
|
||||
) : null}
|
||||
{entity.username ? <p className="mt-1 text-sm text-slate-500">@{entity.username}</p> : null}
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<p className="text-[11px] uppercase tracking-[0.24em] text-slate-500">Score</p>
|
||||
<p className="mt-1 text-2xl font-black text-white">{formatScore(item?.score)}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex flex-wrap items-center gap-3">
|
||||
{type === 'creator' ? <LevelBadge level={entity.level} rank={entity.rank} compact /> : null}
|
||||
{type !== 'creator' && entity.creator_name ? (
|
||||
<span className="rounded-full border border-white/10 bg-white/5 px-3 py-1 text-[11px] uppercase tracking-[0.14em] text-slate-300">
|
||||
{type}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{image ? (
|
||||
<a href={entity.url || '#'} className={cx('block shrink-0 overflow-hidden rounded-2xl border border-white/10 bg-slate-900', type === 'creator' ? 'h-16 w-16' : 'h-20 w-24')}>
|
||||
<img src={image} alt={entity.name || 'Leaderboard item'} className="h-full w-full object-cover" loading="lazy" />
|
||||
</a>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
44
resources/js/components/leaderboard/LeaderboardList.jsx
Normal file
44
resources/js/components/leaderboard/LeaderboardList.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import LeaderboardItem from './LeaderboardItem'
|
||||
|
||||
export default function LeaderboardList({ items = [], type }) {
|
||||
const podium = items.slice(0, 3)
|
||||
const rest = items.slice(3)
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{podium.length > 0 ? (
|
||||
<section>
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-[0.24em] text-slate-400">Top 3</h2>
|
||||
<span className="text-xs text-slate-500">Podium leaders</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-4 xl:grid-cols-3">
|
||||
{podium.map((item) => (
|
||||
<LeaderboardItem key={`${type}-${item.rank}`} item={item} type={type} highlight />
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<section>
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-[0.24em] text-slate-400">Leaderboard</h2>
|
||||
<span className="text-xs text-slate-500">{items.length} ranked entries</span>
|
||||
</div>
|
||||
|
||||
{items.length === 0 ? (
|
||||
<div className="rounded-3xl border border-white/10 bg-white/[0.03] px-6 py-10 text-sm text-slate-400">
|
||||
No leaderboard entries available yet for this period.
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{rest.map((item) => (
|
||||
<LeaderboardItem key={`${type}-${item.rank}`} item={item} type={type} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
resources/js/components/leaderboard/LeaderboardTabs.jsx
Normal file
35
resources/js/components/leaderboard/LeaderboardTabs.jsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import React from 'react'
|
||||
|
||||
function cx(...parts) {
|
||||
return parts.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export default function LeaderboardTabs({ items, active, onChange, sticky = false, label }) {
|
||||
return (
|
||||
<div className={cx(sticky ? 'sticky top-16 z-20' : '', 'rounded-2xl border border-white/10 bg-slate-950/85 p-2 backdrop-blur') }>
|
||||
<div className="flex flex-wrap items-center gap-2" role="tablist" aria-label={label || 'Leaderboard tabs'}>
|
||||
{items.map((item) => {
|
||||
const isActive = item.value === active
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.value}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={isActive}
|
||||
onClick={() => onChange(item.value)}
|
||||
className={cx(
|
||||
'rounded-full px-4 py-2 text-sm font-semibold transition',
|
||||
isActive
|
||||
? 'bg-sky-400 text-slate-950 shadow-[0_12px_30px_rgba(56,189,248,0.28)]'
|
||||
: 'bg-white/5 text-slate-300 hover:bg-white/10 hover:text-white',
|
||||
)}
|
||||
>
|
||||
{item.label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user