|
|
|
@@ -1,10 +1,11 @@
|
|
|
|
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
|
|
|
|
|
|
import { Accordion, AccordionDetails, AccordionSummary, Alert, Avatar, Box, Button, Chip, Divider, FormControl, InputLabel, LinearProgress, MenuItem, Paper, Select, TextField, Typography } from "@mui/material";
|
|
|
|
|
import { Accordion, AccordionDetails, AccordionSummary, Alert, Avatar, Box, Button, Chip, Dialog, DialogContent, DialogTitle, Divider, FormControl, IconButton, InputLabel, LinearProgress, MenuItem, Paper, Select, TextField, Typography } from "@mui/material";
|
|
|
|
|
|
|
|
|
|
import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
|
|
|
|
|
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
|
|
|
|
import PhotoCameraOutlinedIcon from "@mui/icons-material/PhotoCameraOutlined";
|
|
|
|
|
import ZoomInOutlinedIcon from "@mui/icons-material/ZoomInOutlined";
|
|
|
|
|
|
|
|
|
|
import { api } from "../api";
|
|
|
|
|
import GoogleAuthCard from "../components/GoogleAuthCard";
|
|
|
|
@@ -21,10 +22,11 @@ import {
|
|
|
|
|
StructuredCvFieldMetadata,
|
|
|
|
|
StructuredCvProfile,
|
|
|
|
|
} from "../profileCv";
|
|
|
|
|
import { JobApplication } from "../types";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type CvSectionOption = "Professional Summary" | "Core Skills" | "Experience Highlights" | "Selected Achievements" | "Projects";
|
|
|
|
|
type CvSectionStyle = "balanced" | "concise" | "impact" | "ats";
|
|
|
|
|
type CvSectionOption = "" | "Professional Summary" | "Core Skills" | "Experience Highlights" | "Selected Achievements" | "Projects";
|
|
|
|
|
type CvSectionStyle = "ats-minimal" | "harvard" | "auckland" | "edinburgh";
|
|
|
|
|
|
|
|
|
|
type ExtractionRun = {
|
|
|
|
|
id: number;
|
|
|
|
@@ -40,6 +42,24 @@ type ExtractionRun = {
|
|
|
|
|
errorMessage?: string;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type JobListResponse = {
|
|
|
|
|
items: JobApplication[];
|
|
|
|
|
total: number;
|
|
|
|
|
page: number;
|
|
|
|
|
pageSize: number;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type RewriteTemplateOption = {
|
|
|
|
|
id: CvSectionStyle;
|
|
|
|
|
title: string;
|
|
|
|
|
eyebrow: string;
|
|
|
|
|
accent: string;
|
|
|
|
|
blurb: string;
|
|
|
|
|
sampleHeading: string;
|
|
|
|
|
sampleMeta: string;
|
|
|
|
|
sampleBullets: string[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
type MeResponse = {
|
|
|
|
|
provider?: "local" | "google" | "external";
|
|
|
|
|
id?: string;
|
|
|
|
@@ -61,6 +81,48 @@ type MeResponse = {
|
|
|
|
|
|
|
|
|
|
const CV_UPLOAD_ACCEPT = ".pdf,.docx,.txt,.md,image/png,image/jpeg,image/webp,application/pdf,application/vnd.openxmlformats-officedocument.wordprocessingml.document,text/plain,text/markdown";
|
|
|
|
|
const AVATAR_UPLOAD_ACCEPT = "image/png,image/jpeg,image/webp";
|
|
|
|
|
const REWRITE_TEMPLATES: RewriteTemplateOption[] = [
|
|
|
|
|
{
|
|
|
|
|
id: "ats-minimal",
|
|
|
|
|
title: "ATS Minimal",
|
|
|
|
|
eyebrow: "Scanner-friendly",
|
|
|
|
|
accent: "#0f172a",
|
|
|
|
|
blurb: "Compact, direct, and easy for screening systems to parse.",
|
|
|
|
|
sampleHeading: "Senior Backend Engineer",
|
|
|
|
|
sampleMeta: "Acme Systems · Oslo · 2021 - Present",
|
|
|
|
|
sampleBullets: ["Built API workflows with measurable delivery outcomes.", "Kept skills and achievements easy to scan."]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "harvard",
|
|
|
|
|
title: "Harvard",
|
|
|
|
|
eyebrow: "Traditional",
|
|
|
|
|
accent: "#7f1d1d",
|
|
|
|
|
blurb: "Formal hierarchy and restrained tone for conservative hiring flows.",
|
|
|
|
|
sampleHeading: "Professional Summary",
|
|
|
|
|
sampleMeta: "Clear structure · precise dates · credible language",
|
|
|
|
|
sampleBullets: ["Emphasizes polished summaries.", "Works well for broad professional roles."]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "auckland",
|
|
|
|
|
title: "Auckland",
|
|
|
|
|
eyebrow: "Modern sidebar",
|
|
|
|
|
accent: "#0f766e",
|
|
|
|
|
blurb: "Sharper highlights with a more contemporary, design-forward rhythm.",
|
|
|
|
|
sampleHeading: "Selected Impact",
|
|
|
|
|
sampleMeta: "Focused strengths · compact highlights",
|
|
|
|
|
sampleBullets: ["Pulls skills into stronger highlight clusters.", "Good when you want a fresher feel."]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
id: "edinburgh",
|
|
|
|
|
title: "Edinburgh",
|
|
|
|
|
eyebrow: "Editorial",
|
|
|
|
|
accent: "#5b21b6",
|
|
|
|
|
blurb: "More personality and stronger section contrast without losing clarity.",
|
|
|
|
|
sampleHeading: "Experience Highlights",
|
|
|
|
|
sampleMeta: "Premium spacing · stronger visual voice",
|
|
|
|
|
sampleBullets: ["Useful when the CV should feel more distinctive.", "Still keeps wording grounded and factual."]
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
function initialsFrom(values: Array<string | undefined>) {
|
|
|
|
|
const joined = values.map((x) => (x ?? "").trim()).filter(Boolean);
|
|
|
|
@@ -142,10 +204,13 @@ export default function ProfilePage() {
|
|
|
|
|
const [headline, setHeadline] = useState("");
|
|
|
|
|
const [profileCvText, setProfileCvText] = useState("");
|
|
|
|
|
const [rewritingSection, setRewritingSection] = useState(false);
|
|
|
|
|
const [cvSection, setCvSection] = useState<CvSectionOption>("Professional Summary");
|
|
|
|
|
const [cvSectionStyle, setCvSectionStyle] = useState<CvSectionStyle>("balanced");
|
|
|
|
|
const [cvSection, setCvSection] = useState<CvSectionOption>("");
|
|
|
|
|
const [cvSectionStyle, setCvSectionStyle] = useState<CvSectionStyle>("ats-minimal");
|
|
|
|
|
const [cvSectionTargetRole, setCvSectionTargetRole] = useState("");
|
|
|
|
|
const [selectedRewriteJobId, setSelectedRewriteJobId] = useState<string>("");
|
|
|
|
|
const [cvSectionDraft, setCvSectionDraft] = useState("");
|
|
|
|
|
const [rewritePreviewTemplate, setRewritePreviewTemplate] = useState<RewriteTemplateOption | null>(null);
|
|
|
|
|
const [savedJobs, setSavedJobs] = useState<JobApplication[]>([]);
|
|
|
|
|
const [parsingCvSections, setParsingCvSections] = useState(false);
|
|
|
|
|
const [reprocessingCv, setReprocessingCv] = useState(false);
|
|
|
|
|
const [structuredCv, setStructuredCv] = useState<StructuredCvProfile>(emptyStructuredCv());
|
|
|
|
@@ -155,9 +220,10 @@ export default function ProfilePage() {
|
|
|
|
|
|
|
|
|
|
const loadProfile = useCallback(async () => {
|
|
|
|
|
try {
|
|
|
|
|
const [profileResponse, runsResponse] = await Promise.all([
|
|
|
|
|
const [profileResponse, runsResponse, jobsResponse] = await Promise.all([
|
|
|
|
|
api.get<MeResponse>("/auth/me"),
|
|
|
|
|
api.get<ExtractionRun[]>("/profile-cv/runs").catch(() => ({ data: [] as ExtractionRun[] } as any)),
|
|
|
|
|
api.get<JobListResponse>("/jobapplications", { params: { page: 1, pageSize: 100, sortBy: "dateApplied", sortDir: "desc" } }).catch(() => ({ data: { items: [], total: 0, page: 1, pageSize: 100 } } as any)),
|
|
|
|
|
]);
|
|
|
|
|
const r = profileResponse;
|
|
|
|
|
setMe(r.data);
|
|
|
|
@@ -169,10 +235,12 @@ export default function ProfilePage() {
|
|
|
|
|
setProfileCvText(r.data?.profileCvText ?? "");
|
|
|
|
|
setStructuredCv(parseStructuredCvJson(r.data?.profileCvStructureJson));
|
|
|
|
|
setExtractionRuns(runsResponse.data ?? []);
|
|
|
|
|
setSavedJobs(jobsResponse.data?.items ?? []);
|
|
|
|
|
setHeadline(window.localStorage.getItem("profileHeadline") ?? "");
|
|
|
|
|
} catch {
|
|
|
|
|
setMe(null);
|
|
|
|
|
setExtractionRuns([]);
|
|
|
|
|
setSavedJobs([]);
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
@@ -193,6 +261,8 @@ export default function ProfilePage() {
|
|
|
|
|
: t("profileGoogleNotLinked");
|
|
|
|
|
const cvLabel = profileCvText.trim() ? t("profileCvReady", { count: cvWordCount }) : t("profileCvMissing");
|
|
|
|
|
const latestRun = extractionRuns[0];
|
|
|
|
|
const selectedRewriteTemplate = REWRITE_TEMPLATES.find((option) => option.id === cvSectionStyle) ?? REWRITE_TEMPLATES[0];
|
|
|
|
|
const selectedRewriteJob = savedJobs.find((job) => String(job.id) === selectedRewriteJobId) ?? null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Paper sx={{ mt: 0, p: 2.5 }}>
|
|
|
|
@@ -672,22 +742,24 @@ export default function ProfilePage() {
|
|
|
|
|
))}
|
|
|
|
|
</Box>
|
|
|
|
|
</Box>
|
|
|
|
|
<Box sx={{ mt: 2, p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.paper" }}>
|
|
|
|
|
<Box sx={{ mt: 2, p: 2, borderRadius: 3, border: "1px solid", borderColor: "divider", background: "linear-gradient(180deg, rgba(15,23,42,0.03) 0%, rgba(15,23,42,0) 100%)" }}>
|
|
|
|
|
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "center", mb: 1.5 }}>
|
|
|
|
|
<Box>
|
|
|
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>{t("profileCvSectionTools")}</Typography>
|
|
|
|
|
<Typography variant="subtitle1" sx={{ fontWeight: 800 }}>CV style rewrite studio</Typography>
|
|
|
|
|
<Typography variant="body2" sx={{ color: "text.secondary" }}>{t("profileCvSectionToolsHelp")}</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outlined"
|
|
|
|
|
disabled={!isLocal || !profileCvText.trim() || rewritingSection || uploadingCv || improvingCv || rebuildingCv}
|
|
|
|
|
variant="contained"
|
|
|
|
|
disabled={!isLocal || (!profileCvText.trim() && structuredCv.sections.length === 0) || rewritingSection || uploadingCv || improvingCv || rebuildingCv}
|
|
|
|
|
onClick={async () => {
|
|
|
|
|
setRewritingSection(true);
|
|
|
|
|
try {
|
|
|
|
|
const res = await api.post<{ text?: string }>("/profile-cv/rewrite-section", {
|
|
|
|
|
sectionName: cvSection,
|
|
|
|
|
sectionName: cvSection || null,
|
|
|
|
|
style: cvSectionStyle,
|
|
|
|
|
templateId: cvSectionStyle,
|
|
|
|
|
targetRole: cvSectionTargetRole.trim() || null,
|
|
|
|
|
jobApplicationId: selectedRewriteJob ? selectedRewriteJob.id : null,
|
|
|
|
|
});
|
|
|
|
|
setCvSectionDraft(res.data?.text ?? "");
|
|
|
|
|
toast(t("profileCvSectionRewritten"), "success");
|
|
|
|
@@ -701,10 +773,59 @@ export default function ProfilePage() {
|
|
|
|
|
{rewritingSection ? t("profileCvSectionRewriting") : t("profileCvSectionRewrite")}
|
|
|
|
|
</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr 1.2fr" }, gap: 1.5, mb: 1.5 }}>
|
|
|
|
|
|
|
|
|
|
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "repeat(4, minmax(0, 1fr))" }, gap: 1.5, mb: 2 }}>
|
|
|
|
|
{REWRITE_TEMPLATES.map((option) => {
|
|
|
|
|
const selected = option.id === cvSectionStyle;
|
|
|
|
|
return (
|
|
|
|
|
<Paper
|
|
|
|
|
key={option.id}
|
|
|
|
|
role="button"
|
|
|
|
|
tabIndex={0}
|
|
|
|
|
onClick={() => setCvSectionStyle(option.id)}
|
|
|
|
|
onKeyDown={(event) => {
|
|
|
|
|
if (event.key === "Enter" || event.key === " ") {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
setCvSectionStyle(option.id);
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
sx={{
|
|
|
|
|
p: 1.5,
|
|
|
|
|
borderRadius: 3,
|
|
|
|
|
cursor: "pointer",
|
|
|
|
|
border: "1px solid",
|
|
|
|
|
borderColor: selected ? "primary.main" : "divider",
|
|
|
|
|
boxShadow: selected ? "0 0 0 1px rgba(25,118,210,0.15)" : "none",
|
|
|
|
|
backgroundColor: selected ? "rgba(25,118,210,0.06)" : "background.paper",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", gap: 1, mb: 1 }}>
|
|
|
|
|
<Box>
|
|
|
|
|
<Typography variant="overline" sx={{ color: option.accent, fontWeight: 800 }}>{option.eyebrow}</Typography>
|
|
|
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>{option.title}</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
<IconButton size="small" onClick={(event) => { event.stopPropagation(); setRewritePreviewTemplate(option); }}>
|
|
|
|
|
<ZoomInOutlinedIcon fontSize="small" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Box>
|
|
|
|
|
<Box sx={{ p: 1.25, borderRadius: 2.5, border: "1px solid", borderColor: "divider", backgroundColor: "background.default", minHeight: 148 }}>
|
|
|
|
|
<Typography variant="caption" sx={{ display: "block", color: option.accent, fontWeight: 700, mb: 0.5 }}>{option.sampleHeading}</Typography>
|
|
|
|
|
<Typography variant="caption" sx={{ display: "block", color: "text.secondary", mb: 1 }}>{option.sampleMeta}</Typography>
|
|
|
|
|
{option.sampleBullets.map((bullet) => (
|
|
|
|
|
<Typography key={bullet} variant="caption" sx={{ display: "block", color: "text.primary", mb: 0.5 }}>• {bullet}</Typography>
|
|
|
|
|
))}
|
|
|
|
|
</Box>
|
|
|
|
|
<Typography variant="body2" sx={{ color: "text.secondary", mt: 1 }}>{option.blurb}</Typography>
|
|
|
|
|
</Paper>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<Box sx={{ display: "grid", gridTemplateColumns: { xs: "1fr", md: "1fr 1fr" }, gap: 1.5, mb: 1.5 }}>
|
|
|
|
|
<FormControl fullWidth size="small">
|
|
|
|
|
<InputLabel>{t("profileCvSectionLabel")}</InputLabel>
|
|
|
|
|
<Select value={cvSection} label={t("profileCvSectionLabel")} onChange={(e) => setCvSection(e.target.value as CvSectionOption)}>
|
|
|
|
|
<MenuItem value="">Whole CV</MenuItem>
|
|
|
|
|
<MenuItem value="Professional Summary">{t("profileCvSectionSummary")}</MenuItem>
|
|
|
|
|
<MenuItem value="Core Skills">{t("profileCvSectionSkills")}</MenuItem>
|
|
|
|
|
<MenuItem value="Experience Highlights">{t("profileCvSectionExperience")}</MenuItem>
|
|
|
|
@@ -712,31 +833,56 @@ export default function ProfilePage() {
|
|
|
|
|
<MenuItem value="Projects">{t("profileCvSectionProjects")}</MenuItem>
|
|
|
|
|
</Select>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<FormControl fullWidth size="small">
|
|
|
|
|
<InputLabel>{t("profileCvSectionStyle")}</InputLabel>
|
|
|
|
|
<Select value={cvSectionStyle} label={t("profileCvSectionStyle")} onChange={(e) => setCvSectionStyle(e.target.value as CvSectionStyle)}>
|
|
|
|
|
<MenuItem value="balanced">{t("profileCvSectionStyleBalanced")}</MenuItem>
|
|
|
|
|
<MenuItem value="concise">{t("profileCvSectionStyleConcise")}</MenuItem>
|
|
|
|
|
<MenuItem value="impact">{t("profileCvSectionStyleImpact")}</MenuItem>
|
|
|
|
|
<MenuItem value="ats">{t("profileCvSectionStyleAts")}</MenuItem>
|
|
|
|
|
<TextField label={t("profileCvSectionTargetRole")} value={cvSectionTargetRole} onChange={(e) => setCvSectionTargetRole(e.target.value)} fullWidth helperText={selectedRewriteJob ? `Using saved job context: ${selectedRewriteJob.jobTitle}` : undefined} />
|
|
|
|
|
<FormControl fullWidth size="small" sx={{ gridColumn: { xs: "1 / -1", md: "1 / -1" } }}>
|
|
|
|
|
<InputLabel>Saved job context</InputLabel>
|
|
|
|
|
<Select value={selectedRewriteJobId} label="Saved job context" onChange={(e) => setSelectedRewriteJobId(String(e.target.value))}>
|
|
|
|
|
<MenuItem value="">None</MenuItem>
|
|
|
|
|
{savedJobs.map((job) => (
|
|
|
|
|
<MenuItem key={job.id} value={String(job.id)}>{job.jobTitle} · {job.company?.name ?? "Unknown company"}</MenuItem>
|
|
|
|
|
))}
|
|
|
|
|
</Select>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<TextField label={t("profileCvSectionTargetRole")} value={cvSectionTargetRole} onChange={(e) => setCvSectionTargetRole(e.target.value)} fullWidth />
|
|
|
|
|
</Box>
|
|
|
|
|
<TextField
|
|
|
|
|
label={t("profileCvSectionDraft")}
|
|
|
|
|
value={cvSectionDraft}
|
|
|
|
|
onChange={(e) => setCvSectionDraft(e.target.value)}
|
|
|
|
|
multiline
|
|
|
|
|
minRows={6}
|
|
|
|
|
fullWidth
|
|
|
|
|
placeholder={t("profileCvSectionDraftPlaceholder")}
|
|
|
|
|
/>
|
|
|
|
|
<Box sx={{ mt: 1, display: "flex", justifyContent: "flex-end", gap: 1, flexWrap: "wrap" }}>
|
|
|
|
|
<Button variant="text" disabled={!cvSectionDraft.trim()} onClick={() => navigator.clipboard.writeText(cvSectionDraft)}>{t("profileCopyCvText")}</Button>
|
|
|
|
|
<Button variant="outlined" disabled={!cvSectionDraft.trim()} onClick={() => setProfileCvText((prev) => `${prev.trim()}\n\n${cvSection}\n${cvSectionDraft.trim()}`.trim())}>{t("profileCvSectionAppend")}</Button>
|
|
|
|
|
<Button variant="contained" disabled={!cvSectionDraft.trim()} onClick={() => setProfileCvText((prev) => replaceCvSection(prev, cvSection, cvSectionDraft))}>{t("profileCvSectionReplace")}</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<Paper sx={{ p: 1.5, borderRadius: 3, border: "1px solid", borderColor: "divider", backgroundColor: "background.paper" }}>
|
|
|
|
|
<Box sx={{ display: "flex", justifyContent: "space-between", gap: 2, flexWrap: "wrap", alignItems: "center", mb: 1 }}>
|
|
|
|
|
<Box>
|
|
|
|
|
<Typography variant="subtitle2" sx={{ fontWeight: 800 }}>Rewrite preview</Typography>
|
|
|
|
|
<Typography variant="body2" sx={{ color: "text.secondary" }}>{selectedRewriteTemplate.title} · {cvSection || "Whole CV"}</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
{cvSectionDraft.trim() ? <Chip size="small" color="success" label="Draft ready" /> : null}
|
|
|
|
|
</Box>
|
|
|
|
|
<Box sx={{ minHeight: 180, borderRadius: 2.5, backgroundColor: "background.default", border: "1px dashed", borderColor: "divider", p: 1.5 }}>
|
|
|
|
|
{cvSectionDraft.trim() ? (
|
|
|
|
|
<Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>{cvSectionDraft}</Typography>
|
|
|
|
|
) : (
|
|
|
|
|
<Typography variant="body2" sx={{ color: "text.secondary" }}>Choose a CV style, optionally aim it at a saved job, and generate a rewrite preview here.</Typography>
|
|
|
|
|
)}
|
|
|
|
|
</Box>
|
|
|
|
|
<Box sx={{ mt: 1, display: "flex", justifyContent: "flex-end", gap: 1, flexWrap: "wrap" }}>
|
|
|
|
|
<Button variant="text" disabled={!cvSectionDraft.trim()} onClick={() => navigator.clipboard.writeText(cvSectionDraft)}>{t("profileCopyCvText")}</Button>
|
|
|
|
|
<Button variant="outlined" disabled={!cvSectionDraft.trim()} onClick={() => setProfileCvText((prev) => cvSection ? `${prev.trim()}\n\n${cvSection}\n${cvSectionDraft.trim()}`.trim() : cvSectionDraft.trim())}>{cvSection ? t("profileCvSectionAppend") : "Use as full CV"}</Button>
|
|
|
|
|
<Button variant="contained" disabled={!cvSectionDraft.trim()} onClick={() => setProfileCvText((prev) => cvSection ? replaceCvSection(prev, cvSection, cvSectionDraft) : cvSectionDraft.trim())}>{cvSection ? t("profileCvSectionReplace") : "Replace full CV"}</Button>
|
|
|
|
|
</Box>
|
|
|
|
|
</Paper>
|
|
|
|
|
|
|
|
|
|
<Dialog open={Boolean(rewritePreviewTemplate)} onClose={() => setRewritePreviewTemplate(null)} maxWidth="sm" fullWidth>
|
|
|
|
|
<DialogTitle>{rewritePreviewTemplate?.title ?? "Template preview"}</DialogTitle>
|
|
|
|
|
<DialogContent>
|
|
|
|
|
{rewritePreviewTemplate ? (
|
|
|
|
|
<Box sx={{ p: 2, borderRadius: 3, border: "1px solid", borderColor: "divider", background: `linear-gradient(180deg, ${rewritePreviewTemplate.accent}12 0%, rgba(255,255,255,0) 100%)` }}>
|
|
|
|
|
<Typography variant="overline" sx={{ color: rewritePreviewTemplate.accent, fontWeight: 800 }}>{rewritePreviewTemplate.eyebrow}</Typography>
|
|
|
|
|
<Typography variant="h6" sx={{ fontWeight: 900, mb: 0.5 }}>{rewritePreviewTemplate.sampleHeading}</Typography>
|
|
|
|
|
<Typography variant="body2" sx={{ color: "text.secondary", mb: 1.5 }}>{rewritePreviewTemplate.sampleMeta}</Typography>
|
|
|
|
|
{rewritePreviewTemplate.sampleBullets.map((bullet) => (
|
|
|
|
|
<Typography key={bullet} variant="body2" sx={{ mb: 0.75 }}>• {bullet}</Typography>
|
|
|
|
|
))}
|
|
|
|
|
<Typography variant="body2" sx={{ color: "text.secondary", mt: 1.5 }}>{rewritePreviewTemplate.blurb}</Typography>
|
|
|
|
|
</Box>
|
|
|
|
|
) : null}
|
|
|
|
|
</DialogContent>
|
|
|
|
|
</Dialog>
|
|
|
|
|
</Box>
|
|
|
|
|
<Box sx={{ mt: 1, display: "flex", justifyContent: "space-between", gap: 1, flexWrap: "wrap" }}>
|
|
|
|
|
<Typography variant="caption" sx={{ color: "text.secondary" }}>
|
|
|
|
|