This commit is contained in:
Kiran Surendran Pankan
2025-03-30 17:22:59 +05:30
commit 5782fc7832
20 changed files with 5318 additions and 0 deletions
+423
View File
@@ -0,0 +1,423 @@
<script setup>
import JobCard from './components/JobCard.vue'
import SideNav from './components/SideNav.vue'
import Dashboard from './components/Dashboard.vue'
import Applications from './components/Applications.vue'
import SavedJobs from './components/SavedJobs.vue'
import JobDetail from './components/JobDetail.vue'
import JobFilters from './components/JobFilters.vue'
import { ref, provide, onMounted, computed } from 'vue'
// Current active section
const activeSection = ref('dashboard')
// Selected job for detail view
const selectedJobId = ref(null)
const showJobDetail = ref(false)
// Theme state
const isDarkMode = ref(false)
// Initialize theme on mount
onMounted(() => {
// Check for saved preference in localStorage
const savedTheme = localStorage.getItem('theme')
if (savedTheme) {
isDarkMode.value = savedTheme === 'dark'
} else {
// Use system preference as fallback
isDarkMode.value = window.matchMedia('(prefers-color-scheme: dark)').matches
}
// Apply initial theme
applyTheme(isDarkMode.value)
})
// Apply theme to document
const applyTheme = (dark) => {
document.documentElement.classList.toggle('dark-mode', dark)
document.documentElement.classList.toggle('light-mode', !dark)
localStorage.setItem('theme', dark ? 'dark' : 'light')
}
// Handle theme toggle from SideNav
const handleThemeToggle = (dark) => {
isDarkMode.value = dark
applyTheme(dark)
}
// Provide theme state to components
provide('isDarkMode', isDarkMode)
provide('toggleTheme', handleThemeToggle)
// Provide navigation state to components
provide('activeSection', activeSection)
// Handle navigation
const handleNavigation = (section) => {
activeSection.value = section
console.log(`Navigated to: ${section}`)
}
// Sample job listings data
const allJobListings = [
{
id: 1,
title: 'Frontend Developer',
company: 'Tech Innovations Inc.',
location: 'San Francisco, CA',
salary: '$120,000 - $150,000',
employmentType: 'Full-time',
experience: '3+ years',
description: 'We are looking for an experienced Frontend Developer proficient in Vue.js to join our growing team. You will be responsible for building user interfaces and implementing new features.',
applied: false,
tags: ['Vue.js', 'JavaScript', 'CSS', 'UI/UX']
},
{
id: 2,
title: 'Backend Engineer',
company: 'DataSystems Co.',
location: 'Remote',
salary: '$130,000 - $160,000',
employmentType: 'Full-time',
experience: '4+ years',
description: 'Join our backend team to develop scalable APIs and services. Experience with Node.js and database design required.',
applied: false,
tags: ['Node.js', 'API', 'Backend', 'Databases']
},
{
id: 3,
title: 'Full Stack Developer',
company: 'WebSolutions Ltd.',
location: 'New York, NY',
salary: '$140,000 - $170,000',
employmentType: 'Full-time',
experience: '5+ years',
description: 'Looking for a versatile developer who can work across the entire stack. Experience with Vue.js and Node.js is a plus.',
applied: true,
tags: ['Full Stack', 'Vue.js', 'Node.js', 'JavaScript']
},
{
id: 4,
title: 'UX/UI Designer',
company: 'Creative Design Studio',
location: 'Chicago, IL',
salary: '$110,000 - $135,000',
employmentType: 'Full-time',
experience: '2+ years',
description: 'Join our design team to create beautiful and intuitive user interfaces for web and mobile applications.',
applied: false,
tags: ['UI/UX', 'Design', 'Figma', 'Prototyping']
},
{
id: 5,
title: 'DevOps Engineer',
company: 'CloudTech Solutions',
location: 'Remote',
salary: '$135,000 - $165,000',
employmentType: 'Full-time',
experience: '4+ years',
description: 'Looking for a skilled DevOps engineer to help us build and maintain our cloud infrastructure and CI/CD pipelines.',
applied: false,
tags: ['AWS', 'Docker', 'Kubernetes', 'CI/CD']
},
{
id: 6,
title: 'Product Manager',
company: 'Tech Innovations Inc.',
location: 'San Francisco, CA',
salary: '$150,000 - $180,000',
employmentType: 'Full-time',
experience: '5+ years',
description: 'Lead product development initiatives and work closely with engineering, design, and marketing teams to deliver exceptional products.',
applied: false,
tags: ['Product', 'Agile', 'Leadership', 'Strategy']
},
{
id: 7,
title: 'Data Scientist',
company: 'DataSystems Co.',
location: 'Boston, MA',
salary: '$140,000 - $170,000',
employmentType: 'Full-time',
experience: '3+ years',
description: 'Apply machine learning and statistical techniques to analyze large datasets and extract valuable insights for our clients.',
applied: false,
tags: ['Python', 'Machine Learning', 'Data Analysis', 'SQL']
},
{
id: 8,
title: 'Frontend Developer (Contract)',
company: 'WebSolutions Ltd.',
location: 'Remote',
salary: '$90,000 - $120,000',
employmentType: 'Contract',
experience: '2+ years',
description: 'Short-term contract role for a Vue.js developer to help us complete a client project over the next 6 months.',
applied: false,
tags: ['Vue.js', 'JavaScript', 'Contract', 'Remote']
}
];
// Filtered job listings
const filteredJobListings = ref([...allJobListings]);
// Handle job application
const handleJobApplication = (jobTitle) => {
console.log(`Applied for: ${jobTitle}`);
};
// View job details
const viewJobDetails = (jobId) => {
selectedJobId.value = jobId;
showJobDetail.value = true;
};
// Handle filter changes
const handleFilterChange = (filteredJobs) => {
filteredJobListings.value = filteredJobs;
};
// Close job details
const closeJobDetails = () => {
showJobDetail.value = false;
};
</script>
<template>
<div class="app-container">
<SideNav @navigate="handleNavigation" />
<div class="content-wrapper">
<header>
<h1>{{ activeSection === 'dashboard' ? 'Dashboard' : activeSection === 'jobs' ? 'Job Board' : activeSection.charAt(0).toUpperCase() + activeSection.slice(1) }}</h1>
</header>
<main>
<section v-if="activeSection === 'jobs'" class="job-listings">
<div v-if="!showJobDetail" class="jobs-container">
<h2 class="section-heading">Available Positions</h2>
<!-- Job Filters Component -->
<JobFilters
:jobs="allJobListings"
:initially-expanded="false"
@filter-change="filteredJobListings = $event"
/>
<div class="filter-results">
<p class="results-count">{{ filteredJobListings.length }} job{{ filteredJobListings.length !== 1 ? 's' : '' }} found</p>
</div>
<div class="job-cards" v-if="filteredJobListings.length > 0">
<JobCard
v-for="job in filteredJobListings"
:key="job.id"
:title="job.title"
:company="job.company"
:location="job.location"
:salary="job.salary"
:description="job.description"
:applied="job.applied"
:tags="job.tags"
@apply="handleJobApplication"
@click="viewJobDetails(job.id)"
/>
</div>
<div v-else class="no-results">
<div class="no-results-icon">🔍</div>
<h3>No jobs found</h3>
<p>Try adjusting your search filters to find more opportunities.</p>
</div>
</div>
<JobDetail
v-if="showJobDetail"
:jobId="selectedJobId"
@close="closeJobDetails"
@apply="handleJobApplication"
/>
</section>
<section v-else-if="activeSection === 'dashboard'" class="dashboard-section">
<Dashboard />
</section>
<section v-else-if="activeSection === 'applications'" class="applications-section">
<Applications />
</section>
<section v-else-if="activeSection === 'saved'" class="saved-section">
<SavedJobs />
</section>
<section v-else-if="activeSection === 'profile'" class="profile">
<h2>My Profile</h2>
<p>Manage your profile information.</p>
</section>
<section v-else-if="activeSection === 'settings'" class="settings">
<h2>Settings</h2>
<p>Adjust your account settings.</p>
</section>
</main>
<footer>
<p>Job Board Application</p>
</footer>
</div>
</div>
</template>
<style scoped lang="scss">
.app-container {
display: flex;
min-height: 100vh;
}
.content-wrapper {
flex: 1;
margin-left: 240px;
transition: margin-left 0.3s ease;
display: flex;
flex-direction: column;
min-height: 100vh;
.side-nav.collapsed + & {
margin-left: 60px;
}
}
header {
padding: 1.5rem 2rem;
margin-bottom: 1rem;
border-bottom: 1px solid #eee;
h1 {
margin: 0;
font-size: 1.8rem;
}
}
main {
flex: 1;
width: 100%;
max-width: 1100px;
margin: 0 auto;
padding: 0 2rem;
section {
margin-bottom: 2rem;
h2 {
margin-bottom: 1.5rem;
font-size: 1.4rem;
color: var(--text-color);
}
}
.jobs-container {
padding: 1.5rem;
background-color: var(--bg-color, #f8f9fa);
border-radius: 16px;
.section-heading {
font-size: 1.8rem;
font-weight: 600;
margin-bottom: 2rem;
color: var(--text-color, #333);
}
.filter-results {
margin: 1.5rem 0;
.results-count {
font-size: 1rem;
color: var(--text-color-secondary, #666);
font-weight: 500;
}
}
.no-results {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 3rem 1rem;
text-align: center;
background-color: var(--card-bg, #fff);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
.no-results-icon {
font-size: 3rem;
margin-bottom: 1rem;
opacity: 0.7;
}
h3 {
font-size: 1.4rem;
margin: 0 0 0.5rem;
color: var(--text-color, #333);
}
p {
color: var(--text-color-secondary, #666);
max-width: 400px;
margin: 0;
}
}
}
.job-cards {
display: flex;
flex-direction: column;
width: 100%;
max-width: 900px;
margin: 0 auto;
gap: 1.5rem;
}
.dashboard-section {
width: 100%;
}
}
footer {
text-align: center;
padding: 1rem 0;
margin-top: 2rem;
border-top: 1px solid #eee;
font-size: 0.9rem;
color: #888;
}
@media (prefers-color-scheme: dark) {
header {
border-bottom-color: #333;
}
main section h2 {
color: #eee;
}
footer {
border-top-color: #333;
}
}
@media (max-width: 768px) {
.content-wrapper {
margin-left: 60px;
}
header {
padding: 1rem;
}
main {
padding: 0 1rem;
}
}
</style>
+116
View File
@@ -0,0 +1,116 @@
@import './variables.scss';
// Reset and base styles
:root {
font-family: $font-family;
line-height: $line-height;
font-weight: $font-weight-normal;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
// Default dark theme
:root, .dark-mode {
color-scheme: dark;
color: $text-color-dark;
background-color: $bg-color-dark;
// CSS variables for theming
--primary-color: #{$primary-color};
--primary-hover-color: #{$primary-hover-color};
--text-color: #{$text-color-dark};
--bg-color: #{$bg-color-dark};
--sidebar-bg: #1e1e1e;
--sidebar-border: #333;
--card-bg: #2a2a2a;
--card-border: #444;
}
a {
font-weight: $font-weight-medium;
color: $primary-color;
text-decoration: inherit;
&:hover {
color: $primary-hover-color;
}
}
body {
margin: 0;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 2.5em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: $font-weight-medium;
font-family: inherit;
background-color: $button-bg-dark;
cursor: pointer;
transition: border-color 0.25s;
&:hover {
border-color: $primary-color;
}
}
#app {
max-width: $max-content-width;
margin: 0 auto;
padding: 1rem;
}
// Light mode theme
.light-mode {
color-scheme: light;
color: $text-color-light;
background-color: $bg-color-light;
// Update CSS variables for light mode
--primary-color: #{$primary-color};
--primary-hover-color: #{$primary-hover-color};
--text-color: #{$text-color-light};
--bg-color: #{$bg-color-light};
--sidebar-bg: #fff;
--sidebar-border: #eee;
--card-bg: #fff;
--card-border: #ddd;
button {
background-color: $button-bg-light;
}
}
// System preference override
@media (prefers-color-scheme: light) {
:root:not(.dark-mode):not(.light-mode) {
color: $text-color-light;
background-color: $bg-color-light;
// Update CSS variables for light mode
--primary-color: #{$primary-color};
--primary-hover-color: #{$primary-hover-color};
--text-color: #{$text-color-light};
--bg-color: #{$bg-color-light};
--sidebar-bg: #fff;
--sidebar-border: #eee;
--card-bg: #fff;
--card-border: #ddd;
}
:root:not(.dark-mode):not(.light-mode) button {
background-color: $button-bg-light;
}
}
+18
View File
@@ -0,0 +1,18 @@
// Color variables
$primary-color: #3498db;
$primary-hover-color: #2980b9;
$text-color-dark: rgba(255, 255, 255, 0.87);
$text-color-light: #333;
$bg-color-dark: #1a1a1a;
$bg-color-light: #f8f8f8;
$button-bg-dark: #2c3e50;
$button-bg-light: #ecf0f1;
// Typography
$font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
$font-weight-normal: 400;
$font-weight-medium: 500;
$line-height: 1.5;
// Layout
$max-content-width: 1000px;
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>

After

Width:  |  Height:  |  Size: 496 B

+492
View File
@@ -0,0 +1,492 @@
<script setup>
import { ref, computed } from 'vue';
// Sample applications data
const applications = ref([
{
id: 1,
jobTitle: 'Frontend Developer',
company: 'Tech Innovations Inc.',
location: 'San Francisco, CA',
appliedDate: '2025-03-15',
status: 'Interview',
nextStep: 'Technical Interview on April 5, 2025',
notes: 'Prepare for React and Vue questions. Review portfolio projects.'
},
{
id: 2,
jobTitle: 'Senior UI Developer',
company: 'Digital Creations',
location: 'Remote',
appliedDate: '2025-03-18',
status: 'Offer',
nextStep: 'Review offer by April 10, 2025',
notes: 'Salary negotiation pending. Discuss remote work policy.'
},
{
id: 3,
jobTitle: 'Full Stack Engineer',
company: 'WebSolutions Ltd.',
location: 'New York, NY',
appliedDate: '2025-03-20',
status: 'Applied',
nextStep: 'Waiting for response',
notes: 'Follow up if no response by April 3.'
},
{
id: 4,
jobTitle: 'Backend Developer',
company: 'DataSystems Co.',
location: 'Chicago, IL',
appliedDate: '2025-03-22',
status: 'Rejected',
nextStep: 'N/A',
notes: 'Received rejection email. Requested feedback.'
},
{
id: 5,
jobTitle: 'Frontend Team Lead',
company: 'InnovateTech Solutions',
location: 'Boston, MA',
appliedDate: '2025-03-25',
status: 'Screening',
nextStep: 'Phone screening on April 2, 2025',
notes: 'Research company products before the call.'
}
]);
// Filter applications by status
const statusFilter = ref('All');
const statusOptions = ['All', 'Applied', 'Screening', 'Interview', 'Offer', 'Rejected'];
const filteredApplications = computed(() => {
if (statusFilter.value === 'All') {
return applications.value;
}
return applications.value.filter(app => app.status === statusFilter.value);
});
// Application details modal
const selectedApplication = ref(null);
const showModal = ref(false);
const openApplicationDetails = (application) => {
selectedApplication.value = application;
showModal.value = true;
};
const closeModal = () => {
showModal.value = false;
};
// Add note to application
const newNote = ref('');
const addNote = (application) => {
if (newNote.value.trim()) {
application.notes += '\n' + newNote.value.trim();
newNote.value = '';
}
};
</script>
<template>
<div class="applications-container">
<!-- Filters -->
<div class="filters">
<div class="status-filter">
<label for="status-select">Filter by Status:</label>
<select id="status-select" v-model="statusFilter">
<option v-for="option in statusOptions" :key="option" :value="option">
{{ option }}
</option>
</select>
</div>
<div class="search-applications">
<input type="text" placeholder="Search applications..." />
</div>
</div>
<!-- Applications Table -->
<div class="applications-table-container">
<table class="applications-table">
<thead>
<tr>
<th>Job Title</th>
<th>Company</th>
<th>Applied Date</th>
<th>Status</th>
<th>Next Step</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="application in filteredApplications" :key="application.id" :class="{ 'rejected': application.status === 'Rejected' }">
<td class="job-title">{{ application.jobTitle }}</td>
<td>{{ application.company }}</td>
<td>{{ application.appliedDate }}</td>
<td>
<span class="status-badge" :class="application.status.toLowerCase()">
{{ application.status }}
</span>
</td>
<td>{{ application.nextStep }}</td>
<td>
<button class="view-btn" @click="openApplicationDetails(application)">View Details</button>
</td>
</tr>
<tr v-if="filteredApplications.length === 0">
<td colspan="6" class="no-applications">No applications found matching the selected filter.</td>
</tr>
</tbody>
</table>
</div>
<!-- Application Details Modal -->
<div class="modal-overlay" v-if="showModal" @click="closeModal">
<div class="modal-content" @click.stop>
<div class="modal-header">
<h3>{{ selectedApplication?.jobTitle }}</h3>
<button class="close-modal" @click="closeModal">&times;</button>
</div>
<div class="modal-body" v-if="selectedApplication">
<div class="application-details">
<div class="detail-row">
<span class="detail-label">Company:</span>
<span class="detail-value">{{ selectedApplication.company }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Location:</span>
<span class="detail-value">{{ selectedApplication.location }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Applied Date:</span>
<span class="detail-value">{{ selectedApplication.appliedDate }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status:</span>
<span class="detail-value status-badge" :class="selectedApplication.status.toLowerCase()">
{{ selectedApplication.status }}
</span>
</div>
<div class="detail-row">
<span class="detail-label">Next Step:</span>
<span class="detail-value">{{ selectedApplication.nextStep }}</span>
</div>
</div>
<div class="notes-section">
<h4>Notes</h4>
<div class="notes-content">
<p>{{ selectedApplication.notes }}</p>
</div>
<div class="add-note">
<textarea v-model="newNote" placeholder="Add a new note..."></textarea>
<button @click="addNote(selectedApplication)" :disabled="!newNote.trim()">Add Note</button>
</div>
</div>
<div class="modal-actions">
<button class="update-status">Update Status</button>
<button class="withdraw-application" v-if="selectedApplication.status !== 'Rejected'">
Withdraw Application
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.applications-container {
width: 100%;
.filters {
display: flex;
justify-content: space-between;
margin-bottom: 1.5rem;
@media (max-width: 768px) {
flex-direction: column;
gap: 1rem;
}
.status-filter {
display: flex;
align-items: center;
gap: 0.5rem;
label {
font-weight: 500;
}
select {
padding: 0.5rem;
border-radius: 4px;
border: 1px solid var(--card-border);
background-color: var(--card-bg);
color: var(--text-color);
}
}
.search-applications {
input {
padding: 0.5rem;
border-radius: 4px;
border: 1px solid var(--card-border);
background-color: var(--card-bg);
color: var(--text-color);
width: 250px;
&::placeholder {
color: var(--text-color);
opacity: 0.6;
}
}
}
}
.applications-table-container {
overflow-x: auto;
background-color: var(--card-bg);
border-radius: 8px;
border: 1px solid var(--card-border);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
.applications-table {
width: 100%;
border-collapse: collapse;
th, td {
padding: 1rem;
text-align: left;
border-bottom: 1px solid var(--card-border);
}
th {
font-weight: 600;
background-color: rgba(0, 0, 0, 0.03);
}
tr:last-child td {
border-bottom: none;
}
tr.rejected {
opacity: 0.7;
}
.job-title {
font-weight: 500;
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 600;
&.applied {
background-color: rgba(52, 152, 219, 0.1);
color: #3498db;
}
&.screening {
background-color: rgba(155, 89, 182, 0.1);
color: #9b59b6;
}
&.interview {
background-color: rgba(241, 196, 15, 0.1);
color: #f1c40f;
}
&.offer {
background-color: rgba(46, 204, 113, 0.1);
color: #2ecc71;
}
&.rejected {
background-color: rgba(231, 76, 60, 0.1);
color: #e74c3c;
}
}
.view-btn {
padding: 0.4rem 0.75rem;
background-color: transparent;
border: 1px solid var(--primary-color);
color: var(--primary-color);
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: var(--primary-color);
color: white;
}
}
.no-applications {
text-align: center;
padding: 2rem;
color: var(--text-color);
opacity: 0.7;
}
}
// Modal styles
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background-color: var(--card-bg);
border-radius: 8px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--card-border);
h3 {
margin: 0;
font-size: 1.4rem;
}
.close-modal {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
color: var(--text-color);
opacity: 0.7;
&:hover {
opacity: 1;
}
}
}
.modal-body {
padding: 1.5rem;
.application-details {
margin-bottom: 2rem;
.detail-row {
display: flex;
margin-bottom: 0.75rem;
.detail-label {
width: 120px;
font-weight: 500;
color: var(--text-color);
opacity: 0.8;
}
.detail-value {
flex: 1;
}
}
}
.notes-section {
margin-bottom: 2rem;
h4 {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.1rem;
}
.notes-content {
background-color: rgba(0, 0, 0, 0.03);
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
white-space: pre-line;
p {
margin: 0;
}
}
.add-note {
display: flex;
flex-direction: column;
gap: 0.5rem;
textarea {
padding: 0.75rem;
border-radius: 4px;
border: 1px solid var(--card-border);
background-color: var(--card-bg);
color: var(--text-color);
min-height: 80px;
resize: vertical;
}
button {
align-self: flex-end;
padding: 0.5rem 1rem;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
&:disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
}
}
.modal-actions {
display: flex;
gap: 1rem;
button {
padding: 0.75rem 1rem;
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
.update-status {
background-color: var(--primary-color);
color: white;
border: none;
}
.withdraw-application {
background-color: transparent;
border: 1px solid #e74c3c;
color: #e74c3c;
}
}
}
}
}
</style>
+471
View File
@@ -0,0 +1,471 @@
<script setup>
import { ref } from 'vue';
// Sample dashboard data
const stats = ref({
applications: 12,
interviews: 4,
offers: 1,
savedJobs: 8
});
const recentActivity = ref([
{
id: 1,
type: 'application',
company: 'Tech Innovations Inc.',
position: 'Frontend Developer',
date: '2025-03-28',
status: 'Applied'
},
{
id: 2,
type: 'interview',
company: 'DataSystems Co.',
position: 'Backend Engineer',
date: '2025-03-25',
status: 'Interview Scheduled'
},
{
id: 3,
type: 'saved',
company: 'WebSolutions Ltd.',
position: 'Full Stack Developer',
date: '2025-03-23',
status: 'Saved'
},
{
id: 4,
type: 'offer',
company: 'Digital Creations',
position: 'UI/UX Designer',
date: '2025-03-20',
status: 'Offer Received'
}
]);
const upcomingEvents = ref([
{
id: 1,
title: 'Technical Interview',
company: 'DataSystems Co.',
date: '2025-04-02',
time: '10:00 AM'
},
{
id: 2,
title: 'Coding Challenge Due',
company: 'Tech Innovations Inc.',
date: '2025-04-05',
time: '11:59 PM'
},
{
id: 3,
title: 'Follow-up Call',
company: 'Digital Creations',
date: '2025-04-07',
time: '2:30 PM'
}
]);
const recommendedJobs = ref([
{
id: 1,
title: 'Senior Vue Developer',
company: 'InnovateTech Solutions',
location: 'Remote',
salary: '$140,000 - $170,000',
matchScore: 95
},
{
id: 2,
title: 'Frontend Team Lead',
company: 'WebWorks Inc.',
location: 'San Francisco, CA',
salary: '$160,000 - $190,000',
matchScore: 88
},
{
id: 3,
title: 'Full Stack Engineer',
company: 'CloudSystems',
location: 'New York, NY',
salary: '$130,000 - $160,000',
matchScore: 82
}
]);
</script>
<template>
<div class="dashboard">
<div class="dashboard-grid">
<!-- Stats Cards -->
<div class="stats-container">
<div class="stat-card">
<div class="stat-value">{{ stats.applications }}</div>
<div class="stat-label">Applications</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ stats.interviews }}</div>
<div class="stat-label">Interviews</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ stats.offers }}</div>
<div class="stat-label">Offers</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ stats.savedJobs }}</div>
<div class="stat-label">Saved Jobs</div>
</div>
</div>
<!-- Recent Activity -->
<div class="dashboard-card activity-card">
<h3 class="card-title">Recent Activity</h3>
<div class="activity-list">
<div v-for="activity in recentActivity" :key="activity.id" class="activity-item">
<div class="activity-icon" :class="activity.type">
<span v-if="activity.type === 'application'">📝</span>
<span v-else-if="activity.type === 'interview'">🗣</span>
<span v-else-if="activity.type === 'saved'"></span>
<span v-else-if="activity.type === 'offer'">🎉</span>
</div>
<div class="activity-details">
<div class="activity-title">{{ activity.position }}</div>
<div class="activity-company">{{ activity.company }}</div>
<div class="activity-meta">
<span class="activity-date">{{ activity.date }}</span>
<span class="activity-status" :class="activity.type">{{ activity.status }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- Upcoming Events -->
<div class="dashboard-card events-card">
<h3 class="card-title">Upcoming Events</h3>
<div class="events-list">
<div v-for="event in upcomingEvents" :key="event.id" class="event-item">
<div class="event-date">
<div class="event-day">{{ new Date(event.date).getDate() }}</div>
<div class="event-month">{{ new Date(event.date).toLocaleString('default', { month: 'short' }) }}</div>
</div>
<div class="event-details">
<div class="event-title">{{ event.title }}</div>
<div class="event-company">{{ event.company }}</div>
<div class="event-time">{{ event.time }}</div>
</div>
</div>
</div>
</div>
<!-- Recommended Jobs -->
<div class="dashboard-card recommended-card">
<h3 class="card-title">Recommended for You</h3>
<div class="recommended-list">
<div v-for="job in recommendedJobs" :key="job.id" class="recommended-item">
<div class="match-score">{{ job.matchScore }}% Match</div>
<div class="job-details">
<div class="job-title">{{ job.title }}</div>
<div class="job-company">{{ job.company }}</div>
<div class="job-meta">
<span class="job-location">{{ job.location }}</span>
<span class="job-salary">{{ job.salary }}</span>
</div>
</div>
<button class="view-job-btn">View</button>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.dashboard {
width: 100%;
.dashboard-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1.5rem;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
.stats-container {
grid-column: 1 / -1;
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 1rem;
@media (max-width: 768px) {
grid-template-columns: repeat(2, 1fr);
}
}
.stat-card {
background-color: var(--card-bg, #fff);
border: 1px solid var(--card-border, #ddd);
border-radius: 8px;
padding: 1.5rem;
text-align: center;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
.stat-value {
font-size: 2.5rem;
font-weight: 700;
color: var(--primary-color);
margin-bottom: 0.5rem;
}
.stat-label {
font-size: 0.9rem;
color: var(--text-color);
opacity: 0.8;
}
}
.dashboard-card {
background-color: var(--card-bg, #fff);
border: 1px solid var(--card-border, #ddd);
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
.card-title {
margin-top: 0;
margin-bottom: 1rem;
font-size: 1.2rem;
font-weight: 600;
color: var(--text-color);
border-bottom: 1px solid var(--card-border, #ddd);
padding-bottom: 0.75rem;
}
}
.activity-card {
.activity-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.activity-item {
display: flex;
align-items: flex-start;
padding-bottom: 1rem;
border-bottom: 1px solid var(--card-border, #eee);
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
}
.activity-icon {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 1rem;
background-color: rgba(0, 0, 0, 0.05);
&.application {
background-color: rgba(52, 152, 219, 0.1);
}
&.interview {
background-color: rgba(155, 89, 182, 0.1);
}
&.saved {
background-color: rgba(241, 196, 15, 0.1);
}
&.offer {
background-color: rgba(46, 204, 113, 0.1);
}
}
.activity-details {
flex: 1;
}
.activity-title {
font-weight: 600;
margin-bottom: 0.25rem;
}
.activity-company {
font-size: 0.9rem;
margin-bottom: 0.5rem;
opacity: 0.8;
}
.activity-meta {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
}
.activity-status {
font-weight: 600;
&.application {
color: #3498db;
}
&.interview {
color: #9b59b6;
}
&.saved {
color: #f1c40f;
}
&.offer {
color: #2ecc71;
}
}
}
.events-card {
.events-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.event-item {
display: flex;
align-items: center;
padding-bottom: 1rem;
border-bottom: 1px solid var(--card-border, #eee);
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
}
.event-date {
width: 3rem;
height: 3rem;
border-radius: 8px;
background-color: var(--primary-color);
color: white;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
margin-right: 1rem;
.event-day {
font-size: 1.2rem;
font-weight: 700;
line-height: 1;
}
.event-month {
font-size: 0.8rem;
text-transform: uppercase;
}
}
.event-details {
flex: 1;
}
.event-title {
font-weight: 600;
margin-bottom: 0.25rem;
}
.event-company {
font-size: 0.9rem;
margin-bottom: 0.25rem;
opacity: 0.8;
}
.event-time {
font-size: 0.8rem;
opacity: 0.7;
}
}
.recommended-card {
.recommended-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.recommended-item {
display: flex;
align-items: center;
padding-bottom: 1rem;
border-bottom: 1px solid var(--card-border, #eee);
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
}
.match-score {
background-color: rgba(46, 204, 113, 0.1);
color: #2ecc71;
font-size: 0.8rem;
font-weight: 600;
padding: 0.25rem 0.5rem;
border-radius: 4px;
margin-right: 1rem;
white-space: nowrap;
}
.job-details {
flex: 1;
}
.job-title {
font-weight: 600;
margin-bottom: 0.25rem;
}
.job-company {
font-size: 0.9rem;
margin-bottom: 0.25rem;
opacity: 0.8;
}
.job-meta {
display: flex;
gap: 1rem;
font-size: 0.8rem;
opacity: 0.7;
}
.view-job-btn {
background-color: transparent;
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 0.4rem 0.75rem;
border-radius: 4px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: var(--primary-color);
color: white;
}
}
}
}
</style>
+279
View File
@@ -0,0 +1,279 @@
<script setup>
import { ref, computed } from 'vue';
// Props definition using defineProps
const props = defineProps({
title: {
type: String,
required: true
},
company: {
type: String,
required: true
},
location: {
type: String,
default: 'Remote'
},
salary: {
type: String,
default: 'Not specified'
},
description: {
type: String,
required: true
},
applied: {
type: Boolean,
default: false
},
tags: {
type: Array,
default: () => []
}
});
// Reactive state using ref
const isApplied = ref(props.applied);
const applyForJob = () => {
isApplied.value = true;
// Emit an event that parent components can listen to
emit('apply', props.title);
};
// Generate a random pastel color for company logo background
const generateRandomColor = () => {
// Generate pastel colors by using higher base values
const r = Math.floor(Math.random() * 100) + 155;
const g = Math.floor(Math.random() * 100) + 155;
const b = Math.floor(Math.random() * 100) + 155;
return `rgb(${r}, ${g}, ${b})`;
};
// Define emits
const emit = defineEmits(['apply', 'click']);
</script>
<template>
<div class="job-card" @click="emit('click')">
<div class="job-content">
<div class="job-main-info">
<div class="company-logo" :style="{ backgroundColor: generateRandomColor() }">
{{ company.charAt(0) }}
</div>
<div class="job-header">
<h3 class="job-title">{{ title }}</h3>
<div class="job-company">{{ company }}</div>
</div>
</div>
<div class="job-tags" v-if="tags && tags.length > 0">
<span v-for="(tag, index) in tags" :key="index" class="job-tag">
{{ tag }}
</span>
</div>
<div class="job-details-row">
<div class="job-meta">
<div class="meta-item">
<span class="meta-icon">📍</span>
<span class="meta-text">{{ location }}</span>
</div>
<div class="meta-item">
<span class="meta-icon">💰</span>
<span class="meta-text">{{ salary }}</span>
</div>
</div>
<div class="job-actions">
<button
class="action-button apply-button"
:class="{ 'applied': isApplied }"
@click.stop="applyForJob"
:disabled="isApplied"
>
{{ isApplied ? 'Applied' : 'Apply Now' }}
</button>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.job-card {
background-color: var(--card-bg, #fff);
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.08);
border-radius: 12px;
border: 1px solid var(--card-border, #eee);
overflow: hidden;
transition: all 0.3s ease;
width: 100%;
position: relative;
cursor: pointer;
padding: 1.5rem;
&:hover {
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.12);
transform: translateY(-3px);
}
.job-content {
padding: 0;
}
.job-main-info {
display: flex;
align-items: center;
margin-bottom: 1.5rem;
.company-logo {
width: 70px;
height: 70px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8rem;
font-weight: 700;
color: white;
margin-right: 1.25rem;
flex-shrink: 0;
}
}
.job-header {
flex: 1;
.job-title {
margin: 0 0 0.75rem;
color: var(--text-color, #333);
font-size: 1.5rem;
font-weight: 600;
}
.job-company {
font-weight: 500;
color: var(--text-color-secondary, #555);
font-size: 1.1rem;
}
}
.job-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin: 0.75rem 0;
.job-tag {
font-size: 0.8rem;
padding: 0.25rem 0.6rem;
border-radius: 16px;
background-color: var(--tag-bg, rgba(52, 152, 219, 0.1));
color: var(--primary-color, #3498db);
font-weight: 500;
transition: all 0.2s ease;
&:hover {
background-color: var(--tag-bg-hover, rgba(52, 152, 219, 0.2));
}
@media (prefers-color-scheme: dark) {
background-color: rgba(52, 152, 219, 0.2);
&:hover {
background-color: rgba(52, 152, 219, 0.3);
}
}
}
}
.job-details-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1.5rem;
flex-wrap: wrap;
gap: 1.25rem;
@media (max-width: 768px) {
flex-direction: column;
align-items: flex-start;
}
}
.job-meta {
display: flex;
gap: 1.5rem;
.meta-item {
display: flex;
align-items: center;
font-size: 0.95rem;
color: var(--text-color-secondary, #777);
.meta-icon {
margin-right: 0.5rem;
font-size: 1.1rem;
}
}
}
.job-actions {
display: flex;
gap: 1rem;
.action-button {
padding: 0.75rem 1.25rem;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
border: none;
font-size: 0.95rem;
&.toggle-button {
background-color: transparent;
border: 1px solid var(--primary-color, #3498db);
color: var(--primary-color, #3498db);
&:hover {
background-color: rgba(52, 152, 219, 0.1);
}
}
&.apply-button {
background-color: var(--primary-color, #3498db);
color: white;
&:hover:not(:disabled) {
background-color: var(--primary-hover-color, #2980b9);
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
&:disabled {
opacity: 0.7;
cursor: not-allowed;
}
&.applied {
background-color: #27ae60;
}
}
}
}
.job-description-container {
padding-top: 1.25rem;
border-top: 1px solid var(--card-border, #eee);
.job-description {
margin: 0;
line-height: 1.6;
color: var(--text-color, #444);
}
}
}
</style>
File diff suppressed because it is too large Load Diff
+526
View File
@@ -0,0 +1,526 @@
<script setup>
import { ref, computed, watch } from 'vue';
const props = defineProps({
jobs: {
type: Array,
required: true
},
initiallyExpanded: {
type: Boolean,
default: true
}
});
const emit = defineEmits(['filter-change']);
// Filter states
const searchQuery = ref('');
const selectedLocations = ref([]);
const selectedSalaryRanges = ref([]);
const selectedJobTypes = ref([]);
// Collapse state
const isExpanded = ref(props.initiallyExpanded);
// Extract unique locations from jobs
const availableLocations = computed(() => {
const locations = new Set();
props.jobs.forEach(job => {
if (job.location) {
locations.add(job.location);
}
});
return Array.from(locations);
});
// Extract unique job types from jobs
const availableJobTypes = computed(() => {
const types = new Set();
props.jobs.forEach(job => {
if (job.employmentType) {
types.add(job.employmentType);
}
});
return Array.from(types);
});
// Predefined salary ranges
const salaryRanges = [
{ id: 1, label: 'Under $100k', min: 0, max: 100000 },
{ id: 2, label: '$100k - $130k', min: 100000, max: 130000 },
{ id: 3, label: '$130k - $160k', min: 130000, max: 160000 },
{ id: 4, label: '$160k+', min: 160000, max: Infinity }
];
// Filter jobs based on all criteria
const filterJobs = () => {
let filteredJobs = [...props.jobs];
// Apply search query filter
if (searchQuery.value.trim() !== '') {
const query = searchQuery.value.toLowerCase();
filteredJobs = filteredJobs.filter(job =>
job.title.toLowerCase().includes(query) ||
job.company.toLowerCase().includes(query) ||
job.description.toLowerCase().includes(query)
);
}
// Apply location filter
if (selectedLocations.value.length > 0) {
filteredJobs = filteredJobs.filter(job =>
selectedLocations.value.includes(job.location)
);
}
// Apply job type filter
if (selectedJobTypes.value.length > 0) {
filteredJobs = filteredJobs.filter(job =>
selectedJobTypes.value.includes(job.employmentType)
);
}
// Apply salary range filter
if (selectedSalaryRanges.value.length > 0) {
filteredJobs = filteredJobs.filter(job => {
// Extract numeric values from salary string (e.g., "$120,000 - $150,000")
const salaryText = job.salary || '';
const salaryMatches = salaryText.match(/\$(\d+,?\d*)/g);
if (!salaryMatches || salaryMatches.length === 0) return false;
// Convert to numbers
const salaryValues = salaryMatches.map(s =>
parseInt(s.replace(/[$,]/g, ''), 10)
);
// Get min salary (usually the first number)
const minSalary = salaryValues[0];
// Check if it falls within any of the selected ranges
return selectedSalaryRanges.value.some(rangeId => {
const range = salaryRanges.find(r => r.id === rangeId);
return range && minSalary >= range.min && minSalary <= range.max;
});
});
}
emit('filter-change', filteredJobs);
};
// Watch for changes in filter criteria
watch([searchQuery, selectedLocations, selectedSalaryRanges, selectedJobTypes], () => {
filterJobs();
}, { deep: true });
// Clear all filters
const clearFilters = () => {
searchQuery.value = '';
selectedLocations.value = [];
selectedSalaryRanges.value = [];
selectedJobTypes.value = [];
};
// Toggle location selection
const toggleLocation = (location) => {
const index = selectedLocations.value.indexOf(location);
if (index === -1) {
selectedLocations.value.push(location);
} else {
selectedLocations.value.splice(index, 1);
}
};
// Toggle salary range selection
const toggleSalaryRange = (rangeId) => {
const index = selectedSalaryRanges.value.indexOf(rangeId);
if (index === -1) {
selectedSalaryRanges.value.push(rangeId);
} else {
selectedSalaryRanges.value.splice(index, 1);
}
};
// Toggle job type selection
const toggleJobType = (type) => {
const index = selectedJobTypes.value.indexOf(type);
if (index === -1) {
selectedJobTypes.value.push(type);
} else {
selectedJobTypes.value.splice(index, 1);
}
};
// Toggle filter expansion
const toggleFilters = () => {
isExpanded.value = !isExpanded.value;
};
</script>
<template>
<div class="job-filters">
<div class="search-container">
<div class="search-input-wrapper">
<span class="search-icon">🔍</span>
<input
type="text"
v-model="searchQuery"
placeholder="Search jobs by title, company, or keywords..."
class="search-input"
/>
<button
v-if="searchQuery"
@click="searchQuery = ''"
class="clear-search-btn"
>
</button>
</div>
<button @click="toggleFilters" class="toggle-filters-btn">
{{ isExpanded ? 'Hide Filters' : 'Show Filters' }}
<span class="toggle-icon">{{ isExpanded ? '▲' : '▼' }}</span>
</button>
</div>
<div class="filters-container" :class="{ 'collapsed': !isExpanded }">
<div class="filter-section">
<h3 class="filter-title">Location</h3>
<div class="filter-options">
<div
v-for="location in availableLocations"
:key="location"
class="filter-option"
:class="{ 'selected': selectedLocations.includes(location) }"
@click="toggleLocation(location)"
>
<span class="checkbox">
<span v-if="selectedLocations.includes(location)" class="checkbox-inner"></span>
</span>
<span class="option-label">{{ location }}</span>
</div>
</div>
</div>
<div class="filter-section">
<h3 class="filter-title">Salary Range</h3>
<div class="filter-options">
<div
v-for="range in salaryRanges"
:key="range.id"
class="filter-option"
:class="{ 'selected': selectedSalaryRanges.includes(range.id) }"
@click="toggleSalaryRange(range.id)"
>
<span class="checkbox">
<span v-if="selectedSalaryRanges.includes(range.id)" class="checkbox-inner"></span>
</span>
<span class="option-label">{{ range.label }}</span>
</div>
</div>
</div>
<div class="filter-section" v-if="availableJobTypes.length > 0">
<h3 class="filter-title">Job Type</h3>
<div class="filter-options">
<div
v-for="type in availableJobTypes"
:key="type"
class="filter-option"
:class="{ 'selected': selectedJobTypes.includes(type) }"
@click="toggleJobType(type)"
>
<span class="checkbox">
<span v-if="selectedJobTypes.includes(type)" class="checkbox-inner"></span>
</span>
<span class="option-label">{{ type }}</span>
</div>
</div>
</div>
<button
@click="clearFilters"
class="clear-filters-btn"
:disabled="!searchQuery && selectedLocations.length === 0 && selectedSalaryRanges.length === 0 && selectedJobTypes.length === 0"
>
Clear All Filters
</button>
</div>
</div>
</template>
<style lang="scss" scoped>
.job-filters {
background-color: var(--card-bg, #fff);
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
margin-bottom: 2rem;
overflow: hidden;
@media (prefers-color-scheme: dark) {
background-color: var(--card-bg, #222);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.search-container {
padding: 1.5rem;
border-bottom: 1px solid var(--card-border, #eee);
display: flex;
flex-direction: column;
gap: 1rem;
@media (min-width: 768px) {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.search-input-wrapper {
position: relative;
display: flex;
align-items: center;
flex: 1;
.search-icon {
position: absolute;
left: 1rem;
font-size: 1.1rem;
color: var(--text-color-tertiary, #999);
}
.search-input {
width: 100%;
padding: 0.9rem 1rem 0.9rem 2.5rem;
border: 1px solid var(--card-border, #ddd);
border-radius: 8px;
font-size: 1rem;
color: var(--text-color, #333);
background-color: var(--input-bg, #fff);
transition: all 0.2s;
&:focus {
outline: none;
border-color: var(--primary-color, #3498db);
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.2);
}
&::placeholder {
color: var(--text-color-tertiary, #999);
}
@media (prefers-color-scheme: dark) {
background-color: var(--bg-color, #2a2a2a);
color: var(--text-color, #eee);
border-color: var(--card-border, #444);
&::placeholder {
color: var(--text-color-tertiary, #777);
}
}
}
.clear-search-btn {
position: absolute;
right: 1rem;
background: none;
border: none;
color: var(--text-color-tertiary, #999);
cursor: pointer;
font-size: 0.9rem;
padding: 0.25rem;
border-radius: 50%;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
color: var(--text-color, #333);
}
}
}
.toggle-filters-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.6rem 1rem;
background-color: var(--bg-color, #f8f9fa);
border: 1px solid var(--card-border, #ddd);
border-radius: 8px;
font-size: 0.9rem;
font-weight: 500;
color: var(--text-color, #333);
cursor: pointer;
transition: all 0.2s;
white-space: nowrap;
&:hover {
background-color: var(--hover-bg, rgba(0, 0, 0, 0.05));
}
.toggle-icon {
font-size: 0.7rem;
}
@media (prefers-color-scheme: dark) {
background-color: var(--bg-color, #2a2a2a);
color: var(--text-color, #eee);
border-color: var(--card-border, #444);
&:hover {
background-color: var(--hover-bg, rgba(255, 255, 255, 0.05));
}
}
}
}
.filters-container {
padding: 1.5rem;
max-height: 1000px;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease, opacity 0.3s ease;
opacity: 1;
&.collapsed {
max-height: 0;
padding-top: 0;
padding-bottom: 0;
opacity: 0;
}
@media (prefers-color-scheme: dark) {
border-top-color: var(--card-border, #444);
}
.filter-section {
margin-bottom: 1.5rem;
&:last-child {
margin-bottom: 1rem;
}
.filter-title {
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 1rem;
color: var(--text-color, #333);
@media (prefers-color-scheme: dark) {
color: var(--text-color, #eee);
}
}
.filter-options {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
.filter-option {
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
border-radius: 6px;
cursor: pointer;
transition: all 0.2s;
&:hover {
background-color: var(--hover-bg, rgba(0, 0, 0, 0.05));
}
&.selected {
background-color: var(--selected-bg, rgba(52, 152, 219, 0.1));
}
@media (prefers-color-scheme: dark) {
&:hover {
background-color: var(--hover-bg, rgba(255, 255, 255, 0.05));
}
&.selected {
background-color: var(--selected-bg, rgba(52, 152, 219, 0.2));
}
}
.checkbox {
width: 18px;
height: 18px;
border: 2px solid var(--border-color, #ccc);
border-radius: 4px;
margin-right: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
.checkbox-inner {
width: 10px;
height: 10px;
background-color: var(--primary-color, #3498db);
border-radius: 2px;
}
}
&.selected .checkbox {
border-color: var(--primary-color, #3498db);
}
.option-label {
font-size: 0.95rem;
color: var(--text-color, #333);
@media (prefers-color-scheme: dark) {
color: var(--text-color, #eee);
}
}
}
}
}
.clear-filters-btn {
width: 100%;
padding: 0.75rem;
background-color: transparent;
border: 1px solid var(--card-border, #ddd);
color: var(--text-color, #333);
border-radius: 8px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
margin-top: 0.5rem;
&:hover:not(:disabled) {
background-color: var(--hover-bg, rgba(0, 0, 0, 0.05));
border-color: var(--text-color, #333);
}
&:disabled {
opacity: 0.5;
cursor: not-allowed;
}
@media (prefers-color-scheme: dark) {
color: var(--text-color, #eee);
border-color: var(--card-border, #444);
&:hover:not(:disabled) {
background-color: var(--hover-bg, rgba(255, 255, 255, 0.05));
border-color: var(--text-color, #eee);
}
}
}
}
}
@media (max-width: 768px) {
.job-filters {
.filters-container {
.filter-options {
flex-direction: column;
gap: 0.5rem;
.filter-option {
width: 100%;
}
}
}
}
}
</style>
+400
View File
@@ -0,0 +1,400 @@
<script setup>
import { ref, computed } from 'vue';
// Sample saved jobs data
const savedJobs = ref([
{
id: 1,
title: 'Senior Frontend Developer',
company: 'Tech Innovations Inc.',
location: 'San Francisco, CA (Remote)',
salary: '$140,000 - $170,000',
description: 'We are looking for an experienced Frontend Developer proficient in Vue.js to join our growing team. You will be responsible for building user interfaces and implementing new features.',
dateAdded: '2025-03-20',
tags: ['Vue.js', 'JavaScript', 'CSS', 'Senior']
},
{
id: 2,
title: 'Full Stack Engineer',
company: 'WebSolutions Ltd.',
location: 'New York, NY',
salary: '$130,000 - $160,000',
description: 'Looking for a versatile developer who can work across the entire stack. Experience with Vue.js and Node.js is a plus.',
dateAdded: '2025-03-22',
tags: ['Full Stack', 'Node.js', 'Vue.js', 'MongoDB']
},
{
id: 3,
title: 'UI/UX Developer',
company: 'Digital Creations',
location: 'Remote',
salary: '$120,000 - $145,000',
description: 'Join our design team to create beautiful and functional user interfaces. Strong design skills and frontend development experience required.',
dateAdded: '2025-03-23',
tags: ['UI/UX', 'Design', 'Frontend', 'Figma']
},
{
id: 4,
title: 'Frontend Team Lead',
company: 'InnovateTech Solutions',
location: 'Boston, MA (Hybrid)',
salary: '$150,000 - $180,000',
description: 'Lead a team of frontend developers in building modern web applications. Experience with team management and modern frontend frameworks required.',
dateAdded: '2025-03-25',
tags: ['Team Lead', 'Management', 'Vue.js', 'React']
},
{
id: 5,
title: 'JavaScript Developer',
company: 'CodeCraft Inc.',
location: 'Chicago, IL',
salary: '$110,000 - $140,000',
description: 'Develop and maintain JavaScript applications. Strong knowledge of modern JavaScript frameworks and libraries is essential.',
dateAdded: '2025-03-27',
tags: ['JavaScript', 'ES6', 'Frontend', 'Backend']
}
]);
// Search and filter functionality
const searchQuery = ref('');
const selectedTags = ref([]);
// Get all unique tags
const allTags = computed(() => {
const tags = new Set();
savedJobs.value.forEach(job => {
job.tags.forEach(tag => tags.add(tag));
});
return Array.from(tags).sort();
});
// Filter jobs based on search query and selected tags
const filteredJobs = computed(() => {
return savedJobs.value.filter(job => {
// Search query filter
const matchesSearch = searchQuery.value === '' ||
job.title.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
job.company.toLowerCase().includes(searchQuery.value.toLowerCase()) ||
job.description.toLowerCase().includes(searchQuery.value.toLowerCase());
// Tags filter
const matchesTags = selectedTags.value.length === 0 ||
selectedTags.value.every(tag => job.tags.includes(tag));
return matchesSearch && matchesTags;
});
});
// Toggle tag selection
const toggleTag = (tag) => {
if (selectedTags.value.includes(tag)) {
selectedTags.value = selectedTags.value.filter(t => t !== tag);
} else {
selectedTags.value.push(tag);
}
};
// Remove job from saved
const removeJob = (jobId) => {
savedJobs.value = savedJobs.value.filter(job => job.id !== jobId);
};
// Apply for job
const applyForJob = (jobId) => {
console.log(`Applied for job ID: ${jobId}`);
// Here you would typically redirect to application form or mark as applied
};
</script>
<template>
<div class="saved-jobs-container">
<div class="saved-header">
<h2>Saved Jobs ({{ savedJobs.length }})</h2>
<div class="search-bar">
<input
type="text"
v-model="searchQuery"
placeholder="Search saved jobs..."
class="search-input"
/>
</div>
</div>
<div class="tags-filter">
<div class="tags-label">Filter by tags:</div>
<div class="tags-list">
<button
v-for="tag in allTags"
:key="tag"
@click="toggleTag(tag)"
class="tag-button"
:class="{ 'active': selectedTags.includes(tag) }"
>
{{ tag }}
</button>
</div>
</div>
<div class="saved-jobs-list" v-if="filteredJobs.length > 0">
<div
v-for="job in filteredJobs"
:key="job.id"
class="saved-job-card"
>
<div class="job-header">
<h3 class="job-title">{{ job.title }}</h3>
<div class="job-actions">
<button class="remove-btn" @click="removeJob(job.id)">
<span class="icon"></span>
</button>
</div>
</div>
<div class="job-company">{{ job.company }}</div>
<div class="job-details">
<div class="job-location">📍 {{ job.location }}</div>
<div class="job-salary">💰 {{ job.salary }}</div>
</div>
<div class="job-description">{{ job.description }}</div>
<div class="job-tags">
<span
v-for="tag in job.tags"
:key="`${job.id}-${tag}`"
class="job-tag"
>
{{ tag }}
</span>
</div>
<div class="job-footer">
<div class="date-saved">Saved on {{ job.dateAdded }}</div>
<button class="apply-btn" @click="applyForJob(job.id)">Apply Now</button>
</div>
</div>
</div>
<div class="no-jobs-message" v-else>
<div class="message-icon">🔍</div>
<h3>No saved jobs found</h3>
<p>Try adjusting your search or filters to see more results.</p>
</div>
</div>
</template>
<style lang="scss" scoped>
.saved-jobs-container {
width: 100%;
.saved-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
@media (max-width: 768px) {
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
h2 {
margin: 0;
}
.search-bar {
.search-input {
padding: 0.75rem 1rem;
border-radius: 6px;
border: 1px solid var(--card-border);
background-color: var(--card-bg);
color: var(--text-color);
width: 300px;
font-size: 0.9rem;
&::placeholder {
color: var(--text-color);
opacity: 0.6;
}
@media (max-width: 768px) {
width: 100%;
}
}
}
}
.tags-filter {
margin-bottom: 1.5rem;
.tags-label {
font-weight: 500;
margin-bottom: 0.5rem;
}
.tags-list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
.tag-button {
padding: 0.4rem 0.75rem;
border-radius: 20px;
background-color: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--text-color);
font-size: 0.8rem;
cursor: pointer;
transition: all 0.2s;
&:hover {
border-color: var(--primary-color);
}
&.active {
background-color: var(--primary-color);
color: white;
border-color: var(--primary-color);
}
}
}
}
.saved-jobs-list {
display: grid;
grid-template-columns: 1fr;
gap: 1.5rem;
}
.saved-job-card {
background-color: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
transition: transform 0.2s, box-shadow 0.2s;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.job-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
.job-title {
margin: 0;
font-size: 1.2rem;
color: var(--primary-color);
}
.job-actions {
.remove-btn {
background: none;
border: none;
cursor: pointer;
padding: 0.25rem;
opacity: 0.6;
transition: opacity 0.2s;
&:hover {
opacity: 1;
}
.icon {
font-size: 0.9rem;
}
}
}
}
.job-company {
font-weight: 500;
margin-bottom: 0.75rem;
}
.job-details {
display: flex;
gap: 1.5rem;
margin-bottom: 1rem;
font-size: 0.9rem;
color: var(--text-color);
opacity: 0.8;
}
.job-description {
margin-bottom: 1.25rem;
font-size: 0.95rem;
line-height: 1.5;
}
.job-tags {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
margin-bottom: 1.25rem;
.job-tag {
background-color: rgba(52, 152, 219, 0.1);
color: var(--primary-color);
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
}
}
.job-footer {
display: flex;
justify-content: space-between;
align-items: center;
.date-saved {
font-size: 0.85rem;
opacity: 0.7;
}
.apply-btn {
background-color: var(--primary-color);
color: white;
border: none;
padding: 0.6rem 1.25rem;
border-radius: 4px;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: var(--primary-hover-color);
}
}
}
}
.no-jobs-message {
text-align: center;
padding: 3rem 1rem;
background-color: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 8px;
.message-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
}
h3 {
margin-top: 0;
margin-bottom: 0.5rem;
}
p {
margin: 0;
opacity: 0.7;
}
}
}
</style>
+216
View File
@@ -0,0 +1,216 @@
<script setup>
import { ref, inject } from 'vue';
// Navigation state
const isCollapsed = ref(false);
// Get theme state from parent
const isDarkMode = inject('isDarkMode');
const parentToggleTheme = inject('toggleTheme');
// Get current active section from parent
const activeSection = inject('activeSection');
const toggleNav = () => {
isCollapsed.value = !isCollapsed.value;
};
const toggleTheme = () => {
// Call the parent's toggle function with the new value
parentToggleTheme(!isDarkMode.value);
};
defineEmits(['navigate']);
</script>
<template>
<aside class="side-nav" :class="{ 'collapsed': isCollapsed }">
<div class="nav-header">
<h2 class="nav-title">Jobs</h2>
<button class="toggle-btn" @click="toggleNav">
{{ isCollapsed ? '' : '' }}
</button>
</div>
<div class="theme-toggle" :class="{ 'collapsed-mode': isCollapsed }">
<button @click="toggleTheme" class="theme-btn">
<span class="icon">{{ isDarkMode ? '☀️' : '🌙' }}</span>
<span class="text" v-if="!isCollapsed">{{ isDarkMode ? 'Light Mode' : 'Dark Mode' }}</span>
</button>
</div>
<nav class="nav-menu">
<ul>
<li>
<a href="#" @click.prevent="$emit('navigate', 'dashboard')" :class="{ 'active': activeSection === 'dashboard' }">
<span class="icon">📊</span>
<span class="text" v-if="!isCollapsed">Dashboard</span>
</a>
</li>
<li>
<a href="#" @click.prevent="$emit('navigate', 'jobs')" :class="{ 'active': activeSection === 'jobs' }">
<span class="icon">💼</span>
<span class="text" v-if="!isCollapsed">Jobs</span>
</a>
</li>
<li>
<a href="#" @click.prevent="$emit('navigate', 'applications')" :class="{ 'active': activeSection === 'applications' }">
<span class="icon">📝</span>
<span class="text" v-if="!isCollapsed">Applications</span>
</a>
</li>
<li>
<a href="#" @click.prevent="$emit('navigate', 'saved')" :class="{ 'active': activeSection === 'saved' }">
<span class="icon"></span>
<span class="text" v-if="!isCollapsed">Saved</span>
</a>
</li>
<li>
<a href="#" @click.prevent="$emit('navigate', 'profile')" :class="{ 'active': activeSection === 'profile' }">
<span class="icon">👤</span>
<span class="text" v-if="!isCollapsed">Profile</span>
</a>
</li>
<li>
<a href="#" @click.prevent="$emit('navigate', 'settings')" :class="{ 'active': activeSection === 'settings' }">
<span class="icon"></span>
<span class="text" v-if="!isCollapsed">Settings</span>
</a>
</li>
</ul>
</nav>
</aside>
</template>
<style lang="scss" scoped>
.side-nav {
width: 240px;
height: 100vh;
background-color: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
transition: width 0.3s ease;
position: fixed;
top: 0;
left: 0;
z-index: 10;
display: flex;
flex-direction: column;
&.collapsed {
width: 60px;
.nav-title {
display: none;
}
}
.nav-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
border-bottom: 1px solid #eee;
.nav-title {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
}
.toggle-btn {
background: none;
border: none;
cursor: pointer;
font-size: 1.2rem;
padding: 0.25rem 0.5rem;
border-radius: 4px;
color: var(--text-color);
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
}
}
.theme-toggle {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--sidebar-border);
&.collapsed-mode {
.text {
display: none;
}
}
.theme-btn {
width: 100%;
display: flex;
align-items: center;
padding: 0.5rem;
background: none;
border: 1px solid var(--sidebar-border);
border-radius: 4px;
color: var(--text-color);
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.icon {
margin-right: 0.75rem;
font-size: 1.2rem;
width: 1.5rem;
text-align: center;
}
}
}
.nav-menu {
padding: 1rem 0;
flex: 1;
overflow-y: auto;
ul {
list-style: none;
padding: 0;
margin: 0;
}
li {
margin-bottom: 0.25rem;
}
a {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
color: var(--text-color);
text-decoration: none;
transition: background-color 0.2s;
border-radius: 0 4px 4px 0;
&:hover {
background-color: rgba(0, 0, 0, 0.05);
color: var(--primary-color);
}
&.active {
background-color: rgba(52, 152, 219, 0.1);
color: var(--primary-color);
border-left: 3px solid var(--primary-color);
}
.icon {
margin-right: 0.75rem;
font-size: 1.2rem;
width: 1.5rem;
text-align: center;
}
}
}
}
</style>
+5
View File
@@ -0,0 +1,5 @@
import { createApp } from 'vue'
import './assets/scss/main.scss'
import App from './App.vue'
createApp(App).mount('#app')