style(ui): redesign Dashboard and Kanban to match mockups
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
Chip,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
@@ -36,6 +37,36 @@ function toneColor(theme: any, status: Status | "Other"): string {
|
||||
return TONE_PALETTE[statusTone(status)](theme);
|
||||
}
|
||||
|
||||
// Mockup's Applied-column cards carry small skill-tag pills ("React", "TS") sourced from the
|
||||
// job's own `tags` field (JSON array string) -- no new data, just parsed for display.
|
||||
function parseTags(raw?: string): string[] {
|
||||
if (!raw) return [];
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
return Array.isArray(parsed) ? parsed.filter((x) => typeof x === "string").slice(0, 2) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
type PillTone = "error" | "warning" | "success" | "info";
|
||||
|
||||
// Mockup's single status pill per card ("Reply due in 2d", "Tomorrow 14:00", "720k NOK / yr").
|
||||
// Built entirely from fields the board already fetches (needsFollowUp/followUpAt/salary/deadline)
|
||||
// -- there's no interview-datetime field on the board's JobApplication shape, so an "Interview
|
||||
// tomorrow 14:00"-style pill isn't reproducible; deadline is used as the closest real fallback.
|
||||
function cardPill(job: JobApplication, t: (key: any, params?: any) => string): { text: string; tone: PillTone } | null {
|
||||
if (job.needsFollowUp) {
|
||||
const due = job.followUpAt ? new Date(job.followUpAt) : null;
|
||||
if (!due || due.getTime() <= Date.now()) return { text: t("kanbanFollowUpNow"), tone: "error" };
|
||||
const days = Math.max(1, Math.ceil((due.getTime() - Date.now()) / 86400000));
|
||||
return { text: t("kanbanReplyDueIn", { days }), tone: "warning" };
|
||||
}
|
||||
if (job.salary) return { text: job.salary, tone: "success" };
|
||||
if (job.deadline) return { text: new Date(job.deadline).toLocaleDateString(), tone: "info" };
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function KanbanBoard() {
|
||||
const theme = useTheme();
|
||||
const { t } = useI18n();
|
||||
@@ -129,7 +160,7 @@ export default function KanbanBoard() {
|
||||
scrollSnapAlign: { xs: "start", md: "none" },
|
||||
border: "1px solid",
|
||||
borderColor: "divider",
|
||||
background: theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.02) : alpha(theme.palette.text.primary, 0.015),
|
||||
backgroundColor: theme.palette.grey[100],
|
||||
}}
|
||||
>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 1.25, px: 0.25 }}>
|
||||
@@ -139,50 +170,93 @@ export default function KanbanBoard() {
|
||||
{statusLabel(t, status)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", fontWeight: 700 }}>
|
||||
{list.length}
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
minWidth: 22,
|
||||
textAlign: "center",
|
||||
px: 0.75,
|
||||
py: 0.15,
|
||||
borderRadius: 999,
|
||||
backgroundColor: theme.palette.grey[300],
|
||||
}}
|
||||
>
|
||||
<Typography variant="caption" sx={{ color: "text.secondary", fontWeight: 700 }}>
|
||||
{list.length}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
|
||||
{list.map((j) => (
|
||||
<Card
|
||||
key={j.id}
|
||||
draggable
|
||||
onDragStart={() => setDragJobId(j.id)}
|
||||
onDragEnd={() => setDragJobId(null)}
|
||||
sx={{
|
||||
cursor: "grab",
|
||||
borderRadius: 2.5,
|
||||
borderLeft: `4px solid ${c}`,
|
||||
boxShadow: theme.palette.mode === "dark" ? "none" : "0 1px 3px rgba(15,23,42,0.06)",
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: 1.25, "&:last-child": { pb: 1.25 } }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1 }}>
|
||||
<Typography sx={{ fontWeight: 800, lineHeight: 1.25 }}>
|
||||
{j.jobTitle}
|
||||
{list.map((j) => {
|
||||
const pill = cardPill(j, t);
|
||||
const tags = parseTags(j.tags);
|
||||
return (
|
||||
<Card
|
||||
key={j.id}
|
||||
draggable
|
||||
onDragStart={() => setDragJobId(j.id)}
|
||||
onDragEnd={() => setDragJobId(null)}
|
||||
sx={{
|
||||
cursor: "grab",
|
||||
borderRadius: 2.5,
|
||||
borderLeft: `4px solid ${c}`,
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: 1.25, "&:last-child": { pb: 1.25 } }}>
|
||||
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1 }}>
|
||||
<Typography sx={{ fontWeight: 800, lineHeight: 1.25 }}>
|
||||
{j.jobTitle}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setMenuJobId(j.id);
|
||||
setMenuAnchor(e.currentTarget);
|
||||
}}
|
||||
>
|
||||
<MoreHorizIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
||||
{[j.company?.name, j.location].filter(Boolean).join(" · ")}
|
||||
</Typography>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
setMenuJobId(j.id);
|
||||
setMenuAnchor(e.currentTarget);
|
||||
}}
|
||||
>
|
||||
<MoreHorizIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>
|
||||
{[j.company?.name, j.location].filter(Boolean).join(" · ")}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.75 }}>
|
||||
{j.daysSince}d
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
{tags.length > 0 && (
|
||||
<Box sx={{ display: "flex", flexWrap: "wrap", gap: 0.5, mt: 0.75 }}>
|
||||
{tags.map((tag) => (
|
||||
<Chip
|
||||
key={tag}
|
||||
size="small"
|
||||
label={tag}
|
||||
sx={{ height: 22, backgroundColor: alpha(theme.palette.primary.main, 0.12), color: "primary.main", fontWeight: 700 }}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{pill && (
|
||||
<Box sx={{ mt: 0.75 }}>
|
||||
<Chip
|
||||
size="small"
|
||||
label={pill.text}
|
||||
sx={{
|
||||
height: 24,
|
||||
fontWeight: 700,
|
||||
backgroundColor: alpha(theme.palette[pill.tone].main, 0.14),
|
||||
color: theme.palette[pill.tone].main,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Typography variant="caption" sx={{ display: "block", color: "text.secondary", mt: 0.75 }}>
|
||||
{t("kanbanAppliedAgo", { days: j.daysSince })}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{list.length === 0 && (
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", py: 1 }}>
|
||||
{t("kanbanDropHere")}
|
||||
|
||||
Reference in New Issue
Block a user