diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx new file mode 100644 index 0000000..3b0c119 --- /dev/null +++ b/src/app/admin/page.tsx @@ -0,0 +1,510 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { useRouter } from "next/navigation"; +import type { Project, MiniProject, Experience, RealWork } 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"; + +export default function AdminPage() { + const [password, setPassword] = useState(""); + const [isAuthenticated, setIsAuthenticated] = useState(false); + const [error, setError] = useState(""); + const [isLoading, setIsLoading] = useState(false); + + // Editor State + const [selectedFile, setSelectedFile] = useState("real_work.json"); + const [data, setData] = useState([]); + const [isSaving, setIsSaving] = useState(false); + const [saveStatus, setSaveStatus] = useState<{ type: "success" | "error"; message: string } | null>(null); + const [newTagDrafts, setNewTagDrafts] = useState>({}); + // Import state + const [importFileName, setImportFileName] = useState(""); + const [importFileContent, setImportFileContent] = useState(null); + const [importError, setImportError] = useState(""); + // Handle file import + const handleImportFile = async (e: React.ChangeEvent) => { + setImportError(""); + const file = e.target.files?.[0]; + if (!file) return; + setImportFileName(file.name); // Auto-fill filename input + const reader = new FileReader(); + reader.onload = (ev) => { + try { + const text = ev.target?.result as string; + setImportFileContent(text); + } catch (err) { + setImportError("Failed to read file"); + } + }; + reader.onerror = () => setImportError("Failed to read file"); + reader.readAsText(file); + }; + + // Apply imported file content + const handleApplyImport = () => { + setImportError(""); + if (!importFileContent) { + setImportError("No file content loaded"); + return; + } + 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)) { + setSelectedFile(importFileName as EditableFile); + } + setImportFileContent(null); + setImportFileName(""); + } catch (err) { + setImportError("Invalid JSON file"); + } + }; + + const router = useRouter(); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + setError(""); + + try { + const response = await fetch(`${API_BASE}/password`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ password }), + }); + + const result = await response.text(); + if (response.ok && result.trim().toLowerCase() === "yay") { + setIsAuthenticated(true); + } else { + setError("Invalid password"); + } + } catch (err) { + setError("An error occurred. Please try again."); + } finally { + setIsLoading(false); + } + }; + + const fetchData = async (file: EditableFile) => { + setIsLoading(true); + setSaveStatus(null); + try { + const route = file.replace(".json", ""); + const response = await fetch(`${API_BASE}/${route}`); + if (response.ok) { + const json = await response.json(); + setData(Array.isArray(json) ? json : []); + } else if (response.status === 404) { + // No data found for this file, show empty list + setData([]); + } else { + setSaveStatus({ type: "error", message: `Failed to fetch data (${response.status})` }); + } + } catch (err) { + setSaveStatus({ type: "error", message: "Failed to fetch data" }); + } finally { + setIsLoading(false); + } + }; + + const handleSave = async () => { + setIsSaving(true); + setSaveStatus(null); + try { + const response = await fetch(`${API_BASE}/writefile`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + password, + filename: selectedFile, + content: JSON.stringify(data, null, 2), + }), + }); + + const result = await response.text(); + if (response.ok && result.trim().toLowerCase() === "yay") { + setSaveStatus({ type: "success", message: "Changes deployed successfully!" }); + } else { + setSaveStatus({ type: "error", message: "Deploy failed." }); + } + } catch (err) { + setSaveStatus({ type: "error", message: "Network error" }); + } finally { + setIsSaving(false); + } + }; + + useEffect(() => { + if (isAuthenticated) fetchData(selectedFile); + }, [isAuthenticated, selectedFile]); + + const updateEntry = (index: number, field: string, value: any) => { + const newData = [...data]; + const keys = field.split("."); + if (keys.length === 2) { + newData[index][keys[0]] = { ...newData[index][keys[0]], [keys[1]]: value }; + } else { + newData[index][field] = value; + } + setData(newData); + }; + + const addEntry = () => { + const templates: Record = { + "projects.json": { name: "", description: "", link: "", open_source: false }, + "mini_projects.json": { title: "", description: "", why: "" }, + "experience.json": { name: "", type: "experience", description: "" }, + "real_work.json": { company: "", summary: "" } + }; + setData([templates[selectedFile], ...data]); + }; + + const removeEntry = (index: number) => { + setData(data.filter((_, i) => i !== index)); + }; + + const updateRealWorkTag = (index: number, tagIndex: number, value: string) => { + const item = data[index] as RealWork; + const currentTags = [...(item.tags || [])]; + currentTags[tagIndex] = value; + updateEntry(index, "tags", currentTags); + }; + + const removeRealWorkTag = (index: number, tagIndex: number) => { + const item = data[index] as RealWork; + const nextTags = (item.tags || []).filter((_, i) => i !== tagIndex); + updateEntry(index, "tags", nextTags); + }; + + const addRealWorkTag = (index: number) => { + const draft = (newTagDrafts[index] || "").trim(); + if (!draft) return; + + const item = data[index] as RealWork; + const nextTags = [...(item.tags || []), draft]; + updateEntry(index, "tags", nextTags); + setNewTagDrafts((prev) => ({ ...prev, [index]: "" })); + }; + + const renderProjectEditor = (item: Project, idx: number) => ( +
+
+ + updateEntry(idx, "name", e.target.value)} className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm" /> +
+
+ + updateEntry(idx, "link", e.target.value)} className="w-full bg-black/40 border border-white/10 rounded-lg p-2 text-sm" /> +
+
+ +