feat(ui): add A1 dashboard and resumable onboarding
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import React from 'react';
|
||||
import { Box, Link, Typography } from '@mui/material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useI18n } from '../i18n/I18nProvider';
|
||||
import { PageSection } from '../layout/PageLayout';
|
||||
import type { JobApplication } from '../types';
|
||||
|
||||
// Date-only deadlines remain local calendar dates; never infer a time from free text.
|
||||
function storedDate(value: string | undefined): Date | null {
|
||||
if (typeof value !== 'string') return null;
|
||||
const parts = /^(\d{4})-(\d{2})-(\d{2})(?:$|T)/.exec(value);
|
||||
if (!parts) return null;
|
||||
const [year, month, day] = parts.slice(1).map(Number);
|
||||
const calendar = new Date(0); calendar.setFullYear(year, month - 1, day); calendar.setHours(0, 0, 0, 0);
|
||||
if (calendar.getFullYear() !== year || calendar.getMonth() !== month - 1 || calendar.getDate() !== day) return null;
|
||||
if (value.length === 10) return calendar;
|
||||
const time = /T(\d{2}):(\d{2})(?::(\d{2})(?:\.\d{1,7})?)?(?:Z|[+-]\d{2}:\d{2})?$/.exec(value);
|
||||
if (!time || Number(time[1]) > 23 || Number(time[2]) > 59 || Number(time[3] ?? 0) > 59) return null;
|
||||
const parsed = new Date(value);
|
||||
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
||||
}
|
||||
|
||||
export default function DashboardWeek({ jobs }: { jobs: JobApplication[] }) {
|
||||
const { t, language } = useI18n();
|
||||
const start = new Date(); start.setHours(0, 0, 0, 0);
|
||||
const end = new Date(start); end.setDate(end.getDate() + 7);
|
||||
const events = jobs.flatMap(job => ([
|
||||
{ kind: 'deadline', raw: job.deadline, label: t('dashboardWeekDeadline') },
|
||||
{ kind: 'followUp', raw: job.followUpAt, label: t('dashboardWeekFollowUp') },
|
||||
]).flatMap(event => {
|
||||
const date = storedDate(event.raw);
|
||||
return date && date >= start && date < end ? [{ ...event, raw: event.raw!, date, job }] : [];
|
||||
})).sort((a, b) => a.date.getTime() - b.date.getTime());
|
||||
const format = new Intl.DateTimeFormat(language === 'nb' ? 'nb-NO' : 'en-US', { weekday: 'short', month: 'short', day: 'numeric' });
|
||||
return <PageSection title={t('dashboardYourWeek')} description={t('dashboardWeekScope')}
|
||||
actions={<Link component={RouterLink} to="/reminders">{t('reminders')}</Link>}>
|
||||
{events.length ? <Box sx={{ display: 'grid', gridTemplateColumns: { xs: '1fr', sm: 'repeat(3, minmax(0, 1fr))' }, gap: 2 }}>
|
||||
{events.slice(0, 3).map(event => <Box key={`${event.job.id}:${event.kind}`} sx={{ borderInlineStart: '3px solid', borderColor: 'primary.main', pl: 1.5, minWidth: 0 }}>
|
||||
<Typography variant="caption" component="time" dateTime={event.raw}>{format.format(event.date)}</Typography>
|
||||
<Typography variant="body2" sx={{ fontWeight: 700 }}>{event.label}</Typography>
|
||||
<Link component={RouterLink} to={`/jobs/${event.job.id}`} sx={{ display: 'inline-block', py: 1, overflowWrap: 'anywhere' }}>{event.job.jobTitle}</Link>
|
||||
<Typography variant="body2" color="text.secondary">{event.job.company?.name}</Typography>
|
||||
</Box>)}
|
||||
</Box> : <Typography color="text.secondary">{t('dashboardWeekEmpty')}</Typography>}
|
||||
</PageSection>;
|
||||
}
|
||||
Reference in New Issue
Block a user