feat: Major update

This commit is contained in:
Space-Banane
2026-06-03 15:45:02 +02:00
parent da9103faaa
commit 40526e115e
14 changed files with 611 additions and 170 deletions
+151 -7
View File
@@ -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>