initialize project with basic structure and dependencies

This commit is contained in:
ImBenji
2026-04-27 23:59:15 +01:00
parent 83f2837ce6
commit f6f45500f8
13 changed files with 1443 additions and 579 deletions
+100 -49
View File
@@ -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>