feat: add ResumeLibraryCard to Apply workspace

This commit is contained in:
pyr0ball 2026-04-12 11:35:06 -07:00
parent d4a2107411
commit 8245333c9c
2 changed files with 202 additions and 0 deletions

View file

@ -191,6 +191,9 @@
Regenerate
</button>
<!-- Resume Library Card -->
<ResumeLibraryCard :job-id="props.jobId" class="apply__resume-card" />
<!-- ATS Resume Optimizer -->
<ResumeOptimizerPanel :job-id="props.jobId" />
@ -286,6 +289,7 @@ import { useApiFetch } from '../composables/useApi'
import { useAppConfigStore } from '../stores/appConfig'
import type { Job } from '../stores/review'
import ResumeOptimizerPanel from './ResumeOptimizerPanel.vue'
import ResumeLibraryCard from './ResumeLibraryCard.vue'
const config = useAppConfigStore()

View file

@ -0,0 +1,198 @@
<template>
<div class="rlc">
<div class="rlc__header">
<h3 class="rlc__title"><span aria-hidden="true">📄</span> Resume</h3>
<div class="rlc__actions">
<button class="btn-secondary rlc__switch" @click="showPicker = !showPicker">Switch</button>
<RouterLink to="/resumes" class="btn-secondary rlc__manage">Manage</RouterLink>
</div>
</div>
<div v-if="loading" class="rlc__loading">Loading</div>
<div v-else-if="resume" class="rlc__resume">
<span class="rlc__name">{{ resume.name }}</span>
<span class="rlc__meta">{{ resume.word_count }} words</span>
<span v-if="resume.job_id === jobId" class="rlc__optimized-badge"> Optimized for this job</span>
</div>
<div v-else class="rlc__empty">
No resume attached.
<RouterLink to="/resumes" class="rlc__import-link">Import one</RouterLink>
or optimize below.
</div>
<!-- Picker dropdown -->
<div v-if="showPicker && allResumes.length" class="rlc__picker">
<ul class="rlc__picker-list">
<li
v-for="r in allResumes"
:key="r.id"
class="rlc__picker-item"
:class="{ 'rlc__picker-item--active': resume?.id === r.id }"
@click="switchResume(r.id)"
>
<span>{{ r.name }}</span>
<span class="rlc__picker-meta">{{ r.word_count }}w</span>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { RouterLink } from 'vue-router'
import { useApiFetch } from '../composables/useApi'
interface Resume {
id: number
name: string
word_count: number
job_id: number | null
is_default: number
}
const props = defineProps<{ jobId: number }>()
const resume = ref<Resume | null>(null)
const allResumes = ref<Resume[]>([])
const loading = ref(true)
const showPicker = ref(false)
async function load() {
loading.value = true
const [jobRes, listRes] = await Promise.all([
useApiFetch<Resume>(`/api/jobs/${props.jobId}/resume`),
useApiFetch<{ resumes: Resume[] }>('/api/resumes'),
])
resume.value = jobRes.data ?? null
allResumes.value = listRes.data?.resumes ?? []
loading.value = false
}
async function switchResume(resumeId: number) {
showPicker.value = false
const { data } = await useApiFetch<Resume>(
`/api/jobs/${props.jobId}/resume`,
{
method: 'PATCH',
body: JSON.stringify({ resume_id: resumeId }),
headers: { 'Content-Type': 'application/json' },
}
)
if (data) resume.value = data
}
onMounted(load)
</script>
<style scoped>
.rlc {
background: var(--app-surface-alt, #f8fafc);
border: 1px solid var(--app-border, #e2e8f0);
border-radius: var(--radius-md, 0.5rem);
padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
display: flex;
flex-direction: column;
gap: var(--space-2, 0.5rem);
position: relative;
}
.rlc__header {
display: flex;
align-items: center;
justify-content: space-between;
}
.rlc__title {
font-size: var(--font-sm, 0.875rem);
font-weight: 600;
margin: 0;
display: flex;
align-items: center;
gap: var(--space-2, 0.5rem);
}
.rlc__actions {
display: flex;
gap: var(--space-2, 0.5rem);
}
.rlc__resume {
display: flex;
align-items: center;
gap: var(--space-2, 0.5rem);
flex-wrap: wrap;
}
.rlc__name {
font-weight: 500;
font-size: var(--font-sm, 0.875rem);
}
.rlc__meta {
font-size: var(--font-xs, 0.75rem);
color: var(--app-text-muted, #64748b);
}
.rlc__optimized-badge {
font-size: var(--font-xs, 0.75rem);
color: var(--app-accent, #6366f1);
font-weight: 500;
}
.rlc__empty {
font-size: var(--font-sm, 0.875rem);
color: var(--app-text-muted, #64748b);
}
.rlc__import-link {
color: var(--app-accent, #6366f1);
text-decoration: underline;
}
.rlc__loading {
font-size: var(--font-sm, 0.875rem);
color: var(--app-text-muted, #64748b);
}
.rlc__picker {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 10;
background: var(--app-surface, #fff);
border: 1px solid var(--app-border, #e2e8f0);
border-radius: var(--radius-md, 0.5rem);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
margin-top: 4px;
}
.rlc__picker-list {
list-style: none;
margin: 0;
padding: var(--space-1, 0.25rem);
}
.rlc__picker-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-2, 0.5rem) var(--space-3, 0.75rem);
cursor: pointer;
border-radius: var(--radius-sm, 0.25rem);
font-size: var(--font-sm, 0.875rem);
}
.rlc__picker-item:hover,
.rlc__picker-item--active {
background: var(--app-surface-alt, #f8fafc);
}
.rlc__picker-meta {
font-size: var(--font-xs, 0.75rem);
color: var(--app-text-muted, #64748b);
}
</style>