Resume system overhaul :)
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m54s
All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m54s
This commit is contained in:
318
src/components/ResumeSettingsModal.tsx
Normal file
318
src/components/ResumeSettingsModal.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import { useSignal } from "@preact/signals";
|
||||
import { Settings } from "lucide-preact";
|
||||
|
||||
interface ResumeSettingsModalProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ResumeSettingsModal({
|
||||
className = "",
|
||||
}: ResumeSettingsModalProps) {
|
||||
const [tomlContent, setTomlContent] = useState("");
|
||||
const [isGenerating, setIsGenerating] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<"upload" | "edit">("upload");
|
||||
const dragActive = useSignal(false);
|
||||
const modalOpen = useSignal(false);
|
||||
|
||||
const openModal = () => {
|
||||
modalOpen.value = true;
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
modalOpen.value = false;
|
||||
setError(null);
|
||||
setTomlContent("");
|
||||
setActiveTab("upload");
|
||||
};
|
||||
|
||||
const handleFileUpload = (file: File) => {
|
||||
if (!file.name.endsWith(".toml")) {
|
||||
setError("Please upload a .toml file");
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const content = e.target?.result as string;
|
||||
setTomlContent(content);
|
||||
setError(null);
|
||||
setActiveTab("edit");
|
||||
};
|
||||
reader.onerror = () => {
|
||||
setError("Error reading file");
|
||||
};
|
||||
reader.readAsText(file);
|
||||
};
|
||||
|
||||
const handleDrop = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
dragActive.value = false;
|
||||
|
||||
const files = e.dataTransfer?.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileUpload(files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
dragActive.value = true;
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
dragActive.value = false;
|
||||
};
|
||||
|
||||
const handleFileInput = (e: Event) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
const files = target.files;
|
||||
if (files && files.length > 0) {
|
||||
handleFileUpload(files[0]);
|
||||
}
|
||||
};
|
||||
|
||||
const downloadTemplate = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/resume/template");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to download template");
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "resume-template.toml";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
setError("Failed to download template");
|
||||
}
|
||||
};
|
||||
|
||||
const generatePDF = async () => {
|
||||
if (!tomlContent.trim()) {
|
||||
setError("Please provide TOML content");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsGenerating(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/resume/pdf", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "text/plain",
|
||||
},
|
||||
body: tomlContent,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(
|
||||
errorText || `Failed to generate PDF: ${response.status}`,
|
||||
);
|
||||
}
|
||||
|
||||
const blob = await response.blob();
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "resume.pdf";
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.error("Error generating PDF:", err);
|
||||
setError(err instanceof Error ? err.message : "Failed to generate PDF");
|
||||
} finally {
|
||||
setIsGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
const loadTemplate = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/resume/template");
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to load template");
|
||||
}
|
||||
|
||||
const template = await response.text();
|
||||
setTomlContent(template);
|
||||
setActiveTab("edit");
|
||||
setError(null);
|
||||
} catch (err) {
|
||||
setError("Failed to load template");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating Settings Button */}
|
||||
<button
|
||||
onClick={openModal}
|
||||
class={`fixed top-4 right-4 z-20 btn btn-circle btn-secondary hover:bg-primary shadow-lg opacity-100 translate-y-0 min-h-[44px] min-w-[44px] ${className}`}
|
||||
aria-label="Resume Settings"
|
||||
>
|
||||
<Settings class="text-lg" />
|
||||
</button>
|
||||
|
||||
{/* Modal */}
|
||||
<div class={`modal ${modalOpen.value ? "modal-open" : ""}`}>
|
||||
<div class="modal-box w-11/12 max-w-5xl h-[90vh] flex flex-col">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="font-bold text-lg">Resume Generator</h3>
|
||||
<button
|
||||
onClick={closeModal}
|
||||
class="btn btn-sm btn-circle btn-ghost"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-hidden flex flex-col">
|
||||
<p class="text-base-content/70 mb-4">
|
||||
Create a custom PDF resume from a TOML file. Download the
|
||||
template, edit it with your information, and generate your resume.
|
||||
</p>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
<button onClick={downloadTemplate} class="btn btn-primary btn-sm">
|
||||
Download Template
|
||||
</button>
|
||||
<button onClick={loadTemplate} class="btn btn-secondary btn-sm">
|
||||
Load Template in Editor
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div class="flex justify-center mb-4">
|
||||
<div
|
||||
role="tablist"
|
||||
class="inline-flex bg-base-300 border border-base-content/20 rounded-full p-1"
|
||||
>
|
||||
<button
|
||||
role="tab"
|
||||
class={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
|
||||
activeTab === "upload"
|
||||
? "bg-primary text-primary-content shadow-sm"
|
||||
: "text-base-content/70 hover:text-base-content hover:bg-base-200"
|
||||
}`}
|
||||
onClick={() => setActiveTab("upload")}
|
||||
>
|
||||
Upload File
|
||||
</button>
|
||||
<button
|
||||
role="tab"
|
||||
class={`px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 ${
|
||||
activeTab === "edit"
|
||||
? "bg-primary text-primary-content shadow-sm"
|
||||
: "text-base-content/70 hover:text-base-content hover:bg-base-200"
|
||||
}`}
|
||||
onClick={() => setActiveTab("edit")}
|
||||
>
|
||||
Edit TOML
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div class="flex-1 overflow-hidden">
|
||||
{/* Upload Tab */}
|
||||
{activeTab === "upload" && (
|
||||
<div class="h-full">
|
||||
<div
|
||||
class={`border-2 border-dashed rounded-lg p-6 text-center transition-colors h-full flex items-center justify-center ${
|
||||
dragActive.value
|
||||
? "border-primary bg-primary/10"
|
||||
: "border-base-300 hover:border-primary/50"
|
||||
}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="text-4xl">📄</div>
|
||||
<div>
|
||||
<p class="text-lg font-medium">
|
||||
Drop your TOML file here
|
||||
</p>
|
||||
<p class="text-base-content/70">or click to browse</p>
|
||||
</div>
|
||||
<input
|
||||
type="file"
|
||||
accept=".toml"
|
||||
onChange={handleFileInput}
|
||||
class="file-input file-input-primary file-input-sm w-full max-w-xs"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Edit Tab */}
|
||||
{activeTab === "edit" && (
|
||||
<div class="h-full flex flex-col space-y-2">
|
||||
<div class="label">
|
||||
<span class="label-text font-bold">TOML Content</span>
|
||||
<span class="label-text-alt">
|
||||
Edit your resume data below
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
class="textarea textarea-bordered flex-1 font-mono text-xs resize-none w-full min-h-0"
|
||||
placeholder="Paste your TOML content here or load the template..."
|
||||
value={tomlContent}
|
||||
onInput={(e) =>
|
||||
setTomlContent((e.target as HTMLTextAreaElement).value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div class="alert alert-error mt-4">
|
||||
<span class="text-sm">{error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Generate Button */}
|
||||
{tomlContent.trim() && (
|
||||
<div class="mt-4">
|
||||
<button
|
||||
onClick={generatePDF}
|
||||
disabled={isGenerating}
|
||||
class="btn btn-primary btn-sm w-full"
|
||||
>
|
||||
{isGenerating ? (
|
||||
<>
|
||||
<span class="loading loading-spinner loading-xs"></span>
|
||||
Generating PDF...
|
||||
</>
|
||||
) : (
|
||||
"Generate Custom Resume PDF"
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-backdrop backdrop-blur-sm" onClick={closeModal}></div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user