-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Add mock data API and improved dashboard UI #58
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| .vercel |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| import { NextResponse } from 'next/server'; | ||
|
|
||
| // Mock data for demonstration - simulates job application logs | ||
| const mockLogs = [ | ||
| { | ||
| id: 1, | ||
| job_id: 'LI-2024-001', | ||
| platform: 'LinkedIn', | ||
| status: 'success', | ||
| details: 'Applied to Senior Software Engineer at Google', | ||
| created_at: new Date(Date.now() - 1000 * 60 * 5).toISOString(), | ||
| }, | ||
| { | ||
| id: 2, | ||
| job_id: 'GD-2024-042', | ||
| platform: 'Glassdoor', | ||
| status: 'success', | ||
| details: 'Applied to Full Stack Developer at Stripe', | ||
| created_at: new Date(Date.now() - 1000 * 60 * 15).toISOString(), | ||
| }, | ||
| { | ||
| id: 3, | ||
| job_id: 'WF-2024-108', | ||
| platform: 'Wellfound', | ||
| status: 'success', | ||
| details: 'Applied to Founding Engineer at AI Startup', | ||
| created_at: new Date(Date.now() - 1000 * 60 * 30).toISOString(), | ||
| }, | ||
| { | ||
| id: 4, | ||
| job_id: 'LI-2024-002', | ||
| platform: 'LinkedIn', | ||
| status: 'failure', | ||
| details: 'Application blocked - requires Easy Apply', | ||
| created_at: new Date(Date.now() - 1000 * 60 * 45).toISOString(), | ||
| }, | ||
| { | ||
| id: 5, | ||
| job_id: 'LI-2024-003', | ||
| platform: 'LinkedIn', | ||
| status: 'success', | ||
| details: 'Applied to Backend Engineer at Meta', | ||
| created_at: new Date(Date.now() - 1000 * 60 * 60).toISOString(), | ||
| }, | ||
| { | ||
| id: 6, | ||
| job_id: 'GD-2024-043', | ||
| platform: 'Glassdoor', | ||
| status: 'success', | ||
| details: 'Applied to DevOps Engineer at Netflix', | ||
| created_at: new Date(Date.now() - 1000 * 60 * 90).toISOString(), | ||
| }, | ||
| { | ||
| id: 7, | ||
| job_id: 'WF-2024-109', | ||
| platform: 'Wellfound', | ||
| status: 'failure', | ||
| details: 'Position closed before application submitted', | ||
| created_at: new Date(Date.now() - 1000 * 60 * 120).toISOString(), | ||
| }, | ||
| { | ||
| id: 8, | ||
| job_id: 'LI-2024-004', | ||
| platform: 'LinkedIn', | ||
| status: 'success', | ||
| details: 'Applied to ML Engineer at OpenAI', | ||
| created_at: new Date(Date.now() - 1000 * 60 * 180).toISOString(), | ||
| }, | ||
| { | ||
| id: 9, | ||
| job_id: 'GD-2024-044', | ||
| platform: 'Glassdoor', | ||
| status: 'success', | ||
| details: 'Applied to Platform Engineer at Vercel', | ||
| created_at: new Date(Date.now() - 1000 * 60 * 240).toISOString(), | ||
| }, | ||
| { | ||
| id: 10, | ||
| job_id: 'LI-2024-005', | ||
| platform: 'LinkedIn', | ||
| status: 'success', | ||
| details: 'Applied to Senior Frontend Engineer at Airbnb', | ||
| created_at: new Date(Date.now() - 1000 * 60 * 300).toISOString(), | ||
| }, | ||
| ]; | ||
|
Comment on lines
+4
to
+85
|
||
|
|
||
| export async function GET() { | ||
| await new Promise(resolve => setTimeout(resolve, 100)); | ||
| return NextResponse.json(mockLogs); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,7 +2,6 @@ | |||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import { useState, useEffect } from 'react'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Define the type for a single log entry | ||||||||||||||||||||||||||||||
| interface LogEntry { | ||||||||||||||||||||||||||||||
| id: number; | ||||||||||||||||||||||||||||||
| job_id: string; | ||||||||||||||||||||||||||||||
|
|
@@ -12,25 +11,26 @@ interface LogEntry { | |||||||||||||||||||||||||||||
| created_at: string; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // The API URL for the monitoring service | ||||||||||||||||||||||||||||||
| const API_URL = process.env.NEXT_PUBLIC_API_URL | ||||||||||||||||||||||||||||||
| ? `${process.env.NEXT_PUBLIC_API_URL}/api/logs` | ||||||||||||||||||||||||||||||
| : '/api/logs'; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export default function Dashboard() { | ||||||||||||||||||||||||||||||
| const [logs, setLogs] = useState<LogEntry[]>([]); | ||||||||||||||||||||||||||||||
| const [error, setError] = useState<string | null>(null); | ||||||||||||||||||||||||||||||
| const [isInitialLoad, setIsInitialLoad] = useState<boolean>(true); | ||||||||||||||||||||||||||||||
| const [stats, setStats] = useState({ total: 0, success: 0, failure: 0 }); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||
| const fetchLogs = async () => { | ||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||
| const response = await fetch(API_URL); | ||||||||||||||||||||||||||||||
| const response = await fetch('/api/applications'); | ||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The 🔍 Detailed AnalysisThe hardcoded 💡 Suggested FixTo ensure the fetch request reaches the internal Route Handler, either modify the 🤖 Prompt for AI AgentDid we get this right? 👍 / 👎 to inform future reviews. |
||||||||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||||||||
| throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); | ||||||||||||||||||||||||||||||
| throw new Error(`Failed to fetch data: ${response.status}`); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| const data: LogEntry[] = await response.json(); | ||||||||||||||||||||||||||||||
| setLogs(data); | ||||||||||||||||||||||||||||||
| setStats({ | ||||||||||||||||||||||||||||||
| total: data.length, | ||||||||||||||||||||||||||||||
| success: data.filter(l => l.status === 'success').length, | ||||||||||||||||||||||||||||||
| failure: data.filter(l => l.status === 'failure').length, | ||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||
|
Comment on lines
+29
to
+33
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation iterates over the
Suggested change
Comment on lines
+29
to
+33
|
||||||||||||||||||||||||||||||
| setError(null); | ||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||
| console.error('Error fetching logs:', err); | ||||||||||||||||||||||||||||||
|
|
@@ -41,67 +41,144 @@ export default function Dashboard() { | |||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| fetchLogs(); | ||||||||||||||||||||||||||||||
| // Optional: Set up polling to refresh data periodically | ||||||||||||||||||||||||||||||
| const interval = setInterval(fetchLogs, 5000); // Refresh every 5 seconds | ||||||||||||||||||||||||||||||
| const interval = setInterval(fetchLogs, 10000); | ||||||||||||||||||||||||||||||
| return () => clearInterval(interval); | ||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const getPlatformColor = (platform: string) => { | ||||||||||||||||||||||||||||||
| switch (platform.toLowerCase()) { | ||||||||||||||||||||||||||||||
| case 'linkedin': return '#0077B5'; | ||||||||||||||||||||||||||||||
| case 'glassdoor': return '#0CAA41'; | ||||||||||||||||||||||||||||||
| case 'wellfound': return '#CC0000'; | ||||||||||||||||||||||||||||||
| default: return '#666'; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
Comment on lines
+48
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Comment on lines
+48
to
+55
|
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||
| <div style={{ fontFamily: 'sans-serif', padding: '20px' }}> | ||||||||||||||||||||||||||||||
| <h1>Agent Application Dashboard</h1> | ||||||||||||||||||||||||||||||
| <p style={{ color: '#666', marginBottom: '20px' }}> | ||||||||||||||||||||||||||||||
| Monitor your automated job application activities in real-time | ||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {isInitialLoad && <p>Loading application logs...</p>} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <div style={{ fontFamily: 'system-ui, -apple-system, sans-serif', padding: '24px', maxWidth: '1200px', margin: '0 auto' }}> | ||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This component uses extensive inline styling. While this works, it has several disadvantages:
Since For example, this line: <div style={{ fontFamily: 'system-ui, -apple-system, sans-serif', padding: '24px', maxWidth: '1200px', margin: '0 auto' }}>Could be rewritten with Tailwind classes as: <div className="font-sans p-6 max-w-7xl mx-auto"> |
||||||||||||||||||||||||||||||
| <div style={{ marginBottom: '32px' }}> | ||||||||||||||||||||||||||||||
| <h1 style={{ fontSize: '28px', fontWeight: '700', marginBottom: '8px', color: '#111' }}> | ||||||||||||||||||||||||||||||
| Agent Application Dashboard | ||||||||||||||||||||||||||||||
| </h1> | ||||||||||||||||||||||||||||||
| <p style={{ color: '#666', fontSize: '16px' }}> | ||||||||||||||||||||||||||||||
| Monitor your automated job application activities in real-time | ||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {/* Stats Cards */} | ||||||||||||||||||||||||||||||
| <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(200px, 1fr))', gap: '16px', marginBottom: '24px' }}> | ||||||||||||||||||||||||||||||
| <div style={{ background: '#f8f9fa', borderRadius: '12px', padding: '20px', border: '1px solid #e9ecef' }}> | ||||||||||||||||||||||||||||||
| <p style={{ color: '#666', fontSize: '14px', marginBottom: '4px' }}>Total Applications</p> | ||||||||||||||||||||||||||||||
| <p style={{ fontSize: '32px', fontWeight: '700', color: '#111', margin: 0 }}>{stats.total}</p> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| <div style={{ background: '#f0fdf4', borderRadius: '12px', padding: '20px', border: '1px solid #bbf7d0' }}> | ||||||||||||||||||||||||||||||
| <p style={{ color: '#166534', fontSize: '14px', marginBottom: '4px' }}>Successful</p> | ||||||||||||||||||||||||||||||
| <p style={{ fontSize: '32px', fontWeight: '700', color: '#16a34a', margin: 0 }}>{stats.success}</p> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| <div style={{ background: '#fef2f2', borderRadius: '12px', padding: '20px', border: '1px solid #fecaca' }}> | ||||||||||||||||||||||||||||||
| <p style={{ color: '#991b1b', fontSize: '14px', marginBottom: '4px' }}>Failed</p> | ||||||||||||||||||||||||||||||
| <p style={{ fontSize: '32px', fontWeight: '700', color: '#dc2626', margin: 0 }}>{stats.failure}</p> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| <div style={{ background: '#eff6ff', borderRadius: '12px', padding: '20px', border: '1px solid #bfdbfe' }}> | ||||||||||||||||||||||||||||||
| <p style={{ color: '#1e40af', fontSize: '14px', marginBottom: '4px' }}>Success Rate</p> | ||||||||||||||||||||||||||||||
| <p style={{ fontSize: '32px', fontWeight: '700', color: '#2563eb', margin: 0 }}> | ||||||||||||||||||||||||||||||
| {stats.total > 0 ? Math.round((stats.success / stats.total) * 100) : 0}% | ||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {isInitialLoad && ( | ||||||||||||||||||||||||||||||
| <div style={{ textAlign: 'center', padding: '40px', color: '#666' }}> | ||||||||||||||||||||||||||||||
| <p>Loading application logs...</p> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {error && ( | ||||||||||||||||||||||||||||||
| <div style={{ | ||||||||||||||||||||||||||||||
| backgroundColor: '#fff3cd', | ||||||||||||||||||||||||||||||
| border: '1px solid #ffc107', | ||||||||||||||||||||||||||||||
| padding: '12px', | ||||||||||||||||||||||||||||||
| borderRadius: '4px', | ||||||||||||||||||||||||||||||
| padding: '16px', | ||||||||||||||||||||||||||||||
| borderRadius: '8px', | ||||||||||||||||||||||||||||||
| marginBottom: '20px' | ||||||||||||||||||||||||||||||
| }}> | ||||||||||||||||||||||||||||||
| <p style={{ color: '#856404', margin: 0 }}> | ||||||||||||||||||||||||||||||
| <strong>Notice:</strong> The monitoring service is not currently available. | ||||||||||||||||||||||||||||||
| This is expected if the backend services are not running. {error} | ||||||||||||||||||||||||||||||
| <strong>Notice:</strong> Could not connect to monitoring service. {error} | ||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <table style={{ width: '100%', borderCollapse: 'collapse' }}> | ||||||||||||||||||||||||||||||
| <thead> | ||||||||||||||||||||||||||||||
| <tr style={{ backgroundColor: '#f2f2f2' }}> | ||||||||||||||||||||||||||||||
| <th style={{ padding: '8px', border: '1px solid #ddd', textAlign: 'left' }}>Platform</th> | ||||||||||||||||||||||||||||||
| <th style={{ padding: '8px', border: '1px solid #ddd', textAlign: 'left' }}>Job ID</th> | ||||||||||||||||||||||||||||||
| <th style={{ padding: '8px', border: '1px solid #ddd', textAlign: 'left' }}>Status</th> | ||||||||||||||||||||||||||||||
| <th style={{ padding: '8px', border: '1px solid #ddd', textAlign: 'left' }}>Timestamp</th> | ||||||||||||||||||||||||||||||
| <th style={{ padding: '8px', border: '1px solid #ddd', textAlign: 'left' }}>Details</th> | ||||||||||||||||||||||||||||||
| </tr> | ||||||||||||||||||||||||||||||
| </thead> | ||||||||||||||||||||||||||||||
| <tbody> | ||||||||||||||||||||||||||||||
| {!isInitialLoad && logs.length === 0 ? ( | ||||||||||||||||||||||||||||||
| <tr> | ||||||||||||||||||||||||||||||
| <td colSpan={5} style={{ padding: '20px', textAlign: 'center', color: '#666' }}> | ||||||||||||||||||||||||||||||
| {error ? 'Unable to load application logs.' : 'No application logs available yet.'} | ||||||||||||||||||||||||||||||
| </td> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {/* Application Logs Table */} | ||||||||||||||||||||||||||||||
| <div style={{ background: 'white', borderRadius: '12px', border: '1px solid #e5e7eb', overflow: 'hidden' }}> | ||||||||||||||||||||||||||||||
| <div style={{ padding: '16px 20px', borderBottom: '1px solid #e5e7eb' }}> | ||||||||||||||||||||||||||||||
| <h2 style={{ fontSize: '18px', fontWeight: '600', margin: 0 }}>Recent Applications</h2> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| <table style={{ width: '100%', borderCollapse: 'collapse' }}> | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| <table style={{ width: '100%', borderCollapse: 'collapse' }}> | |
| <table style={{ width: '100%', borderCollapse: 'collapse' }}> | |
| <caption style={{ | |
| position: 'absolute', | |
| width: '1px', | |
| height: '1px', | |
| padding: 0, | |
| margin: '-1px', | |
| overflow: 'hidden', | |
| clip: 'rect(0,0,0,0)', | |
| border: 0 | |
| }}> | |
| Job application history and status | |
| </caption> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| {!isInitialLoad && logs.length === 0 ? ( | |
| {!isInitialLoad && logs.length === 0 && !error ? ( |
Error state is conflated with empty data state. When the API fetch fails, both the error message and "No application logs available yet" message appear together, which is confusing to users.
View Details
Analysis
Error state conflated with empty data state in Dashboard
What fails: Dashboard component renders both error notification banner and "No application logs available yet" message simultaneously when API fetch fails, creating a confusing UX that doesn't clearly indicate whether the empty state is due to an error or absence of data.
How to reproduce:
- Start the dashboard with a working API endpoint
- Trigger an API failure (simulate network error or server error by mocking fetch to reject)
- Observe the rendered output
Result: Both appear together:
- Error message: "Could not connect to monitoring service. [error details]"
- Empty state message: "No application logs available yet."
Expected: When an error occurs, only the error message should display. The empty state message should only appear when there's genuinely no data and no error.
Root cause: The table tbody condition !isInitialLoad && logs.length === 0 doesn't check for the presence of an error state. When an API call fails, logs remains empty but error is set, causing both messages to render.
Fix: Added && !error to the condition so the empty state message only displays when there's no error: !isInitialLoad && logs.length === 0 && !error
This ensures:
- Error scenario: Only error notification is shown
- No-data scenario: Only empty state message is shown
- Clear user communication in all cases
Copilot
AI
Dec 15, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The platform badges use white text on colored backgrounds. For the default gray color (#666), white text may not meet WCAG AA contrast ratio requirements. Ensure adequate contrast ratios (at least 4.5:1 for normal text) for all platform badges to improve accessibility.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To improve type safety and maintainability, consider explicitly typing the
mockLogsarray. You could define an interface for the log entries (like theLogEntryinpage.tsx) and apply it to this constant. For better code organization, this type could be defined in a shared file (e.g.,src/types.ts) and imported both here and inpage.tsx.