feat: Major update
This commit is contained in:
+151
-7
@@ -3,11 +3,11 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { Project, MiniProject, Experience, RealWork } from "../../types";
|
||||
import type { Project, MiniProject, Experience, RealWork, Skill } from "../../types";
|
||||
|
||||
const API_BASE = "https://shsf-api.reversed.dev/api/exec/4/e942538c-caa1-49d1-8953-dfab1e62f8cb";
|
||||
|
||||
type EditableFile = "projects.json" | "mini_projects.json" | "experience.json" | "real_work.json";
|
||||
type EditableFile = "projects.json" | "mini_projects.json" | "experience.json" | "real_work.json" | "skills.json";
|
||||
|
||||
export default function AdminPage() {
|
||||
const [password, setPassword] = useState("");
|
||||
@@ -21,10 +21,12 @@ export default function AdminPage() {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [saveStatus, setSaveStatus] = useState<{ type: "success" | "error"; message: string } | null>(null);
|
||||
const [newTagDrafts, setNewTagDrafts] = useState<Record<number, string>>({});
|
||||
const [newSkillListDrafts, setNewSkillListDrafts] = useState<Record<string, string>>({});
|
||||
// Import state
|
||||
const [importFileName, setImportFileName] = useState("");
|
||||
const [importFileContent, setImportFileContent] = useState<string | null>(null);
|
||||
const [importError, setImportError] = useState("");
|
||||
const getSectionLabel = (file: EditableFile) => (file === "skills.json" ? "Agent Skills" : file.replace(".json", "").replace("_", " "));
|
||||
// Handle file import
|
||||
const handleImportFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setImportError("");
|
||||
@@ -54,7 +56,7 @@ export default function AdminPage() {
|
||||
try {
|
||||
const parsed = JSON.parse(importFileContent);
|
||||
setData(Array.isArray(parsed) ? parsed : []);
|
||||
if (importFileName && ["projects.json", "mini_projects.json", "experience.json", "real_work.json"].includes(importFileName)) {
|
||||
if (importFileName && ["projects.json", "mini_projects.json", "experience.json", "real_work.json", "skills.json"].includes(importFileName)) {
|
||||
setSelectedFile(importFileName as EditableFile);
|
||||
}
|
||||
setImportFileContent(null);
|
||||
@@ -160,7 +162,8 @@ export default function AdminPage() {
|
||||
"projects.json": { name: "", description: "", link: "", open_source: false },
|
||||
"mini_projects.json": { title: "", description: "", why: "" },
|
||||
"experience.json": { name: "", type: "experience", description: "" },
|
||||
"real_work.json": { company: "", role: "", from: "", until: "", summary: "" }
|
||||
"real_work.json": { company: "", role: "", from: "", until: "", summary: "" },
|
||||
"skills.json": { name: "", description: "", purpose: "", what_it_solves: "", link: "", emojis: [], tags: [] }
|
||||
};
|
||||
setData([templates[selectedFile], ...data]);
|
||||
};
|
||||
@@ -192,6 +195,27 @@ export default function AdminPage() {
|
||||
setNewTagDrafts((prev) => ({ ...prev, [index]: "" }));
|
||||
};
|
||||
|
||||
const updateSkillArrayItem = (index: number, field: "emojis" | "tags", itemIndex: number, value: string) => {
|
||||
const current = [...((data[index] as Skill)?.[field] || [])];
|
||||
current[itemIndex] = value;
|
||||
updateEntry(index, field, current);
|
||||
};
|
||||
|
||||
const removeSkillArrayItem = (index: number, field: "emojis" | "tags", itemIndex: number) => {
|
||||
const current = [...((data[index] as Skill)?.[field] || [])];
|
||||
updateEntry(index, field, current.filter((_, i) => i !== itemIndex));
|
||||
};
|
||||
|
||||
const addSkillArrayItem = (index: number, field: "emojis" | "tags") => {
|
||||
const key = `${index}:${field}`;
|
||||
const draft = (newSkillListDrafts[key] || "").trim();
|
||||
if (!draft) return;
|
||||
|
||||
const current = [...((data[index] as Skill)?.[field] || []), draft];
|
||||
updateEntry(index, field, current);
|
||||
setNewSkillListDrafts((prev) => ({ ...prev, [key]: "" }));
|
||||
};
|
||||
|
||||
const renderProjectEditor = (item: Project, idx: number) => (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
@@ -327,6 +351,123 @@ export default function AdminPage() {
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderSkillEditor = (item: Skill, idx: number) => (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase">Name</label>
|
||||
<input type="text" value={item.name || ""} onChange={(e) => updateEntry(idx, "name", e.target.value)} className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm" />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase">Link</label>
|
||||
<input type="text" value={item.link || ""} onChange={(e) => updateEntry(idx, "link", e.target.value)} className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm" />
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-1">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase">Description</label>
|
||||
<textarea rows={3} value={item.description || ""} onChange={(e) => updateEntry(idx, "description", e.target.value)} className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm" />
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-1">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase">Purpose</label>
|
||||
<textarea rows={3} value={item.purpose || ""} onChange={(e) => updateEntry(idx, "purpose", e.target.value)} className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm" />
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-1">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase">What It Solves</label>
|
||||
<textarea rows={3} value={item.what_it_solves || ""} onChange={(e) => updateEntry(idx, "what_it_solves", e.target.value)} className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm" />
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-1">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase">Emojis</label>
|
||||
<div className="space-y-2 rounded-lg border border-white/10 bg-black/30 p-3">
|
||||
{(item.emojis || []).length === 0 && <p className="text-xs text-gray-500">No emojis yet. Add one below.</p>}
|
||||
{(item.emojis || []).map((emoji, emojiIndex) => (
|
||||
<div key={`${idx}-emoji-${emojiIndex}`} className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={emoji}
|
||||
onChange={(e) => updateSkillArrayItem(idx, "emojis", emojiIndex, e.target.value)}
|
||||
className="flex-1 bg-black/40 border border-white/10 rounded-lg p-2 text-sm"
|
||||
placeholder="Emoji"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeSkillArrayItem(idx, "emojis", emojiIndex)}
|
||||
className="px-3 py-2 rounded-lg border border-red-500/40 text-red-300 hover:bg-red-500/15 text-xs font-bold"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<input
|
||||
type="text"
|
||||
value={newSkillListDrafts[`${idx}:emojis`] || ""}
|
||||
onChange={(e) => setNewSkillListDrafts((prev) => ({ ...prev, [`${idx}:emojis`]: e.target.value }))}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
addSkillArrayItem(idx, "emojis");
|
||||
}
|
||||
}}
|
||||
className="flex-1 bg-black/40 border border-white/10 rounded-lg p-2 text-sm"
|
||||
placeholder="Add emoji"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addSkillArrayItem(idx, "emojis")}
|
||||
className="px-3 py-2 rounded-lg border border-green-500/40 text-green-300 hover:bg-green-500/15 text-xs font-bold"
|
||||
>
|
||||
Add Emoji
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:col-span-2 space-y-1">
|
||||
<label className="text-xs font-bold text-gray-500 uppercase">Tags</label>
|
||||
<div className="space-y-2 rounded-lg border border-white/10 bg-black/30 p-3">
|
||||
{(item.tags || []).length === 0 && <p className="text-xs text-gray-500">No tags yet. Add one below.</p>}
|
||||
{(item.tags || []).map((tag, tagIndex) => (
|
||||
<div key={`${idx}-skill-tag-${tagIndex}`} className="flex items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={tag}
|
||||
onChange={(e) => updateSkillArrayItem(idx, "tags", tagIndex, e.target.value)}
|
||||
className="flex-1 bg-black/40 border border-white/10 rounded-lg p-2 text-sm"
|
||||
placeholder="Tag"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => removeSkillArrayItem(idx, "tags", tagIndex)}
|
||||
className="px-3 py-2 rounded-lg border border-red-500/40 text-red-300 hover:bg-red-500/15 text-xs font-bold"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<div className="flex items-center gap-2 pt-1">
|
||||
<input
|
||||
type="text"
|
||||
value={newSkillListDrafts[`${idx}:tags`] || ""}
|
||||
onChange={(e) => setNewSkillListDrafts((prev) => ({ ...prev, [`${idx}:tags`]: e.target.value }))}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
addSkillArrayItem(idx, "tags");
|
||||
}
|
||||
}}
|
||||
className="flex-1 bg-black/40 border border-white/10 rounded-lg p-2 text-sm"
|
||||
placeholder="Add tag"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => addSkillArrayItem(idx, "tags")}
|
||||
className="px-3 py-2 rounded-lg border border-green-500/40 text-green-300 hover:bg-green-500/15 text-xs font-bold"
|
||||
>
|
||||
Add Tag
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const renderRealWorkEditor = (item: RealWork, idx: number) => (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-1">
|
||||
@@ -416,6 +557,9 @@ export default function AdminPage() {
|
||||
if (selectedFile === "experience.json") {
|
||||
return renderExperienceEditor(item as Experience, idx);
|
||||
}
|
||||
if (selectedFile === "skills.json") {
|
||||
return renderSkillEditor(item as Skill, idx);
|
||||
}
|
||||
return renderRealWorkEditor(item as RealWork, idx);
|
||||
};
|
||||
|
||||
@@ -478,16 +622,16 @@ export default function AdminPage() {
|
||||
|
||||
<div className="flex flex-col lg:flex-row gap-8">
|
||||
<aside className="lg:w-64 flex-shrink-0 space-y-2">
|
||||
{(["projects.json", "mini_projects.json", "experience.json", "real_work.json"] as EditableFile[]).map(file => (
|
||||
{(["projects.json", "mini_projects.json", "experience.json", "real_work.json", "skills.json"] as EditableFile[]).map(file => (
|
||||
<button key={file} onClick={() => setSelectedFile(file)} className={`w-full text-left px-4 py-3 rounded-xl border transition-all text-sm font-bold ${selectedFile === file ? "bg-white text-black border-white" : "bg-white/5 border-transparent text-gray-400 hover:bg-white/10"}`}>
|
||||
{file.replace(".json", "").replace("_", " ")}
|
||||
{getSectionLabel(file)}
|
||||
</button>
|
||||
))}
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 space-y-6">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-xl font-bold text-white capitalize">{selectedFile.replace(".json", "").replace("_", " ")}</h2>
|
||||
<h2 className="text-xl font-bold text-white capitalize">{getSectionLabel(selectedFile)}</h2>
|
||||
<button onClick={addEntry} className="px-4 py-2 rounded-lg bg-green-600/20 text-green-400 border border-green-500/30 hover:bg-green-600/30 text-xs font-bold transition-all">+ New Entry</button>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user