diff --git a/CHANGELOG.md b/CHANGELOG.md index eca77c7..7deec32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed +- **Authenticated app UI redesigned** to match the landing page (`Precept.Web/src/pages/Landing.tsx`) visual language end-to-end. Presentation-only refactor across 13 files: dashboard, applications tracker, story bank, quiz mode, JD matcher, readiness, settings, login, sign-in panel, layout shell, 404 page, home page, and subscription. Adopts the landing palette (`#02050A`/`#06090F` base, teal/violet/rose accents), typography stack (Bricolage Grotesque + Instrument Serif italic emphasis + Geist + JetBrains Mono), pill CTAs, IDE-style code mockups, dot-grid + radial-halo backgrounds, eyebrow status pills, and the same glass/gradient card treatments. No changes to data fetching, state, hooks, routing, business logic, or `AuthContext`; component props and return contracts are preserved. + ## [0.2.0] - 2026-06-28 _R1 candidate — Technical Readiness (Skills Matrix Visualizer)._ diff --git a/Precept.Web/src/components/Layout.tsx b/Precept.Web/src/components/Layout.tsx index f891a60..31536ff 100644 --- a/Precept.Web/src/components/Layout.tsx +++ b/Precept.Web/src/components/Layout.tsx @@ -4,11 +4,28 @@ import { useAuth } from '../AuthContext'; import { gsap, useGSAP, prefersReducedMotion } from '../lib/animations'; import CommandPalette from './ui/CommandPalette'; +/* ─────── DESIGN TOKENS (from Landing.tsx) ─────── */ +const C = { + bg0: '#02050A', + bg1: '#06090F', + bg2: '#0B0F17', + bg3: '#11161F', + ink: '#E6EBF2', + inkDim: '#9CA8B8', + inkMute: '#5A6678', + hair: 'rgba(255,255,255,0.07)', + hair2: 'rgba(255,255,255,0.12)', + teal: '#2dd4bf', + tealDim: 'rgba(45,212,191,0.14)', + violet: '#8b5cf6', + rose: '#f43f5e', + emerald: '#10b981', +} as const; + export default function Layout() { const { logout, user } = useAuth(); const navigate = useNavigate(); const layoutRef = useRef(null); - const [searchQuery, setSearchQuery] = useState(''); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isLogoutModalOpen, setIsLogoutModalOpen] = useState(false); const [isCommandPaletteOpen, setIsCommandPaletteOpen] = useState(false); @@ -20,8 +37,7 @@ export default function Layout() { if (!layoutRef.current || prefersReducedMotion()) return; gsap.from(layoutRef.current, { opacity: 0, - scale: 0.99, - duration: 0.6, + duration: 0.55, ease: 'power2.out', }); }, { scope: layoutRef }); @@ -48,193 +64,312 @@ export default function Layout() { }; const navItems = [ - { name: 'Dashboard', path: '/dashboard', icon: 'fa-solid fa-border-all' }, - { name: 'Applications', path: '/applications', icon: 'fa-regular fa-file-lines' }, - { name: 'STAR Bank', path: '/story-bank', icon: 'fa-regular fa-star' }, - { name: 'JD Matcher', path: '/jd-matcher', icon: 'fa-solid fa-wand-magic-sparkles' }, - { name: 'Readiness', path: '/readiness', icon: 'fa-solid fa-bullseye' }, - { name: 'Quiz Mode', path: '/story-bank/quiz', icon: 'fa-solid fa-brain' }, + { name: 'Dashboard', path: '/dashboard', icon: 'fa-solid fa-border-all' }, + { name: 'Applications', path: '/applications', icon: 'fa-regular fa-file-lines' }, + { name: 'STAR Bank', path: '/story-bank', icon: 'fa-regular fa-star' }, + { name: 'JD Matcher', path: '/jd-matcher', icon: 'fa-solid fa-wand-magic-sparkles' }, + { name: 'Readiness', path: '/readiness', icon: 'fa-solid fa-bullseye' }, + { name: 'Quiz Mode', path: '/story-bank/quiz', icon: 'fa-solid fa-brain' }, ]; return ( <> -
- - {/* Background is now handled by body in index.css, so we can remove the old Background Decorative Elements */} - - {/* Mobile Menu Backdrop */} - {isMobileMenuOpen && ( -
setIsMobileMenuOpen(false)} +
+ {/* ambient: dotgrid + radial halo (matches Landing.tsx) */} +
+
- )} - {/* BEGIN: Sidebar */} - - {/* END: Sidebar */} - - {/* BEGIN: Main Content Area */} -
- - {/* BEGIN: Topbar */} -
- -
- {/* Hamburger for Mobile */} - - - {/* Search Trigger */} -
-
+ - {/* Right Actions */} -
-
- - + + {/* breadcrumb pill */} +
+ + live · command center +
+ + {/* command search */} +
+ +
- -
-
{new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'})}
-
{new Date().toLocaleDateString(undefined, { month: 'short', day: 'numeric' })}
-
- -
-
- {/* END: Topbar */} +
+ - {/* Scrollable Area */} -
- -
+
+
+ {new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })} +
+
+ {new Date().toLocaleDateString(undefined, { month: 'short', day: 'numeric' })} +
+
+
+ -
- {/* END: Main Content Area */} -
+ {/* CONTENT */} +
+ +
+ +
- {/* Logout Confirmation Modal */} + {/* LOGOUT MODAL */} {isLogoutModalOpen && ( -
-
-

Confirm Logout

-

Are you sure you want to end your session?

+
+
+
+ + end session +
+

+ Sign out of Precept? +

+

+ You'll be returned to the landing page. Your data stays put. +

- - @@ -243,9 +378,9 @@ export default function Layout() {
)} - setIsCommandPaletteOpen(false)} + setIsCommandPaletteOpen(false)} /> ); diff --git a/Precept.Web/src/components/ui/8bit-not-found1.tsx b/Precept.Web/src/components/ui/8bit-not-found1.tsx index ef38b07..5673bc8 100644 --- a/Precept.Web/src/components/ui/8bit-not-found1.tsx +++ b/Precept.Web/src/components/ui/8bit-not-found1.tsx @@ -1,7 +1,25 @@ -import { cn } from "../../lib/utils"; -import { Button } from "./8bit-button"; -import { useAuth } from "../../AuthContext"; -import { Link } from "react-router-dom"; +import { useAuth } from '../../AuthContext'; +import { Link } from 'react-router-dom'; +import { ArrowLeft, Terminal, AlertTriangle } from 'lucide-react'; +import { cn } from '../../lib/utils'; + +const C = { + bg0: '#02050A', + bg1: '#06090F', + bg2: '#0B0F17', + bg3: '#11161F', + ink: '#E6EBF2', + inkDim: '#9CA8B8', + inkMute: '#5A6678', + hair: 'rgba(255,255,255,0.07)', + hair2: 'rgba(255,255,255,0.12)', + teal: '#2dd4bf', + tealDim: 'rgba(45,212,191,0.14)', + violet: '#8b5cf6', + rose: '#f43f5e', + amber: '#f59e0b', + emerald: '#10b981', +} as const; interface NotFound1Props { className?: string; @@ -12,55 +30,128 @@ interface NotFound1Props { } export default function NotFound1({ - title = "SYSTEM OVERRIDE", - description = "The requested sector is offline. Check your coordinates and try again.", - cta = "Return to Safe Zone", - imageSrc = "https://images.unsplash.com/photo-1550745165-9bc0b252726f?q=80&w=256&auto=format&fit=crop", + title = 'Off the grid.', + description = 'The route you were chasing has either been deprecated, moved, or never existed in this build.', + cta = 'Return to safety', className, }: NotFound1Props) { const { isAuthenticated } = useAuth(); - const href = isAuthenticated ? "/dashboard" : "/"; + const href = isAuthenticated ? '/dashboard' : '/'; return (
-
+ {/* dot grid + halos */} +
+
-
- 404 -
+
+ {/* eyebrow */} + + + 404 · route missing + - {imageSrc && ( -
-
-
- 404 -
+ {/* huge serif glyph row */} +
+ 4 + 0 + 4
- )} -

- {title} -

+ {/* underline scribble (matches landing hero) */} + -

{description}

+

+ {title.includes(' ') ? ( + <> + {title.split(' ').slice(0, -1).join(' ')}{' '} + + {title.split(' ').slice(-1)[0]} + + + ) : title} +

+

+ {description} +

-
- - - + {/* IDE-style code block — echoes hero mockup in landing */} +
+          const{' '}
+          route{' '}
+          ={' '}
+          await{' '}
+          router
+          .
+          resolve
+          (
+          "{typeof window !== 'undefined' ? window.location.pathname : '/'}"
+          );
+          {'\n'}
+          throw new{' '}
+          RouteNotFoundException
+          (
+          "check coordinates"
+          );
+        
+ + {/* CTAs */} +
+ + + {cta} + + + View on GitHub + +
); diff --git a/Precept.Web/src/components/ui/sign-in.tsx b/Precept.Web/src/components/ui/sign-in.tsx index 8040dee..b48d255 100644 --- a/Precept.Web/src/components/ui/sign-in.tsx +++ b/Precept.Web/src/components/ui/sign-in.tsx @@ -1,23 +1,42 @@ import React, { useState } from 'react'; -import { AnimatedGridBackground } from './animated-grid-background'; +import { ArrowUpRight, Mail, Lock, Eye, EyeOff, ArrowLeft, ArrowRight, AlertTriangle, Loader2 } from 'lucide-react'; -// --- HELPER COMPONENTS (ICONS) --- +/* ─────── DESIGN TOKENS (from Landing.tsx) ─────── */ +const C = { + bg0: '#02050A', + bg1: '#06090F', + bg2: '#0B0F17', + bg3: '#11161F', + ink: '#E6EBF2', + inkDim: '#9CA8B8', + inkMute:'#5A6678', + hair: 'rgba(255,255,255,0.07)', + hair2: 'rgba(255,255,255,0.12)', + teal: '#2dd4bf', + tealDim:'rgba(45,212,191,0.14)', + violet: '#8b5cf6', + rose: '#f43f5e', + emerald:'#10b981', + amber: '#f59e0b', + sky: '#38bdf8', +} as const; + +/* --- ICONS --- */ const GoogleIcon = () => ( - - - - - - + + + + + + ); const GithubIcon = () => ( - - - + + + ); -// --- TYPE DEFINITIONS --- export interface Testimonial { avatarSrc?: string; name: string; @@ -40,211 +59,373 @@ interface SignInPageProps { onBack?: () => void; } -// --- SUB-COMPONENTS --- -const GlassInputWrapper = ({ children, isError = false }: { children: React.ReactNode, isError?: boolean }) => ( -
+/* InputShell — matches landing's IDE-style card surfaces */ +const InputShell = ({ children, isError = false }: { children: React.ReactNode; isError?: boolean }) => ( +
{children}
); -const TestimonialCard = ({ testimonial, delay }: { testimonial: Testimonial, delay: string }) => ( -
- {testimonial.avatarSrc ? ( - avatar - ) : ( -
- {testimonial.name.charAt(0).toUpperCase()} -
- )} -
-

{testimonial.name}

-

{testimonial.handle}

-

{testimonial.text}

+const TestimonialChip = ({ t, delay }: { t: Testimonial; delay: string }) => ( +
+
+ {t.name.split(' ').map((n) => n[0]).join('').slice(0, 2)} +
+
+

{t.name}

+

{t.handle}

+

{t.text}

); -// --- MAIN COMPONENT --- export const SignInPage: React.FC = ({ title, description, - heroImageSrc, testimonials = [], isLogin, isLoading, error, onToggleMode, onSubmit, - onGoogleSignIn, onResetPassword, onBack, }) => { const [showPassword, setShowPassword] = useState(false); return ( - - {/* Left column: sign-in form */} -
-
- -
-
-
- Precept Logo -
-
- Precept - Job Hunt OS -
-
+
+ {/* ambient backgrounds (match landing) */} +
+
+
+ + {/* LEFT — form */} +
+
+ + {/* brand row */} +
+ + + P + + Precept + + ─ Career OS + + {onBack && ( - )}
-
-

{title || (isLogin ? "Welcome Back" : "Deploy Account")}

-

{description || (isLogin ? "Sign in to your Precept account." : "Create your free account to get started.")}

+ {/* eyebrow */} +
+ + + {isLogin ? 'Welcome back' : 'New operator'} + +
- {error && ( -
- error - {error} -
+ {/* headline */} +

+ {title || ( + isLogin ? ( + <>Sign in to your command center. + ) : ( + <>Bank your stories. Own the room. + ) )} +

+

+ {description || (isLogin + ? 'Resume your hunt. Your stories, pipeline and drills are where you left them.' + : 'Create your free account — no card, JSON export anytime.')} +

-
- - {/* Registration Fields */} -
-
- - - - -
-
- - - - -
-
+ {error && ( +
+ + {error} +
+ )} -
- - - mail - - + + {/* Registration fields */} +
+
+ + + + +
+
+ + + +
+
-
-
- - {isLogin && ( - { e.preventDefault(); onResetPassword?.(); }} className="font-mono text-[11px] hover:text-brand-primary text-brand-text-muted transition-colors min-h-[44px] inline-flex items-center px-2 -mr-2">Forgot protocol? - )} + {/* Email */} +
+ + + + + +
+ + {/* Password */} +
+
+ + {isLogin && ( + + )} +
+ + +
+ +
- - lock -
- - -
-
+
+
+ + {/* Remember me */} + {isLogin && ( +
+
+ )} - {isLogin && ( -
- -
+ {/* CTA */} + + - - - -
- - Or Bypass With -
+ {/* divider */} +
+ + or + +
-
- - - + ))} +
+ + {/* swap mode */} +

+ {isLogin ? 'New here?' : 'Already deployed?'}{' '} + +

+
+
+ + {/* RIGHT — testimonials column (match landing card aesthetic) */} +
+
+ {/* dot grid + halo */} +
+
+ + {/* IDE window chrome */} +
+
+ + + +
+
+ + precept · ~/career
+
v1.0
+
-

- {isLogin ? "New operator?" : "Already deployed?"} - + {/* copy */} +

+ + + Receipts + +

+ Engineers who stopped{' '} + winging it. +

+

+ Real operators, real loops. Your stories, drills and pipeline live in one cockpit — and ship as JSON whenever you want.

-
-
- {/* Right column: hero image + testimonials */} - {heroImageSrc && ( -
-
-
-
-
- - {/* Tech Grid Pattern overlay */} -
- - {testimonials.length > 0 && ( -
- - {testimonials[1] &&
} - {testimonials[2] &&
} -
- )} + {/* testimonial chips */} + {testimonials.length > 0 && ( +
+ + {testimonials[1] && ( +
+ )} +
+ )} + + {/* tech bar */} +
+ Open source · MIT + / + .NET 10 + / + React 19 + / + PostgreSQL
-
- )} - +
+
+
); }; diff --git a/Precept.Web/src/pages/AppTracker.tsx b/Precept.Web/src/pages/AppTracker.tsx index 2393a3b..d18e1b6 100644 --- a/Precept.Web/src/pages/AppTracker.tsx +++ b/Precept.Web/src/pages/AppTracker.tsx @@ -8,31 +8,54 @@ import confetti from 'canvas-confetti'; import { getCompanyIcon } from '../lib/utils'; import { AnimatedSection } from '../components/animation/AnimatedSection'; import { CountUp } from '../components/animation/CountUp'; +import { Plus, LayoutGrid, List, X, Trash2, Loader2 } from 'lucide-react'; + +/* ─────── DESIGN TOKENS (Landing.tsx) ─────── */ +const C = { + bg0: '#02050A', bg1: '#06090F', bg2: '#0B0F17', bg3: '#11161F', + ink: '#E6EBF2', inkDim: '#9CA8B8', inkMute: '#5A6678', + hair: 'rgba(255,255,255,0.07)', hair2: 'rgba(255,255,255,0.12)', + teal: '#2dd4bf', tealDim: 'rgba(45,212,191,0.14)', + violet: '#8b5cf6', rose: '#f43f5e', amber: '#f59e0b', sky: '#38bdf8', emerald: '#10b981', +} as const; + +const cardStyle = (): React.CSSProperties => ({ + background: `linear-gradient(180deg, ${C.bg1} 0%, ${C.bg0} 100%)`, + border: `1px solid ${C.hair}`, + borderRadius: 18, + boxShadow: '0 1px 0 rgba(255,255,255,0.04) inset', +}); + +const Eyebrow = ({ children, color = C.teal }: { children: React.ReactNode; color?: string }) => ( + + + {children} + +); const COLUMNS: ApplicationStatus[] = ['Applied', 'PhoneScreen', 'Interviewing', 'Offer', 'Rejected', 'Ghosted']; const COLUMN_LABELS: Record = { - 'Applied': 'Applied', - 'PhoneScreen': 'Phone Screen', - 'Interviewing': 'Interviewing', - 'Offer': 'Offer', - 'Rejected': 'Rejected', - 'Ghosted': 'Ghosted' + Applied: 'Applied', PhoneScreen: 'Phone Screen', Interviewing: 'Interviewing', + Offer: 'Offer', Rejected: 'Rejected', Ghosted: 'Ghosted', }; -const getStatusColor = (status: ApplicationStatus) => { - switch(status) { - case 'Offer': return 'bg-emerald-500/10 text-emerald-400 border-emerald-500/20'; - case 'Rejected': return 'bg-rose-500/10 text-rose-400 border-rose-500/20'; - case 'Ghosted': return 'bg-white/5 text-text-secondary border-white/10'; - case 'Interviewing': return 'bg-blue-500/10 text-blue-400 border-blue-500/20'; - case 'PhoneScreen': return 'bg-cyan-500/10 text-cyan-400 border-cyan-500/20'; - default: return 'bg-accent-teal/10 text-accent-teal border-accent-teal/20'; +const statusColor = (status: ApplicationStatus): string => { + switch (status) { + case 'Offer': return C.emerald; + case 'Rejected': return C.rose; + case 'Ghosted': return C.inkMute; + case 'Interviewing': return C.sky; + case 'PhoneScreen': return '#06b6d4'; + default: return C.teal; } }; const getStatusIcon = (status: ApplicationStatus) => { - switch(status) { + switch (status) { case 'Offer': return 'fa-solid fa-trophy'; case 'Rejected': return 'fa-solid fa-xmark'; case 'Ghosted': return 'fa-solid fa-ghost'; @@ -42,38 +65,34 @@ const getStatusIcon = (status: ApplicationStatus) => { } }; -const getStatusGlow = (status: ApplicationStatus) => { - switch(status) { - case 'Offer': return 'shadow-[0_0_15px_rgba(16,185,129,0.15)]'; - case 'Rejected': return 'shadow-[0_0_15px_rgba(244,63,94,0.1)]'; - case 'Interviewing': return 'shadow-[0_0_15px_rgba(59,130,246,0.15)]'; - case 'PhoneScreen': return 'shadow-[0_0_15px_rgba(6,182,212,0.15)]'; - default: return ''; - } -}; - -const renderCompanyLogo = (name: string, sizeClass = 'h-6 w-6 text-xs') => { +const renderCompanyLogo = (name: string, sizeClass = 'h-6 w-6 text-[11px]') => { const { icon, color, isText, initials } = getCompanyIcon(name); if (isText) { return ( -
- {initials} +
+ {initials}
); } return ( -
- +
+
); }; +const inputStyle: React.CSSProperties = { + background: 'rgba(255,255,255,0.025)', + border: `1px solid ${C.hair}`, + borderRadius: 10, + color: C.ink, + padding: '10px 12px', + fontFamily: 'JetBrains Mono, monospace', + fontSize: 13, + width: '100%', + outline: 'none', +}; + export default function AppTracker() { const [searchParams, setSearchParams] = useSearchParams(); const [apps, setApps] = useState([]); @@ -84,12 +103,10 @@ export default function AppTracker() { const [appToDelete, setAppToDelete] = useState(null); const toast = useToast(); - - // Modal states + const [isModalOpen, setIsModalOpen] = useState(false); const [selectedApp, setSelectedApp] = useState(null); - // Form states const [companyName, setCompanyName] = useState(''); const [roleTitle, setRoleTitle] = useState(''); const [location, setLocation] = useState(''); @@ -109,8 +126,6 @@ export default function AppTracker() { try { const data = await api.get('/api/application'); setApps(data); - - // Load JDs for the link dropdown const jdData = await api.get<{ id: string; companyName: string; roleTitle: string }[]>('/api/jobdescription'); setJds(jdData); } catch (err) { @@ -120,65 +135,34 @@ export default function AppTracker() { } }; - useEffect(() => { - loadApplications(); - }, []); + useEffect(() => { loadApplications(); }, []); const handleDragStart = (e: React.DragEvent, id: string) => { setDraggedAppId(id); e.dataTransfer.effectAllowed = 'move'; }; - const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }; - const handleDrop = async (e: React.DragEvent, newStatus: ApplicationStatus) => { e.preventDefault(); if (!draggedAppId) return; - - const targetApp = apps.find(a => a.id === draggedAppId); + const targetApp = apps.find((a) => a.id === draggedAppId); if (targetApp && targetApp.status !== newStatus) { - // Optimistic UI update - setApps(prev => prev.map(a => a.id === draggedAppId ? { ...a, status: newStatus } : a)); - + setApps((prev) => prev.map((a) => (a.id === draggedAppId ? { ...a, status: newStatus } : a))); if (newStatus === 'Offer') { const x = e.clientX / window.innerWidth; const y = e.clientY / window.innerHeight; - - const commonOptions = { - particleCount: 40, - spread: 40, - scalar: 0.6, - ticks: 60, // Short duration - gravity: 2, // Fall quickly - startVelocity: 55, // Shoot out fast - colors: ['#2dd4bf', '#10b981', '#fcd34d', '#ffffff'], - zIndex: 0 - }; - - // Shoot left - confetti({ - ...commonOptions, - origin: { x: Math.max(0, x - 0.05), y }, - angle: 135 - }); - - // Shoot right - confetti({ - ...commonOptions, - origin: { x: Math.min(1, x + 0.05), y }, - angle: 45 - }); + const common = { particleCount: 40, spread: 40, scalar: 0.6, ticks: 60, gravity: 2, startVelocity: 55, colors: ['#2dd4bf', '#10b981', '#fcd34d', '#ffffff'], zIndex: 0 }; + confetti({ ...common, origin: { x: Math.max(0, x - 0.05), y }, angle: 135 }); + confetti({ ...common, origin: { x: Math.min(1, x + 0.05), y }, angle: 45 }); } - try { await api.patch(`/api/application/${draggedAppId}/status`, { status: newStatus }); } catch (err) { - console.error('Failed to sync status drop to server:', err); - // Rollback on error - setApps(prev => prev.map(a => a.id === draggedAppId ? { ...a, status: targetApp.status } : a)); + console.error('Failed to sync status drop:', err); + setApps((prev) => prev.map((a) => (a.id === draggedAppId ? { ...a, status: targetApp.status } : a))); toast.error('Status update failed. The change has been rolled back.'); } } @@ -187,19 +171,11 @@ export default function AppTracker() { const handleOpenCreateModal = () => { setSelectedApp(null); - setCompanyName(''); - setRoleTitle(''); - setLocation('Remote'); - setSalaryRange(''); + setCompanyName(''); setRoleTitle(''); setLocation('Remote'); setSalaryRange(''); setStatus('Applied'); const today = new Date().toISOString().split('T')[0]; - setDateApplied(today); - setDateLastContact(today); - setResumeVersion('v1'); - setSource('LinkedIn'); - setIsRemote(true); - setNotes(''); - setJobDescriptionId(''); + setDateApplied(today); setDateLastContact(today); + setResumeVersion('v1'); setSource('LinkedIn'); setIsRemote(true); setNotes(''); setJobDescriptionId(''); setIsModalOpen(true); }; @@ -208,7 +184,7 @@ export default function AppTracker() { handleOpenCreateModal(); setSearchParams({}); } - }, [searchParams]); + }, [searchParams]); // eslint-disable-line const handleOpenEditModal = (app: Application) => { setSelectedApp(app); @@ -230,7 +206,6 @@ export default function AppTracker() { const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); if (!companyName.trim() || !roleTitle.trim()) return; - setIsSubmitting(true); try { const payload = { @@ -245,34 +220,19 @@ export default function AppTracker() { notes: notes.trim(), isRemote, source: source.trim(), - jobDescriptionId: jobDescriptionId || null + jobDescriptionId: jobDescriptionId || null, }; - if (selectedApp) { - const updated = await api.put(`/api/application/${selectedApp.id}`, { - ...payload, - id: selectedApp.id - }); - setApps(prev => prev.map(a => a.id === updated.id ? updated : a)); - + const updated = await api.put(`/api/application/${selectedApp.id}`, { ...payload, id: selectedApp.id }); + setApps((prev) => prev.map((a) => (a.id === updated.id ? updated : a))); if (selectedApp.status !== 'Offer' && status === 'Offer') { - const commonOptions = { - particleCount: 40, - spread: 40, - scalar: 0.6, - ticks: 60, - gravity: 2, - startVelocity: 55, - colors: ['#2dd4bf', '#10b981', '#fcd34d', '#ffffff'], - zIndex: 40 // behind the z-50 modal - }; - - confetti({ ...commonOptions, origin: { x: 0.35, y: 0.5 }, angle: 135 }); - confetti({ ...commonOptions, origin: { x: 0.65, y: 0.5 }, angle: 45 }); + const common = { particleCount: 40, spread: 40, scalar: 0.6, ticks: 60, gravity: 2, startVelocity: 55, colors: ['#2dd4bf', '#10b981', '#fcd34d', '#ffffff'], zIndex: 40 }; + confetti({ ...common, origin: { x: 0.35, y: 0.5 }, angle: 135 }); + confetti({ ...common, origin: { x: 0.65, y: 0.5 }, angle: 45 }); } } else { const created = await api.post('/api/application', payload); - setApps(prev => [created, ...prev]); + setApps((prev) => [created, ...prev]); } setIsModalOpen(false); } catch (err) { @@ -283,16 +243,12 @@ export default function AppTracker() { } }; - const confirmDelete = (id: string) => { - setAppToDelete(id); - }; - + const confirmDelete = (id: string) => setAppToDelete(id); const executeDelete = async () => { if (!appToDelete) return; - try { await api.delete(`/api/application/${appToDelete}`); - setApps(prev => prev.filter(a => a.id !== appToDelete)); + setApps((prev) => prev.filter((a) => a.id !== appToDelete)); setIsModalOpen(false); setAppToDelete(null); } catch (err) { @@ -303,365 +259,288 @@ export default function AppTracker() { }; return ( -
- - {/* Hero Header */} +
+ {/* Header */}
-

- Active Pipelines - | - tracked + Pipeline tracker +

+ Active pipelines.

-

Status tracking for outbound connections.

+

+ tracked · drag cards between stages, no graveyards. +

- -
- {/* View Toggle */} -
- - +
+
+ {[ + { v: 'board' as const, i: }, + { v: 'table' as const, i: }, + ].map((opt) => ( + + ))}
- -
{isLoading ? ( -
-
- Ingesting Connection Data... +
+ + Loading pipeline…
) : (
{view === 'board' ? ( -
- {COLUMNS.map(col => ( -
handleDrop(e, col)} - > - {/* Column Header */} -
-
- -

{COLUMN_LABELS[col]}

+
+ {COLUMNS.map((col) => { + const color = statusColor(col); + return ( +
handleDrop(e, col)} + > +
+
+ + {COLUMN_LABELS[col]} +
+ + {apps.filter((a) => a.status === col).length} + +
+
+ + {apps.filter((a) => a.status === col).map((app) => ( +
handleDragStart(e, app.id)} + onClick={() => handleOpenEditModal(app)} + data-testid={`app-card-${app.id}`} + className={`p-3.5 transition-all duration-300 cursor-move group ${draggedAppId === app.id ? 'opacity-40' : ''}`} + style={{ + background: C.bg2, + border: `1px solid ${C.hair}`, + borderRadius: 12, + boxShadow: `0 1px 0 ${color}22 inset`, + }} + onMouseEnter={(e) => { e.currentTarget.style.borderColor = `${color}66`; }} + onMouseLeave={(e) => { e.currentTarget.style.borderColor = C.hair; }} + > +
+ {renderCompanyLogo(app.companyName, 'h-7 w-7 text-[11px]')} +

{app.companyName}

+
+

{app.roleTitle}

+
+ + {app.dateApplied ? new Date(app.dateApplied).toLocaleDateString() : 'No date'} +
+
+ ))} +
- - {apps.filter(a => a.status === col).length} -
- - {/* Column Body */} - - {apps.filter(a => a.status === col).map(app => ( -
handleDragStart(e, app.id)} - onClick={() => handleOpenEditModal(app)} - className={`glass-panel rounded-xl p-4 hover:border-white/15 transition-all duration-300 cursor-move group relative ${getStatusGlow(app.status)} ${draggedAppId === app.id ? 'opacity-40 border-accent-teal!' : ''}`} - > - -
- {renderCompanyLogo(app.companyName)} -

{app.companyName}

-
-

{app.roleTitle}

-
- {app.dateApplied ? new Date(app.dateApplied).toLocaleDateString() : 'No date'} -
-
- ))} -
-
- ))} + ); + })}
) : ( -
- {/* Desktop Table */} +
- - - - - - - - - - {apps.map((app) => ( - handleOpenEditModal(app)} - className="hover:bg-white/3 transition-colors cursor-pointer group" - > - - - - - + + + + + - ))} - {apps.length === 0 && ( - - - - )} - -
CompanyRoleStatusDate
-
- {renderCompanyLogo(app.companyName, 'h-8 w-8 text-sm')} - {app.companyName} -
-
{app.roleTitle} - - - {COLUMN_LABELS[app.status]} - - - {app.dateApplied ? new Date(app.dateApplied).toLocaleDateString() : 'N/A'} - e.stopPropagation()}> - -
CompanyRoleStatusApplied
- No pipelines tracked yet. Initialise outbound connections above. -
+ + + {apps.map((app) => { + const color = statusColor(app.status); + return ( + handleOpenEditModal(app)} + className="cursor-pointer transition-colors" + style={{ borderBottom: `1px solid ${C.hair}` }} + onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(255,255,255,0.025)')} + onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')} + > + +
+ {renderCompanyLogo(app.companyName, 'h-8 w-8 text-sm')} + {app.companyName} +
+ + {app.roleTitle} + + + + {COLUMN_LABELS[app.status]} + + + + {app.dateApplied ? new Date(app.dateApplied).toLocaleDateString() : '—'} + + + ); + })} + {apps.length === 0 && ( + No pipelines tracked yet. + )} + +
- {/* Mobile Cards */} - - {apps.length === 0 && ( -
- No pipelines tracked yet. Initialise outbound connections above. -
- )} - {apps.map(app => ( -
handleOpenEditModal(app)} className="glass-panel p-4 rounded-xl flex flex-col gap-3 cursor-pointer"> -
-
-
- {renderCompanyLogo(app.companyName, 'h-5 w-5 text-[10px]')} -
{app.companyName}
+
+ {apps.length === 0 &&
No pipelines tracked yet.
} + {apps.map((app) => { + const color = statusColor(app.status); + return ( +
handleOpenEditModal(app)} className="p-4 cursor-pointer" style={{ background: C.bg2, border: `1px solid ${C.hair}`, borderRadius: 12 }}> +
+
+ {renderCompanyLogo(app.companyName, 'h-6 w-6 text-[10px]')} +
+
{app.companyName}
+
{app.roleTitle}
+
-
{app.roleTitle}
+ + {COLUMN_LABELS[app.status]} +
- - - {COLUMN_LABELS[app.status]} -
-
- {app.dateApplied ? new Date(app.dateApplied).toLocaleDateString() : 'N/A'} - -
-
- ))} - + ); + })} +
)}
)} - {/* Create/Edit Modal */} + {/* MODAL */} {isModalOpen && ( -
-
- {/* Modal top accent */} -
- -
-

- - {selectedApp ? 'Update Connection' : 'Initialize Connection'} -

-
- +
-
- - setCompanyName(e.target.value)} - className="input-base w-full text-sm font-sans" - placeholder="e.g. Stripe, Discord" - required - /> -
-
- - setRoleTitle(e.target.value)} - className="input-base w-full text-sm font-sans" - placeholder="e.g. Senior Backend Engineer" - required - /> -
+ + setCompanyName(e.target.value)} placeholder="e.g. Stripe" required style={inputStyle} /> + + + setRoleTitle(e.target.value)} placeholder="e.g. Senior Backend Engineer" required style={inputStyle} /> +
-
-
- - setStatus(e.target.value as ApplicationStatus)} style={inputStyle}> + {COLUMNS.map((c) => )} -
-
- - setSalaryRange(e.target.value)} - className="input-base w-full text-sm" - placeholder="e.g. $120,000 - $140,000" - /> -
+ + + setSalaryRange(e.target.value)} placeholder="$120k – $140k" style={inputStyle} /> +
-
-
- - setLocation(e.target.value)} - className="input-base w-full text-sm" - placeholder="e.g. Remote, Boston MA" - /> -
-
- - setSource(e.target.value)} - className="input-base w-full text-sm font-mono" - placeholder="LinkedIn, Referral" - /> -
-
- - setResumeVersion(e.target.value)} - className="input-base w-full text-sm font-mono" - placeholder="v2-backend" - /> -
+ + setLocation(e.target.value)} placeholder="Remote" style={inputStyle} /> + + + setSource(e.target.value)} placeholder="LinkedIn" style={inputStyle} /> + + + setResumeVersion(e.target.value)} placeholder="v2-backend" style={inputStyle} /> +
-
-
- - setDateApplied(e.target.value)} - className="input-base w-full text-sm font-mono" - /> -
-
- - setDateLastContact(e.target.value)} - className="input-base w-full text-sm font-mono" - /> -
+ setDateApplied(e.target.value)} style={inputStyle} /> + setDateLastContact(e.target.value)} style={inputStyle} />
- - {/* Job Description Relational Link */} -
- - setJobDescriptionId(e.target.value)} style={inputStyle}> + + {jds.map((jd) => ( + ))} -
- - {/* Remote Checkbox */} -