first commit
This commit is contained in:
commit
7a72438fd1
74 changed files with 7548 additions and 0 deletions
491
frontend/src/views/RequirementsView.vue
Normal file
491
frontend/src/views/RequirementsView.vue
Normal file
|
|
@ -0,0 +1,491 @@
|
|||
<template>
|
||||
<div class="requirements-page section">
|
||||
<div class="container">
|
||||
<div class="page-header animate-fade-up">
|
||||
<div class="section-label">📋 Step 2 of 5</div>
|
||||
<h1 class="section-heading">Interview Requirements</h1>
|
||||
<p class="section-sub">Define the role and expected skills. Our AI will use this to generate targeted interview questions.</p>
|
||||
</div>
|
||||
|
||||
<div class="requirements-layout">
|
||||
<!-- Form Column -->
|
||||
<div class="form-col animate-fade-up" style="animation-delay: 100ms;">
|
||||
<div class="glass-card form-card">
|
||||
<h3 class="form-title">Role Configuration</h3>
|
||||
|
||||
<form @submit.prevent="saveRequirements" class="req-form">
|
||||
<!-- Role Name -->
|
||||
<div class="form-group">
|
||||
<label for="roleName">Role / Job Title *</label>
|
||||
<input
|
||||
type="text"
|
||||
id="roleName"
|
||||
v-model="form.role_name"
|
||||
class="form-control"
|
||||
placeholder="e.g. Senior Frontend Developer"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Experience -->
|
||||
<div class="form-group">
|
||||
<label for="expYears">Required Experience (Years) *</label>
|
||||
<input
|
||||
type="number"
|
||||
id="expYears"
|
||||
v-model.number="form.experience_years"
|
||||
class="form-control"
|
||||
min="0"
|
||||
step="0.5"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Mandatory Skills -->
|
||||
<div class="form-group">
|
||||
<label>Mandatory Skills</label>
|
||||
<div class="skill-input-wrap">
|
||||
<input
|
||||
type="text"
|
||||
v-model="newMandatorySkill"
|
||||
@keydown.enter.prevent="addMandatorySkill"
|
||||
class="form-control"
|
||||
placeholder="Type a skill and press Enter..."
|
||||
/>
|
||||
<button type="button" class="btn btn-primary add-btn" @click="addMandatorySkill">+</button>
|
||||
</div>
|
||||
<div class="skills-list" v-if="form.mandatory_skills.length">
|
||||
<span class="skill-chip mandatory" v-for="(skill, i) in form.mandatory_skills" :key="'m'+i">
|
||||
{{ skill }}
|
||||
<button type="button" class="chip-remove" @click="removeMandatorySkill(i)">✕</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Nice to have Skills -->
|
||||
<div class="form-group">
|
||||
<label>Nice-to-Have Skills</label>
|
||||
<div class="skill-input-wrap">
|
||||
<input
|
||||
type="text"
|
||||
v-model="newNiceSkill"
|
||||
@keydown.enter.prevent="addNiceSkill"
|
||||
class="form-control"
|
||||
placeholder="Type a skill and press Enter..."
|
||||
/>
|
||||
<button type="button" class="btn btn-outline add-btn" @click="addNiceSkill">+</button>
|
||||
</div>
|
||||
<div class="skills-list" v-if="form.nice_to_have_skills.length">
|
||||
<span class="skill-chip nice" v-for="(skill, i) in form.nice_to_have_skills" :key="'n'+i">
|
||||
{{ skill }}
|
||||
<button type="button" class="chip-remove" @click="removeNiceSkill(i)">✕</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Job Description -->
|
||||
<div class="form-group">
|
||||
<label for="jobDesc">Brief Job Description (Optional)</label>
|
||||
<textarea
|
||||
id="jobDesc"
|
||||
v-model="form.job_description"
|
||||
class="form-control"
|
||||
rows="4"
|
||||
placeholder="Paste key responsibilities or a brief summary..."
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Submit -->
|
||||
<div class="form-actions">
|
||||
<button type="submit" class="btn btn-primary" :disabled="saving">
|
||||
{{ saving ? 'Saving...' : 'Save Requirements' }}
|
||||
</button>
|
||||
<span class="save-msg success" v-if="saveSuccess">✓ Saved successfully</span>
|
||||
<span class="save-msg error" v-if="saveError">{{ saveError }}</span>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- History/List Column -->
|
||||
<div class="list-col animate-fade-up" style="animation-delay: 200ms;">
|
||||
<h3 class="list-title">Saved Configurations</h3>
|
||||
|
||||
<div class="loading-state" v-if="loadingList">
|
||||
<div class="spinner"></div> Loading...
|
||||
</div>
|
||||
|
||||
<div class="empty-state glass-card" v-else-if="!reqList.length">
|
||||
<div class="empty-icon">📝</div>
|
||||
<h4>No configurations yet</h4>
|
||||
<p class="text-muted">Save your first job requirement configuration on the left.</p>
|
||||
</div>
|
||||
|
||||
<div class="req-list" v-else>
|
||||
<div class="req-card glass-card" v-for="req in reqList" :key="req.id">
|
||||
<div class="req-header">
|
||||
<h4>{{ req.role_name }}</h4>
|
||||
<div class="req-actions">
|
||||
<button class="btn-icon" @click="editReq(req)" title="Edit">✏️</button>
|
||||
<button class="btn-icon delete" @click="deleteReq(req.id)" title="Delete">🗑</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="req-meta">{{ req.experience_years }} years exp. · {{ req.mandatory_skills.length }} core skills</p>
|
||||
|
||||
<div class="req-skills">
|
||||
<span class="tiny-chip" v-for="skill in req.mandatory_skills.slice(0,3)" :key="skill">{{ skill }}</span>
|
||||
<span class="tiny-chip more" v-if="req.mandatory_skills.length > 3">+{{ req.mandatory_skills.length - 3 }}</span>
|
||||
</div>
|
||||
|
||||
<RouterLink :to="`/questions?req=${req.id}`" class="btn btn-outline btn-sm req-action-btn">
|
||||
Generate Questions
|
||||
</RouterLink>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import api from '../api/index.js'
|
||||
|
||||
const form = ref({
|
||||
role_name: '',
|
||||
experience_years: 0,
|
||||
mandatory_skills: [],
|
||||
nice_to_have_skills: [],
|
||||
job_description: ''
|
||||
})
|
||||
const editingId = ref(null)
|
||||
|
||||
const newMandatorySkill = ref('')
|
||||
const newNiceSkill = ref('')
|
||||
|
||||
const saving = ref(false)
|
||||
const saveSuccess = ref(false)
|
||||
const saveError = ref('')
|
||||
|
||||
const loadingList = ref(false)
|
||||
const reqList = ref([])
|
||||
|
||||
onMounted(() => {
|
||||
loadRequirements()
|
||||
})
|
||||
|
||||
function addMandatorySkill() {
|
||||
const skill = newMandatorySkill.value.trim()
|
||||
if (skill && !form.value.mandatory_skills.includes(skill)) {
|
||||
form.value.mandatory_skills.push(skill)
|
||||
}
|
||||
newMandatorySkill.value = ''
|
||||
}
|
||||
|
||||
function removeMandatorySkill(index) {
|
||||
form.value.mandatory_skills.splice(index, 1)
|
||||
}
|
||||
|
||||
function addNiceSkill() {
|
||||
const skill = newNiceSkill.value.trim()
|
||||
if (skill && !form.value.nice_to_have_skills.includes(skill)) {
|
||||
form.value.nice_to_have_skills.push(skill)
|
||||
}
|
||||
newNiceSkill.value = ''
|
||||
}
|
||||
|
||||
function removeNiceSkill(index) {
|
||||
form.value.nice_to_have_skills.splice(index, 1)
|
||||
}
|
||||
|
||||
async function loadRequirements() {
|
||||
loadingList.value = true
|
||||
try {
|
||||
const { data } = await api.get('/api/requirements/')
|
||||
reqList.value = data
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loadingList.value = false
|
||||
}
|
||||
}
|
||||
|
||||
async function saveRequirements() {
|
||||
saving.value = true
|
||||
saveSuccess.value = false
|
||||
saveError.value = ''
|
||||
|
||||
try {
|
||||
if (editingId.value) {
|
||||
await api.put(`/api/requirements/${editingId.value}`, form.value)
|
||||
} else {
|
||||
await api.post('/api/requirements/', form.value)
|
||||
}
|
||||
saveSuccess.value = true
|
||||
setTimeout(() => saveSuccess.value = false, 3000)
|
||||
|
||||
// reset form if creating new, else keep it
|
||||
if (!editingId.value) {
|
||||
form.value = {
|
||||
role_name: '',
|
||||
experience_years: 0,
|
||||
mandatory_skills: [],
|
||||
nice_to_have_skills: [],
|
||||
job_description: ''
|
||||
}
|
||||
}
|
||||
await loadRequirements()
|
||||
} catch (e) {
|
||||
saveError.value = e.response?.data?.detail || 'Failed to save'
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function editReq(req) {
|
||||
editingId.value = req.id
|
||||
form.value = {
|
||||
role_name: req.role_name,
|
||||
experience_years: req.experience_years,
|
||||
mandatory_skills: [...req.mandatory_skills],
|
||||
nice_to_have_skills: [...req.nice_to_have_skills],
|
||||
job_description: req.job_description || ''
|
||||
}
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}
|
||||
|
||||
async function deleteReq(id) {
|
||||
if (!confirm('Are you sure you want to delete this configuration?')) return
|
||||
try {
|
||||
await api.delete(`/api/requirements/${id}`)
|
||||
if (editingId.value === id) {
|
||||
editingId.value = null
|
||||
form.value = { role_name: '', experience_years: 0, mandatory_skills: [], nice_to_have_skills: [], job_description: '' }
|
||||
}
|
||||
await loadRequirements()
|
||||
} catch (e) {
|
||||
console.error('Delete failed', e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.requirements-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 1.5fr 1fr;
|
||||
gap: var(--space-8);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.requirements-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.form-card {
|
||||
padding: var(--space-6);
|
||||
}
|
||||
|
||||
.form-title {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
margin-bottom: var(--space-5);
|
||||
padding-bottom: var(--space-3);
|
||||
border-bottom: 1px solid var(--clr-border);
|
||||
}
|
||||
|
||||
.req-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-5);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: var(--clr-text-secondary);
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
background: var(--clr-surface-2);
|
||||
border: 1px solid var(--clr-border);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--clr-text-primary);
|
||||
font-size: var(--text-md);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.form-control:focus {
|
||||
outline: none;
|
||||
border-color: var(--clr-accent);
|
||||
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
|
||||
}
|
||||
|
||||
.skill-input-wrap {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
padding: 0 var(--space-4);
|
||||
font-size: 1.2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.skills-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--space-2);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.skill-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
border-radius: var(--radius-full);
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 600;
|
||||
}
|
||||
.skill-chip.mandatory {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: var(--clr-accent);
|
||||
border: 1px solid rgba(34, 197, 94, 0.3);
|
||||
}
|
||||
.skill-chip.nice {
|
||||
background: rgba(139, 92, 246, 0.15);
|
||||
color: #a78bfa;
|
||||
border: 1px solid rgba(139, 92, 246, 0.3);
|
||||
}
|
||||
|
||||
.chip-remove {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
font-size: 10px;
|
||||
cursor: pointer;
|
||||
padding: 2px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.chip-remove:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-4);
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.save-msg {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
}
|
||||
.save-msg.success { color: var(--clr-accent); }
|
||||
.save-msg.error { color: #f87171; }
|
||||
|
||||
/* List Column */
|
||||
.list-title {
|
||||
font-size: var(--text-md);
|
||||
font-weight: 600;
|
||||
color: var(--clr-text-secondary);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: var(--space-8) var(--space-4);
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.empty-icon {
|
||||
font-size: 2.5rem;
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
|
||||
.req-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
}
|
||||
|
||||
.req-card {
|
||||
padding: var(--space-4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.req-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.req-header h4 {
|
||||
font-size: var(--text-md);
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.req-actions {
|
||||
display: flex;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
transition: background var(--transition-fast);
|
||||
}
|
||||
.btn-icon:hover { background: var(--clr-surface-2); }
|
||||
.btn-icon.delete:hover { background: rgba(239, 68, 68, 0.1); }
|
||||
|
||||
.req-meta {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--clr-text-muted);
|
||||
}
|
||||
|
||||
.req-skills {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-top: var(--space-2);
|
||||
}
|
||||
|
||||
.tiny-chip {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
background: var(--clr-surface-2);
|
||||
border: 1px solid var(--clr-border);
|
||||
border-radius: 4px;
|
||||
color: var(--clr-text-secondary);
|
||||
}
|
||||
.tiny-chip.more {
|
||||
background: transparent;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.req-action-btn {
|
||||
margin-top: var(--space-3);
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Add a link
Reference in a new issue