fix: aria-label binding, dead import, guardAndLoad network error handling
- Fix 1: Add missing `:` binding prefix to aria-label on score badge (was emitting literal backtick template string to DOM) - Fix 2: Remove unused `watch` import from InterviewPrepView.vue - Fix 3: guardAndLoad now checks interviewsStore.error after fetchAll; shows pageError banner instead of silently redirecting to /interviews on network failure; job is now a ref set explicitly in the guard - Fix 4: Remove unconditional research-badge from InterviewCard.vue (added in this branch; card has no access to prep store so badge always showed regardless of whether research exists)
This commit is contained in:
parent
048edb6cb4
commit
fc645d276f
2 changed files with 18 additions and 24 deletions
|
|
@ -176,7 +176,6 @@ const columnColor = computed(() => {
|
||||||
<div v-if="interviewDateLabel" class="date-chip">
|
<div v-if="interviewDateLabel" class="date-chip">
|
||||||
{{ dateChipIcon }} {{ interviewDateLabel }}
|
{{ dateChipIcon }} {{ interviewDateLabel }}
|
||||||
</div>
|
</div>
|
||||||
<div class="research-badge research-badge--done">🔬 Research ready</div>
|
|
||||||
</div>
|
</div>
|
||||||
<footer class="card-footer">
|
<footer class="card-footer">
|
||||||
<button class="card-action" @click.stop="emit('move', job.id)">Move to… ›</button>
|
<button class="card-action" @click.stop="emit('move', job.id)">Move to… ›</button>
|
||||||
|
|
@ -331,23 +330,6 @@ const columnColor = computed(() => {
|
||||||
align-self: flex-start;
|
align-self: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.research-badge {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 3px;
|
|
||||||
border-radius: 99px;
|
|
||||||
padding: 2px 8px;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
font-weight: 700;
|
|
||||||
align-self: flex-start;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.research-badge--done {
|
|
||||||
background: color-mix(in srgb, var(--status-phone) 12%, var(--color-surface-raised));
|
|
||||||
color: var(--status-phone);
|
|
||||||
border: 1px solid color-mix(in srgb, var(--status-phone) 30%, var(--color-surface-raised));
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-footer {
|
.card-footer {
|
||||||
border-top: 1px solid var(--color-border-light);
|
border-top: 1px solid var(--color-border-light);
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { usePrepStore } from '../stores/prep'
|
import { usePrepStore } from '../stores/prep'
|
||||||
import { useInterviewsStore } from '../stores/interviews'
|
import { useInterviewsStore } from '../stores/interviews'
|
||||||
|
import type { PipelineJob } from '../stores/interviews'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -22,10 +23,7 @@ const jobId = computed<number | null>(() => {
|
||||||
// ── Current job (from interviews store) ───────────────────────────────────────
|
// ── Current job (from interviews store) ───────────────────────────────────────
|
||||||
const PREP_VALID_STATUSES = ['phone_screen', 'interviewing', 'offer'] as const
|
const PREP_VALID_STATUSES = ['phone_screen', 'interviewing', 'offer'] as const
|
||||||
|
|
||||||
const job = computed(() => {
|
const job = ref<PipelineJob | null>(null)
|
||||||
if (jobId.value === null) return null
|
|
||||||
return interviewsStore.jobs.find(j => j.id === jobId.value) ?? null
|
|
||||||
})
|
|
||||||
|
|
||||||
// ── Tabs ──────────────────────────────────────────────────────────────────────
|
// ── Tabs ──────────────────────────────────────────────────────────────────────
|
||||||
type TabId = 'jd' | 'email' | 'letter'
|
type TabId = 'jd' | 'email' | 'letter'
|
||||||
|
|
@ -35,6 +33,9 @@ const activeTab = ref<TabId>('jd')
|
||||||
const notesKey = computed(() => `cf-prep-notes-${jobId.value ?? 'none'}`)
|
const notesKey = computed(() => `cf-prep-notes-${jobId.value ?? 'none'}`)
|
||||||
const callNotes = useStorage(notesKey, '')
|
const callNotes = useStorage(notesKey, '')
|
||||||
|
|
||||||
|
// ── Page-level error (e.g. network failure during guard) ──────────────────────
|
||||||
|
const pageError = ref<string | null>(null)
|
||||||
|
|
||||||
// ── Routing / guard ───────────────────────────────────────────────────────────
|
// ── Routing / guard ───────────────────────────────────────────────────────────
|
||||||
async function guardAndLoad() {
|
async function guardAndLoad() {
|
||||||
if (jobId.value === null) {
|
if (jobId.value === null) {
|
||||||
|
|
@ -45,6 +46,11 @@ async function guardAndLoad() {
|
||||||
// Ensure the interviews store is populated
|
// Ensure the interviews store is populated
|
||||||
if (interviewsStore.jobs.length === 0) {
|
if (interviewsStore.jobs.length === 0) {
|
||||||
await interviewsStore.fetchAll()
|
await interviewsStore.fetchAll()
|
||||||
|
if (interviewsStore.error) {
|
||||||
|
// Store fetch failed — don't redirect, show error
|
||||||
|
pageError.value = 'Failed to load job data. Please try again.'
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const found = interviewsStore.jobs.find(j => j.id === jobId.value)
|
const found = interviewsStore.jobs.find(j => j.id === jobId.value)
|
||||||
|
|
@ -53,6 +59,7 @@ async function guardAndLoad() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
job.value = found
|
||||||
await prepStore.fetchFor(jobId.value)
|
await prepStore.fetchFor(jobId.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -361,7 +368,7 @@ async function onGenerate() {
|
||||||
<span
|
<span
|
||||||
class="score-badge"
|
class="score-badge"
|
||||||
:class="matchScoreBadge(matchScore).cls"
|
:class="matchScoreBadge(matchScore).cls"
|
||||||
aria-label="`Match score: ${matchScore ?? 'unknown'}%`"
|
:aria-label="`Match score: ${matchScore ?? 'unknown'}%`"
|
||||||
>
|
>
|
||||||
{{ matchScoreBadge(matchScore).icon }}
|
{{ matchScoreBadge(matchScore).icon }}
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -446,6 +453,11 @@ async function onGenerate() {
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- Network/load error — don't redirect, show message -->
|
||||||
|
<div v-else-if="pageError" class="error-banner" role="alert">
|
||||||
|
{{ pageError }}
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Fallback while redirecting -->
|
<!-- Fallback while redirecting -->
|
||||||
<div v-else class="prep-loading" aria-live="polite">
|
<div v-else class="prep-loading" aria-live="polite">
|
||||||
Redirecting…
|
Redirecting…
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue