Polish UI, harden company creation, and add error pages
This commit is contained in:
@@ -5,12 +5,13 @@ import {
|
||||
Autocomplete,
|
||||
Box,
|
||||
Button,
|
||||
Checkbox,
|
||||
Chip,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Divider,
|
||||
FormControlLabel,
|
||||
IconButton,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
@@ -19,7 +20,10 @@ import {
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
|
||||
import { api } from "../api";
|
||||
import CloseIcon from "@mui/icons-material/Close";
|
||||
import UploadFileOutlinedIcon from "@mui/icons-material/UploadFileOutlined";
|
||||
|
||||
import { api, getApiErrorMessage } from "../api";
|
||||
import { Company, JobImportResult } from "../types";
|
||||
import { invalidateCompaniesCache, useCompanies } from "../hooks/useCompanies";
|
||||
import { useToast } from "../toast";
|
||||
@@ -47,19 +51,43 @@ type DuplicateCheckResult = {
|
||||
matches: DuplicateCandidate[];
|
||||
};
|
||||
|
||||
type CreatedJobResponse = {
|
||||
id?: number;
|
||||
};
|
||||
|
||||
type AttachmentBucketKey = "resume" | "coverLetter" | "portfolio" | "other";
|
||||
type AttachmentBuckets = Record<AttachmentBucketKey, File[]>;
|
||||
|
||||
const STATUS_OPTIONS = ["Applied", "Waiting", "Interview", "Offer", "Rejected", "Ghosted"] as const;
|
||||
const ACCEPTED_DOCUMENT_TYPES = ".pdf,.doc,.docx,.txt,.md,image/*,application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain,text/markdown";
|
||||
|
||||
function getTodayIso() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
function emptyAttachmentBuckets(): AttachmentBuckets {
|
||||
return {
|
||||
resume: [],
|
||||
coverLetter: [],
|
||||
portfolio: [],
|
||||
other: [],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeLanguage(value?: string | null) {
|
||||
const raw = (value || "").trim().toLowerCase();
|
||||
if (!raw) return "";
|
||||
if (["en", "eng", "english"].includes(raw)) return "en";
|
||||
if (["no", "nb", "nn", "norwegian", "norwegian bokmål", "bokmal", "bokmål"].includes(raw)) return "no";
|
||||
return raw;
|
||||
}
|
||||
|
||||
export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
const { toast } = useToast();
|
||||
const { t } = useI18n();
|
||||
const { t, language } = useI18n();
|
||||
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [importing, setImporting] = useState(false);
|
||||
const [saveAndAddAnother, setSaveAndAddAnother] = useState(false);
|
||||
const [duplicateCheck, setDuplicateCheck] = useState<DuplicateCheckResult | null>(null);
|
||||
|
||||
const { companies: cachedCompanies } = useCompanies();
|
||||
@@ -75,8 +103,6 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
const [status, setStatus] = useState<(typeof STATUS_OPTIONS)[number]>("Applied");
|
||||
const [location, setLocation] = useState("");
|
||||
const [salary, setSalary] = useState("");
|
||||
const [nextAction, setNextAction] = useState("");
|
||||
const [followUpAt, setFollowUpAt] = useState("");
|
||||
const [jobUrl, setJobUrl] = useState("");
|
||||
const [deadline, setDeadline] = useState("");
|
||||
|
||||
@@ -85,14 +111,8 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
const [descriptionLanguage, setDescriptionLanguage] = useState("");
|
||||
|
||||
const [tags, setTags] = useState<string[]>([]);
|
||||
|
||||
const [notes, setNotes] = useState("");
|
||||
const [coverLetter, setCoverLetter] = useState("");
|
||||
|
||||
const [hasResume, setHasResume] = useState(false);
|
||||
const [hasCoverLetter, setHasCoverLetter] = useState(false);
|
||||
const [hasPortfolio, setHasPortfolio] = useState(false);
|
||||
const [hasOtherAttachment, setHasOtherAttachment] = useState(false);
|
||||
const [attachments, setAttachments] = useState<AttachmentBuckets>(() => emptyAttachmentBuckets());
|
||||
|
||||
useEffect(() => {
|
||||
setCompanies(cachedCompanies);
|
||||
@@ -108,8 +128,6 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
setStatus("Applied");
|
||||
setLocation("");
|
||||
setSalary("");
|
||||
setNextAction("");
|
||||
setFollowUpAt("");
|
||||
setJobUrl("");
|
||||
setDeadline("");
|
||||
setDescription("");
|
||||
@@ -117,11 +135,7 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
setDescriptionLanguage("");
|
||||
setTags([]);
|
||||
setNotes("");
|
||||
setCoverLetter("");
|
||||
setHasResume(false);
|
||||
setHasCoverLetter(false);
|
||||
setHasPortfolio(false);
|
||||
setHasOtherAttachment(false);
|
||||
setAttachments(emptyAttachmentBuckets());
|
||||
setDuplicateCheck(null);
|
||||
};
|
||||
|
||||
@@ -133,6 +147,10 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
|
||||
const selectedCompanyId = company?.id ?? matchingCompany?.id ?? 0;
|
||||
const showNewCompanyFields = !company && !!normalizedCompanyName && !matchingCompany;
|
||||
const preferredLanguage = normalizeLanguage(language);
|
||||
const sourceLanguage = normalizeLanguage(descriptionLanguage);
|
||||
const shouldShowTranslatedDescription = Boolean(sourceLanguage && preferredLanguage && sourceLanguage !== preferredLanguage);
|
||||
const attachmentCount = Object.values(attachments).reduce((sum, files) => sum + files.length, 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
@@ -180,8 +198,8 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
setNewCompanyLocation("");
|
||||
setNewCompanySource("");
|
||||
return res.data;
|
||||
} catch {
|
||||
toast("Failed to create company.", "error");
|
||||
} catch (error: any) {
|
||||
toast(getApiErrorMessage(error, t("addJobModalFailedCreateCompany")), "error");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
@@ -189,7 +207,7 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
const importFromUrl = async () => {
|
||||
if (importing) return;
|
||||
if (!jobUrl.trim()) {
|
||||
toast("Paste a job URL first.", "warning");
|
||||
toast(t("addJobModalPasteUrlFirst"), "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -197,7 +215,7 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
try {
|
||||
const res = await api.post<JobImportResult>("/jobimport/preview", { url: jobUrl.trim() });
|
||||
const r = res.data;
|
||||
if (!r?.success) throw new Error(r?.error || "Import failed");
|
||||
if (!r?.success) throw new Error(r?.error || t("addJobModalImportFailed"));
|
||||
|
||||
if (r.title) setJobTitle(r.title);
|
||||
if (r.location) setLocation(r.location);
|
||||
@@ -217,15 +235,28 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
setTags(r.tags || []);
|
||||
setDeadline(r.deadline ? r.deadline.slice(0, 10) : "");
|
||||
|
||||
toast("Imported.", "success");
|
||||
toast(t("addJobModalImported"), "success");
|
||||
} catch (e: any) {
|
||||
toast(e?.message || "Import failed.", "error");
|
||||
toast(e?.message || t("addJobModalImportFailed"), "error");
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const createJob = async () => {
|
||||
const uploadAttachments = async (jobId: number) => {
|
||||
const files = Object.values(attachments).flat();
|
||||
if (!files.length) return;
|
||||
|
||||
const data = new FormData();
|
||||
files.forEach((file) => data.append("files", file));
|
||||
data.append("jobId", String(jobId));
|
||||
|
||||
await api.post("/attachments", data, {
|
||||
headers: { "Content-Type": "multipart/form-data" },
|
||||
});
|
||||
};
|
||||
|
||||
const createJob = async (addAnother = false) => {
|
||||
if (saving) return;
|
||||
|
||||
setSaving(true);
|
||||
@@ -235,41 +266,53 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
selectedCompany = await createCompany();
|
||||
}
|
||||
if (!selectedCompany) {
|
||||
toast("Select or create a company.", "warning");
|
||||
toast(t("addJobModalSelectCompany"), "warning");
|
||||
return;
|
||||
}
|
||||
|
||||
await api.post("/jobapplications", {
|
||||
const response = await api.post<CreatedJobResponse>("/jobapplications", {
|
||||
jobTitle,
|
||||
companyId: selectedCompany.id,
|
||||
status,
|
||||
location,
|
||||
salary,
|
||||
nextAction,
|
||||
followUpAt: followUpAt || null,
|
||||
nextAction: null,
|
||||
followUpAt: null,
|
||||
jobUrl,
|
||||
description: description || null,
|
||||
translatedDescription: translatedDescription || null,
|
||||
translatedDescription: shouldShowTranslatedDescription ? translatedDescription || null : null,
|
||||
descriptionLanguage: descriptionLanguage || null,
|
||||
tags: tags.length ? JSON.stringify(tags) : null,
|
||||
deadline: deadline || null,
|
||||
notes,
|
||||
coverLetterText: coverLetter,
|
||||
coverLetterText: null,
|
||||
dateApplied,
|
||||
hasResume,
|
||||
hasCoverLetter,
|
||||
hasPortfolio,
|
||||
hasOtherAttachment,
|
||||
hasResume: attachments.resume.length > 0,
|
||||
hasCoverLetter: attachments.coverLetter.length > 0,
|
||||
hasPortfolio: attachments.portfolio.length > 0,
|
||||
hasOtherAttachment: attachments.other.length > 0,
|
||||
});
|
||||
|
||||
if (response.data?.id && attachmentCount > 0) {
|
||||
try {
|
||||
await uploadAttachments(response.data.id);
|
||||
toast(t("addJobModalJobAndFilesAdded"), "success");
|
||||
} catch {
|
||||
toast(t("addJobModalJobCreatedUploadFailed"), "warning");
|
||||
}
|
||||
} else if (attachmentCount > 0) {
|
||||
toast(t("addJobModalJobCreatedFilesNotAttached"), "warning");
|
||||
} else {
|
||||
toast(t("addJobModalJobAdded"), "success");
|
||||
}
|
||||
|
||||
onCreated();
|
||||
toast("Job added.", "success");
|
||||
resetForm();
|
||||
if (!saveAndAddAnother) {
|
||||
if (!addAnother) {
|
||||
onClose();
|
||||
}
|
||||
} catch {
|
||||
toast("Failed to add job.", "error");
|
||||
toast(t("addJobModalFailedAddJob"), "error");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -277,12 +320,64 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
|
||||
const canSave = normalizedCompanyName.length > 0 && jobTitle.trim().length > 0;
|
||||
|
||||
const setFilesForBucket = (bucket: AttachmentBucketKey, files: FileList | null) => {
|
||||
setAttachments((prev) => ({
|
||||
...prev,
|
||||
[bucket]: files ? Array.from(files) : [],
|
||||
}));
|
||||
};
|
||||
|
||||
const statusLabel = (value: typeof STATUS_OPTIONS[number]) => {
|
||||
const map = {
|
||||
Applied: t("statusApplied"),
|
||||
Waiting: t("statusWaiting"),
|
||||
Interview: t("statusInterview"),
|
||||
Offer: t("statusOffer"),
|
||||
Rejected: t("statusRejected"),
|
||||
Ghosted: t("statusGhosted"),
|
||||
} as const;
|
||||
return map[value];
|
||||
};
|
||||
|
||||
const filesLabel = (files: File[]) => {
|
||||
if (files.length === 0) return t("addJobModalNoFilesSelected");
|
||||
if (files.length === 1) return files[0].name;
|
||||
return t("addJobModalFilesSelected", { count: files.length });
|
||||
};
|
||||
|
||||
const uploadField = (
|
||||
bucket: AttachmentBucketKey,
|
||||
label: string,
|
||||
helperText: string,
|
||||
) => (
|
||||
<Box sx={{ p: 1.5, borderRadius: 2, border: "1px solid", borderColor: "divider", backgroundColor: "background.paper" }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 1, flexWrap: "wrap" }}>
|
||||
<Box>
|
||||
<Typography sx={{ fontWeight: 800 }}>{label}</Typography>
|
||||
<Typography variant="body2" sx={{ color: "text.secondary" }}>{helperText}</Typography>
|
||||
</Box>
|
||||
<Button component="label" variant="outlined" size="small" startIcon={<UploadFileOutlinedIcon />}>
|
||||
{t("addJobModalChooseFiles")}
|
||||
<input hidden type="file" multiple accept={ACCEPTED_DOCUMENT_TYPES} onChange={(e) => setFilesForBucket(bucket, e.target.files)} />
|
||||
</Button>
|
||||
</Box>
|
||||
<Typography variant="caption" sx={{ display: "block", mt: 1, color: "text.secondary" }}>
|
||||
{filesLabel(attachments[bucket])}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} fullWidth maxWidth="md">
|
||||
<DialogTitle>{t("addJob")}</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="overline" sx={{ display: "block", mt: 1 }}>
|
||||
Company
|
||||
<DialogTitle sx={{ pr: 6 }}>
|
||||
{t("addJob")}
|
||||
<IconButton aria-label={t("close")} onClick={onClose} sx={{ position: "absolute", right: 12, top: 12 }}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</DialogTitle>
|
||||
<DialogContent dividers>
|
||||
<Typography variant="overline" sx={{ display: "block", mt: 0.5 }}>
|
||||
{t("addJobModalCompanySection")}
|
||||
</Typography>
|
||||
|
||||
<Autocomplete<Company, false, false, true>
|
||||
@@ -312,12 +407,12 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
/>
|
||||
|
||||
{showNewCompanyFields ? (
|
||||
<Box sx={{ mt: 1, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2 }}>
|
||||
<TextField label="Company location" value={newCompanyLocation} onChange={(e) => setNewCompanyLocation(e.target.value)} />
|
||||
<TextField label="Company source" value={newCompanySource} onChange={(e) => setNewCompanySource(e.target.value)} />
|
||||
<Box sx={{ mt: 1, display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2 }}>
|
||||
<TextField label={t("addJobModalCompanyLocation")} value={newCompanyLocation} onChange={(e) => setNewCompanyLocation(e.target.value)} />
|
||||
<TextField label={t("addJobModalCompanySource")} value={newCompanySource} onChange={(e) => setNewCompanySource(e.target.value)} />
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Button variant="outlined" onClick={() => void createCompany()}>
|
||||
Create "{normalizedCompanyName}"
|
||||
{t("addJobModalCreateCompany", { name: normalizedCompanyName })}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
@@ -325,7 +420,7 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
|
||||
{duplicateCheck?.hasDuplicates ? (
|
||||
<Alert severity="warning" sx={{ mt: 2 }}>
|
||||
<Typography sx={{ fontWeight: 800, mb: 0.75 }}>Possible duplicates found</Typography>
|
||||
<Typography sx={{ fontWeight: 800, mb: 0.75 }}>{t("addJobModalPossibleDuplicates")}</Typography>
|
||||
<List dense sx={{ py: 0 }}>
|
||||
{duplicateCheck.matches.map((match) => (
|
||||
<ListItem key={match.id} sx={{ px: 0 }}>
|
||||
@@ -342,68 +437,96 @@ export default function AddJobModal({ open, onClose, onCreated }: Props) {
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Typography variant="overline" sx={{ display: "block" }}>
|
||||
Job application
|
||||
{t("addJobModalJobApplicationSection")}
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2, mt: 1 }}>
|
||||
<TextField label="Job URL" value={jobUrl} onChange={(e) => setJobUrl(e.target.value)} sx={{ gridColumn: "1 / -1" }} />
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 2, mt: 1 }}>
|
||||
<TextField label={t("addJobModalJobUrl")} value={jobUrl} onChange={(e) => setJobUrl(e.target.value)} sx={{ gridColumn: "1 / -1" }} />
|
||||
<Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "flex-end" }}>
|
||||
<Button onClick={() => void importFromUrl()} disabled={importing || !jobUrl.trim()}>
|
||||
{importing ? "Importing..." : "Import from URL"}
|
||||
{importing ? t("addJobModalImporting") : t("addJobModalImportFromUrl")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<TextField label="Date applied" type="date" value={dateApplied} onChange={(e) => setDateApplied(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
<TextField label={t("addJobModalDateApplied")} type="date" value={dateApplied} onChange={(e) => setDateApplied(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
|
||||
<TextField select label="Status" value={status} onChange={(e) => setStatus(e.target.value as any)}>
|
||||
<TextField select label={t("addJobModalStatus")} value={status} onChange={(e) => setStatus(e.target.value as any)}>
|
||||
{STATUS_OPTIONS.map((s) => (
|
||||
<MenuItem key={s} value={s}>
|
||||
{s}
|
||||
{statusLabel(s)}
|
||||
</MenuItem>
|
||||
))}
|
||||
</TextField>
|
||||
|
||||
<TextField label="Job title" value={jobTitle} onChange={(e) => setJobTitle(e.target.value)} />
|
||||
<TextField label={t("addJobModalJobTitle")} value={jobTitle} onChange={(e) => setJobTitle(e.target.value)} />
|
||||
<TextField label={t("location")} value={location} onChange={(e) => setLocation(e.target.value)} />
|
||||
|
||||
<TextField label="Salary" value={salary} onChange={(e) => setSalary(e.target.value)} />
|
||||
<TextField label="Next action" value={nextAction} onChange={(e) => setNextAction(e.target.value)} />
|
||||
|
||||
<TextField label="Follow up" type="date" value={followUpAt} onChange={(e) => setFollowUpAt(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
|
||||
<TextField label="Deadline" type="date" value={deadline} onChange={(e) => setDeadline(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
<TextField label={t("addJobModalSalary")} value={salary} onChange={(e) => setSalary(e.target.value)} />
|
||||
<TextField label={t("addJobModalDeadline")} type="date" value={deadline} onChange={(e) => setDeadline(e.target.value)} InputLabelProps={{ shrink: true }} />
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<TagsInput value={tags} onChange={setTags} />
|
||||
</Box>
|
||||
|
||||
<TextField label="Description (original)" multiline rows={6} value={description} onChange={(e) => setDescription(e.target.value)} helperText={`${description.length} characters`} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label="Translated description" multiline rows={6} value={translatedDescription} onChange={(e) => setTranslatedDescription(e.target.value)} helperText={`${translatedDescription.length} characters`} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label="Description language (optional)" value={descriptionLanguage} onChange={(e) => setDescriptionLanguage(e.target.value)} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label="Notes" multiline rows={3} value={notes} onChange={(e) => setNotes(e.target.value)} helperText={`${notes.length} characters`} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField label="Cover letter" multiline rows={6} value={coverLetter} onChange={(e) => setCoverLetter(e.target.value)} helperText={`${coverLetter.length} characters`} sx={{ gridColumn: "1 / -1" }} />
|
||||
<TextField
|
||||
label={t("addJobModalDescriptionOriginal")}
|
||||
multiline
|
||||
rows={6}
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
helperText={`${description.length} characters`}
|
||||
sx={{ gridColumn: "1 / -1" }}
|
||||
/>
|
||||
|
||||
{shouldShowTranslatedDescription ? (
|
||||
<TextField
|
||||
label={t("addJobModalTranslatedDescription", { language: preferredLanguage.toUpperCase() })}
|
||||
multiline
|
||||
rows={6}
|
||||
value={translatedDescription}
|
||||
onChange={(e) => setTranslatedDescription(e.target.value)}
|
||||
helperText={`${translatedDescription.length} characters`}
|
||||
sx={{ gridColumn: "1 / -1" }}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
<TextField
|
||||
label={t("addJobModalDescriptionLanguage")}
|
||||
value={descriptionLanguage}
|
||||
onChange={(e) => setDescriptionLanguage(e.target.value)}
|
||||
helperText={shouldShowTranslatedDescription ? t("addJobModalTranslatedShown", { language: preferredLanguage.toUpperCase() }) : t("addJobModalTranslatedHidden")}
|
||||
sx={{ gridColumn: "1 / -1" }}
|
||||
/>
|
||||
|
||||
<TextField label={t("addJobModalNotes")} multiline rows={3} value={notes} onChange={(e) => setNotes(e.target.value)} helperText={`${notes.length} characters`} sx={{ gridColumn: "1 / -1" }} />
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Typography variant="overline" sx={{ display: "block", mt: 1 }}>Attachments checklist</Typography>
|
||||
<Box sx={{ display: "flex", gap: 2, flexWrap: "wrap" }}>
|
||||
<FormControlLabel control={<Checkbox checked={hasResume} onChange={(e) => setHasResume(e.target.checked)} />} label="Resume" />
|
||||
<FormControlLabel control={<Checkbox checked={hasCoverLetter} onChange={(e) => setHasCoverLetter(e.target.checked)} />} label="Cover letter" />
|
||||
<FormControlLabel control={<Checkbox checked={hasPortfolio} onChange={(e) => setHasPortfolio(e.target.checked)} />} label="Portfolio" />
|
||||
<FormControlLabel control={<Checkbox checked={hasOtherAttachment} onChange={(e) => setHasOtherAttachment(e.target.checked)} />} label="Other" />
|
||||
<Typography variant="overline" sx={{ display: "block", mb: 1 }}>{t("addJobModalDocuments")}</Typography>
|
||||
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5 }}>
|
||||
{uploadField("resume", t("addJobModalResume"), t("addJobModalResumeHelp"))}
|
||||
{uploadField("coverLetter", t("addJobModalCoverLetter"), t("addJobModalCoverLetterHelp"))}
|
||||
{uploadField("portfolio", t("addJobModalPortfolio"), t("addJobModalPortfolioHelp"))}
|
||||
{uploadField("other", t("addJobModalOtherFiles"), t("addJobModalOtherFilesHelp"))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1", display: "flex", alignItems: "center", justifyContent: "space-between", gap: 2, mt: 1, flexWrap: "wrap" }}>
|
||||
<FormControlLabel control={<Checkbox checked={saveAndAddAnother} onChange={(e) => setSaveAndAddAnother(e.target.checked)} />} label="Save and add another" />
|
||||
<Box sx={{ display: "flex", gap: 1 }}>
|
||||
<Button variant="outlined" onClick={onClose}>Cancel</Button>
|
||||
<Button variant="contained" onClick={() => void createJob()} disabled={saving || !canSave}>
|
||||
{saving ? "Adding..." : saveAndAddAnother ? "Save and continue" : "Add job"}
|
||||
</Button>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap", mt: 1.25 }}>
|
||||
<Chip size="small" variant="outlined" label={attachmentCount === 1 ? t("addJobModalFileReady", { count: attachmentCount }) : t("addJobModalFilesReady", { count: attachmentCount })} />
|
||||
<Chip size="small" variant="outlined" label={t("addJobModalPreferredFiles")} />
|
||||
<Chip size="small" variant="outlined" label={t("addJobModalTextImageAllowed")} />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ px: 3, py: 2, justifyContent: "space-between", flexWrap: "wrap", gap: 1.5 }}>
|
||||
<Button variant="outlined" onClick={onClose}>{t("close")}</Button>
|
||||
<Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
|
||||
<Button variant="outlined" onClick={() => void createJob(true)} disabled={saving || !canSave}>
|
||||
{saving ? t("rulesSaving") : t("createAndAddAnother")}
|
||||
</Button>
|
||||
<Button variant="contained" onClick={() => void createJob(false)} disabled={saving || !canSave}>
|
||||
{saving ? t("rulesSaving") : t("createJob")}
|
||||
</Button>
|
||||
</Box>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user