Add job application quick reference for storing profile links and experience
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m34s

Auth-protected CRUD for personal info (LinkedIn, GitHub, etc.) and
experience entries, stored in the database so nothing sensitive is in
the public repo. Displayed as a categorized panel on the Job Applications page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-13 12:38:53 +01:00
parent 81c5684102
commit 759614e92d
10 changed files with 1207 additions and 18 deletions

View File

@@ -16,6 +16,13 @@ const form = ref({
appliedAt: "",
});
const references = ref([]);
const refForm = ref({ category: "profile", label: "", value: "" });
const editingRefId = ref(null);
const editRefForm = ref({});
const REF_CATEGORIES = ["profile", "experience"];
const REF_FIELDS = `id category label value sortOrder createdAt`;
const STATUS_OPTIONS = ["Applied", "Screening", "Interview", "Offer", "Rejected", "Withdrawn"];
const APP_FIELDS = `id jobTitle company location url status notes appliedAt createdAt`;
@@ -132,6 +139,87 @@ function exportCsv() {
URL.revokeObjectURL(url);
}
async function fetchReferences() {
try {
const data = await gql(`query { jobAppReferences { ${REF_FIELDS} } }`);
references.value = data.jobAppReferences;
} catch (err) {
console.error(err);
}
}
async function createReference() {
if (!refForm.value.label || !refForm.value.value) return;
try {
const input = {
category: refForm.value.category,
label: refForm.value.label,
value: refForm.value.value,
};
const data = await gql(
`mutation CreateJobAppReference($input: CreateJobAppReferenceInput!) {
createJobAppReference(input: $input) { ${REF_FIELDS} }
}`,
{ input },
);
references.value.push(data.createJobAppReference);
refForm.value = { category: refForm.value.category, label: "", value: "" };
} catch (err) {
console.error(err);
}
}
function startRefEdit(ref) {
editingRefId.value = ref.id;
editRefForm.value = { category: ref.category, label: ref.label, value: ref.value };
}
function cancelRefEdit() {
editingRefId.value = null;
editRefForm.value = {};
}
async function saveRefEdit(id) {
try {
const input = {
category: editRefForm.value.category || undefined,
label: editRefForm.value.label || undefined,
value: editRefForm.value.value || undefined,
};
const data = await gql(
`mutation UpdateJobAppReference($id: ID!, $input: UpdateJobAppReferenceInput!) {
updateJobAppReference(id: $id, input: $input) { ${REF_FIELDS} }
}`,
{ id, input },
);
const idx = references.value.findIndex((r) => r.id === id);
if (idx !== -1) references.value[idx] = data.updateJobAppReference;
editingRefId.value = null;
} catch (err) {
console.error(err);
}
}
async function deleteReference(id) {
try {
await gql(
`mutation DeleteJobAppReference($id: ID!) { deleteJobAppReference(id: $id) }`,
{ id },
);
references.value = references.value.filter((r) => r.id !== id);
} catch (err) {
console.error(err);
}
}
function refsByCategory(category) {
return references.value.filter((r) => r.category === category);
}
function copyToClipboard(text) {
navigator.clipboard.writeText(text);
}
function statusClass(status) {
const map = {
Applied: "status-applied",
@@ -144,7 +232,10 @@ function statusClass(status) {
return map[status] ?? "";
}
onMounted(fetchApplications);
onMounted(() => {
fetchApplications();
fetchReferences();
});
</script>
<template>
@@ -157,6 +248,40 @@ onMounted(fetchApplications);
<button class="ja-btn" @click="exportCsv" :disabled="!applications.length">Export CSV</button>
</div>
<div class="ja-ref-section">
<h3 class="ja-ref-heading">Quick Reference</h3>
<div v-for="cat in REF_CATEGORIES" :key="cat" class="ja-ref-category">
<h4 class="ja-ref-cat-label">{{ cat }}</h4>
<div v-for="ref in refsByCategory(cat)" :key="ref.id" class="ja-ref-item">
<template v-if="editingRefId !== ref.id">
<span class="ja-ref-label">{{ ref.label }}</span>
<span class="ja-ref-value" :title="ref.value">{{ ref.value }}</span>
<button class="ja-btn ja-btn-sm" @click="copyToClipboard(ref.value)" title="Copy">Copy</button>
<button class="ja-btn ja-btn-sm" @click="startRefEdit(ref)">Edit</button>
<button class="ja-btn ja-btn-sm ja-btn-danger" @click="deleteReference(ref.id)">Delete</button>
</template>
<template v-else>
<select v-model="editRefForm.category" class="ja-input ja-input-sm ja-select">
<option v-for="c in REF_CATEGORIES" :key="c" :value="c">{{ c }}</option>
</select>
<input v-model="editRefForm.label" class="ja-input ja-input-sm" placeholder="Label" />
<input v-model="editRefForm.value" class="ja-input ja-input-sm" placeholder="Value" />
<button class="ja-btn ja-btn-sm ja-btn-primary" @click="saveRefEdit(ref.id)">Save</button>
<button class="ja-btn ja-btn-sm" @click="cancelRefEdit">Cancel</button>
</template>
</div>
<p v-if="!refsByCategory(cat).length" class="ja-ref-empty">No {{ cat }} items yet.</p>
</div>
<form class="ja-ref-form" @submit.prevent="createReference">
<select v-model="refForm.category" class="ja-input ja-select">
<option v-for="c in REF_CATEGORIES" :key="c" :value="c">{{ c }}</option>
</select>
<input v-model="refForm.label" class="ja-input" placeholder="Label *" required />
<input v-model="refForm.value" class="ja-input" placeholder="Value *" required />
<button type="submit" class="ja-btn ja-btn-primary">Add</button>
</form>
</div>
<form class="ja-form" @submit.prevent="createApplication">
<div class="ja-form-row">
<input v-model="form.jobTitle" class="ja-input" placeholder="Job title *" required />
@@ -429,4 +554,67 @@ onMounted(fetchApplications);
color: #888;
font-size: 0.9rem;
}
.ja-ref-section {
margin-bottom: 1.5rem;
padding: 1rem;
border: 1px solid #e0e0e0;
border-radius: 6px;
background: #fff;
}
.ja-ref-heading {
font-size: 1rem;
font-weight: 600;
margin: 0 0 0.75rem 0;
color: #333;
}
.ja-ref-category {
margin-bottom: 0.75rem;
}
.ja-ref-cat-label {
font-size: 0.85rem;
font-weight: 600;
color: #555;
text-transform: capitalize;
margin: 0 0 0.35rem 0;
}
.ja-ref-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.3rem 0;
}
.ja-ref-label {
font-size: 0.85rem;
font-weight: 500;
color: #333;
min-width: 80px;
}
.ja-ref-value {
font-size: 0.85rem;
color: #555;
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.ja-ref-empty {
font-size: 0.8rem;
color: #999;
margin: 0.2rem 0;
}
.ja-ref-form {
display: flex;
gap: 0.5rem;
margin-top: 0.75rem;
flex-wrap: wrap;
}
</style>