137 lines
8.5 KiB
Vue
137 lines
8.5 KiB
Vue
<script setup lang="ts">
|
||
interface CampaignRow {
|
||
id: string
|
||
title: string
|
||
current_scene: string
|
||
status: string
|
||
updated_at: string
|
||
}
|
||
interface DraftSession {
|
||
id: string
|
||
status: string
|
||
messages: Array<{ role: 'user' | 'assistant'; content: string }>
|
||
generated_world?: { title?: string; premise?: string } | null
|
||
updated_at: string
|
||
}
|
||
|
||
const { api } = useDngApi()
|
||
const auth = useDngAuth()
|
||
const filter = ref<'all' | 'active' | 'drafts'>('all')
|
||
const campaigns = ref<CampaignRow[]>([])
|
||
const drafts = ref<DraftSession[]>([])
|
||
const loading = ref(true)
|
||
const error = ref('')
|
||
|
||
const visibleCampaigns = computed(() => filter.value === 'active'
|
||
? campaigns.value.filter(campaign => campaign.status === 'active')
|
||
: filter.value === 'drafts' ? [] : campaigns.value)
|
||
const visibleDrafts = computed(() => filter.value === 'active' ? [] : drafts.value)
|
||
const displayName = computed(() => {
|
||
const value = auth.session.value?.user.user_metadata?.display_name
|
||
return typeof value === 'string' && value.trim() ? value.trim() : 'ADVENTURER'
|
||
})
|
||
|
||
function draftTitle(draft: DraftSession) {
|
||
return draft.generated_world?.title || 'UNFINISHED UNIVERSE'
|
||
}
|
||
|
||
function draftSummary(draft: DraftSession) {
|
||
return draft.generated_world?.premise
|
||
|| draft.messages.find(message => message.role === 'user')?.content
|
||
|| 'Continue your conversation with the coauthor.'
|
||
}
|
||
|
||
onMounted(async () => {
|
||
try {
|
||
await auth.restore()
|
||
const [campaignResult, draftResult] = await Promise.all([
|
||
api<{ campaigns: CampaignRow[] }>('/api/v1/campaigns'),
|
||
api<{ sessions: DraftSession[] }>('/api/v1/coauthor/sessions'),
|
||
])
|
||
campaigns.value = campaignResult.campaigns
|
||
drafts.value = draftResult.sessions
|
||
} catch (cause) {
|
||
const value = cause as { data?: { statusMessage?: string }; message?: string }
|
||
error.value = value.data?.statusMessage ?? value.message ?? 'Could not load your campaigns.'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<template>
|
||
<AppShell section="COMMAND DECK">
|
||
<div class="dash-wrap noise">
|
||
<section class="dash-head">
|
||
<div><p class="kicker">WELCOME BACK, {{ displayName.toUpperCase() }}</p><h1>YOUR UNIVERSES<span>.</span></h1><p>Every world remembers where you left it.</p></div>
|
||
<NuxtLink to="/worlds/new" class="create-button"><span>+</span> CREATE A UNIVERSE</NuxtLink>
|
||
</section>
|
||
|
||
<div class="filter-row">
|
||
<button v-for="item in ['all','active','drafts']" :key="item" :class="{active:filter===item}" @click="filter=item as typeof filter">{{ item }}</button>
|
||
<span>{{ campaigns.length }} CAMPAIGNS · {{ drafts.length }} DRAFTS</span>
|
||
</div>
|
||
|
||
<section class="world-grid">
|
||
<p v-if="loading" class="load-state">RECEIVING PRIVATE CAMPAIGNS…</p>
|
||
<p v-else-if="error" class="load-state error">{{ error }} <NuxtLink to="/auth/sign-in">SIGN IN AGAIN</NuxtLink></p>
|
||
<NuxtLink v-for="campaign in visibleCampaigns" :key="campaign.id" :to="`/campaign/${campaign.id}`" class="world-card active-world">
|
||
<div class="world-art"><div class="eclipse" /><span>{{ campaign.status.toUpperCase() }} CAMPAIGN</span></div>
|
||
<div class="world-info"><small>PRIVATE MULTIPLAYER</small><h2>{{ campaign.title }}</h2><p>{{ campaign.current_scene }}</p><div><b>CONTINUE STORY</b><span>{{ new Date(campaign.updated_at).toLocaleDateString() }}</span></div></div>
|
||
</NuxtLink>
|
||
<NuxtLink v-for="draft in visibleDrafts" :key="draft.id" :to="`/worlds/new?session=${draft.id}`" class="world-card active-world draft-world">
|
||
<div class="world-art"><div class="eclipse" /><span>{{ draft.status.toUpperCase() }} DRAFT</span></div>
|
||
<div class="world-info"><small>PRIVATE COAUTHOR SESSION</small><h2>{{ draftTitle(draft) }}</h2><p>{{ draftSummary(draft) }}</p><div><b>RESUME CREATION</b><span>{{ new Date(draft.updated_at).toLocaleDateString() }}</span></div></div>
|
||
</NuxtLink>
|
||
<NuxtLink to="/worlds/new" class="world-card new-card">
|
||
<span class="plus">+</span><h2>MAKE THE NEXT<br>IMPOSSIBLE PLACE</h2><p>Begin with a sentence. The coauthor will ask the rest.</p>
|
||
</NuxtLink>
|
||
</section>
|
||
|
||
<section class="system-strip"><span><i /> AI COAUTHOR READY</span><span>SERVER DICE <b>ONLINE</b></span><span>PRIVATE BY DEFAULT</span></section>
|
||
</div>
|
||
</AppShell>
|
||
</template>
|
||
|
||
<style scoped>
|
||
.dash-wrap{min-height:calc(100vh - 76px);padding:clamp(42px,6vw,86px) clamp(20px,6vw,88px)}
|
||
.dash-head{display:flex;align-items:end;justify-content:space-between;gap:30px}
|
||
.kicker{font:500 9px var(--mono);letter-spacing:.2em;color:var(--acid)}
|
||
.dash-head h1{margin:14px 0 10px;font:600 clamp(44px,6vw,82px)/1 var(--display);letter-spacing:-.06em}
|
||
.dash-head h1 span{color:var(--acid)}
|
||
.dash-head p{color:var(--muted)}
|
||
.create-button{display:flex;align-items:center;gap:18px;padding:18px 22px;background:var(--acid);color:#090909;text-decoration:none;font:600 10px var(--mono);letter-spacing:.12em}
|
||
.create-button span{font-size:20px}
|
||
.filter-row{display:flex;align-items:center;gap:10px;margin:58px 0 24px;border-bottom:1px solid var(--line)}
|
||
.filter-row button{padding:0 4px 16px;margin-right:18px;background:none;border:0;color:var(--muted);font:500 9px var(--mono);text-transform:uppercase;letter-spacing:.14em}
|
||
.filter-row button.active{color:var(--ink);border-bottom:2px solid var(--acid)}
|
||
.filter-row>span{margin-left:auto;padding-bottom:16px;font:500 8px var(--mono);color:var(--muted)}
|
||
.world-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:18px}
|
||
.load-state{grid-column:1/-1;padding:34px;border:1px solid var(--line);font:500 9px var(--mono);color:var(--muted)}
|
||
.load-state.error{color:#ff9d85}
|
||
.load-state a{color:var(--acid)}
|
||
.world-card{min-height:420px;border:1px solid var(--line);color:var(--ink);text-decoration:none;background:#0e0e0d;transition:border-color .25s ease,transform .25s ease}
|
||
.world-card:hover{border-color:#65655e;transform:translateY(-3px)}
|
||
.active-world{display:grid;grid-template-columns:.75fr 1.25fr}
|
||
.world-art{position:relative;overflow:hidden;display:grid;place-items:center;background:radial-gradient(circle at 50% 60%,#7d8240 0 2%,#303018 4%,#0b0b0a 36%,#030303 72%)}
|
||
.world-art::before{content:"";position:absolute;width:320px;height:320px;border:1px solid #38382d;border-radius:50%;box-shadow:0 0 0 34px #111,0 0 0 35px #26261d}
|
||
.eclipse{position:absolute;width:126px;height:126px;border-radius:50%;background:#020202;box-shadow:0 0 50px var(--acid-dim)}
|
||
.world-art span{position:absolute;left:20px;top:20px;padding:9px 11px;background:var(--acid);color:#0a0a0a;font:600 8px var(--mono);letter-spacing:.12em}
|
||
.draft-world .world-art{background:radial-gradient(circle at 50% 60%,#3f4720 0 2%,#1c2011 9%,#080808 58%)}
|
||
.draft-world .world-art span{background:transparent;color:var(--acid);border:1px solid var(--acid-dim)}
|
||
.world-info{padding:42px;display:flex;flex-direction:column;min-width:0}
|
||
.world-info small{font:500 8px var(--mono);letter-spacing:.14em;color:var(--acid);text-transform:uppercase}
|
||
.world-info h2,.new-card h2{font:600 clamp(25px,3vw,42px)/1.05 var(--display);letter-spacing:-.05em;margin:22px 0;overflow-wrap:anywhere}
|
||
.world-info p,.new-card p{color:var(--muted);font-size:13px;line-height:1.7;display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden}
|
||
.world-info div{margin-top:auto;padding-top:26px;border-top:1px solid var(--line);display:flex;justify-content:space-between;gap:16px;font:500 8px var(--mono);color:var(--muted)}
|
||
.world-info b{color:var(--acid)}
|
||
.new-card{padding:48px;display:flex;flex-direction:column;justify-content:flex-end;background:linear-gradient(145deg,#121211,#090909)}
|
||
.new-card .plus{margin-bottom:auto;font:300 42px var(--body);color:var(--acid)}
|
||
.system-strip{margin-top:32px;padding:18px 0;border-top:1px solid var(--line);display:flex;justify-content:space-between;font:500 8px var(--mono);letter-spacing:.12em;color:var(--muted)}
|
||
.system-strip i{display:inline-block;width:6px;height:6px;background:var(--acid);border-radius:50%;margin-right:8px}
|
||
.system-strip b{color:var(--acid)}
|
||
@media(max-width:1050px){.world-grid{grid-template-columns:1fr}}
|
||
@media(max-width:850px){.dash-head{align-items:start;flex-direction:column}.active-world{grid-template-columns:1fr}.world-art{min-height:280px}.system-strip{gap:16px;flex-wrap:wrap}}
|
||
@media(max-width:520px){.dash-head h1{font-size:clamp(34px,12vw,52px)}.create-button{width:100%;justify-content:center}.filter-row>span{display:none}.world-info,.new-card{padding:28px}.active-world{min-height:580px}.world-info div{align-items:flex-start;flex-direction:column}.system-strip{flex-direction:column}}
|
||
</style>
|