update
This commit is contained in:
32
resources/js/components/achievements/AchievementBadge.jsx
Normal file
32
resources/js/components/achievements/AchievementBadge.jsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
|
||||
const TYPE_TONES = {
|
||||
Uploads: 'border-amber-400/40 bg-amber-500/10 text-amber-100',
|
||||
Engagement: 'border-rose-400/40 bg-rose-500/10 text-rose-100',
|
||||
Social: 'border-sky-400/40 bg-sky-500/10 text-sky-100',
|
||||
Stories: 'border-emerald-400/40 bg-emerald-500/10 text-emerald-100',
|
||||
Milestones: 'border-violet-400/40 bg-violet-500/10 text-violet-100',
|
||||
}
|
||||
|
||||
function cx(...parts) {
|
||||
return parts.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export default function AchievementBadge({ achievement, compact = false, className = '' }) {
|
||||
const tone = TYPE_TONES[achievement?.type] || TYPE_TONES.Milestones
|
||||
const unlocked = Boolean(achievement?.unlocked)
|
||||
|
||||
return (
|
||||
<span
|
||||
className={cx(
|
||||
'inline-flex items-center gap-2 rounded-full border px-2.5 py-1 font-semibold tracking-[0.08em]',
|
||||
compact ? 'text-[10px] uppercase' : 'text-[11px] uppercase',
|
||||
unlocked ? tone : 'border-white/10 bg-white/5 text-slate-300',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<i className={`fa-solid ${achievement?.icon || 'fa-trophy'} text-[0.9em]`} />
|
||||
<span>{achievement?.type || 'Achievement'}</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
54
resources/js/components/achievements/AchievementCard.jsx
Normal file
54
resources/js/components/achievements/AchievementCard.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import React from 'react'
|
||||
import AchievementBadge from './AchievementBadge'
|
||||
|
||||
function cx(...parts) {
|
||||
return parts.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
export default function AchievementCard({ achievement, className = '' }) {
|
||||
const unlocked = Boolean(achievement?.unlocked)
|
||||
const progress = Number(achievement?.progress || 0)
|
||||
const target = Number(achievement?.condition_value || 0)
|
||||
const percent = Math.max(0, Math.min(100, Number(achievement?.progress_percent || 0)))
|
||||
|
||||
return (
|
||||
<article
|
||||
className={cx(
|
||||
'rounded-2xl border p-4 shadow-lg transition',
|
||||
unlocked
|
||||
? 'border-emerald-400/25 bg-[linear-gradient(180deg,rgba(16,185,129,0.08),rgba(15,23,42,0.72))]'
|
||||
: 'border-white/10 bg-white/[0.04]',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<AchievementBadge achievement={achievement} compact />
|
||||
<h3 className="mt-3 text-base font-semibold text-white">{achievement?.name}</h3>
|
||||
<p className="mt-1 text-sm text-slate-300">{achievement?.description}</p>
|
||||
</div>
|
||||
<div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl border border-white/10 bg-white/5 text-lg text-white/90">
|
||||
<i className={`fa-solid ${achievement?.icon || 'fa-trophy'}`} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between gap-3 text-xs text-slate-400">
|
||||
<span>{achievement?.xp_reward || 0} XP reward</span>
|
||||
{unlocked ? <span>Unlocked</span> : <span>{progress} / {target}</span>}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 h-2 overflow-hidden rounded-full bg-white/10 ring-1 ring-white/10">
|
||||
<div
|
||||
className={cx('h-full rounded-full transition-[width] duration-500', unlocked ? 'bg-emerald-400' : 'bg-sky-400')}
|
||||
style={{ width: `${unlocked ? 100 : percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{achievement?.unlocked_at ? (
|
||||
<p className="mt-3 text-xs text-slate-500">
|
||||
Unlocked {new Date(achievement.unlocked_at).toLocaleDateString()}
|
||||
</p>
|
||||
) : null}
|
||||
</article>
|
||||
)
|
||||
}
|
||||
44
resources/js/components/achievements/AchievementsList.jsx
Normal file
44
resources/js/components/achievements/AchievementsList.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react'
|
||||
import AchievementCard from './AchievementCard'
|
||||
|
||||
export default function AchievementsList({ unlocked = [], locked = [], limitLocked }) {
|
||||
const visibleLocked = typeof limitLocked === 'number' ? locked.slice(0, limitLocked) : locked
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<section>
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-[0.2em] text-slate-400">Unlocked</h2>
|
||||
<span className="text-xs text-slate-500">{unlocked.length} earned</span>
|
||||
</div>
|
||||
|
||||
{unlocked.length === 0 ? (
|
||||
<div className="rounded-2xl border border-white/10 bg-white/[0.03] px-5 py-8 text-sm text-slate-400">
|
||||
No achievements unlocked yet.
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{unlocked.map((achievement) => (
|
||||
<AchievementCard key={achievement.id} achievement={achievement} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div className="mb-4 flex items-center justify-between gap-3">
|
||||
<h2 className="text-sm font-semibold uppercase tracking-[0.2em] text-slate-400">In Progress</h2>
|
||||
<span className="text-xs text-slate-500">{locked.length} still locked</span>
|
||||
</div>
|
||||
|
||||
{visibleLocked.length === 0 ? null : (
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||
{visibleLocked.map((achievement) => (
|
||||
<AchievementCard key={achievement.id} achievement={achievement} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user