initialize project with basic structure and dependencies
This commit is contained in:
+100
-49
@@ -10,7 +10,7 @@ const { data: course, error, refresh } = await useAsyncData(
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
function hasUnreadyTopics(): boolean {
|
||||
return course.value?.topics?.some((t: any) => !t.hasLesson) ?? false;
|
||||
return course.value?.topics?.some((t: any) => t.status === "generating") ?? false;
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -333,60 +333,73 @@ async function saveTitle() {
|
||||
<!-- topic list -->
|
||||
<div class="topic-list">
|
||||
<TransitionGroup name="topic-appear" tag="div" class="topic-list-inner">
|
||||
<template v-for="(topic, idx) in course.topics" :key="topic.id">
|
||||
<NuxtLink
|
||||
v-if="topic.hasLesson"
|
||||
:to="`/learn/${topic.id}`"
|
||||
class="topic-row topic-row--unlocked"
|
||||
:class="topic.progress?.lessonComplete ? 'topic-row--complete' : 'topic-row--available'"
|
||||
>
|
||||
<span class="topic-index">{{ String(idx + 1).padStart(2, "0") }}</span>
|
||||
<NuxtLink
|
||||
v-for="(topic, idx) in course.topics"
|
||||
:key="topic.id"
|
||||
:to="`/learn/${topic.id}`"
|
||||
class="topic-row topic-row--unlocked"
|
||||
:class="{
|
||||
'topic-row--complete': topic.progress?.lessonComplete,
|
||||
'topic-row--available': !topic.progress?.lessonComplete && topic.status === 'ready',
|
||||
'topic-row--pending': topic.status === 'pending' || topic.status === 'generating',
|
||||
}"
|
||||
>
|
||||
<span class="topic-index" :style="topic.status === 'pending' ? 'opacity:0.4' : ''">
|
||||
{{ String(idx + 1).padStart(2, "0") }}
|
||||
</span>
|
||||
|
||||
<div class="topic-info">
|
||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||||
<span class="topic-name">{{ topic.title }}</span>
|
||||
<span v-if="topic.progress?.lessonComplete" class="done-badge">done</span>
|
||||
<div class="topic-info">
|
||||
<div style="display: flex; align-items: center; gap: 10px; flex-wrap: wrap;">
|
||||
<span class="topic-name" :style="topic.status === 'pending' ? 'opacity:0.55' : ''">{{ topic.title }}</span>
|
||||
<span v-if="topic.progress?.lessonComplete" class="done-badge">done</span>
|
||||
<span
|
||||
v-if="topic.progress?.lessonComplete && topic.progress?.tookBranches"
|
||||
class="branch-dot"
|
||||
title="You needed a little extra help here — that's completely normal."
|
||||
/>
|
||||
<span v-if="topic.status === 'pending'" class="topic-status-label topic-status-label--pending">Not yet generated</span>
|
||||
<span v-else-if="topic.status === 'generating'" class="topic-status-label topic-status-label--generating">
|
||||
<span class="generating-dot" />Generating...
|
||||
</span>
|
||||
<span v-else-if="topic.status === 'error'" class="topic-status-label topic-status-label--error">Error</span>
|
||||
</div>
|
||||
|
||||
<div class="topic-meta">
|
||||
<div class="diff-dots">
|
||||
<span
|
||||
v-if="topic.progress?.lessonComplete && topic.progress?.tookBranches"
|
||||
class="branch-dot"
|
||||
title="You needed a little extra help here — that's completely normal."
|
||||
v-for="d in 5"
|
||||
:key="d"
|
||||
class="diff-dot"
|
||||
:class="d <= topic.difficulty ? 'diff-dot--filled' : 'diff-dot--empty'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="topic-meta">
|
||||
<div class="diff-dots">
|
||||
<span
|
||||
v-for="d in 5"
|
||||
:key="d"
|
||||
class="diff-dot"
|
||||
:class="d <= topic.difficulty ? 'diff-dot--filled' : 'diff-dot--empty'"
|
||||
/>
|
||||
</div>
|
||||
<span class="diff-label">
|
||||
{{ ['', 'intro', 'basic', 'intermediate', 'advanced', 'expert'][topic.difficulty] }}
|
||||
</span>
|
||||
<span v-if="topic.progress?.quizScore != null" class="diff-label">
|
||||
· quiz {{ topic.progress.quizScore }}/4
|
||||
</span>
|
||||
</div>
|
||||
<span class="diff-label">
|
||||
{{ ['', 'intro', 'basic', 'intermediate', 'advanced', 'expert'][topic.difficulty] }}
|
||||
</span>
|
||||
<span v-if="topic.progress?.quizScore != null" class="diff-label">
|
||||
· quiz {{ topic.progress.quizScore }}/4
|
||||
</span>
|
||||
<span
|
||||
v-if="topic.progress?.lessonComplete && topic.lessonCost != null"
|
||||
class="lesson-cost-badge"
|
||||
title="AI generation cost for this lesson"
|
||||
>
|
||||
~${{ topic.lessonCost.toFixed(2) }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<svg class="topic-arrow" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
</NuxtLink>
|
||||
|
||||
<div v-else class="topic-row topic-row--generating">
|
||||
<span class="topic-index" style="opacity: 0.4;">{{ String(idx + 1).padStart(2, "0") }}</span>
|
||||
<div class="topic-info">
|
||||
<span class="topic-name" style="opacity: 0.5;">{{ topic.title }}</span>
|
||||
</div>
|
||||
<span class="generating-label">
|
||||
<span class="generating-dot" />
|
||||
Generating...
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<svg v-if="topic.status !== 'error'" class="topic-arrow" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 5l7 7-7 7"/>
|
||||
</svg>
|
||||
<button
|
||||
v-else
|
||||
class="topic-retry-btn"
|
||||
@click.prevent="$fetch(`/api/topics/${topic.id}/generate`, { method: 'POST' }).then(() => refresh())"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</NuxtLink>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
|
||||
@@ -827,4 +840,42 @@ async function saveTitle() {
|
||||
cursor: pointer; transition: border-color 0.15s, color 0.15s;
|
||||
}
|
||||
.audit-show-btn:hover { border-color: var(--border-2); color: var(--text); }
|
||||
|
||||
/* topic status labels */
|
||||
.topic-status-label {
|
||||
font-size: 10px; letter-spacing: 0.08em;
|
||||
padding: 1px 7px; border-radius: 20px; font-weight: 500;
|
||||
}
|
||||
.topic-status-label--pending {
|
||||
color: var(--text-3); background: var(--surface-2);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.topic-status-label--generating {
|
||||
color: var(--accent); background: var(--surface-2);
|
||||
border: 1px solid var(--accent-dim);
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
}
|
||||
.topic-status-label--error {
|
||||
color: oklch(50% 0.18 25); background: oklch(95% 0.03 15);
|
||||
border: 1px solid oklch(80% 0.06 15);
|
||||
}
|
||||
|
||||
/* topic row states */
|
||||
.topic-row--pending { border-left-color: var(--border); }
|
||||
|
||||
/* lesson cost badge */
|
||||
.lesson-cost-badge {
|
||||
font-family: "IBM Plex Mono", monospace;
|
||||
font-size: 10px; color: var(--text-3); letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
/* retry button on error topics */
|
||||
.topic-retry-btn {
|
||||
font-size: 11px; letter-spacing: 0.06em;
|
||||
background: none; border: 1px solid oklch(80% 0.06 15);
|
||||
color: oklch(50% 0.18 25); border-radius: 20px;
|
||||
padding: 3px 10px; cursor: pointer; flex-shrink: 0;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.topic-retry-btn:hover { background: oklch(95% 0.03 15); }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user