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
@@ -8,16 +8,19 @@ import {
DialogActions,
DialogContent,
DialogTitle,
IconButton,
Paper,
Stack,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
TextField,
Typography,
IconButton,
} from "@mui/material";
import useMediaQuery from "@mui/material/useMediaQuery";
import { api, getApiErrorMessage } from "../api";
import { Company } from "../types";
@@ -26,6 +29,7 @@ import { useToast } from "../toast";
import { useI18n } from "../i18n/I18nProvider";
export default function CompaniesTable() {
const isMobile = useMediaQuery("(max-width:767.95px)");
const { toast } = useToast();
const { t } = useI18n();
const location = useLocation();
@@ -93,60 +97,101 @@ export default function CompaniesTable() {
}
};
return (
<Paper sx={{ mt: 0 }}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("companiesName")}</TableCell>
<TableCell>{t("companiesLocation")}</TableCell>
<TableCell>{t("companiesSource")}</TableCell>
<TableCell>{t("companiesPipeline")}</TableCell>
<TableCell>{t("companiesRecruiter")}</TableCell>
<TableCell>{t("companiesNextContact")}</TableCell>
<TableCell width={1} align="right" />
</TableRow>
</TableHead>
<TableBody>
{companies.map((c) => (
<TableRow key={c.id}>
<TableCell>{c.name}</TableCell>
<TableCell>{c.location ?? ""}</TableCell>
<TableCell>{c.source ?? ""}</TableCell>
<TableCell>{c.pipelineStage ?? ""}</TableCell>
<TableCell>
{c.recruiterName ?? ""}
{c.recruiterEmail ? ` (${c.recruiterEmail})` : ""}
</TableCell>
<TableCell>{c.nextContactAt ? new Date(c.nextContactAt).toLocaleDateString() : ""}</TableCell>
<TableCell align="right">
<IconButton size="small" onClick={() => openEdit(c)}>
<EditOutlinedIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
))}
{companies.length === 0 && (
<TableRow>
<TableCell colSpan={7}>
<Typography sx={{ py: 2, textAlign: "center" }}>
{t("companiesEmpty")}
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
const renderCompanyMeta = (label: string, value?: string | null) => (
<Box>
<Typography variant="overline" sx={{ color: "text.secondary" }}>{label}</Typography>
<Typography variant="body2" sx={{ overflowWrap: "anywhere" }}>{value || "—"}</Typography>
</Box>
);
<Dialog open={editOpen} onClose={() => setEditOpen(false)} fullWidth maxWidth="sm">
return (
<Paper sx={{ mt: 0, p: { xs: 1.5, sm: 0 } }}>
{isMobile ? (
<Stack spacing={1.5}>
{companies.map((c) => (
<Paper key={c.id} sx={{ p: 1.5, borderRadius: 3 }}>
<Stack spacing={1.25}>
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 1 }}>
<Box>
<Typography sx={{ fontWeight: 900, overflowWrap: "anywhere" }}>{c.name}</Typography>
<Typography variant="body2" sx={{ color: "text.secondary", overflowWrap: "anywhere" }}>{c.location || t("companiesLocation")}</Typography>
</Box>
<IconButton size="small" onClick={() => openEdit(c)}>
<EditOutlinedIcon fontSize="small" />
</IconButton>
</Box>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 1.25 }}>
{renderCompanyMeta(t("companiesSource"), c.source)}
{renderCompanyMeta(t("companiesPipeline"), c.pipelineStage)}
{renderCompanyMeta(t("companiesRecruiter"), [c.recruiterName, c.recruiterEmail].filter(Boolean).join(" · "))}
{renderCompanyMeta(t("companiesNextContact"), c.nextContactAt ? new Date(c.nextContactAt).toLocaleDateString() : null)}
</Box>
</Stack>
</Paper>
))}
{companies.length === 0 ? (
<Typography sx={{ py: 2, textAlign: "center" }}>
{t("companiesEmpty")}
</Typography>
) : null}
</Stack>
) : (
<TableContainer sx={{ borderRadius: 3, border: "1px solid", borderColor: "divider" }}>
<Table>
<TableHead>
<TableRow>
<TableCell>{t("companiesName")}</TableCell>
<TableCell>{t("companiesLocation")}</TableCell>
<TableCell>{t("companiesSource")}</TableCell>
<TableCell>{t("companiesPipeline")}</TableCell>
<TableCell>{t("companiesRecruiter")}</TableCell>
<TableCell>{t("companiesNextContact")}</TableCell>
<TableCell width={1} align="right" />
</TableRow>
</TableHead>
<TableBody>
{companies.map((c) => (
<TableRow key={c.id}>
<TableCell>{c.name}</TableCell>
<TableCell>{c.location ?? ""}</TableCell>
<TableCell>{c.source ?? ""}</TableCell>
<TableCell>{c.pipelineStage ?? ""}</TableCell>
<TableCell>
{c.recruiterName ?? ""}
{c.recruiterEmail ? ` (${c.recruiterEmail})` : ""}
</TableCell>
<TableCell>{c.nextContactAt ? new Date(c.nextContactAt).toLocaleDateString() : ""}</TableCell>
<TableCell align="right">
<IconButton size="small" onClick={() => openEdit(c)}>
<EditOutlinedIcon fontSize="small" />
</IconButton>
</TableCell>
</TableRow>
))}
{companies.length === 0 && (
<TableRow>
<TableCell colSpan={7}>
<Typography sx={{ py: 2, textAlign: "center" }}>
{t("companiesEmpty")}
</Typography>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
)}
<Dialog open={editOpen} onClose={() => setEditOpen(false)} fullWidth fullScreen={isMobile} maxWidth="sm">
<DialogTitle>{t("companiesEdit")}</DialogTitle>
<DialogContent>
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2, mt: 1 }}>
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2, mt: 1 }}>
<TextField
label={t("companiesName")}
value={editing?.name ?? ""}
onChange={(e) => setEditing((p) => (p ? { ...p, name: e.target.value } : p))}
sx={{ gridColumn: "1 / -1" }}
sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }}
/>
<TextField
label={t("companiesLocation")}
@@ -178,7 +223,7 @@ export default function CompaniesTable() {
label={t("companiesRecruiterLinkedIn")}
value={recruiterLinkedIn}
onChange={(e) => setRecruiterLinkedIn(e.target.value)}
sx={{ gridColumn: "1 / -1" }}
sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }}
/>
<TextField
@@ -197,9 +242,9 @@ export default function CompaniesTable() {
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setEditOpen(false)}>{t("cancel")}</Button>
<Button variant="contained" onClick={save} disabled={!canSave}>
<DialogActions sx={{ px: 3, pb: 3, flexDirection: { xs: "column-reverse", sm: "row" }, gap: 1 }}>
<Button onClick={() => setEditOpen(false)} fullWidth={isMobile}>{t("cancel")}</Button>
<Button variant="contained" onClick={save} disabled={!canSave} fullWidth={isMobile}>
{t("save")}
</Button>
</DialogActions>