Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aebeed3964 | |||
| 40526e115e |
+151
-7
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ export default function ContactPage() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen px-4 space-y-12 w-full max-w-4xl mx-auto pt-24 pb-20 text-white">
|
<div className="flex min-h-screen w-full max-w-4xl flex-col items-center justify-center space-y-10 px-4 pb-16 pt-20 text-white sm:space-y-12 sm:pb-20 sm:pt-24">
|
||||||
<div className="text-center space-y-4 w-full">
|
<div className="text-center space-y-4 w-full">
|
||||||
<h1 className="text-5xl md:text-6xl font-extrabold text-white">
|
<h1 className="text-4xl font-extrabold text-white sm:text-5xl md:text-6xl">
|
||||||
Let's <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">Connect</span>
|
Let's <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">Connect</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-gray-400 text-xl max-w-xl mx-auto">
|
<p className="mx-auto max-w-xl text-base text-gray-400 sm:text-lg md:text-xl">
|
||||||
Whatever you've got in mind, I'm just a few clicks away.
|
Whatever you've got in mind, I'm just a few clicks away.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -22,20 +22,20 @@ export default function ContactPage() {
|
|||||||
<ContactSection />
|
<ContactSection />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 w-full max-w-4xl text-center">
|
<div className="grid w-full max-w-4xl grid-cols-1 gap-4 text-center sm:grid-cols-2 md:grid-cols-3 sm:gap-6">
|
||||||
<div className="bg-white/5 border border-white/10 p-6 rounded-3xl backdrop-blur-sm group hover:border-blue-500/30 transition-all duration-300">
|
<div className="group mx-auto w-full max-w-[20rem] rounded-3xl border border-white/10 bg-white/5 p-5 backdrop-blur-sm transition-all duration-300 hover:border-blue-500/30 sm:max-w-none sm:p-6">
|
||||||
<div className="text-3xl mb-4 group-hover:scale-110 transition-transform">📧</div>
|
<div className="text-3xl mb-4 group-hover:scale-110 transition-transform">📧</div>
|
||||||
<h3 className="font-bold mb-1">Email</h3>
|
<h3 className="font-bold mb-1">Email</h3>
|
||||||
<p className="text-sm text-gray-500 underline underline-offset-4 decoration-white/20 hover:decoration-blue-500 transition-colors">
|
<p className="text-sm text-gray-500 underline underline-offset-4 decoration-white/20 hover:decoration-blue-500 transition-colors">
|
||||||
space@reversed.dev
|
space@reversed.dev
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/5 border border-white/10 p-6 rounded-3xl backdrop-blur-sm group hover:border-purple-500/30 transition-all duration-300">
|
<div className="group mx-auto w-full max-w-[20rem] rounded-3xl border border-white/10 bg-white/5 p-5 backdrop-blur-sm transition-all duration-300 hover:border-purple-500/30 sm:max-w-none sm:p-6">
|
||||||
<div className="text-3xl mb-4 group-hover:scale-110 transition-transform">👾</div>
|
<div className="text-3xl mb-4 group-hover:scale-110 transition-transform">👾</div>
|
||||||
<h3 className="font-bold mb-1">Discord</h3>
|
<h3 className="font-bold mb-1">Discord</h3>
|
||||||
<p className="text-sm text-gray-500">@getspaced</p>
|
<p className="text-sm text-gray-500">@getspaced</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-white/5 border border-white/10 p-6 rounded-3xl backdrop-blur-sm group hover:border-pink-500/30 transition-all duration-300">
|
<div className="group mx-auto w-full max-w-[20rem] rounded-3xl border border-white/10 bg-white/5 p-5 backdrop-blur-sm transition-all duration-300 hover:border-pink-500/30 sm:max-w-none sm:p-6">
|
||||||
<div className="text-3xl mb-4 group-hover:scale-110 transition-transform">💻</div>
|
<div className="text-3xl mb-4 group-hover:scale-110 transition-transform">💻</div>
|
||||||
<h3 className="font-bold mb-1">GitHub</h3>
|
<h3 className="font-bold mb-1">GitHub</h3>
|
||||||
<p className="text-sm text-gray-500">github.com/Space-Banane</p>
|
<p className="text-sm text-gray-500">github.com/Space-Banane</p>
|
||||||
|
|||||||
+26
-50
@@ -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,22 +44,15 @@ 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 (
|
||||||
<>
|
<>
|
||||||
<TypingRoomIntro active={showTypingIntro} onFinish={handleIntroFinish} />
|
{ENABLE_PAGE_ANIMATION && (
|
||||||
|
<TypingRoomIntro active={showTypingIntro} onFinish={handleIntroFinish} />
|
||||||
|
)}
|
||||||
{!showTypingIntro && (
|
{!showTypingIntro && (
|
||||||
<>
|
<>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<div className="space-y-24 pb-20 pt-20">
|
<div className="space-y-20 pb-16 pt-16 sm:space-y-24 sm:pb-20 sm:pt-20">
|
||||||
<Hero
|
<Hero
|
||||||
glowColor={glowColor}
|
glowColor={glowColor}
|
||||||
borderStatus={borderStatus}
|
borderStatus={borderStatus}
|
||||||
@@ -74,32 +69,30 @@ export default function Home() {
|
|||||||
|
|
||||||
<Activity />
|
<Activity />
|
||||||
|
|
||||||
<Affiliates />
|
<AgentSkills />
|
||||||
|
|
||||||
<TechStack />
|
<section className="w-full max-w-6xl mx-auto space-y-8 px-4 sm:space-y-12">
|
||||||
|
|
||||||
<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">
|
||||||
<h2 className="text-4xl 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 sm:text-4xl">
|
||||||
Featured Projects
|
Featured Projects
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-400 max-w-2xl mx-auto">
|
<p className="mx-auto max-w-2xl text-sm text-gray-400 sm:text-base">
|
||||||
A selection of my personal favorites. Many more on my GitHub.
|
A selection of my personal favorites. Many more on my GitHub.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3 sm:gap-8">
|
||||||
{projects.map((project, index) => (
|
{projects.map((project, index) => (
|
||||||
<ProjectCard key={index} project={project} />
|
<ProjectCard key={index} project={project} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<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-8 px-4 sm:space-y-12">
|
||||||
<div className="text-center space-y-4">
|
<div className="text-center space-y-4">
|
||||||
<h2 className="text-3xl font-bold">More Projects</h2>
|
<h2 className="text-2xl font-bold sm:text-3xl">More Projects</h2>
|
||||||
<p className="text-gray-400">Smaller projects or tools I've built.</p>
|
<p className="text-sm text-gray-400 sm:text-base">Smaller projects or tools I've built.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-3 sm:gap-6">
|
||||||
{miniProjects.map((project, index) => (
|
{miniProjects.map((project, index) => (
|
||||||
<MiniProjectCard
|
<MiniProjectCard
|
||||||
key={index}
|
key={index}
|
||||||
@@ -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() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export function ExperienceCard({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={isClickable ? onClick : undefined}
|
onClick={isClickable ? onClick : undefined}
|
||||||
className={`group flex items-center gap-3 p-3 rounded-lg bg-gradient-to-br from-white/5 to-white/2 backdrop-blur-sm border border-white/10 transition-all duration-200 ${
|
className={`group mx-auto flex w-full max-w-[20rem] items-center gap-3 rounded-lg border border-white/10 bg-gradient-to-br from-white/5 to-white/2 p-3 backdrop-blur-sm transition-all duration-200 sm:max-w-none ${
|
||||||
isClickable
|
isClickable
|
||||||
? "cursor-pointer hover:border-purple-500/50 hover:bg-white/10 hover:-translate-y-0.5"
|
? "cursor-pointer hover:border-purple-500/50 hover:bg-white/10 hover:-translate-y-0.5"
|
||||||
: ""
|
: ""
|
||||||
|
|||||||
@@ -12,17 +12,17 @@ export function ExperienceModal({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-fade-in"
|
className="fixed inset-0 z-50 flex items-center justify-center p-3 bg-black/80 backdrop-blur-sm animate-fade-in sm:p-4"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative max-w-2xl w-full max-h-[90vh] overflow-y-auto bg-gradient-to-br from-gray-900 to-black border border-purple-500/30 rounded-2xl shadow-2xl animate-scale-in"
|
className="relative max-h-[92vh] w-full max-w-[20rem] overflow-y-auto rounded-2xl border border-purple-500/30 bg-gradient-to-br from-gray-900 to-black shadow-2xl animate-scale-in sm:max-w-2xl"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Close Button */}
|
{/* Close Button */}
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="absolute top-4 right-4 p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors z-10"
|
className="absolute right-3 top-3 z-10 rounded-full bg-white/10 p-2 text-white transition-colors hover:bg-white/20 sm:right-4 sm:top-4"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="w-6 h-6"
|
className="w-6 h-6"
|
||||||
@@ -40,10 +40,10 @@ export function ExperienceModal({
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-8 space-y-6">
|
<div className="space-y-4 p-4 sm:space-y-6 sm:p-8">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:gap-4">
|
||||||
<div
|
<div
|
||||||
className={`text-4xl p-3 rounded-lg ${getTypeColor(experience.type)} flex items-center justify-center shrink-0 ${experience.image ? "w-16 h-16" : ""}`}
|
className={`flex shrink-0 items-center justify-center rounded-lg p-3 text-2xl ${getTypeColor(experience.type)} ${experience.image ? "h-12 w-12 sm:h-16 sm:w-16" : ""}`}
|
||||||
>
|
>
|
||||||
{experience.image ? (
|
{experience.image ? (
|
||||||
<img
|
<img
|
||||||
@@ -56,7 +56,7 @@ export function ExperienceModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-500">
|
<h2 className="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-500 sm:text-3xl">
|
||||||
{experience.name}
|
{experience.name}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-sm text-gray-400 mt-1">
|
<p className="text-sm text-gray-400 mt-1">
|
||||||
|
|||||||
@@ -12,31 +12,31 @@ export function MiniProjectCard({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
className="group cursor-pointer relative flex flex-col p-6 rounded-2xl bg-gradient-to-br from-white/5 to-white/2 backdrop-blur-sm border border-white/10 hover:border-purple-500/50 transition-all duration-300 hover:-translate-y-2 hover:shadow-2xl hover:shadow-purple-500/20"
|
className="group relative mx-auto flex w-full max-w-[20rem] cursor-pointer flex-col rounded-2xl border border-white/10 bg-gradient-to-br from-white/5 to-white/2 p-4 backdrop-blur-sm transition-all duration-300 hover:-translate-y-2 hover:border-purple-500/50 hover:shadow-2xl hover:shadow-purple-500/20 sm:max-w-none sm:p-6"
|
||||||
>
|
>
|
||||||
{project.image && (
|
{project.image && (
|
||||||
<div className="mb-4 overflow-hidden rounded-xl bg-black/20 aspect-video flex items-center justify-center">
|
<div className="mb-4 flex aspect-video items-center justify-center overflow-hidden rounded-xl bg-black/20">
|
||||||
<img
|
<img
|
||||||
src={project.image}
|
src={project.image}
|
||||||
alt={project.title}
|
alt={project.title}
|
||||||
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
|
className="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<h3 className="text-xl font-bold text-white mb-2 group-hover:text-purple-400 transition-colors">
|
<h3 className="mb-2 text-lg font-bold text-white transition-colors group-hover:text-purple-400 sm:text-xl">
|
||||||
{project.title}
|
{project.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-400 text-sm line-clamp-2 mb-4">
|
<p className="mb-4 line-clamp-2 text-sm text-gray-400">
|
||||||
{project.description}
|
{project.description}
|
||||||
</p>
|
</p>
|
||||||
{project.last_commit && (
|
{project.last_commit && (
|
||||||
<p className="text-gray-400 text-xs mb-2">
|
<p className="mb-2 text-xs text-gray-400">
|
||||||
Last commit: {new Date(project.last_commit).toLocaleDateString()}
|
Last commit: {new Date(project.last_commit).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center text-purple-400 text-sm font-medium mt-auto">
|
<div className="mt-auto flex items-center text-sm font-medium text-purple-400">
|
||||||
Click to learn more
|
Click to learn more
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform"
|
className="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform"
|
||||||
|
|||||||
@@ -11,17 +11,17 @@ export function MiniProjectModal({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/80 backdrop-blur-sm animate-fade-in"
|
className="fixed inset-0 z-50 flex items-center justify-center p-3 bg-black/80 backdrop-blur-sm animate-fade-in sm:p-4"
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="relative max-w-3xl w-full max-h-[90vh] overflow-y-auto bg-gradient-to-br from-gray-900 to-black border border-purple-500/30 rounded-2xl shadow-2xl animate-scale-in"
|
className="relative max-h-[92vh] w-full max-w-[20rem] overflow-y-auto rounded-2xl border border-purple-500/30 bg-gradient-to-br from-gray-900 to-black shadow-2xl animate-scale-in sm:max-w-3xl"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{/* Close Button */}
|
{/* Close Button */}
|
||||||
<button
|
<button
|
||||||
onClick={onClose}
|
onClick={onClose}
|
||||||
className="absolute top-4 right-4 p-2 rounded-full bg-white/10 hover:bg-white/20 text-white transition-colors z-10"
|
className="absolute right-3 top-3 z-10 rounded-full bg-white/10 p-2 text-white transition-colors hover:bg-white/20 sm:right-4 sm:top-4"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="w-6 h-6"
|
className="w-6 h-6"
|
||||||
@@ -40,7 +40,7 @@ export function MiniProjectModal({
|
|||||||
|
|
||||||
{/* Image */}
|
{/* Image */}
|
||||||
{project.image && (
|
{project.image && (
|
||||||
<div className="w-full h-64 overflow-hidden bg-black/40 rounded-t-2xl">
|
<div className="h-36 w-full overflow-hidden rounded-t-2xl bg-black/40 sm:h-64">
|
||||||
<img
|
<img
|
||||||
src={project.image}
|
src={project.image}
|
||||||
alt={project.title}
|
alt={project.title}
|
||||||
@@ -50,8 +50,8 @@ export function MiniProjectModal({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className="p-8 space-y-6">
|
<div className="space-y-4 p-4 sm:space-y-6 sm:p-8">
|
||||||
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-500">
|
<h2 className="text-xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-purple-400 to-pink-500 sm:text-3xl">
|
||||||
{project.title}
|
{project.title}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ export function Navbar() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="fixed top-8 left-1/2 -translate-x-1/2 z-[100] w-[min(90%,400px)]">
|
<nav className="fixed top-4 left-1/2 z-[100] w-[min(100vw-1rem,400px)] -translate-x-1/2 sm:top-8 sm:w-[min(90%,400px)]">
|
||||||
<div className="bg-black/20 backdrop-blur-xl border border-white/10 rounded-full px-6 py-3 flex items-center justify-between gap-4 shadow-2xl">
|
<div className="bg-black/20 backdrop-blur-xl border border-white/10 rounded-full px-3 py-2 sm:px-6 sm:py-3 flex items-center justify-between gap-2 sm:gap-4 shadow-2xl">
|
||||||
{navItems.map((item) => (
|
{navItems.map((item) => (
|
||||||
<Link
|
<Link
|
||||||
key={item.path}
|
key={item.path}
|
||||||
href={item.path}
|
href={item.path}
|
||||||
onClick={() => handleSwitch(item.path)}
|
onClick={() => handleSwitch(item.path)}
|
||||||
className={`text-sm font-semibold transition-all duration-300 px-4 py-2 rounded-full
|
className={`flex-1 text-center text-xs sm:text-sm font-semibold transition-all duration-300 px-3 py-2 rounded-full
|
||||||
${
|
${
|
||||||
pathname === item.path
|
pathname === item.path
|
||||||
? "bg-white text-black scale-105 shadow-xl shadow-white/10"
|
? "bg-white text-black scale-105 shadow-xl shadow-white/10"
|
||||||
|
|||||||
@@ -4,36 +4,36 @@ import type { Project } from "../types";
|
|||||||
|
|
||||||
export function ProjectCard({ project }: { project: Project }) {
|
export function ProjectCard({ project }: { project: Project }) {
|
||||||
return (
|
return (
|
||||||
<div className="group relative flex flex-col p-6 rounded-2xl bg-gradient-to-br from-white/10 to-white/5 backdrop-blur-sm border border-white/10 hover:border-purple-500/30 transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl hover:shadow-purple-500/10">
|
<div className="group relative mx-auto flex w-full max-w-[20rem] flex-col rounded-2xl border border-white/10 bg-gradient-to-br from-white/10 to-white/5 p-4 backdrop-blur-sm transition-all duration-300 hover:-translate-y-1 hover:border-purple-500/30 hover:shadow-2xl hover:shadow-purple-500/10 sm:max-w-none sm:p-6">
|
||||||
{project.image && (
|
{project.image && (
|
||||||
<div className="mb-6 overflow-hidden rounded-xl bg-black/20 aspect-video flex items-center justify-center relative">
|
<div className="relative mb-4 flex aspect-video items-center justify-center overflow-hidden rounded-xl bg-black/20 sm:mb-6">
|
||||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
|
||||||
<img
|
<img
|
||||||
src={project.image}
|
src={project.image}
|
||||||
alt={project.name}
|
alt={project.name}
|
||||||
className={`object-contain ${project.rounded === false ? "w-full h-auto max-h-24 rounded-lg" : "h-24 w-24 rounded-full"} shadow-lg group-hover:scale-110 transition-transform duration-500`}
|
className={`object-contain shadow-lg transition-transform duration-500 group-hover:scale-110 ${project.rounded === false ? "h-auto w-full max-h-20 rounded-lg sm:max-h-24" : "h-20 w-20 rounded-full sm:h-24 sm:w-24"}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<h3 className="text-xl font-bold text-white mb-2 group-hover:text-purple-400 transition-colors">
|
<h3 className="mb-2 text-lg font-bold text-white transition-colors group-hover:text-purple-400 sm:text-xl">
|
||||||
{project.name}
|
{project.name}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-gray-400 text-sm flex-grow mb-6 leading-relaxed">
|
<p className="mb-5 flex-grow text-sm leading-relaxed text-gray-400 sm:mb-6">
|
||||||
{project.description}
|
{project.description}
|
||||||
</p>
|
</p>
|
||||||
{project.last_commit && (
|
{project.last_commit && (
|
||||||
<p className="text-gray-400 text-xs mb-2">
|
<p className="mb-2 text-xs text-gray-400">
|
||||||
Last commit: {new Date(project.last_commit).toLocaleDateString()}
|
Last commit: {new Date(project.last_commit).toLocaleDateString()}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex gap-3 mt-auto">
|
<div className="mt-auto flex flex-col gap-3 sm:flex-row">
|
||||||
<a
|
<a
|
||||||
href={project.link + "?utm_source=portfolio&ref=space"}
|
href={project.link + "?utm_source=portfolio&ref=space"}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="flex-1 py-2 px-4 rounded-lg bg-white/10 hover:bg-white/20 text-white text-sm font-medium text-center transition-colors flex items-center justify-center gap-2"
|
className="flex flex-1 items-center justify-center gap-2 rounded-lg bg-white/10 px-4 py-2 text-center text-sm font-medium text-white transition-colors hover:bg-white/20"
|
||||||
>
|
>
|
||||||
Visit
|
Visit
|
||||||
<svg
|
<svg
|
||||||
@@ -55,7 +55,7 @@ export function ProjectCard({ project }: { project: Project }) {
|
|||||||
href={project.open_source.link}
|
href={project.open_source.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="p-2 rounded-lg bg-white/5 hover:bg-white/10 text-gray-400 hover:text-white transition-colors border border-white/5"
|
className="self-center rounded-lg border border-white/5 bg-white/5 p-2 text-gray-400 transition-colors hover:bg-white/10 hover:text-white sm:self-auto"
|
||||||
title="View Source"
|
title="View Source"
|
||||||
>
|
>
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
@@ -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 px-4">
|
||||||
|
<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 w-full max-w-[11rem] rounded-2xl border border-white/15 bg-white/5 px-4 py-3 shadow-lg backdrop-blur-sm sm:w-auto">
|
||||||
|
<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 w-full max-w-[11rem] rounded-2xl border border-white/15 bg-white/5 px-4 py-3 shadow-lg backdrop-blur-sm sm:w-auto">
|
||||||
|
<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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -112,10 +112,10 @@ export function TypingRoomIntro({ active, onFinish }: TypingRoomIntroProps) {
|
|||||||
initial={{ y: 32, scale: 0.96, opacity: 0 }}
|
initial={{ y: 32, scale: 0.96, opacity: 0 }}
|
||||||
animate={{ y: 0, scale: 1, opacity: 1 }}
|
animate={{ y: 0, scale: 1, opacity: 1 }}
|
||||||
exit={{ y: -24, scale: 0.98, opacity: 0 }}
|
exit={{ y: -24, scale: 0.98, opacity: 0 }}
|
||||||
transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1] }}
|
transition={{ duration: 0.55, ease: [0.22, 1, 0.36, 1] }}
|
||||||
className="relative w-full max-w-2xl overflow-hidden rounded-3xl border border-cyan-300/20 bg-black/55 backdrop-blur-xl shadow-[0_32px_90px_rgba(14,165,233,0.25)]"
|
className="relative w-full max-w-[20rem] overflow-hidden rounded-3xl border border-cyan-300/20 bg-black/55 backdrop-blur-xl shadow-[0_32px_90px_rgba(14,165,233,0.25)] sm:max-w-2xl"
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-5 py-3">
|
<div className="flex items-center justify-between border-b border-white/10 bg-white/5 px-4 py-3 sm:px-5">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-red-400/80" />
|
<span className="h-2.5 w-2.5 rounded-full bg-red-400/80" />
|
||||||
<span className="h-2.5 w-2.5 rounded-full bg-amber-300/80" />
|
<span className="h-2.5 w-2.5 rounded-full bg-amber-300/80" />
|
||||||
@@ -129,7 +129,7 @@ export function TypingRoomIntro({ active, onFinish }: TypingRoomIntroProps) {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 p-6 md:p-8">
|
<div className="space-y-4 p-4 sm:p-6 md:p-8">
|
||||||
<motion.p
|
<motion.p
|
||||||
initial={{ opacity: 0 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
@@ -142,7 +142,7 @@ export function TypingRoomIntro({ active, onFinish }: TypingRoomIntroProps) {
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: -16 }}
|
initial={{ opacity: 0, x: -16 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
className="max-w-[80%] rounded-2xl rounded-bl-md border border-white/10 bg-white/10 px-4 py-3 text-sm text-gray-100"
|
className="max-w-[86%] rounded-2xl rounded-bl-md border border-white/10 bg-white/10 px-3 py-2.5 text-xs text-gray-100 sm:px-4 sm:py-3 sm:text-sm"
|
||||||
>
|
>
|
||||||
Why is this page taking so long to load??
|
Why is this page taking so long to load??
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -152,7 +152,7 @@ export function TypingRoomIntro({ active, onFinish }: TypingRoomIntroProps) {
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: 16 }}
|
initial={{ opacity: 0, x: 16 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
className="ml-auto max-w-[85%] rounded-2xl rounded-br-md border border-cyan-300/30 bg-cyan-400/15 px-4 py-3 text-sm text-cyan-50"
|
className="ml-auto max-w-[88%] rounded-2xl rounded-br-md border border-cyan-300/30 bg-cyan-400/15 px-3 py-2.5 text-xs text-cyan-50 sm:px-4 sm:py-3 sm:text-sm"
|
||||||
>
|
>
|
||||||
I have to somehow hide the fact that my discord status is taking a while to load... :D
|
I have to somehow hide the fact that my discord status is taking a while to load... :D
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -162,18 +162,18 @@ export function TypingRoomIntro({ active, onFinish }: TypingRoomIntroProps) {
|
|||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, x: 12 }}
|
initial={{ opacity: 0, x: 12 }}
|
||||||
animate={{ opacity: 1, x: 0 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
className="ml-auto max-w-[85%] rounded-2xl rounded-br-md border border-cyan-300/35 bg-cyan-400/20 px-4 py-3 text-sm text-cyan-50"
|
className="ml-auto max-w-[88%] rounded-2xl rounded-br-md border border-cyan-300/35 bg-cyan-400/20 px-3 py-2.5 text-xs text-cyan-50 sm:px-4 sm:py-3 sm:text-sm"
|
||||||
>
|
>
|
||||||
Ok done, just one more animation
|
Ok done, just one more animation
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative border-t border-white/10 bg-black/35 px-6 pb-5 pt-4">
|
<div className="relative border-t border-white/10 bg-black/35 px-4 pb-4 pt-4 sm:px-6 sm:pb-5">
|
||||||
{/* Small badge left-above the input showing typing status (hidden once final message shows) */}
|
{/* Small badge left-above the input showing typing status (hidden once final message shows) */}
|
||||||
{step < 4 && (
|
{step < 4 && (
|
||||||
<div className="absolute left-6 -top-6 flex items-center gap-2">
|
<div className="absolute left-4 -top-6 flex items-center gap-2 sm:left-6">
|
||||||
<span className="text-sm font-medium text-cyan-100">Space is typing...</span>
|
<span className="text-xs font-medium text-cyan-100 sm:text-sm">Space is typing...</span>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{[0, 1, 2].map((dot) => (
|
{[0, 1, 2].map((dot) => (
|
||||||
<motion.span
|
<motion.span
|
||||||
@@ -195,13 +195,13 @@ export function TypingRoomIntro({ active, onFinish }: TypingRoomIntroProps) {
|
|||||||
<span className="text-[11px] text-gray-400">Esc to skip</span>
|
<span className="text-[11px] text-gray-400">Esc to skip</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 rounded-xl border border-white/10 bg-white/5 px-4 py-2.5 min-h-[44px] flex items-center">
|
<div className="mt-3 flex min-h-[44px] items-center rounded-xl border border-white/10 bg-white/5 px-3 py-2.5 sm:px-4">
|
||||||
{step >= 3 && step < 4 ? (
|
{step >= 3 && step < 4 ? (
|
||||||
<motion.div initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} className="flex items-center gap-2.5 font-mono text-sm text-cyan-100">
|
<motion.div initial={{ opacity: 0, y: 6 }} animate={{ opacity: 1, y: 0 }} className="flex items-center gap-2.5 font-mono text-xs text-cyan-100 sm:text-sm">
|
||||||
<span className="select-all">{garble || "…"}</span>
|
<span className="select-all">{garble || "…"}</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-sm text-gray-500">Message Space</p>
|
<p className="text-xs text-gray-500 sm:text-sm">Message Space</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,21 +1,46 @@
|
|||||||
"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-6 px-4 sm:space-y-8">
|
||||||
<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-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500 sm:text-3xl">
|
||||||
Activity
|
Code Activity
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-400 max-w-2xl mx-auto">
|
<p className="mx-auto max-w-2xl text-sm text-gray-400 sm:text-base">
|
||||||
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>
|
||||||
|
|
||||||
<div className="max-w-5xl mx-auto rounded-3xl border border-white/10 bg-white/5 backdrop-blur-sm p-4 md:p-6 shadow-[0_20px_80px_rgba(0,0,0,0.35)]">
|
<div className="mx-auto max-w-5xl rounded-3xl border border-white/10 bg-white/5 p-3 shadow-[0_20px_80px_rgba(0,0,0,0.35)] backdrop-blur-sm sm:p-4 md:p-6">
|
||||||
<div className="overflow-hidden rounded-2xl border border-white/10 bg-[#0a0a12]">
|
<div className="overflow-hidden rounded-2xl border border-white/10 bg-[#0a0a12]">
|
||||||
<img
|
<img
|
||||||
src={activitySvg}
|
src={activitySvg}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,42 +6,51 @@ 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"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Affiliates() {
|
export function Affiliates() {
|
||||||
return (
|
return (
|
||||||
<section className="w-full max-w-5xl mx-auto space-y-8 px-4">
|
<section className="w-full max-w-5xl mx-auto space-y-6 px-4 sm:space-y-8">
|
||||||
<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-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500 sm:text-3xl">
|
||||||
Affiliates
|
Affiliates
|
||||||
</h2>
|
</h2>
|
||||||
|
<p className="mx-auto max-w-2xl text-sm text-gray-400 sm:text-base">
|
||||||
|
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 gap-4 md:grid-cols-2 sm:gap-5">
|
||||||
{affiliates.map((affiliate) => (
|
{affiliates.map((affiliate) => (
|
||||||
<a
|
<a
|
||||||
key={affiliate.name}
|
key={affiliate.name}
|
||||||
href={affiliate.link}
|
href={affiliate.link}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="rounded-xl border border-white/10 bg-white/5 backdrop-blur-sm p-4 hover:bg-white/10 hover:border-purple-500/40 transition-all duration-300"
|
className="mx-auto w-full max-w-[20rem] rounded-xl border border-white/10 bg-white/5 p-4 backdrop-blur-sm transition-all duration-300 hover:border-purple-500/40 hover:bg-white/10 sm:max-w-none sm:p-4"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center text-center gap-2 mb-3">
|
<div className="flex flex-col items-center text-center gap-2 mb-3">
|
||||||
<img
|
<img
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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-6 sm:space-y-8">
|
||||||
|
<div className="text-center space-y-3">
|
||||||
|
<h2 className="text-3xl font-bold leading-tight text-transparent bg-clip-text bg-gradient-to-r from-amber-400 to-orange-500 sm:text-4xl">
|
||||||
|
Agent Skills
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto max-w-2xl text-sm text-gray-400 sm:text-base">
|
||||||
|
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-5 py-8 text-center text-gray-400 sm:px-6 sm:py-10">
|
||||||
|
No agent skills published yet.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 gap-5 lg:grid-cols-2 sm:gap-6">
|
||||||
|
{skills.map((skill, index) => (
|
||||||
|
<article
|
||||||
|
key={`${skill.name}-${index}`}
|
||||||
|
className="group mx-auto w-full max-w-[20rem] rounded-3xl border border-amber-500/20 bg-gradient-to-br from-amber-500/10 via-white/5 to-orange-500/5 p-5 shadow-[0_0_0_1px_rgba(245,158,11,0.08)] backdrop-blur-sm transition-all duration-300 hover:border-amber-500/40 sm:max-w-none sm:p-6 md:p-7"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-4 sm:flex-row sm:flex-wrap sm:items-start sm:justify-between">
|
||||||
|
<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-xl font-semibold text-white sm:text-2xl">{skill.name}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{skill.link ? (
|
||||||
|
<a
|
||||||
|
href={skill.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="inline-flex items-center justify-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 sm:self-start"
|
||||||
|
>
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,18 +2,18 @@
|
|||||||
|
|
||||||
export function Contact() {
|
export function Contact() {
|
||||||
return (
|
return (
|
||||||
<section className="w-full max-w-2xl text-center space-y-4 pb-8 mx-auto">
|
<section className="mx-auto w-full max-w-[20rem] space-y-4 pb-8 text-center sm:max-w-2xl">
|
||||||
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-green-400 via-blue-500 to-purple-500">
|
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-green-400 via-blue-500 to-purple-500 sm:text-3xl">
|
||||||
Want to talk?
|
Want to talk?
|
||||||
</h2>
|
</h2>
|
||||||
<div className="p-8 rounded-3xl bg-gradient-to-br from-blue-500/10 via-purple-500/10 to-pink-500/10 border border-blue-500/20 backdrop-blur-md">
|
<div className="rounded-3xl border border-blue-500/20 bg-gradient-to-br from-blue-500/10 via-purple-500/10 to-pink-500/10 p-5 backdrop-blur-md sm:p-8">
|
||||||
<p className="text-gray-300 mb-8 text-lg">
|
<p className="mb-6 text-sm text-gray-300 sm:mb-8 sm:text-lg">
|
||||||
Im almost always down to talk about anything. Tech, life and whatever. Feel free to reach out to me on Discord or via Email, i usually respond pretty quickly.
|
Im almost always down to talk about anything. Tech, life and whatever. Feel free to reach out to me on Discord or via Email, i usually respond pretty quickly.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
<div className="flex flex-col justify-center gap-3 sm:flex-row sm:gap-4">
|
||||||
<a
|
<a
|
||||||
href="mailto:space@reversed.dev"
|
href="mailto:space@reversed.dev"
|
||||||
className="px-8 py-3 rounded-full bg-gradient-to-r from-white to-gray-100 text-black font-bold hover:from-gray-100 hover:to-white transition-all shadow-lg shadow-white/20 hover:shadow-white/30"
|
className="rounded-full bg-gradient-to-r from-white to-gray-100 px-6 py-3 font-bold text-black transition-all shadow-lg shadow-white/20 hover:from-gray-100 hover:to-white hover:shadow-white/30"
|
||||||
>
|
>
|
||||||
Shoot me an Email
|
Shoot me an Email
|
||||||
</a>
|
</a>
|
||||||
@@ -21,7 +21,7 @@ export function Contact() {
|
|||||||
href="https://discord.com/users/456443941169004545"
|
href="https://discord.com/users/456443941169004545"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="px-8 py-3 rounded-full bg-gradient-to-r from-[#5865F2] to-[#4752C4] text-white font-bold hover:from-[#4752C4] hover:to-[#5865F2] transition-all shadow-lg shadow-[#5865F2]/30 hover:shadow-[#5865F2]/50"
|
className="rounded-full bg-gradient-to-r from-[#5865F2] to-[#4752C4] px-6 py-3 font-bold text-white transition-all shadow-lg shadow-[#5865F2]/30 hover:from-[#4752C4] hover:to-[#5865F2] hover:shadow-[#5865F2]/50"
|
||||||
>
|
>
|
||||||
Chat on Discord
|
Chat on Discord
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+21
-14
@@ -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);
|
||||||
@@ -57,7 +60,7 @@ export function Hero({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className="mt-20 flex flex-col items-center text-center space-y-8 animate-fade-in"
|
className="mt-16 flex flex-col items-center text-center space-y-6 px-4 sm:mt-20 sm:space-y-8 animate-fade-in"
|
||||||
data-old-usernames={oldUsernames.join(",")}
|
data-old-usernames={oldUsernames.join(",")}
|
||||||
>
|
>
|
||||||
<div className="relative group" onMouseMove={handleMouseMove}>
|
<div className="relative group" onMouseMove={handleMouseMove}>
|
||||||
@@ -66,17 +69,17 @@ export function Hero({
|
|||||||
style={{ backgroundColor: glowColor }}
|
style={{ backgroundColor: glowColor }}
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
className={`relative w-48 h-48 rounded-full border-4 transition-colors duration-500 overflow-hidden ${borderStatus} bg-black`}
|
className={`relative h-32 w-32 overflow-hidden rounded-full border-4 bg-black transition-colors duration-500 sm:h-48 sm:w-48 ${borderStatus}`}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="https://cdn.reversed.dev/pictures/20250405_120402.png"
|
src="https://cdn.reversed.dev/pictures/20250405_120402.png"
|
||||||
alt="Space"
|
alt="Space"
|
||||||
className="w-full h-full object-cover scale-110 transition-transform duration-700 group-hover:scale-125"
|
className="h-full w-full scale-110 object-cover transition-transform duration-700 group-hover:scale-125"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rotating Message Bubble (Left Side) */}
|
{/* Rotating Message Bubble (Left Side) */}
|
||||||
<div className="absolute -left-4 top-4 -translate-x-full">
|
<div className="absolute -left-4 top-4 hidden -translate-x-full md:block">
|
||||||
<div className="relative bg-gradient-to-br from-purple-500/20 to-pink-500/10 backdrop-blur-sm border border-purple-500/30 rounded-2xl px-4 py-3 shadow-lg">
|
<div className="relative bg-gradient-to-br from-purple-500/20 to-pink-500/10 backdrop-blur-sm border border-purple-500/30 rounded-2xl px-4 py-3 shadow-lg">
|
||||||
{/* Arrow pointing to profile */}
|
{/* Arrow pointing to profile */}
|
||||||
<div className="absolute right-0 top-1/2 translate-x-2 -translate-y-1/2 w-0 h-0 border-t-8 border-t-transparent border-b-8 border-b-transparent border-l-8 border-l-purple-500/30"></div>
|
<div className="absolute right-0 top-1/2 translate-x-2 -translate-y-1/2 w-0 h-0 border-t-8 border-t-transparent border-b-8 border-b-transparent border-l-8 border-l-purple-500/30"></div>
|
||||||
@@ -89,7 +92,7 @@ export function Hero({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Status Bubble (Right Side) */}
|
{/* Status Bubble (Right Side) */}
|
||||||
<div className="absolute -right-4 top-8 translate-x-full">
|
<div className="absolute -right-4 top-8 hidden translate-x-full md:block">
|
||||||
<div className="relative bg-gradient-to-br from-white/10 to-white/5 backdrop-blur-sm border border-white/20 rounded-2xl px-4 py-3 shadow-lg">
|
<div className="relative bg-gradient-to-br from-white/10 to-white/5 backdrop-blur-sm border border-white/20 rounded-2xl px-4 py-3 shadow-lg">
|
||||||
{/* Arrow pointing to profile */}
|
{/* Arrow pointing to profile */}
|
||||||
<div className="absolute left-0 top-1/2 -translate-x-2 -translate-y-1/2 w-0 h-0 border-t-8 border-t-transparent border-b-8 border-b-transparent border-r-8 border-r-white/20"></div>
|
<div className="absolute left-0 top-1/2 -translate-x-2 -translate-y-1/2 w-0 h-0 border-t-8 border-t-transparent border-b-8 border-b-transparent border-r-8 border-r-white/20"></div>
|
||||||
@@ -104,12 +107,15 @@ 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>
|
||||||
|
|
||||||
{/* Name & Description */}
|
{/* Name & Description */}
|
||||||
<div className="space-y-4 max-w-2xl">
|
<div className="space-y-4 max-w-2xl">
|
||||||
<h1 className="text-6xl md:text-7xl font-extrabold">
|
<h1 className="text-4xl font-extrabold leading-tight sm:text-5xl md:text-7xl">
|
||||||
Hey, I'm{" "}
|
Hey, I'm{" "}
|
||||||
<span
|
<span
|
||||||
className="relative inline-block text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-purple-500 to-pink-500 px-2"
|
className="relative inline-block text-transparent bg-clip-text bg-gradient-to-r from-blue-400 via-purple-500 to-pink-500 px-2"
|
||||||
@@ -118,7 +124,7 @@ export function Hero({
|
|||||||
Space²
|
Space²
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-2xl text-gray-400 font-light">
|
<p className="text-base font-light text-gray-400 sm:text-xl md:text-2xl">
|
||||||
A{" "}
|
A{" "}
|
||||||
<span className="line-through decoration-purple-500/50 decoration-2">
|
<span className="line-through decoration-purple-500/50 decoration-2">
|
||||||
Self-proclaimed
|
Self-proclaimed
|
||||||
@@ -128,13 +134,12 @@ export function Hero({
|
|||||||
|
|
||||||
{/* Luna AI Profile Link */}
|
{/* Luna AI Profile Link */}
|
||||||
<div className="pt-4 flex justify-center">
|
<div className="pt-4 flex justify-center">
|
||||||
|
<div className="flex flex-wrap items-center justify-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 w-full items-center justify-center gap-3 rounded-full border border-white/10 bg-white/5 px-5 py-2.5 text-center shadow-lg transition-all duration-300 hover:border-purple-500/50 hover:bg-white/10 hover:shadow-purple-500/10 sm:w-auto"
|
||||||
>
|
>
|
||||||
<div className="w-3 h-3 rounded-full bg-purple-500" />
|
<div className="w-3 h-3 rounded-full bg-purple-500" />
|
||||||
<span className="text-sm font-medium text-gray-300 group-hover:text-white transition-colors">
|
<span className="text-sm font-medium text-gray-300 group-hover:text-white transition-colors">
|
||||||
@@ -146,7 +151,7 @@ export function Hero({
|
|||||||
href="https://github.com/Space-Banane"
|
href="https://github.com/Space-Banane"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group relative flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 hover:bg-white/10 hover:border-gray-400 transition-all duration-300 shadow-sm"
|
className="group relative flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-2 shadow-sm transition-all duration-300 hover:border-gray-400 hover:bg-white/10"
|
||||||
aria-label="GitHub"
|
aria-label="GitHub"
|
||||||
>
|
>
|
||||||
<svg className="w-6 h-6 text-gray-300 group-hover:text-white" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
<svg className="w-6 h-6 text-gray-300 group-hover:text-white" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
|
||||||
@@ -158,7 +163,7 @@ export function Hero({
|
|||||||
href="https://gitea.reversed.dev/space"
|
href="https://gitea.reversed.dev/space"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="group relative flex items-center gap-2 px-4 py-2 rounded-full bg-white/5 border border-white/10 hover:bg-white/10 hover:border-yellow-400 transition-all duration-300 shadow-sm"
|
className="group relative flex items-center gap-2 rounded-full border border-white/10 bg-white/5 px-4 py-2 shadow-sm transition-all duration-300 hover:border-yellow-400 hover:bg-white/10"
|
||||||
aria-label="Gitea"
|
aria-label="Gitea"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
@@ -169,6 +174,8 @@ export function Hero({
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<TimezoneClockBlock warningThresholdHours={timeGapWarningThreshold} />
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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-8 px-4 pb-20 sm:space-y-12">
|
||||||
|
<div className="text-center space-y-4">
|
||||||
|
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500 sm:text-3xl">
|
||||||
|
Skills & Experience
|
||||||
|
</h2>
|
||||||
|
<p className="text-sm text-gray-400 sm:text-base">
|
||||||
|
<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 gap-8 md:grid-cols-2 sm:gap-12">
|
||||||
|
{Object.entries(groupedExperiences).map(([type, items]) => (
|
||||||
|
<div key={type} className="space-y-6">
|
||||||
|
<h3 className="border-l-4 border-blue-500 pl-4 text-lg font-semibold capitalize sm:text-xl">
|
||||||
|
{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
@@ -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 (
|
||||||
|
<section className="w-full max-w-4xl mx-auto px-4 space-y-10 mt-16 mb-24">
|
||||||
|
<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 sm:text-4xl">
|
||||||
|
Tech Stack
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto max-w-2xl text-sm text-gray-400 md:text-base">
|
||||||
|
This section is currently disabled. It may be re-enabled in the future, but for now, it's hidden.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
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-10 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-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500 sm:text-4xl">
|
||||||
Tech Stack
|
Tech Stack
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-400 max-w-2xl mx-auto">
|
<p className="mx-auto max-w-2xl text-sm text-gray-400 md:text-base">
|
||||||
My current infrastructure and software stack, from server to
|
A overview of the tools and technologies I use to build and host my applications.
|
||||||
monitoring.
|
|
||||||
</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>
|
||||||
|
|||||||
+13
-10
@@ -2,29 +2,32 @@
|
|||||||
|
|
||||||
export function Uptime() {
|
export function Uptime() {
|
||||||
return (
|
return (
|
||||||
<section className="w-full max-w-6xl mx-auto space-y-10 px-4">
|
<section className="w-full max-w-6xl mx-auto space-y-8 px-4 sm:space-y-10">
|
||||||
<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-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500 sm:text-3xl">
|
||||||
Downtime
|
Downtime
|
||||||
</h2>
|
</h2>
|
||||||
|
<p className="mx-auto max-w-2xl text-sm text-gray-400 sm:text-base">
|
||||||
|
I love selfhosting stuff, can you tell?
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-3xl mx-auto">
|
<div className="mx-auto max-w-3xl">
|
||||||
<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="mx-auto w-full max-w-[20rem] rounded-2xl border border-white/6 bg-gradient-to-br from-gray-800/20 to-transparent p-5 backdrop-blur-sm sm:max-w-none sm:p-6">
|
||||||
<h3 className="text-2xl font-semibold text-white mb-4 text-center">
|
<h3 className="mb-4 text-center text-xl font-semibold text-white sm:text-2xl">
|
||||||
I love my homelab
|
Github vs HomeLab Uptime
|
||||||
</h3>
|
</h3>
|
||||||
<div className="flex gap-6 items-center justify-between">
|
<div className="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
||||||
<div className="flex-1 text-center">
|
<div className="flex-1 text-center">
|
||||||
<img src="/gh_down.png" alt="GitHub downtime" className="mx-auto h-28 object-contain" />
|
<img src="/gh_down.png" alt="GitHub downtime" className="mx-auto h-24 object-contain sm:h-28" />
|
||||||
<h4 className="mt-3 font-medium text-white">GitHub</h4>
|
<h4 className="mt-3 font-medium text-white">GitHub</h4>
|
||||||
<p className="text-gray-400 mt-1">Goes down more.</p>
|
<p className="text-gray-400 mt-1">Goes down more.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-px h-24 bg-white/6" />
|
<div className="h-px w-24 bg-white/6 sm:h-24 sm:w-px" />
|
||||||
|
|
||||||
<div className="flex-1 text-center">
|
<div className="flex-1 text-center">
|
||||||
<img src="/homelab_down.png" alt="HomeLab" className="mx-auto h-28 object-contain" />
|
<img src="/homelab_down.png" alt="HomeLab" className="mx-auto h-24 object-contain sm:h-28" />
|
||||||
<h4 className="mt-3 font-medium text-white">HomeLab</h4>
|
<h4 className="mt-3 font-medium text-white">HomeLab</h4>
|
||||||
<p className="text-gray-400 mt-1">Goes down less.</p>
|
<p className="text-gray-400 mt-1">Goes down less.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -51,18 +51,18 @@ export function WorkExperience({ realWork }: WorkExperienceProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full max-w-4xl mx-auto px-4 space-y-8">
|
<section className="w-full max-w-4xl mx-auto px-4 space-y-6 sm:space-y-8">
|
||||||
<div className="text-center space-y-2">
|
<div className="text-center space-y-2">
|
||||||
<h2 className="text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500">
|
<h2 className="text-2xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-cyan-400 to-blue-500 sm:text-3xl">
|
||||||
Work Experience
|
Work Experience
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-400 max-w-2xl mx-auto">
|
<p className="mx-auto max-w-2xl text-sm text-gray-400 sm:text-base">
|
||||||
Professional collaborations and product work I have contributed to.
|
Actual work that i did at actual companies.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{realWork.length === 0 ? (
|
{realWork.length === 0 ? (
|
||||||
<div 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 text-center">
|
<div className="rounded-2xl border border-cyan-500/20 bg-gradient-to-br from-cyan-500/10 to-blue-500/5 p-5 text-center backdrop-blur-sm sm:p-6 md:p-8">
|
||||||
<p className="text-gray-300">No work experience entries available yet.</p>
|
<p className="text-gray-300">No work experience entries available yet.</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
@@ -70,11 +70,11 @@ export function WorkExperience({ realWork }: WorkExperienceProps) {
|
|||||||
{sortedRealWork.map((entry, index) => (
|
{sortedRealWork.map((entry, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${entry.company}-${index}`}
|
key={`${entry.company}-${index}`}
|
||||||
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="mx-auto w-full max-w-[20rem] space-y-4 rounded-2xl border border-cyan-500/20 bg-gradient-to-br from-cyan-500/10 to-blue-500/5 p-5 backdrop-blur-sm sm:max-w-none sm:space-y-5 sm:p-6 md:p-8"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between sm:gap-2">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-2xl font-semibold text-white">{entry.company}</h3>
|
<h3 className="text-xl font-semibold text-white sm:text-2xl">{entry.company}</h3>
|
||||||
{entry.role ? (
|
{entry.role ? (
|
||||||
<p className="text-sm font-medium text-cyan-200/90 mt-1">{entry.role}</p>
|
<p className="text-sm font-medium text-cyan-200/90 mt-1">{entry.role}</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user