Polish mobile layout and add collapsible sidebar

This commit is contained in:
2026-03-29 14:24:43 +02:00
parent 4253d33dfd
commit 99fc94bc18
7 changed files with 833 additions and 321 deletions
+37 -23
View File
@@ -13,6 +13,7 @@ import {
Stack,
Typography,
} from "@mui/material";
import useMediaQuery from "@mui/material/useMediaQuery";
import { alpha, useTheme } from "@mui/material/styles";
import TuneIcon from "@mui/icons-material/Tune";
import TrendingUpIcon from "@mui/icons-material/TrendingUp";
@@ -110,7 +111,7 @@ function SectionCard({ children, sx = {} }: { children: React.ReactNode; sx?: an
return (
<Paper
sx={{
p: 2.25,
p: { xs: 1.5, sm: 2.25 },
borderRadius: 4,
border: "1px solid",
borderColor: "divider",
@@ -126,6 +127,7 @@ function SectionCard({ children, sx = {} }: { children: React.ReactNode; sx?: an
export default function DashboardView() {
const theme = useTheme();
const isMobile = useMediaQuery("(max-width:767.95px)");
const navigate = useNavigate();
const { t } = useI18n();
const [stats, setStats] = useState<JobStats | null>(null);
@@ -153,8 +155,8 @@ export default function DashboardView() {
const appliedValues = analytics.map((x) => x.applied);
const responseValues = analytics.map((x) => x.responses);
const chartWidth = 860;
const chartHeight = 250;
const chartWidth = isMobile ? Math.max(420, analytics.length * 70) : 860;
const chartHeight = isMobile ? 210 : 250;
const appliedPath = buildLinePath(appliedValues, chartWidth, chartHeight);
const responsePath = buildLinePath(responseValues, chartWidth, chartHeight);
const tagColors = [theme.palette.primary.main, theme.palette.success.main, theme.palette.warning.main, theme.palette.info.main, theme.palette.error.main];
@@ -245,7 +247,7 @@ export default function DashboardView() {
<Typography variant="overline" sx={{ color: theme.palette.primary.main, fontWeight: 800 }}>
{t("dashboardHeroLabel")}
</Typography>
<Typography variant="h4" sx={{ fontWeight: 950, mt: 0.5, letterSpacing: -0.6, color: "text.primary" }}>
<Typography variant="h4" sx={{ fontWeight: 950, mt: 0.5, letterSpacing: -0.6, color: "text.primary", overflowWrap: "anywhere" }}>
{t("dashboardOverviewTitle")}
</Typography>
<Typography variant="body1" sx={{ color: "text.secondary", mt: 1.25, maxWidth: 680 }}>
@@ -259,7 +261,18 @@ export default function DashboardView() {
</Stack>
</Box>
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center" }}>
<Box
sx={{
display: "flex",
gap: 1,
flexWrap: "wrap",
alignItems: "center",
width: { xs: "100%", sm: "auto" },
'& .MuiButton-root': {
flex: { xs: '1 1 calc(50% - 8px)', sm: '0 0 auto' },
},
}}
>
{([6, 12, 24] as const).map((m) => (
<Button key={m} size="small" variant={months === m ? "contained" : "outlined"} onClick={() => setMonths(m)}>
{t("dashboardMonthsShort", { count: m })}
@@ -300,7 +313,7 @@ export default function DashboardView() {
{card.icon}
</Box>
</Box>
<Box sx={{ mt: 1.5 }}>
<Box sx={{ mt: 1.5, overflowX: "auto" }}>
<MiniSpark values={card.spark.length ? card.spark : [0, 0, 0]} color={alpha(card.tone, 0.95)} />
</Box>
</SectionCard>
@@ -322,7 +335,7 @@ export default function DashboardView() {
</Stack>
</Box>
<Box sx={{ mt: 2, overflowX: "auto" }}>
<Box sx={{ mt: 2, overflowX: "auto", mx: isMobile ? -0.5 : 0, px: isMobile ? 0.5 : 0 }}>
<Box sx={{ minWidth: chartWidth }}>
<svg width={chartWidth} height={chartHeight} viewBox={`0 0 ${chartWidth} ${chartHeight}`}>
{[0.2, 0.4, 0.6, 0.8].map((tick) => (
@@ -359,7 +372,7 @@ export default function DashboardView() {
const width = funnelMax ? clamp((item.count / funnelMax) * 100, 0, 100) : 0;
return (
<Box key={item.label}>
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 0.5 }}>
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 0.5, gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 700 }}>{item.label}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>{item.count}</Typography>
</Box>
@@ -402,16 +415,17 @@ export default function DashboardView() {
{priorityJobs.map((job) => {
const action = getReminderAction(job);
return (
<Box key={job.id} sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: alpha(theme.palette.primary.main, 0.03), display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}>
<Box>
<Typography sx={{ fontWeight: 900 }}>{job.company?.name ?? t("jobTableCompany")} {job.jobTitle}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>{action?.detail ?? job.workflowSignal?.reason ?? job.followUpReason ?? t("remindersFollowUpLabel")}</Typography>
<Box key={job.id} sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: alpha(theme.palette.primary.main, 0.03), display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}>
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontWeight: 900, overflowWrap: "anywhere" }}>{job.company?.name ?? t("jobTableCompany")} {job.jobTitle}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", overflowWrap: "anywhere" }}>{action?.detail ?? job.workflowSignal?.reason ?? job.followUpReason ?? t("remindersFollowUpLabel")}</Typography>
</Box>
<Button variant="outlined" onClick={() => openReminderJob(job)} sx={{ width: { xs: "100%", sm: "auto" } }}>
{action?.label ?? t("remindersOpen")}
</Button>
</Box>
<Button variant="outlined" onClick={() => openReminderJob(job)}>
{action?.label ?? t("remindersOpen")}
</Button>
</Box>
)})}
);
})}
</Stack>
)}
<Box sx={{ mt: 1.5 }}>
@@ -426,9 +440,9 @@ export default function DashboardView() {
{(overview?.topCompanies ?? []).map((item, index) => (
<Box key={item.companyId} sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: alpha(theme.palette.primary.main, index === 0 ? 0.05 : 0.02) }}>
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, alignItems: "center", flexWrap: "wrap" }}>
<Box>
<Typography sx={{ fontWeight: 900 }}>{item.company}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("dashboardCompanyJobsResponses", { jobs: item.count, responses: item.responses })}</Typography>
<Box sx={{ minWidth: 0 }}>
<Typography sx={{ fontWeight: 900, overflowWrap: "anywhere" }}>{item.company}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", overflowWrap: "anywhere" }}>{t("dashboardCompanyJobsResponses", { jobs: item.count, responses: item.responses })}</Typography>
</Box>
<Chip label={`${item.responseRate}%`} color={item.responseRate >= 50 ? "success" : item.responseRate >= 25 ? "warning" : "default"} variant="outlined" />
</Box>
@@ -451,7 +465,7 @@ export default function DashboardView() {
return (
<Box key={tag.tag}>
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 1, mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 800 }}>{tag.tag}</Typography>
<Typography variant="body2" sx={{ fontWeight: 800, overflowWrap: "anywhere" }}>{tag.tag}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary" }}>{tag.count}</Typography>
</Box>
<Box sx={{ height: 10, borderRadius: 999, bgcolor: alpha(theme.palette.text.primary, 0.08), overflow: "hidden" }}>
@@ -470,8 +484,8 @@ export default function DashboardView() {
<Stack spacing={1.1}>
{tagTrends.series.map((series, index) => (
<Box key={series.tag}>
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 0.5 }}>
<Typography variant="body2" sx={{ fontWeight: 800 }}>{series.tag}</Typography>
<Box sx={{ display: "flex", justifyContent: "space-between", mb: 0.5, gap: 1 }}>
<Typography variant="body2" sx={{ fontWeight: 800, overflowWrap: "anywhere" }}>{series.tag}</Typography>
<Typography variant="caption" sx={{ color: "text.secondary" }}>{series.counts.reduce((sum, value) => sum + value, 0)} total</Typography>
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: `repeat(${series.counts.length}, 1fr)`, gap: 0.5 }}>