Implement creator studio and upload updates
This commit is contained in:
194
resources/js/Pages/Studio/StudioChallenges.jsx
Normal file
194
resources/js/Pages/Studio/StudioChallenges.jsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import React from 'react'
|
||||
import StudioLayout from '../../Layouts/StudioLayout'
|
||||
import { usePage } from '@inertiajs/react'
|
||||
import { studioSurface, trackStudioEvent } from '../../utils/studioEvents'
|
||||
|
||||
const summaryCards = [
|
||||
['active_challenges', 'Active challenges', 'fa-bolt'],
|
||||
['joined_challenges', 'Joined challenges', 'fa-trophy'],
|
||||
['entries_submitted', 'Entries submitted', 'fa-paper-plane'],
|
||||
['featured_entries', 'Featured entries', 'fa-star'],
|
||||
['winner_entries', 'Winner entries', 'fa-crown'],
|
||||
['cards_available', 'Challenge-ready cards', 'fa-layer-group'],
|
||||
]
|
||||
|
||||
function formatDate(value) {
|
||||
if (!value) return 'TBD'
|
||||
const date = new Date(value)
|
||||
if (Number.isNaN(date.getTime())) return value
|
||||
return date.toLocaleDateString(undefined, { month: 'short', day: 'numeric' })
|
||||
}
|
||||
|
||||
export default function StudioChallenges() {
|
||||
const { props } = usePage()
|
||||
const { summary, spotlight, activeChallenges, recentEntries, cardLeaders, reminders } = props
|
||||
|
||||
return (
|
||||
<StudioLayout title={props.title} subtitle={props.description}>
|
||||
<div className="grid grid-cols-2 gap-4 xl:grid-cols-6">
|
||||
{summaryCards.map(([key, label, icon]) => (
|
||||
<div key={key} className="rounded-[26px] border border-white/10 bg-white/[0.03] p-5">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">{label}</span>
|
||||
<i className={`fa-solid ${icon} text-sky-200`} />
|
||||
</div>
|
||||
<div className="mt-3 text-3xl font-semibold text-white">{Number(summary?.[key] || 0).toLocaleString()}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{spotlight ? (
|
||||
<section className="mt-6 rounded-[30px] border border-white/10 bg-[radial-gradient(circle_at_top_left,_rgba(56,189,248,0.14),_transparent_34%),radial-gradient(circle_at_bottom_right,_rgba(250,204,21,0.12),_transparent_40%),linear-gradient(135deg,_rgba(15,23,42,0.88),_rgba(2,6,23,0.96))] p-6 shadow-[0_22px_60px_rgba(2,6,23,0.28)]">
|
||||
<div className="flex flex-col gap-5 lg:flex-row lg:items-end lg:justify-between">
|
||||
<div className="max-w-3xl">
|
||||
<p className="text-[11px] font-semibold uppercase tracking-[0.22em] text-sky-200/70">Challenge spotlight</p>
|
||||
<h2 className="mt-2 text-3xl font-semibold text-white">{spotlight.title}</h2>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-300">{spotlight.prompt || spotlight.description || 'A featured challenge run is active in Nova Cards right now.'}</p>
|
||||
<div className="mt-4 flex flex-wrap gap-3 text-xs uppercase tracking-[0.16em] text-slate-400">
|
||||
<span>{spotlight.status}</span>
|
||||
<span>{spotlight.official ? 'Official' : 'Community'}</span>
|
||||
<span>{spotlight.entries_count} entries</span>
|
||||
<span>{spotlight.is_joined ? `${spotlight.submission_count} submitted` : 'Not joined yet'}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<a
|
||||
href={spotlight.url}
|
||||
onClick={() => trackStudioEvent('studio_challenge_action_taken', {
|
||||
surface: studioSurface(),
|
||||
module: 'challenges',
|
||||
meta: {
|
||||
action: 'open_spotlight',
|
||||
challenge_id: spotlight.id,
|
||||
},
|
||||
})}
|
||||
className="inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 transition hover:border-sky-300/35 hover:bg-sky-300/15"
|
||||
>
|
||||
Open challenge
|
||||
</a>
|
||||
<a href="/studio/cards" className="inline-flex items-center gap-2 rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white transition hover:bg-white/[0.08]">Review cards</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<h2 className="text-lg font-semibold text-white">Open and recent challenge runs</h2>
|
||||
<a href="/cards/challenges" className="text-xs font-semibold uppercase tracking-[0.18em] text-sky-100">Public archive</a>
|
||||
</div>
|
||||
|
||||
<div className="mt-5 space-y-3">
|
||||
{(activeChallenges || []).map((challenge) => (
|
||||
<a
|
||||
key={challenge.id}
|
||||
href={challenge.url}
|
||||
onClick={() => trackStudioEvent('studio_challenge_action_taken', {
|
||||
surface: studioSurface(),
|
||||
module: 'challenges',
|
||||
meta: {
|
||||
action: 'open_challenge',
|
||||
challenge_id: challenge.id,
|
||||
},
|
||||
})}
|
||||
className="block rounded-[22px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-base font-semibold text-white">{challenge.title}</div>
|
||||
<div className="mt-1 text-xs uppercase tracking-[0.16em] text-slate-500">{challenge.status} • {challenge.official ? 'official' : 'community'} • {challenge.entries_count} entries</div>
|
||||
<p className="mt-3 text-sm leading-6 text-slate-400">{challenge.prompt || challenge.description || 'Challenge details are available in the public challenge view.'}</p>
|
||||
</div>
|
||||
<div className="text-right text-xs uppercase tracking-[0.16em] text-slate-500">
|
||||
<div>{formatDate(challenge.starts_at)} start</div>
|
||||
<div className="mt-2">{formatDate(challenge.ends_at)} end</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 flex flex-wrap gap-2 text-xs">
|
||||
<span className={`rounded-full border px-2.5 py-1 ${challenge.is_joined ? 'border-emerald-300/20 bg-emerald-300/10 text-emerald-100' : 'border-white/10 text-slate-300'}`}>{challenge.is_joined ? `${challenge.submission_count} submitted` : 'Not joined'}</span>
|
||||
{challenge.featured ? <span className="rounded-full border border-amber-300/20 bg-amber-300/10 px-2.5 py-1 text-amber-100">Featured run</span> : null}
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Workflow reminders</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{(reminders || []).map((item) => (
|
||||
<a key={item.title} href={item.href} className="block rounded-[22px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
|
||||
<h3 className="text-sm font-semibold text-white">{item.title}</h3>
|
||||
<p className="mt-2 text-sm leading-6 text-slate-400">{item.body}</p>
|
||||
<span className="mt-3 inline-flex items-center gap-2 text-sm font-medium text-sky-100">{item.cta}<i className="fa-solid fa-arrow-right" /></span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Recent submissions</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{(recentEntries || []).map((entry) => (
|
||||
<div key={entry.id} className="rounded-[22px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">{entry.card.title}</div>
|
||||
<div className="mt-1 text-xs uppercase tracking-[0.16em] text-slate-500">{entry.challenge.title} • {entry.status}</div>
|
||||
</div>
|
||||
<div className="text-xs uppercase tracking-[0.16em] text-slate-500">{formatDate(entry.submitted_at)}</div>
|
||||
</div>
|
||||
{entry.note ? <p className="mt-3 text-sm leading-6 text-slate-400">{entry.note}</p> : null}
|
||||
<div className="mt-4 flex flex-wrap gap-3 text-sm">
|
||||
<a
|
||||
href={entry.challenge.url}
|
||||
onClick={() => trackStudioEvent('studio_challenge_action_taken', {
|
||||
surface: studioSurface(),
|
||||
module: 'challenges',
|
||||
item_module: 'cards',
|
||||
item_id: entry.card?.id,
|
||||
meta: {
|
||||
action: 'open_submission_challenge',
|
||||
challenge_id: entry.challenge?.id,
|
||||
entry_id: entry.id,
|
||||
},
|
||||
})}
|
||||
className="text-sky-100"
|
||||
>
|
||||
Challenge
|
||||
</a>
|
||||
<a href={entry.card.edit_url} className="text-slate-300">Edit card</a>
|
||||
<a href={entry.card.analytics_url} className="text-slate-300">Analytics</a>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-[28px] border border-white/10 bg-white/[0.03] p-6">
|
||||
<h2 className="text-lg font-semibold text-white">Cards with challenge traction</h2>
|
||||
<div className="mt-4 space-y-3">
|
||||
{(cardLeaders || []).map((card) => (
|
||||
<div key={card.id} className="rounded-[22px] border border-white/10 bg-black/20 p-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-white">{card.title}</div>
|
||||
<div className="mt-1 text-xs uppercase tracking-[0.16em] text-slate-500">{card.status} • {card.challenge_entries_count} challenge entries</div>
|
||||
</div>
|
||||
<a href={card.edit_url} className="text-xs font-semibold uppercase tracking-[0.16em] text-sky-100">Open</a>
|
||||
</div>
|
||||
<div className="mt-4 grid grid-cols-2 gap-3 text-sm text-slate-400">
|
||||
<div><div>Views</div><div className="mt-1 font-semibold text-white">{Number(card.views_count || 0).toLocaleString()}</div></div>
|
||||
<div><div>Comments</div><div className="mt-1 font-semibold text-white">{Number(card.comments_count || 0).toLocaleString()}</div></div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</StudioLayout>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user