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 { useState, useEffect } from "react";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
import { useRouter } from "next/navigation"; 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"; 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() { export default function AdminPage() {
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
@@ -21,10 +21,12 @@ export default function AdminPage() {
const [isSaving, setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [saveStatus, setSaveStatus] = useState<{ type: "success" | "error"; message: string } | null>(null); const [saveStatus, setSaveStatus] = useState<{ type: "success" | "error"; message: string } | null>(null);
const [newTagDrafts, setNewTagDrafts] = useState<Record<number, string>>({}); const [newTagDrafts, setNewTagDrafts] = useState<Record<number, string>>({});
const [newSkillListDrafts, setNewSkillListDrafts] = useState<Record<string, string>>({});
// Import state // Import state
const [importFileName, setImportFileName] = useState(""); const [importFileName, setImportFileName] = useState("");
const [importFileContent, setImportFileContent] = useState<string | null>(null); const [importFileContent, setImportFileContent] = useState<string | null>(null);
const [importError, setImportError] = useState(""); const [importError, setImportError] = useState("");
const getSectionLabel = (file: EditableFile) => (file === "skills.json" ? "Agent Skills" : file.replace(".json", "").replace("_", " "));
// Handle file import // Handle file import
const handleImportFile = async (e: React.ChangeEvent<HTMLInputElement>) => { const handleImportFile = async (e: React.ChangeEvent<HTMLInputElement>) => {
setImportError(""); setImportError("");
@@ -54,7 +56,7 @@ export default function AdminPage() {
try { try {
const parsed = JSON.parse(importFileContent); const parsed = JSON.parse(importFileContent);
setData(Array.isArray(parsed) ? parsed : []); 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); setSelectedFile(importFileName as EditableFile);
} }
setImportFileContent(null); setImportFileContent(null);
@@ -160,7 +162,8 @@ export default function AdminPage() {
"projects.json": { name: "", description: "", link: "", open_source: false }, "projects.json": { name: "", description: "", link: "", open_source: false },
"mini_projects.json": { title: "", description: "", why: "" }, "mini_projects.json": { title: "", description: "", why: "" },
"experience.json": { name: "", type: "experience", description: "" }, "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]); setData([templates[selectedFile], ...data]);
}; };
@@ -192,6 +195,27 @@ export default function AdminPage() {
setNewTagDrafts((prev) => ({ ...prev, [index]: "" })); 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) => ( const renderProjectEditor = (item: Project, idx: number) => (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-1"> <div className="space-y-1">
@@ -327,6 +351,123 @@ export default function AdminPage() {
</div> </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) => ( const renderRealWorkEditor = (item: RealWork, idx: number) => (
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="space-y-1"> <div className="space-y-1">
@@ -416,6 +557,9 @@ export default function AdminPage() {
if (selectedFile === "experience.json") { if (selectedFile === "experience.json") {
return renderExperienceEditor(item as Experience, idx); return renderExperienceEditor(item as Experience, idx);
} }
if (selectedFile === "skills.json") {
return renderSkillEditor(item as Skill, idx);
}
return renderRealWorkEditor(item as RealWork, 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"> <div className="flex flex-col lg:flex-row gap-8">
<aside className="lg:w-64 flex-shrink-0 space-y-2"> <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"}`}> <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> </button>
))} ))}
</aside> </aside>
<main className="flex-1 space-y-6"> <main className="flex-1 space-y-6">
<div className="flex justify-between items-center mb-4"> <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> <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> </div>
+15 -39
View File
@@ -6,9 +6,9 @@ import { WorkExperience } from "../sections/WorkExperience";
import { Uptime } from "../sections/Uptime"; import { Uptime } from "../sections/Uptime";
import { Activity } from "../sections/Activity"; import { Activity } from "../sections/Activity";
import { Affiliates } from "../sections/Affiliates"; import { Affiliates } from "../sections/Affiliates";
import { AgentSkills } from "../sections/AgentSkills";
import { ProjectCard } from "../components/ProjectCard"; import { ProjectCard } from "../components/ProjectCard";
import { MiniProjectCard } from "../components/MiniProjectCard"; import { MiniProjectCard } from "../components/MiniProjectCard";
import { ExperienceCard } from "../components/ExperienceCard";
import { ExperienceModal } from "../components/ExperienceModal"; import { ExperienceModal } from "../components/ExperienceModal";
import { MiniProjectModal } from "../components/MiniProjectModal"; import { MiniProjectModal } from "../components/MiniProjectModal";
import { Navbar } from "../components/Navbar"; import { Navbar } from "../components/Navbar";
@@ -16,7 +16,9 @@ import { Footer } from "../sections/Footer";
import { TechStack } from "../sections/TechStack"; import { TechStack } from "../sections/TechStack";
import { TypingRoomIntro } from "../components/TypingRoomIntro"; import { TypingRoomIntro } from "../components/TypingRoomIntro";
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import type { Experience } from "../types"; import { SkillsExperience } from "@/sections/SkillsExperience";
const ENABLE_PAGE_ANIMATION = false;
export default function Home() { export default function Home() {
const { const {
@@ -27,7 +29,7 @@ export default function Home() {
selectedMiniProject, selectedExperience selectedMiniProject, selectedExperience
} = useProfile(); } = useProfile();
const [showTypingIntro, setShowTypingIntro] = useState(true); const [showTypingIntro, setShowTypingIntro] = useState(ENABLE_PAGE_ANIMATION);
const oldUsernames = [ const oldUsernames = [
"getspaced (ingame)", "getspaced (ingame)",
"Space (alternative)", "Space (alternative)",
@@ -42,18 +44,11 @@ export default function Home() {
setShowTypingIntro(false); setShowTypingIntro(false);
}, []); }, []);
const groupedExperiences = experiences.reduce(
(acc, exp) => {
if (!acc[exp.type]) acc[exp.type] = [];
acc[exp.type].push(exp);
return acc;
},
{} as Record<string, Experience[]>,
);
return ( return (
<> <>
{ENABLE_PAGE_ANIMATION && (
<TypingRoomIntro active={showTypingIntro} onFinish={handleIntroFinish} /> <TypingRoomIntro active={showTypingIntro} onFinish={handleIntroFinish} />
)}
{!showTypingIntro && ( {!showTypingIntro && (
<> <>
<Navbar /> <Navbar />
@@ -74,9 +69,7 @@ export default function Home() {
<Activity /> <Activity />
<Affiliates /> <AgentSkills />
<TechStack />
<section className="w-full max-w-6xl mx-auto space-y-12 px-4"> <section className="w-full max-w-6xl mx-auto space-y-12 px-4">
<div className="text-center space-y-4"> <div className="text-center space-y-4">
@@ -110,30 +103,14 @@ export default function Home() {
</div> </div>
</section> </section>
<section className="w-full max-w-6xl mx-auto space-y-12 px-4 pb-20"> <Affiliates />
<div className="text-center space-y-4">
<h2 className="text-3xl font-bold">Skills & Experience</h2> <TechStack />
<p className="text-gray-400">Things I've worked with over the years.</p>
</div> <SkillsExperience
<div className="grid grid-cols-1 md:grid-cols-2 gap-12"> experiences={experiences}
{Object.entries(groupedExperiences).map(([type, items]) => ( onSelectExperience={setSelectedExperience}
<div key={type} className="space-y-6">
<h3 className="text-xl font-semibold border-l-4 border-blue-500 pl-4 capitalize">
{type}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{items.map((exp, index) => (
<ExperienceCard
key={index}
experience={exp}
onClick={() => setSelectedExperience(exp)}
/> />
))}
</div>
</div>
))}
</div>
</section>
</div> </div>
<Footer /> <Footer />
@@ -155,4 +132,3 @@ export default function Home() {
</> </>
); );
} }
+127
View File
@@ -0,0 +1,127 @@
"use client";
import { useEffect, useMemo, useState } from "react";
const BERLIN_TIME_ZONE = "Europe/Berlin";
const MATCH_COPY = "Your local time matches mine! No issues there.";
function getTimeZoneOffsetMinutes(date: Date, timeZone: string) {
const formatter = new Intl.DateTimeFormat("en-US", {
timeZone,
hour12: false,
hourCycle: "h23",
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
const parts = formatter.formatToParts(date);
const values = parts.reduce<Record<string, string>>((acc, part) => {
if (part.type !== "literal") acc[part.type] = part.value;
return acc;
}, {});
const asUTC = Date.UTC(
Number(values.year),
Number(values.month) - 1,
Number(values.day),
Number(values.hour),
Number(values.minute),
Number(values.second),
);
return (asUTC - date.getTime()) / 60000;
}
export function TimezoneClockBlock({ warningThresholdHours = 5 }: { warningThresholdHours?: number }) {
const [now, setNow] = useState<Date | null>(null);
const localFormatter = useMemo(
() =>
new Intl.DateTimeFormat("en-GB", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
}),
[],
);
const berlinFormatter = useMemo(
() =>
new Intl.DateTimeFormat("en-GB", {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
timeZone: BERLIN_TIME_ZONE,
}),
[],
);
useEffect(() => {
const updateNow = () => setNow(new Date());
updateNow();
const interval = setInterval(updateNow, 1000);
return () => clearInterval(interval);
}, []);
const localTime = now ? localFormatter.format(now) : "";
const berlinTime = now ? berlinFormatter.format(now) : "";
const clocksMatch = localTime !== "" && localTime === berlinTime;
const timeGapHours =
now === null
? 0
: Math.abs(
getTimeZoneOffsetMinutes(now, BERLIN_TIME_ZONE) + now.getTimezoneOffset(),
) / 60;
return (
<div className="pt-3 flex flex-col items-center gap-3">
<p className="text-[11px] uppercase tracking-[0.3em] text-gray-500">Time</p>
{clocksMatch && (
<div className="rounded-2xl border border-emerald-400/30 bg-emerald-500/10 px-4 py-3 backdrop-blur-sm shadow-lg text-center">
<p className="text-sm font-semibold text-emerald-100">{MATCH_COPY}</p>
</div>
)}
{!clocksMatch && timeGapHours >= warningThresholdHours && (
<div className="flex flex-col items-center gap-1.5 rounded-2xl border border-amber-500/30 bg-amber-500/10 px-5 py-4 backdrop-blur-sm shadow-lg max-w-sm text-center">
<p className="text-sm font-bold text-amber-400">
Large Time Gap ({Math.round(timeGapHours)}h)
</p>
<p className="text-xs font-medium text-amber-200/80 leading-relaxed">
Because we have a {Math.round(timeGapHours)}-hour time difference, our waking hours might barely overlap. Please expect delayed responses!
</p>
</div>
)}
<div className="flex flex-wrap items-stretch justify-center gap-3">
<div className="relative rounded-2xl border border-white/15 bg-white/5 px-4 py-3 shadow-lg backdrop-blur-sm">
<div className="absolute right-0 top-1/2 h-0 w-0 -translate-y-1/2 translate-x-2 border-b-8 border-b-transparent border-l-8 border-l-white/15 border-t-8 border-t-transparent" />
<p className="text-[11px] uppercase tracking-[0.24em] text-gray-400">
Local time
</p>
<p className="mt-1 font-mono text-sm font-semibold text-gray-100">
{localTime}
</p>
</div>
<div className="relative rounded-2xl border border-white/15 bg-white/5 px-4 py-3 shadow-lg backdrop-blur-sm">
<div className="absolute left-0 top-1/2 h-0 w-0 -translate-x-2 -translate-y-1/2 border-b-8 border-b-transparent border-r-8 border-r-white/15 border-t-8 border-t-transparent" />
<p className="text-[11px] uppercase tracking-[0.24em] text-gray-400">
My time
</p>
<p className="mt-1 font-mono text-sm font-semibold text-gray-100">
{berlinTime}
</p>
</div>
</div>
</div>
);
}
+6 -2
View File
@@ -1,7 +1,7 @@
"use client"; "use client";
import React, { createContext, useContext, useEffect, useState, useMemo } from "react"; import React, { createContext, useContext, useEffect, useState, useMemo } from "react";
import type { Experience, MiniProject, Project, RealWork } from "../types"; import type { Experience, MiniProject, Project, RealWork, Skill } from "../types";
interface ProfileContextType { interface ProfileContextType {
status: string; status: string;
@@ -15,6 +15,7 @@ interface ProfileContextType {
miniProjects: MiniProject[]; miniProjects: MiniProject[];
experiences: Experience[]; experiences: Experience[];
realWork: RealWork[]; realWork: RealWork[];
skills: Skill[];
selectedMiniProject: MiniProject | null; selectedMiniProject: MiniProject | null;
setSelectedMiniProject: (p: MiniProject | null) => void; setSelectedMiniProject: (p: MiniProject | null) => void;
selectedExperience: Experience | null; selectedExperience: Experience | null;
@@ -39,6 +40,7 @@ export function ProfileProvider({ children }: { children: React.ReactNode }) {
const [miniProjects, setMiniProjects] = useState<MiniProject[]>([]); const [miniProjects, setMiniProjects] = useState<MiniProject[]>([]);
const [experiences, setExperiences] = useState<Experience[]>([]); const [experiences, setExperiences] = useState<Experience[]>([]);
const [realWork, setRealWork] = useState<RealWork[]>([]); const [realWork, setRealWork] = useState<RealWork[]>([]);
const [skills, setSkills] = useState<Skill[]>([]);
const rotatingMessages = useMemo( const rotatingMessages = useMemo(
() => [ () => [
@@ -104,6 +106,8 @@ export function ProfileProvider({ children }: { children: React.ReactNode }) {
.then(res => res.json()).then(setMiniProjects).catch(() => setMiniProjects([])); .then(res => res.json()).then(setMiniProjects).catch(() => setMiniProjects([]));
fetch("https://shsf-api.reversed.dev/api/exec/4/e942538c-caa1-49d1-8953-dfab1e62f8cb/experience") fetch("https://shsf-api.reversed.dev/api/exec/4/e942538c-caa1-49d1-8953-dfab1e62f8cb/experience")
.then(res => res.json()).then(setExperiences).catch(() => setExperiences([])); .then(res => res.json()).then(setExperiences).catch(() => setExperiences([]));
fetch("https://shsf-api.reversed.dev/api/exec/4/e942538c-caa1-49d1-8953-dfab1e62f8cb/skills")
.then(res => res.json()).then(setSkills).catch(() => setSkills([]));
fetchRealWork(); fetchRealWork();
}, []); }, []);
@@ -141,7 +145,7 @@ export function ProfileProvider({ children }: { children: React.ReactNode }) {
const value = { const value = {
status, statusMessage, borderStatus, glowColor, displayMessage, isScrambling, status, statusMessage, borderStatus, glowColor, displayMessage, isScrambling,
rotatingMessages, rotatingMessages,
projects, miniProjects, experiences, realWork, projects, miniProjects, experiences, realWork, skills,
selectedMiniProject, setSelectedMiniProject, selectedMiniProject, setSelectedMiniProject,
selectedExperience, setSelectedExperience selectedExperience, setSelectedExperience
}; };
+59 -2
View File
@@ -1,17 +1,42 @@
"use client"; "use client";
import { useEffect, useState } from "react";
const activitySvg = const activitySvg =
"https://gitact.spaceistyping.com/activity.svg?days=365&source=all&theme=dark"; "https://gitact.spaceistyping.com/activity.svg?days=365&source=all&theme=dark";
export function Activity() { export function Activity() {
const [dailyActivity, setDailyActivity] = useState<string | null>(null);
const [projectCount, setProjectCount] = useState<number | null>(null);
useEffect(() => {
// Fetch a string of how long i have coded today, e.g. "2h 35m"
fetch(
"https://shsf-api.reversed.dev/api/exec/6/842aa52f-1a9e-43e1-b630-00286b44897a",
)
.then((response) => response.json())
.then((hourText) => {
setDailyActivity(hourText.text);
setProjectCount(hourText.across);
});
}, []);
return ( return (
<section className="w-full max-w-6xl mx-auto space-y-8 px-4"> <section className="w-full max-w-6xl mx-auto space-y-8 px-4">
<div className="text-center space-y-4"> <div className="text-center space-y-4">
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500"> <h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">
Activity Code Activity
</h2> </h2>
<p className="text-gray-400 max-w-2xl mx-auto"> <p className="text-gray-400 max-w-2xl mx-auto">
A live snapshot of my recent Git activity. Today, i've already been coding for
<span className="font-mono text-base text-green-400 ml-1">
{dailyActivity || "..."}
</span>
, across approximately
<span className="font-mono text-base text-green-400 ml-1">
{projectCount !== null ? projectCount : "..."}
</span>{" "}
projects .
</p> </p>
</div> </div>
@@ -26,6 +51,38 @@ export function Activity() {
/> />
</div> </div>
</div> </div>
<div>
<p className="text-center text-gray-500 text-sm">
Data from my <strong>public</strong>{" "}
<a
href="https://github.com/Space-Banane"
className="text-blue-400 hover:underline"
>
Github
</a>{" "}
&{" "}
<a
href="https://gitea.reversed.dev/space"
className="text-blue-400 hover:underline"
>
Gitea
</a>{" "}
contributions. Via{" "}
<a
href="https://gitea.reversed.dev/space/git-activity-merger"
className="text-blue-400 hover:underline"
>
Git Activity Merger
</a>
. Possibly cached & delayed.
</p>
<p className="text-center text-gray-500 text-sm max-w-2xl justify-center mx-auto mt-1">
Coding time is estimated based on Wakapi data, which tracks my coding
activity across a lot of projects, but also gets things wrong. So take
it with a grain of salt. An <a href="https://github.com/Space-Banane/shsf" className="text-blue-400 hover:underline">SHSF Function</a> fetches the data and makes it available via an API.
</p>
</div>
</section> </section>
); );
} }
+40 -5
View File
@@ -6,22 +6,28 @@ type Affiliate = {
bad: string[]; bad: string[];
link: string; link: string;
icon: string; icon: string;
location: string;
provides: string[];
}; };
const affiliates: Affiliate[] = [ const affiliates: Affiliate[] = [
{ {
name: "SparkedHost", name: "SparkedHost",
good: ["Decent Support", "Good Bot Hosting", "Generous Webhosting"], good: ["Decent Support", "Good Bot Hosting", "Generous Webhosting"],
bad: ["Staff"], bad: ["Staff can be hit or miss"],
link: "https://billing.sparkedhost.com/aff.php?aff=1843", link: "https://billing.sparkedhost.com/aff.php?aff=1843",
icon: "https://sparkedhost.com/_next/static/media/logo-text.fce7e4c5.svg", icon: "https://sparkedhost.com/_next/static/media/logo-text.fce7e4c5.svg",
location: "Global",
provides: ["A ton of Game Hosting", "Web Hosting", "VPS", "Bot Hosting", "Domains"],
}, },
{ {
name: "Datalix", name: "Datalix",
good: ["Uptime", "Hardware", "Prices", "Support"], good: ["Uptime", "Hardware", "Prices", "Support"],
bad: ["nothin"], bad: ["Nothing"],
link: "https://datalix.de/a/space", link: "https://datalix.de/a/space",
icon: "https://cdn.datalix.de/images/header.png", icon: "https://cdn.datalix.de/images/header.png",
location: "Germany",
provides: ["KVM VPS", "Web Hosting", "Dedicated Servers", "Game Servers", "S3", "Nextcloud", "Reselling"],
}, },
]; ];
@@ -32,6 +38,9 @@ export function Affiliates() {
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500"> <h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">
Affiliates Affiliates
</h2> </h2>
<p className="text-gray-400 max-w-2xl mx-auto">
Looking for hosting? I recommend checking out Datalix!
</p>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5"> <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
@@ -49,12 +58,36 @@ export function Affiliates() {
alt={affiliate.name} alt={affiliate.name}
className="h-7 max-w-[130px] w-auto object-contain rounded" className="h-7 max-w-[130px] w-auto object-contain rounded"
/> />
<h3 className="text-lg font-semibold text-white">{affiliate.name}</h3> <div>
<h3 className="text-lg font-semibold text-white">
{affiliate.name}
</h3>
<p className="text-[10px] uppercase tracking-widest text-gray-500 font-medium leading-none mt-1">
{affiliate.location}
</p>
</div>
</div> </div>
<div className="space-y-3"> <div className="space-y-3">
<div> <div>
<p className="text-xs font-semibold text-emerald-300 mb-1.5">Good</p> <p className="text-xs font-semibold text-blue-300 mb-1.5">
Provides
</p>
<div className="flex flex-wrap gap-2">
{affiliate.provides.map((item) => (
<span
key={item}
className="inline-flex items-center gap-1.5 rounded-full border border-blue-400/30 bg-blue-500/10 px-2.5 py-1 text-xs text-blue-200"
>
{item}
</span>
))}
</div>
</div>
<div>
<p className="text-xs font-semibold text-emerald-300 mb-1.5">
Good
</p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{affiliate.good.map((item) => ( {affiliate.good.map((item) => (
<span <span
@@ -68,7 +101,9 @@ export function Affiliates() {
</div> </div>
</div> </div>
<div> <div>
<p className="text-xs font-semibold text-rose-300 mb-1.5">Bad</p> <p className="text-xs font-semibold text-rose-300 mb-1.5">
Bad
</p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{affiliate.bad.map((item) => ( {affiliate.bad.map((item) => (
<span <span
+86
View File
@@ -0,0 +1,86 @@
"use client";
import { useProfile } from "../context/ProfileContext";
export function AgentSkills() {
const { skills } = useProfile();
return (
<section className="w-full max-w-6xl mx-auto px-4 space-y-8">
<div className="text-center space-y-3">
<h2 className="text-4xl font-bold leading-tight text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-orange-500">
Agent Skills
</h2>
<p className="text-gray-400 max-w-2xl mx-auto">
Skills i throw at my Agents to help them solve my prompts.
</p>
</div>
{skills.length === 0 ? (
<div className="rounded-3xl border border-white/10 bg-white/5 px-6 py-10 text-center text-gray-400">
No agent skills published yet.
</div>
) : (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{skills.map((skill, index) => (
<article
key={`${skill.name}-${index}`}
className="group rounded-3xl border border-amber-500/20 bg-gradient-to-br from-amber-500/10 via-white/5 to-orange-500/5 p-6 md:p-7 shadow-[0_0_0_1px_rgba(245,158,11,0.08)] backdrop-blur-sm transition-all duration-300 hover:border-amber-500/40"
>
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="space-y-2">
<div className="flex flex-wrap gap-2">
{(skill.emojis || []).map((emoji, emojiIndex) => (
<span
key={`${skill.name}-emoji-${emojiIndex}`}
className="inline-flex h-9 w-9 items-center justify-center rounded-xl bg-amber-500/15 text-lg border border-amber-500/20"
title={emoji}
>
{emoji}
</span>
))}
</div>
<h3 className="text-2xl font-semibold text-white">{skill.name}</h3>
</div>
{skill.link ? (
<a
href={skill.link}
target="_blank"
rel="noreferrer"
className="inline-flex items-center rounded-full border border-amber-400/30 bg-amber-400/10 px-4 py-2 text-xs font-semibold text-amber-100 transition-colors hover:bg-amber-400/20"
>
Open Link
</a>
) : null}
</div>
<div className="mt-5 space-y-4 text-sm leading-6 text-gray-300">
<p>
<span className="font-semibold text-amber-200">Description:</span> {skill.description}
</p>
<p>
<span className="font-semibold text-amber-200">Purpose:</span> {skill.purpose}
</p>
<p>
<span className="font-semibold text-amber-200">Solves:</span> {skill.what_it_solves}
</p>
</div>
<div className="mt-6 flex flex-wrap gap-2">
{(skill.tags || []).map((tag, tagIndex) => (
<span
key={`${skill.name}-tag-${tagIndex}`}
className="rounded-full border border-white/10 bg-black/20 px-3 py-1 text-xs font-medium text-gray-200"
>
{tag}
</span>
))}
</div>
</article>
))}
</div>
)}
</section>
);
}
-93
View File
@@ -1,93 +0,0 @@
"use client";
export function Goals() {
return (
<section className="w-full space-y-8">
<div className="text-center space-y-2">
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">
My Goals
</h2>
<p className="text-gray-400 max-w-2xl mx-auto">Next 4 Years are going to look fun</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="p-6 rounded-2xl bg-gradient-to-br from-blue-500/10 to-purple-500/5 backdrop-blur-sm border border-blue-500/20">
<div className="flex items-start gap-4">
<div className="p-3 rounded-lg bg-blue-500/20 text-3xl">🚀</div>
<div>
<h3 className="text-xl font-bold text-white mb-2">
Not Just Semi-Fullstack
</h3>
<p className="text-gray-400 leading-relaxed">
I want to work more with Serverless Architectures and Cloud
Services to build scalable applications.
</p>
</div>
</div>
</div>
<div className="p-6 rounded-2xl bg-gradient-to-br from-purple-500/10 to-pink-500/5 backdrop-blur-sm border border-purple-500/20">
<div className="flex items-start gap-4">
<div className="p-3 rounded-lg bg-purple-500/20 text-3xl">
💻
</div>
<div>
<h3 className="text-xl font-bold text-white mb-2">
Contribute More to Open Source
</h3>
<p className="text-gray-400 leading-relaxed">
I want to commit more to Open-Source Projects. That's it...
</p>
</div>
</div>
</div>
<div className="p-6 rounded-2xl bg-gradient-to-br from-green-500/10 to-blue-500/5 backdrop-blur-sm border border-green-500/20">
<div className="flex items-start gap-4">
<div className="p-3 rounded-lg bg-green-500/20 text-3xl">
</div>
<div>
<h3 className="text-xl font-bold text-white mb-2">
Expand on Existing Projects
</h3>
<p className="text-gray-400 leading-relaxed">
I want to make SHSF more stable and usable.
</p>
</div>
</div>
</div>
<div className="p-6 rounded-2xl bg-gradient-to-br from-yellow-500/10 to-orange-500/5 backdrop-blur-sm border border-yellow-500/20">
<div className="flex items-start gap-4">
<div className="p-3 rounded-lg bg-yellow-500/20 text-3xl">
🧪
</div>
<div>
<h3 className="text-xl font-bold text-white mb-2">
Testing before Breaking
</h3>
<p className="text-gray-400 leading-relaxed">In the future i want to write more tests and improve my code quality.</p>
</div>
</div>
</div>
<div className="md:col-span-2 p-6 rounded-2xl bg-gradient-to-br from-yellow-500/10 to-orange-500/5 backdrop-blur-sm border border-yellow-500/20">
<div className="flex items-start gap-4">
<div className="p-3 rounded-lg bg-yellow-500/20 text-3xl">
🏠
</div>
<div>
<h3 className="text-xl font-bold text-white mb-2">
Embrace Self-Hosting
</h3>
<p className="text-gray-400 leading-relaxed">
I want to learn more about self-hosting my own services and reduce reliance on cloud providers for personal projects.
</p>
</div>
</div>
</div>
</div>
</section>
);
}
+10 -2
View File
@@ -1,5 +1,6 @@
import { useState, useRef, useEffect } from "react"; import { useState, useRef } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { TimezoneClockBlock } from "../components/TimezoneClockBlock";
export function Hero({ export function Hero({
glowColor, glowColor,
@@ -8,6 +9,7 @@ export function Hero({
rotatingMessages, rotatingMessages,
statusMessage, statusMessage,
oldUsernames, oldUsernames,
timeGapWarningThreshold = 5,
}: { }: {
glowColor: string; glowColor: string;
borderStatus: string; borderStatus: string;
@@ -15,6 +17,7 @@ export function Hero({
rotatingMessages: string[]; rotatingMessages: string[];
statusMessage: string; statusMessage: string;
oldUsernames: string[]; oldUsernames: string[];
timeGapWarningThreshold?: number;
}) { }) {
const router = useRouter(); const router = useRouter();
const [movementCount, setMovementCount] = useState(0); const [movementCount, setMovementCount] = useState(0);
@@ -104,6 +107,9 @@ export function Hero({
on Discord on Discord
</p> </p>
</div> </div>
<p className="text-xs text-gray-400/70 mt-1">
Data from <a href="https://github.com/Space-Banane/shsf-discord-status" className="text-blue-400 hover:underline">Discord</a> (cached & delayed)
</p>
</div> </div>
</div> </div>
@@ -131,7 +137,7 @@ export function Hero({
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<a <a
href="https://luna.reversed.dev" href="https://luna.spaceistyping.com"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="group relative flex items-center gap-3 px-6 py-2.5 rounded-full bg-white/5 border border-white/10 hover:bg-white/10 hover:border-purple-500/50 transition-all duration-300 shadow-lg hover:shadow-purple-500/10" className="group relative flex items-center gap-3 px-6 py-2.5 rounded-full bg-white/5 border border-white/10 hover:bg-white/10 hover:border-purple-500/50 transition-all duration-300 shadow-lg hover:shadow-purple-500/10"
@@ -169,6 +175,8 @@ export function Hero({
</a> </a>
</div> </div>
</div> </div>
<TimezoneClockBlock warningThresholdHours={timeGapWarningThreshold} />
</div> </div>
</section> </section>
); );
+63
View File
@@ -0,0 +1,63 @@
"use client";
import { ExperienceCard } from "@/components/ExperienceCard";
import type { Experience } from "../types";
const ENABLE_SKILLS_EXPERIENCE = true;
interface SkillsExperienceProps {
experiences: Experience[];
onSelectExperience: (experience: Experience) => void;
}
export function SkillsExperience({
experiences,
onSelectExperience,
}: SkillsExperienceProps) {
if (!ENABLE_SKILLS_EXPERIENCE) {
return null;
}
const groupedExperiences = experiences.reduce(
(acc, exp) => {
if (!acc[exp.type]) acc[exp.type] = [];
acc[exp.type].push(exp);
return acc;
},
{} as Record<string, Experience[]>,
);
return (
<section className="w-full max-w-6xl mx-auto space-y-12 px-4 pb-20">
<div className="text-center space-y-4">
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">
Skills & Experience
</h2>
<p className="text-gray-400">
<span className="text-red-400">
THIS IS MISSING A LOT OF THINGS, BE AWARE
</span>{" "}
- Things I've worked with over the years.
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-12">
{Object.entries(groupedExperiences).map(([type, items]) => (
<div key={type} className="space-y-6">
<h3 className="text-xl font-semibold border-l-4 border-blue-500 pl-4 capitalize">
{type}
</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{items.map((exp, index) => (
<ExperienceCard
key={index}
experience={exp}
onClick={() => onSelectExperience(exp)}
/>
))}
</div>
</div>
))}
</div>
</section>
);
}
+36 -15
View File
@@ -1,5 +1,6 @@
"use client"; "use client";
import { cn } from "../components/cn";
const ENABLE_TECH_STACK = false;
const techStack = [ const techStack = [
{ {
@@ -45,40 +46,60 @@ const techStack = [
{ {
title: "Servers", title: "Servers",
items: [ items: [
{ name: "KVMS from datalix.de", description: "Cloud server provider" }, { name: "KVMs from datalix.de", description: "Cloud provider" },
{ name: "Home server", description: "Self-hosted option" }, { name: "Home server", description: "Self-hosted option" },
{ name: "AWS", description: "Cloud computing" }
], ],
}, },
]; ];
export function TechStack() { export function TechStack() {
if (!ENABLE_TECH_STACK) {
// Disabled
return ( return (
<section className="w-full max-w-4xl mx-auto px-4 space-y-8 mt-8"> <section className="w-full max-w-4xl mx-auto px-4 space-y-12 mt-16 mb-24">
<div className="text-center space-y-2"> <div className="text-center space-y-3">
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500"> <h2 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500">
Tech Stack Tech Stack
</h2> </h2>
<p className="text-gray-400 max-w-2xl mx-auto"> <p className="text-gray-400 max-w-2xl mx-auto text-sm md:text-base">
My current infrastructure and software stack, from server to This section is currently disabled. It may be re-enabled in the future, but for now, it's hidden.
monitoring. </p>
</div>
</section>
)
}
return (
<section className="w-full max-w-4xl mx-auto px-4 space-y-12 mt-16 mb-24">
<div className="text-center space-y-3">
<h2 className="text-4xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500">
Tech Stack
</h2>
<p className="text-gray-400 max-w-2xl mx-auto text-sm md:text-base">
A overview of the tools and technologies I use to build and host my applications.
</p> </p>
</div> </div>
<div className="space-y-4"> <div className="max-w-2xl mx-auto space-y-4">
{techStack.map((layer) => ( {techStack.map((layer) => (
<div <div
key={layer.title} key={layer.title}
className="p-6 md:p-8 rounded-2xl bg-gradient-to-br from-cyan-500/10 to-blue-500/5 backdrop-blur-sm border border-cyan-500/20 space-y-5" className="p-5 rounded-2xl bg-gradient-to-br from-cyan-500/[0.07] to-blue-500/[0.03] backdrop-blur-md border border-cyan-500/10 hover:border-cyan-500/30 transition-all duration-500"
> >
<h3 className="text-2xl font-semibold text-white">{layer.title}</h3> <h3 className="text-lg font-bold text-white mb-3 flex items-center gap-2">
<ul className="flex flex-wrap gap-2"> <span className="w-1.5 h-1.5 rounded-full bg-cyan-500" />
{layer.title}
</h3>
<ul className="space-y-3">
{layer.items.map((item) => ( {layer.items.map((item) => (
<li <li
key={item.name} key={item.name}
className="px-3 py-1 rounded-full text-xs font-semibold bg-cyan-500/20 text-cyan-200 border border-cyan-500/30" className="flex flex-col group cursor-default"
> >
<span className="font-medium">{item.name}</span> <span className="text-sm font-semibold text-cyan-100 group-hover:text-cyan-400 transition-colors">
<span className="ml-2 text-[10px] text-cyan-100/70 font-normal border-l border-cyan-500/30 pl-2"> {item.name}
</span>
<span className="text-[11px] text-gray-500 font-normal leading-relaxed">
{item.description} {item.description}
</span> </span>
</li> </li>
+4 -1
View File
@@ -7,12 +7,15 @@ export function Uptime() {
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500"> <h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">
Downtime Downtime
</h2> </h2>
<p className="text-gray-400 max-w-2xl mx-auto">
I love selfhosting stuff, can you tell?
</p>
</div> </div>
<div className="max-w-3xl mx-auto"> <div className="max-w-3xl mx-auto">
<div className="p-6 rounded-2xl bg-gradient-to-br from-gray-800/20 to-transparent backdrop-blur-sm border border-white/6"> <div className="p-6 rounded-2xl bg-gradient-to-br from-gray-800/20 to-transparent backdrop-blur-sm border border-white/6">
<h3 className="text-2xl font-semibold text-white mb-4 text-center"> <h3 className="text-2xl font-semibold text-white mb-4 text-center">
I love my homelab Github vs HomeLab Uptime
</h3> </h3>
<div className="flex gap-6 items-center justify-between"> <div className="flex gap-6 items-center justify-between">
<div className="flex-1 text-center"> <div className="flex-1 text-center">
+1 -1
View File
@@ -57,7 +57,7 @@ export function WorkExperience({ realWork }: WorkExperienceProps) {
Work Experience Work Experience
</h2> </h2>
<p className="text-gray-400 max-w-2xl mx-auto"> <p className="text-gray-400 max-w-2xl mx-auto">
Professional collaborations and product work I have contributed to. Actual work that i did at actual companies.
</p> </p>
</div> </div>
+10
View File
@@ -22,6 +22,16 @@ export interface Experience {
learned_because?: string; learned_because?: string;
} }
export interface Skill {
name: string;
description: string;
purpose: string;
what_it_solves: string;
link: string;
emojis?: string[];
tags?: string[];
}
export interface Project { export interface Project {
name: string; name: string;
description: string; description: string;