99 lines
4.5 KiB
Vue
99 lines
4.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>
|
||
.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)}
|
||
</style>
|