Polish UI, harden company creation, and add error pages
This commit is contained in:
@@ -12,7 +12,6 @@ import {
|
||||
Select,
|
||||
Tab,
|
||||
Tabs,
|
||||
TextField,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
@@ -22,8 +21,8 @@ import GoogleAuthCard from "./GoogleAuthCard";
|
||||
import RulesSettingsCard from "./RulesSettingsCard";
|
||||
import BackupCard from "./BackupCard";
|
||||
import AuthStatusCard from "./AuthStatusCard";
|
||||
|
||||
export type ThemeModePref = "system" | "light" | "dark";
|
||||
import { ThemeModePref } from "../themePrefs";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
interface Props {
|
||||
pageSize: 15 | 20 | 25;
|
||||
@@ -42,7 +41,7 @@ function TabPanel({ value, index, children }: { value: number; index: number; ch
|
||||
return <Box sx={{ mt: 2 }}>{children}</Box>;
|
||||
}
|
||||
|
||||
const ACCENTS = ["#7c4dff", "#4f8cff", "#16a34a", "#f59e0b", "#e11d48", "#06b6d4"]; // violet, blue, green, amber, rose, cyan
|
||||
const ACCENTS = ["#15803d", "#16a34a", "#22c55e", "#0f766e", "#0f766e", "#65a30d"];
|
||||
|
||||
export default function SettingsView({
|
||||
pageSize,
|
||||
@@ -56,56 +55,60 @@ export default function SettingsView({
|
||||
onResetAccentColor,
|
||||
}: Props) {
|
||||
const [tab, setTab] = useState(0);
|
||||
const { language, setLanguage, t } = useI18n();
|
||||
|
||||
const accentOk = useMemo(() => /^#[0-9a-fA-F]{6}$/.test(accentColor), [accentColor]);
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 0, p: 2 }}>
|
||||
<Typography variant="h5" sx={{ mb: 1, fontWeight: 900 }}>
|
||||
Settings
|
||||
{t("settingsTitle")}
|
||||
</Typography>
|
||||
<Typography sx={{ color: "text.secondary", mb: 2 }}>
|
||||
Preferences and admin tools.
|
||||
{t("settingsSubtitle")}
|
||||
</Typography>
|
||||
|
||||
<Tabs value={tab} onChange={(_, v) => setTab(v)} sx={{ mb: 1 }}>
|
||||
<Tab label="General" />
|
||||
<Tab label="Follow-ups" />
|
||||
<Tab label="Notifications" />
|
||||
<Tab label="Account" />
|
||||
<Tab label="Backup" />
|
||||
<Tab label={t("settingsTabGeneral")} />
|
||||
<Tab label={t("settingsTabFollowUps")} />
|
||||
<Tab label={t("settingsTabNotifications")} />
|
||||
<Tab label={t("settingsTabAccount")} />
|
||||
<Tab label={t("settingsTabBackup")} />
|
||||
</Tabs>
|
||||
|
||||
<TabPanel value={tab} index={0}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2 }}>
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography sx={{ fontWeight: 950, mb: 1 }}>Appearance</Typography>
|
||||
<Typography sx={{ fontWeight: 950, mb: 1 }}>{t("settingsAppearance")}</Typography>
|
||||
|
||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||
<InputLabel id="theme-mode-label">Theme</InputLabel>
|
||||
<InputLabel id="theme-mode-label">{t("settingsTheme")}</InputLabel>
|
||||
<Select
|
||||
labelId="theme-mode-label"
|
||||
value={themeMode}
|
||||
label="Theme"
|
||||
label={t("settingsTheme")}
|
||||
onChange={(e) => onThemeModeChange(e.target.value as ThemeModePref)}
|
||||
>
|
||||
<MenuItem value="system">System</MenuItem>
|
||||
<MenuItem value="dark">Dark</MenuItem>
|
||||
<MenuItem value="light">Light</MenuItem>
|
||||
<MenuItem value="system">{t("settingsThemeSystem")}</MenuItem>
|
||||
<MenuItem value="dark">{t("settingsThemeDark")}</MenuItem>
|
||||
<MenuItem value="light">{t("settingsThemeLight")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 2, flexWrap: "wrap" }}>
|
||||
<TextField
|
||||
label="Accent"
|
||||
type="color"
|
||||
value={accentOk ? accentColor : "#7c4dff"}
|
||||
onChange={(e) => onAccentColorChange(e.target.value)}
|
||||
sx={{ width: 160 }}
|
||||
InputLabelProps={{ shrink: true }}
|
||||
/>
|
||||
<input type="hidden" />
|
||||
<FormControl sx={{ width: 160 }}>
|
||||
<Typography variant="caption" sx={{ mb: 0.5 }}>{t("settingsAccent")}</Typography>
|
||||
<input
|
||||
aria-label={t("settingsAccent")}
|
||||
type="color"
|
||||
value={accentOk ? accentColor : "#15803d"}
|
||||
onChange={(e) => onAccentColorChange(e.target.value)}
|
||||
style={{ width: 160, height: 40, border: "none", background: "transparent", padding: 0 }}
|
||||
/>
|
||||
</FormControl>
|
||||
<Button variant="outlined" onClick={onResetAccentColor}>
|
||||
Reset
|
||||
{t("settingsReset")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
@@ -129,24 +132,48 @@ export default function SettingsView({
|
||||
</Box>
|
||||
|
||||
<Typography variant="caption" sx={{ color: "text.secondary", display: "block", mt: 1 }}>
|
||||
Saved per user on this browser.
|
||||
{t("settingsSavedPerUser")}
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography sx={{ fontWeight: 950, mb: 1 }}>Jobs</Typography>
|
||||
<Typography sx={{ fontWeight: 950, mb: 1 }}>{t("settingsLanguageTitle")}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mb: 2 }}>
|
||||
{t("settingsLanguageBody")}
|
||||
</Typography>
|
||||
|
||||
<FormControl fullWidth sx={{ mb: 2 }}>
|
||||
<InputLabel id="language-label">{t("settingsPreferredLanguage")}</InputLabel>
|
||||
<Select
|
||||
labelId="language-label"
|
||||
value={language}
|
||||
label={t("settingsPreferredLanguage")}
|
||||
onChange={(e) => setLanguage(e.target.value as "en" | "no")}
|
||||
>
|
||||
<MenuItem value="en">{t("settingsEnglish")}</MenuItem>
|
||||
<MenuItem value="no">{t("settingsNorwegian")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Typography variant="caption" sx={{ color: "text.secondary" }}>
|
||||
{t("settingsMorePagesSoon")}
|
||||
</Typography>
|
||||
</Paper>
|
||||
|
||||
<Paper sx={{ p: 2, gridColumn: { xs: "1 / -1", md: "1 / -1" } }}>
|
||||
<Typography sx={{ fontWeight: 950, mb: 1 }}>{t("settingsJobs")}</Typography>
|
||||
|
||||
<Box sx={{ display: "flex", gap: 3, flexWrap: "wrap" }}>
|
||||
<Box sx={{ minWidth: 240 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
Pagination
|
||||
{t("settingsPagination")}
|
||||
</Typography>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="page-size-label">Rows per page</InputLabel>
|
||||
<InputLabel id="page-size-label">{t("settingsRowsPerPage")}</InputLabel>
|
||||
<Select
|
||||
labelId="page-size-label"
|
||||
value={pageSize}
|
||||
label="Rows per page"
|
||||
label={t("settingsRowsPerPage")}
|
||||
onChange={(e) => onPageSizeChange(e.target.value as 15 | 20 | 25)}
|
||||
>
|
||||
<MenuItem value={15}>15</MenuItem>
|
||||
@@ -158,14 +185,14 @@ export default function SettingsView({
|
||||
|
||||
<Box sx={{ minWidth: 240 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
Columns
|
||||
{t("settingsColumns")}
|
||||
</Typography>
|
||||
{(
|
||||
[
|
||||
["status", "Status"],
|
||||
["dateApplied", "Date applied"],
|
||||
["daysSince", "Days"],
|
||||
["jobUrl", "Job URL"],
|
||||
["status", t("settingsColumnStatus")],
|
||||
["dateApplied", t("settingsColumnDateApplied")],
|
||||
["daysSince", t("settingsColumnDays")],
|
||||
["jobUrl", t("settingsColumnJobUrl")],
|
||||
] as const
|
||||
).map(([key, label]) => (
|
||||
<FormControlLabel
|
||||
@@ -193,9 +220,9 @@ export default function SettingsView({
|
||||
|
||||
<TabPanel value={tab} index={2}>
|
||||
<Paper sx={{ p: 2, mt: 2 }}>
|
||||
<Typography sx={{ fontWeight: 950, mb: 0.5 }}>Email notifications</Typography>
|
||||
<Typography sx={{ fontWeight: 950, mb: 0.5 }}>{t("settingsNotificationsTitle")}</Typography>
|
||||
<Typography sx={{ color: "text.secondary" }}>
|
||||
Notifications are sent via SMTP (Gmail works). Configure SMTP in the API (`Email:*` settings or env vars like `EMAIL_SMTP_HOST`).
|
||||
{t("settingsNotificationsBody")}
|
||||
</Typography>
|
||||
</Paper>
|
||||
</TabPanel>
|
||||
|
||||
Reference in New Issue
Block a user