feat(ui): establish Settings page layout primitives
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Alert, Box, Button, FormControlLabel, Paper, Skeleton, Switch, Typography } from "@mui/material";
|
||||
import { Alert, Box, Button, FormControlLabel, Skeleton, Switch, Typography } from "@mui/material";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
import { api } from "../api";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { useToast } from "../toast";
|
||||
@@ -46,9 +47,7 @@ export default function AiPrivacySettingsCard() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2.5, borderRadius: 4, border: "none" }}>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary", fontWeight: 800 }}>{t("settingsAiPrivacyTitle")}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mt: 0.25, mb: 1.5 }}>{t("settingsAiPrivacyBody")}</Typography>
|
||||
<PageSection surface title={t("settingsAiPrivacyTitle")} description={t("settingsAiPrivacyBody")}>
|
||||
{failed ? <Alert severity="error" sx={{ mb: 1.5 }}>{t("settingsAiPrivacyUnavailable")}</Alert> : null}
|
||||
{!settings ? <Skeleton variant="rounded" height={96} /> : <Box sx={{ display: "grid", gap: 1 }}>
|
||||
<FormControlLabel
|
||||
@@ -70,6 +69,6 @@ export default function AiPrivacySettingsCard() {
|
||||
</Typography>
|
||||
<Box><Button variant="contained" disabled={saving} onClick={() => void save()}>{t("settingsAiSave")}</Button></Box>
|
||||
</Box>}
|
||||
</Paper>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Alert, Box, Button, LinearProgress, Paper, Skeleton, Stack, Typography } from "@mui/material";
|
||||
import { Alert, Box, Button, LinearProgress, Skeleton, Typography } from "@mui/material";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
import { api } from "../api";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
|
||||
@@ -59,11 +60,7 @@ export default function AiUsageCard() {
|
||||
const storagePercent = Math.min(100, usage.storageUsedBytes / usage.storageLimitBytes * 100);
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2.5, borderRadius: 4, border: "none" }}>
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="baseline" sx={{ mb: 2 }}>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary", fontWeight: 800 }}>{t("settingsUsageTitle")}</Typography>
|
||||
<Typography variant="caption" sx={{ textTransform: "capitalize", fontWeight: 700 }}>{t("settingsUsagePlan", { plan: usage.plan })}</Typography>
|
||||
</Stack>
|
||||
<PageSection surface title={t("settingsUsageTitle")} actions={<Typography variant="caption" sx={{ textTransform: "capitalize", fontWeight: 700 }}>{t("settingsUsagePlan", { plan: usage.plan })}</Typography>}>
|
||||
{usage.monthlyCallLimit === 0 ? <Alert severity="info" sx={{ mb: 2 }}>{t("settingsUsageNoAi")}</Alert> : <><Box sx={{ mb: 2 }}>
|
||||
<Typography variant="body2">{t("settingsUsageGenerations", { used: usage.currentMonth.calls.toLocaleString(), limit: usage.monthlyCallLimit.toLocaleString() })}</Typography>
|
||||
<LinearProgress variant="determinate" value={callsPercent} aria-label={t("settingsUsageGenerationsProgress")} sx={{ mt: 0.75, height: 7, borderRadius: 99 }} />
|
||||
@@ -85,6 +82,6 @@ export default function AiUsageCard() {
|
||||
{t("settingsBillingNotConfigured")}
|
||||
</Typography>
|
||||
) : null}
|
||||
</Paper>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { Alert, Box, Button, Divider, Paper, Typography } from "@mui/material";
|
||||
import { Alert, Box, Button, Divider, Typography } from "@mui/material";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { clearAuthClientState } from "../auth";
|
||||
import { useToast } from "../toast";
|
||||
@@ -95,10 +96,7 @@ export default function BackupCard() {
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 2, p: 2 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
{t("backupTitle")}
|
||||
</Typography>
|
||||
<PageSection surface title={t("backupTitle")}>
|
||||
<Typography sx={{ color: "text.secondary", mb: 1 }}>
|
||||
{t("accountExportBody")}
|
||||
</Typography>
|
||||
@@ -109,7 +107,7 @@ export default function BackupCard() {
|
||||
</Box>
|
||||
<Alert severity="info" sx={{ mt: 1.5 }}>{t("accountExportRecentSignIn")}</Alert>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 0.5 }}>{t("backupEncryptedTitle")}</Typography>
|
||||
<Typography component="h3" variant="sectionTitle">{t("backupEncryptedTitle")}</Typography>
|
||||
<Typography sx={{ color: "text.secondary", mb: 1 }}>
|
||||
{t("backupBody")}
|
||||
</Typography>
|
||||
@@ -119,7 +117,7 @@ export default function BackupCard() {
|
||||
</Button>
|
||||
</Box>
|
||||
<Divider sx={{ my: 2 }} />
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 800, mb: 0.5 }}>{t("accountDeleteTitle")}</Typography>
|
||||
<Typography component="h3" variant="sectionTitle">{t("accountDeleteTitle")}</Typography>
|
||||
{lifecycle?.deletionStatus === "pending" ? (
|
||||
<Alert severity="warning">{t("accountDeletePending", { stage: lifecycle.request?.stage ?? "pending" })}</Alert>
|
||||
) : lifecycle?.deletionEnabled ? (
|
||||
@@ -132,6 +130,6 @@ export default function BackupCard() {
|
||||
) : (
|
||||
<Alert severity="info">{t("accountDeleteDisabled")}</Alert>
|
||||
)}
|
||||
</Paper>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useMemo, useState } from "react";
|
||||
|
||||
import { Box, Button, Paper, Typography } from "@mui/material";
|
||||
import { Box, Button, Typography } from "@mui/material";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
|
||||
import { api } from "../api";
|
||||
import { Company, JobApplication } from "../types";
|
||||
@@ -248,13 +249,7 @@ export default function ImportExportJobs() {
|
||||
const helper = useMemo(() => t("importExportBody"), [t]);
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 2, p: 2 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
{t("importExportTitle")}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mb: 2 }}>
|
||||
{helper}
|
||||
</Typography>
|
||||
<PageSection level="h3" title={t("importExportTitle")} description={helper}>
|
||||
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", alignItems: "center" }}>
|
||||
<Button variant="outlined" onClick={() => void exportAll("json")}>{t("exportJson")}</Button>
|
||||
@@ -276,6 +271,6 @@ export default function ImportExportJobs() {
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Paper>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useRef } from "react";
|
||||
|
||||
import { Box, Paper, TextField, Typography } from "@mui/material";
|
||||
import { Box, TextField, Typography } from "@mui/material";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { useToast } from "../toast";
|
||||
@@ -24,9 +25,7 @@ export default function QuickCaptureCard() {
|
||||
}, [bookmarklet]);
|
||||
|
||||
return (
|
||||
<Paper sx={{ p: 2 }}>
|
||||
<Typography sx={{ fontWeight: 950, mb: 0.5 }}>{t("settingsQuickCaptureTitle")}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>{t("settingsQuickCaptureSubtitle")}</Typography>
|
||||
<PageSection surface title={t("settingsQuickCaptureTitle")} description={t("settingsQuickCaptureSubtitle")}>
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 1.5, flexWrap: "wrap", mb: 1.5 }}>
|
||||
<Box
|
||||
@@ -64,6 +63,6 @@ export default function QuickCaptureCard() {
|
||||
InputProps={{ readOnly: true }}
|
||||
onFocus={(e) => e.target.select()}
|
||||
/>
|
||||
</Paper>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
import { Alert, Box, Button, Paper, TextField, Typography } from "@mui/material";
|
||||
import { Alert, Box, Button, TextField, Typography } from "@mui/material";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { useToast } from "../toast";
|
||||
@@ -50,12 +51,9 @@ export default function RulesSettingsCard() {
|
||||
|
||||
if (!s) {
|
||||
return (
|
||||
<Paper sx={{ mt: 2, p: 2 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
{t("rulesTitle")}
|
||||
</Typography>
|
||||
<PageSection surface title={t("rulesTitle")}>
|
||||
{loadError ? <Alert severity="error">{loadError}</Alert> : <Typography sx={{ color: "text.secondary" }}>{t("rulesLoading")}</Typography>}
|
||||
</Paper>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -68,13 +66,7 @@ export default function RulesSettingsCard() {
|
||||
});
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 2, p: 2 }}>
|
||||
<Typography variant="h6" sx={{ mb: 1 }}>
|
||||
{t("rulesTitle")}
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary", mb: 2 }}>
|
||||
{t("rulesBody")}
|
||||
</Typography>
|
||||
<PageSection surface title={t("rulesTitle")} description={t("rulesBody")}>
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr 1fr" }, gap: 2 }}>
|
||||
<TextField label={t("rulesAppliedFollowUpDays")} {...num("appliedFollowUpDays")} />
|
||||
@@ -93,6 +85,6 @@ export default function RulesSettingsCard() {
|
||||
{saving ? t("rulesSaving") : t("rulesSave")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Paper>
|
||||
</PageSection>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
FormControlLabel,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Paper,
|
||||
Select,
|
||||
Skeleton,
|
||||
Tab,
|
||||
@@ -30,6 +29,7 @@ import { ThemeModePref } from "../themePrefs";
|
||||
import { useI18n } from "../i18n/I18nProvider";
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { useToast } from "../toast";
|
||||
import { PageSection } from "../layout/PageLayout";
|
||||
|
||||
interface Props {
|
||||
pageSize: 15 | 20 | 25;
|
||||
@@ -42,17 +42,7 @@ interface Props {
|
||||
|
||||
function TabPanel({ value, index, children }: { value: number; index: number; children: React.ReactNode }) {
|
||||
if (value !== index) return null;
|
||||
return <Box sx={{ mt: 2.5 }}>{children}</Box>;
|
||||
}
|
||||
|
||||
function SectionCard({ title, subtitle, children }: { title: string; subtitle?: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<Paper sx={{ p: "var(--app-card-padding)", borderRadius: 4, border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)" }}>
|
||||
<Typography variant="overline" sx={{ color: "text.secondary", fontWeight: 800 }}>{title}</Typography>
|
||||
{subtitle ? <Typography variant="body2" sx={{ color: "text.secondary", mt: 0.25, mb: 1.5 }}>{subtitle}</Typography> : <Box sx={{ mb: 1.5 }} />}
|
||||
{children}
|
||||
</Paper>
|
||||
);
|
||||
return <Box sx={{ mt: "var(--app-section-gap)", minWidth: 0 }}>{children}</Box>;
|
||||
}
|
||||
|
||||
type NotificationPrefs = {
|
||||
@@ -97,7 +87,7 @@ export default function SettingsView({
|
||||
};
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 0, p: "var(--app-card-padding)", borderRadius: 4, border: "none", boxShadow: "0px 1px 2px 0px rgba(15,23,42,0.04), 0px 8px 24px -12px rgba(15,23,42,0.12)", maxWidth: 960, mx: "auto" }}>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<FormControl fullWidth sx={{ display: { xs: "flex", sm: "none" }, mb: 1 }}>
|
||||
<InputLabel id="settings-section-label">{t("settingsNavigation")}</InputLabel>
|
||||
<Select
|
||||
@@ -113,7 +103,7 @@ export default function SettingsView({
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
<Tabs value={tab} onChange={(_, v) => setTab(v)} variant="scrollable" scrollButtons="auto" sx={{ display: { xs: "none", sm: "flex" }, mb: 1 }}>
|
||||
<Tabs aria-label={t("settings")} value={tab} onChange={(_, v) => setTab(v)} variant="scrollable" scrollButtons="auto" sx={{ display: { xs: "none", sm: "flex" }, mb: 1 }}>
|
||||
<Tab label={t("settingsTabGeneral")} />
|
||||
<Tab label={t("settingsTabFollowUps")} />
|
||||
<Tab label={t("settingsTabNotifications")} />
|
||||
@@ -121,9 +111,9 @@ export default function SettingsView({
|
||||
</Tabs>
|
||||
|
||||
<TabPanel value={tab} index={0}>
|
||||
<Box sx={{ display: "grid", gap: 2.5 }}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2.5 }}>
|
||||
<SectionCard title={t("settingsAppearance")}>
|
||||
<Box sx={{ display: "grid", gap: "var(--app-section-gap)", minWidth: 0 }}>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "minmax(0, 1fr)", md: "repeat(2, minmax(0, 1fr))" }, gap: "var(--app-section-gap)" }}>
|
||||
<PageSection surface title={t("settingsAppearance")}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="theme-mode-label">{t("settingsTheme")}</InputLabel>
|
||||
<Select
|
||||
@@ -137,9 +127,9 @@ export default function SettingsView({
|
||||
<MenuItem value="light">{t("settingsThemeLight")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</SectionCard>
|
||||
</PageSection>
|
||||
|
||||
<SectionCard title={t("settingsLanguageTitle")} subtitle={t("settingsLanguageBody")}>
|
||||
<PageSection surface title={t("settingsLanguageTitle")} description={t("settingsLanguageBody")}>
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="language-label">{t("settingsPreferredLanguage")}</InputLabel>
|
||||
<Select
|
||||
@@ -152,13 +142,13 @@ export default function SettingsView({
|
||||
<MenuItem value="nb">{t("settingsNorwegian")}</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</SectionCard>
|
||||
</PageSection>
|
||||
</Box>
|
||||
|
||||
<SectionCard title={t("settingsJobs")}>
|
||||
<PageSection surface title={t("settingsJobs")}>
|
||||
<Box sx={{ display: "flex", gap: 4, flexWrap: "wrap" }}>
|
||||
<Box sx={{ minWidth: 240 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 700 }}>
|
||||
<Box sx={{ flex: "1 1 15rem", minWidth: 0 }}>
|
||||
<Typography component="h3" variant="subtitle2" sx={{ mb: 1, fontWeight: 700 }}>
|
||||
{t("settingsPagination")}
|
||||
</Typography>
|
||||
<FormControl fullWidth>
|
||||
@@ -176,8 +166,8 @@ export default function SettingsView({
|
||||
</FormControl>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ minWidth: 240 }}>
|
||||
<Typography variant="subtitle2" sx={{ mb: 1, fontWeight: 700 }}>
|
||||
<Box sx={{ flex: "1 1 15rem", minWidth: 0 }}>
|
||||
<Typography component="h3" variant="subtitle2" sx={{ mb: 1, fontWeight: 700 }}>
|
||||
{t("settingsColumns")}
|
||||
</Typography>
|
||||
{(
|
||||
@@ -200,14 +190,13 @@ export default function SettingsView({
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<ImportExportJobs />
|
||||
</Box>
|
||||
</SectionCard>
|
||||
</PageSection>
|
||||
|
||||
<AiPrivacySettingsCard />
|
||||
<AiUsageCard />
|
||||
<QuickCaptureCard />
|
||||
<SectionCard title={t("connectedAccounts")} subtitle={t("settingsConnectedAccountsBody")}>
|
||||
<Button variant="outlined" onClick={() => navigate("/settings/connected-accounts")}>{t("settingsManageConnectedAccounts")}</Button>
|
||||
</SectionCard>
|
||||
<PageSection surface title={t("connectedAccounts")} description={t("settingsConnectedAccountsBody")}
|
||||
actions={<Button variant="outlined" onClick={() => navigate("/settings/connected-accounts")}>{t("settingsManageConnectedAccounts")}</Button>} />
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
@@ -216,7 +205,7 @@ export default function SettingsView({
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tab} index={2}>
|
||||
<SectionCard title={t("settingsNotificationsTitle")} subtitle={t("settingsNotificationsBody")}>
|
||||
<PageSection surface title={t("settingsNotificationsTitle")} description={t("settingsNotificationsBody")}>
|
||||
{notificationError ? <Alert severity="error" sx={{ mb: 1.5 }}>{notificationError}</Alert> : null}
|
||||
{!notificationPrefs ? <Skeleton variant="rounded" height={70} /> : <Box sx={{ display: "grid", gap: 1 }}>
|
||||
<FormControlLabel
|
||||
@@ -232,12 +221,12 @@ export default function SettingsView({
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1.5 }}>
|
||||
<Button variant="outlined" onClick={() => navigate("/reminders")}>{t("settingsOpenReminderInbox")}</Button>
|
||||
</Box>
|
||||
</SectionCard>
|
||||
</PageSection>
|
||||
</TabPanel>
|
||||
|
||||
<TabPanel value={tab} index={3}>
|
||||
<BackupCard />
|
||||
</TabPanel>
|
||||
</Paper>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user