+
+ {/* Left content */}
+
+ {/* Job title and company */}
+
+
+
+ {application.job_title}
+
+
+ {application.company}
+
+
+
+ {/* Status badge */}
+
+ {statusConfig.icon}
+ {statusConfig.label}
+
+
+
+ {/* Job characteristics */}
+
+ {characteristics.map((char, index) => (
+
+ {char.icon}
+ {char.label}
+
+ ))}
+
+
+ {/* Application date */}
+
+
+ Applied {formatDate(application.application_date)}
+
+
+ {/* Stage notes preview */}
+ {application.stage_notes && (
+
+ Notes: {application.stage_notes}
+
+ )}
+
- {/* Right buttons */}
-
-
-
+ {/* Right buttons */}
+
+
+
+
);
diff --git a/frontend/applyday/src/pages/Application.jsx b/frontend/applyday/src/pages/Application.jsx
index 5be937a..26fb85f 100644
--- a/frontend/applyday/src/pages/Application.jsx
+++ b/frontend/applyday/src/pages/Application.jsx
@@ -1,6 +1,7 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useMemo } from "react";
import ApplicationForm from "../components/ApplicationForm";
import ApplicationItem from "../components/ApplicationItem";
+import ApplicationControls from "../components/ApplicationControls";
import ResumeManager from "../components/ResumeManager";
import ReportGenerator from "../components/ReportGenerator";
import { AnimatePresence, motion } from "framer-motion";
@@ -13,6 +14,7 @@ import {
deleteApplication,
getApplication,
} from "../service/application";
+import { processApplications } from "../utils/applicationUtils";
const ApplicationManager = () => {
const [applications, setApplications] = useState([]);
@@ -31,13 +33,63 @@ const ApplicationManager = () => {
const [selectedResumeId, setSelectedResumeId] = useState(null);
const [activeTab, setActiveTab] = useState("dashboard"); // New: Tab state
+ // Search, sort, and filter state
+ const [searchTerm, setSearchTerm] = useState('');
+ const [sortBy, setSortBy] = useState('date');
+ const [sortOrder, setSortOrder] = useState('desc');
+ const [statusFilters, setStatusFilters] = useState([]);
+ const [locationFilters, setLocationFilters] = useState([]);
+ const [roleFilters, setRoleFilters] = useState([]);
+
const navigate = useNavigate();
+
+ // Process applications with search, sort, and filter
+ const processedApplications = useMemo(() => {
+ return processApplications(applications, {
+ searchTerm,
+ statuses: statusFilters,
+ locationTypes: locationFilters,
+ roleTypes: roleFilters,
+ sortBy,
+ sortOrder
+ });
+ }, [applications, searchTerm, statusFilters, locationFilters, roleFilters, sortBy, sortOrder]);
+
// Select/Deselect application for report
const handleSelect = (id) => {
setSelectedIds((prev) =>
prev.includes(id) ? prev.filter((sid) => sid !== id) : [...prev, id]
);
};
+
+ // Search, sort, and filter handlers
+ const handleSearchChange = (term) => {
+ setSearchTerm(term);
+ };
+
+ const handleSortChange = (newSortBy, newSortOrder) => {
+ setSortBy(newSortBy);
+ setSortOrder(newSortOrder);
+ };
+
+ const handleStatusFiltersChange = (filters) => {
+ setStatusFilters(filters);
+ };
+
+ const handleLocationFiltersChange = (filters) => {
+ setLocationFilters(filters);
+ };
+
+ const handleRoleFiltersChange = (filters) => {
+ setRoleFilters(filters);
+ };
+
+ const handleClearFilters = () => {
+ setSearchTerm('');
+ setStatusFilters([]);
+ setLocationFilters([]);
+ setRoleFilters([]);
+ };
const loadApplications = async () => {
setLoading(true);
@@ -55,25 +107,50 @@ const ApplicationManager = () => {
loadApplications();
}, []);
- // Adding ESC keyboard event listener for modal close (Esc key)
+ // Adding ESC keyboard event listener for modal close (Esc key) and keyboard shortcuts
useEffect(() => {
- const handleEscape = (e) => {
+ const handleKeyDown = (e) => {
+ // ESC to close modals
if (e.key === 'Escape' && (showCreateForm || showEditForm)) {
handleCancel();
+ return;
+ }
+
+ // Keyboard shortcuts (only when not in input/textarea/modal)
+ if (!showCreateForm && !showEditForm && !['INPUT', 'TEXTAREA', 'SELECT'].includes(e.target.tagName)) {
+ // Ctrl/Cmd + K to focus search
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
+ e.preventDefault();
+ const searchInput = document.querySelector('input[type="text"]');
+ if (searchInput) searchInput.focus();
+ }
+
+ // Ctrl/Cmd + N to create new application
+ if ((e.ctrlKey || e.metaKey) && e.key === 'n') {
+ e.preventDefault();
+ setShowCreateForm(true);
+ }
+
+ // Ctrl/Cmd + Shift + C to clear filters
+ if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'C') {
+ e.preventDefault();
+ handleClearFilters();
+ }
}
};
+ document.addEventListener('keydown', handleKeyDown);
+
+ // Prevent background scrolling when modal is open
if (showCreateForm || showEditForm) {
- document.addEventListener('keydown', handleEscape);
- // Prevent background scrolling when modal is open
document.body.style.overflow = 'hidden';
}
return () => {
- document.removeEventListener('keydown', handleEscape);
+ document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = 'unset';
};
- }, [showCreateForm, showEditForm]);
+ }, [showCreateForm, showEditForm, handleClearFilters]);
const handleInputChange = (e) => {
const { name, value } = e.target;
@@ -373,6 +450,23 @@ const ApplicationManager = () => {
{/* Applications Section */}
+ {/* Search, Sort, and Filter Controls */}
+
+
{/* Applications Header */}
@@ -381,13 +475,18 @@ const ApplicationManager = () => {
Applications
- {applications.length} total
+ {processedApplications.length} of {applications.length}
{selectedIds.length > 0 && (
{selectedIds.length} selected
)}
+ {(searchTerm || statusFilters.length > 0 || locationFilters.length > 0 || roleFilters.length > 0) && (
+
+ filtered
+
+ )}
{selectedIds.length > 0 && (
@@ -409,7 +508,7 @@ const ApplicationManager = () => {
Loading applications...
- ) : applications.length === 0 ? (
+ ) : processedApplications.length === 0 && applications.length === 0 ? (
📝
@@ -428,9 +527,25 @@ const ApplicationManager = () => {
Add First Application
+ ) : processedApplications.length === 0 ? (
+
+
🔍
+
+ No applications found
+
+
+ Try adjusting your search term or filters
+
+
+
) : (
-
- {applications.map((application) => (
+
+ {processedApplications.map((application) => (
{
+ if (!searchTerm || !searchTerm.trim()) {
+ return applications;
+ }
+
+ const term = searchTerm.toLowerCase().trim();
+
+ return applications.filter(app => {
+ const company = app.company?.toLowerCase() || '';
+ const jobTitle = app.job_title?.toLowerCase() || '';
+ const status = app.status?.toLowerCase() || '';
+ const stageNotes = app.stage_notes?.toLowerCase() || '';
+
+ // Check if search term matches any of the searchable fields
+ return (
+ company.includes(term) ||
+ jobTitle.includes(term) ||
+ status.includes(term) ||
+ stageNotes.includes(term)
+ );
+ });
+};
+
+/**
+ * Filters applications based on status
+ * @param {Array} applications - Array of application objects
+ * @param {Array} selectedStatuses - Array of selected status filters
+ * @returns {Array} Filtered applications
+ */
+export const filterByStatus = (applications, selectedStatuses) => {
+ if (!selectedStatuses || selectedStatuses.length === 0) {
+ return applications;
+ }
+
+ return applications.filter(app => selectedStatuses.includes(app.status));
+};
+
+/**
+ * Filters applications based on location type (remote/onsite/hybrid)
+ * @param {Array} applications - Array of application objects
+ * @param {Array} selectedLocationTypes - Array of selected location type filters
+ * @returns {Array} Filtered applications
+ */
+export const filterByLocationType = (applications, selectedLocationTypes) => {
+ if (!selectedLocationTypes || selectedLocationTypes.length === 0) {
+ return applications;
+ }
+
+ return applications.filter(app => {
+ // This would need to be enhanced when job description data includes remote_work field
+ // For now, we'll assume remote if location contains "remote" keyword
+ const hasRemote = app.job_title?.toLowerCase().includes('remote') ||
+ app.stage_notes?.toLowerCase().includes('remote') ||
+ app.company?.toLowerCase().includes('remote');
+
+ if (selectedLocationTypes.includes('remote') && hasRemote) return true;
+ if (selectedLocationTypes.includes('onsite') && !hasRemote) return true;
+ if (selectedLocationTypes.includes('hybrid')) return true; // Default assumption
+
+ return false;
+ });
+};
+
+/**
+ * Filters applications based on role type (full-time/part-time/internship)
+ * @param {Array} applications - Array of application objects
+ * @param {Array} selectedRoleTypes - Array of selected role type filters
+ * @returns {Array} Filtered applications
+ */
+export const filterByRoleType = (applications, selectedRoleTypes) => {
+ if (!selectedRoleTypes || selectedRoleTypes.length === 0) {
+ return applications;
+ }
+
+ return applications.filter(app => {
+ const title = app.job_title?.toLowerCase() || '';
+ const notes = app.stage_notes?.toLowerCase() || '';
+
+ // Check for keywords in job title or notes
+ const hasInternship = title.includes('intern') || notes.includes('intern');
+ const hasPartTime = title.includes('part-time') || title.includes('part time') ||
+ notes.includes('part-time') || notes.includes('part time');
+ const hasFullTime = title.includes('full-time') || title.includes('full time') ||
+ notes.includes('full-time') || notes.includes('full time') ||
+ (!hasInternship && !hasPartTime); // Default to full-time
+
+ if (selectedRoleTypes.includes('internship') && hasInternship) return true;
+ if (selectedRoleTypes.includes('part-time') && hasPartTime) return true;
+ if (selectedRoleTypes.includes('full-time') && hasFullTime) return true;
+
+ return false;
+ });
+};
+
+/**
+ * Sorts applications based on specified criteria
+ * @param {Array} applications - Array of application objects
+ * @param {string} sortBy - Sort criteria ('date', 'company', 'status', 'title')
+ * @param {string} sortOrder - Sort order ('asc' or 'desc')
+ * @returns {Array} Sorted applications
+ */
+export const sortApplications = (applications, sortBy, sortOrder = 'desc') => {
+ const sorted = [...applications].sort((a, b) => {
+ let valueA, valueB;
+
+ switch (sortBy) {
+ case 'date':
+ valueA = new Date(a.application_date || a.created_at);
+ valueB = new Date(b.application_date || b.created_at);
+ break;
+ case 'company':
+ valueA = (a.company || '').toLowerCase();
+ valueB = (b.company || '').toLowerCase();
+ break;
+ case 'status':
+ // Custom status order: offered > interviewed > applied > prepared > rejected
+ const statusOrder = {
+ 'offered': 5,
+ 'interviewed': 4,
+ 'applied': 3,
+ 'prepared': 2,
+ 'rejected': 1
+ };
+ valueA = statusOrder[a.status] || 0;
+ valueB = statusOrder[b.status] || 0;
+ break;
+ case 'title':
+ valueA = (a.job_title || '').toLowerCase();
+ valueB = (b.job_title || '').toLowerCase();
+ break;
+ default:
+ // Default to date sorting
+ valueA = new Date(a.application_date || a.created_at);
+ valueB = new Date(b.application_date || b.created_at);
+ }
+
+ if (valueA < valueB) return sortOrder === 'asc' ? -1 : 1;
+ if (valueA > valueB) return sortOrder === 'asc' ? 1 : -1;
+ return 0;
+ });
+
+ return sorted;
+};
+
+/**
+ * Applies all filters and sorting to applications
+ * @param {Array} applications - Array of application objects
+ * @param {Object} filters - Filter options object
+ * @param {string} filters.searchTerm - Search term
+ * @param {Array} filters.statuses - Selected status filters
+ * @param {Array} filters.locationTypes - Selected location type filters
+ * @param {Array} filters.roleTypes - Selected role type filters
+ * @param {string} filters.sortBy - Sort criteria
+ * @param {string} filters.sortOrder - Sort order
+ * @returns {Array} Filtered and sorted applications
+ */
+export const processApplications = (applications, filters) => {
+ let processed = [...applications];
+
+ // Apply search filter
+ if (filters.searchTerm) {
+ processed = searchApplications(processed, filters.searchTerm);
+ }
+
+ // Apply status filter
+ if (filters.statuses && filters.statuses.length > 0) {
+ processed = filterByStatus(processed, filters.statuses);
+ }
+
+ // Apply location type filter
+ if (filters.locationTypes && filters.locationTypes.length > 0) {
+ processed = filterByLocationType(processed, filters.locationTypes);
+ }
+
+ // Apply role type filter
+ if (filters.roleTypes && filters.roleTypes.length > 0) {
+ processed = filterByRoleType(processed, filters.roleTypes);
+ }
+
+ // Apply sorting
+ if (filters.sortBy) {
+ processed = sortApplications(processed, filters.sortBy, filters.sortOrder);
+ }
+
+ return processed;
+};
+
+/**
+ * Gets available filter options from applications array
+ * @param {Array} applications - Array of application objects
+ * @returns {Object} Available filter options
+ */
+export const getFilterOptions = (applications) => {
+ const statuses = [...new Set(applications.map(app => app.status).filter(Boolean))];
+
+ return {
+ statuses: statuses.sort(),
+ locationTypes: ['remote', 'hybrid', 'onsite'],
+ roleTypes: ['full-time', 'part-time', 'internship'],
+ sortOptions: [
+ { value: 'date', label: 'Application Date' },
+ { value: 'company', label: 'Company Name' },
+ { value: 'status', label: 'Status Priority' },
+ { value: 'title', label: 'Job Title' }
+ ]
+ };
+};
\ No newline at end of file