diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/App.tsx | 27 | ||||
| -rw-r--r-- | src/blog/3dprint.md | 66 | ||||
| -rw-r--r-- | src/blog/homelab.md | 29 | ||||
| -rw-r--r-- | src/blog/nix.md | 106 | ||||
| -rw-r--r-- | src/components/Footer.tsx | 20 | ||||
| -rw-r--r-- | src/components/Gallery.tsx | 100 | ||||
| -rw-r--r-- | src/components/Map.tsx | 40 | ||||
| -rw-r--r-- | src/components/Navbar.tsx | 24 | ||||
| -rw-r--r-- | src/components/ThemeToggle.tsx | 29 | ||||
| -rw-r--r-- | src/components/ui/aspect-ratio.tsx | 11 | ||||
| -rw-r--r-- | src/components/ui/badge.tsx | 46 | ||||
| -rw-r--r-- | src/components/ui/button.tsx | 59 | ||||
| -rw-r--r-- | src/components/ui/card.tsx | 92 | ||||
| -rw-r--r-- | src/components/ui/navigation-menu.tsx | 168 | ||||
| -rw-r--r-- | src/components/ui/separator.tsx | 26 | ||||
| -rw-r--r-- | src/components/ui/sheet.tsx | 137 | ||||
| -rw-r--r-- | src/data/projects.ts | 31 | ||||
| -rw-r--r-- | src/data/tools.ts | 38 | ||||
| -rw-r--r-- | src/hooks/theme.tsx | 33 | ||||
| -rw-r--r-- | src/index.css | 118 | ||||
| -rw-r--r-- | src/lib/utils.ts | 6 | ||||
| -rw-r--r-- | src/main.tsx | 10 | ||||
| -rw-r--r-- | src/pages/404.tsx | 22 | ||||
| -rw-r--r-- | src/pages/Blog.tsx | 90 | ||||
| -rw-r--r-- | src/pages/Home.tsx | 108 | ||||
| -rw-r--r-- | src/pages/Post.tsx | 58 | ||||
| -rw-r--r-- | src/vite-env.d.ts | 9 |
27 files changed, 0 insertions, 1503 deletions
diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index 0d4b55e..0000000 --- a/src/App.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Routes, Route } from 'react-router-dom'; -import Navbar from './components/Navbar'; -import Home from './pages/Home'; -import Footer from './components/Footer'; -import NotFound from './pages/404'; -import Blog from './pages/Blog'; -import Post from './pages/Post'; - -function App() { - - return ( - <> - <Navbar /> - <section className="max-w-4xl mx-auto py-5 px-4"> - <Routes> - <Route path="/" element={<Home />} /> - <Route path='/blog' element={<Blog />} /> - <Route path='/blog/:slug' element={<Post />} /> - <Route path='*' element={<NotFound />} /> - </Routes> - </section> - <Footer /> - </> - ); -} - -export default App diff --git a/src/blog/3dprint.md b/src/blog/3dprint.md deleted file mode 100644 index 3a65f02..0000000 --- a/src/blog/3dprint.md +++ /dev/null @@ -1,66 +0,0 @@ ---- -title: "3D Printing" -date: "2025-06-25" -excerpt: "My 3D-printing projects: from a robotic arm to a DIY drone." -cover: "/images/a1.webp" ---- - -# Projects - -## Robotic Arm - -3D model on [MakerWorld](https://makerworld.com/en/models/528885-robotic-arm#profileId-445995) – modified to work with my servo motors. - -```cpp -#include <Bluepad32.h> -#include <ESP32Servo.h> - -#define DEADZONE 30 -#define BASE_PIN 15 -#define SHOULDER_PIN 2 -#define ELBOW_PIN 4 -#define WRIST_PIN 16 -#define HAND_PIN 17 - -Servo base, shoulder, elbow, wrist, hand; -ControllerPtr pad; - -void onConnectedGamepad(ControllerPtr ctl) { - Serial.printf("Gamepad #%d connected\n", ctl->index()); - pad = ctl; -} - -void onDisconnectedGamepad(ControllerPtr ctl) { - Serial.printf("Gamepad disconnected\n"); -} - -int16_t mapAxis(int16_t v) { - if (abs(v) < DEADZONE) v = 0; - return map(v, -512, 512, 0, 180); -} - -void setup() { - BP32.setup(&onConnectedGamepad, &onDisconnectedGamepad); - BP32.enableNewBluetoothConnections(true); - - base.attach(BASE_PIN); - shoulder.attach(SHOULDER_PIN); - elbow.attach(ELBOW_PIN); - wrist.attach(WRIST_PIN); - hand.attach(HAND_PIN); -} - -void loop() { - BP32.update(); - if (pad && pad->isConnected()) { - base.write(mapAxis(pad->axisX())); - shoulder.write(mapAxis(-pad->axisY())); - elbow.write(mapAxis(-pad->axisRY())); - hand.write(mapAxis(pad->throttle() - pad->brake())); - - if (pad->l1()) wrist.write(0); - else if (pad->r1()) wrist.write(180); - else wrist.write(90); - } - delay(15); -} diff --git a/src/blog/homelab.md b/src/blog/homelab.md deleted file mode 100644 index f61d0e9..0000000 --- a/src/blog/homelab.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -title: "Homelab" -date: "2025-06-25" -excerpt: "My Homelab setup" -cover: "/images/nas.webp" ---- - -## NAS - -My custom-built NAS running Unraid hosts the following services. See [gear](/gear) for specs. - -- Jellyfin: Media library -- Kavita: Ebooks and manga -- AdGuard Home: Ad blocking -- Nginx: Reverse proxy -- Ollama: Enough for small LLM testing - - -Replaced the rear fan with a [Nocuta NF-A12x15](https://noctua.at/en/nf-a12x15-pwm-chromax-black-swap) that is much quieter. - -For remote access, I connect to the machine via VPN. I back up my MacBook to that machine with Time Machine, and I back up my desktop and VPS to it using rsync. - -## Raspberry Pi - -Raspberry Pi 5 (8GB) running [Homebridge](https://homebridge.io/) to integrate non-HomeKit devices. It also serves as a precision NTP server using a [Uputronics](https://store.uputronics.com/products/raspberry-pi-gps-rtc-expansion-board) GPS module. - -## VPS -Cheap Ionos VPS running nixos via [nixos-infect](https://github.com/elitak/nixos-infect) for services exposed to the internet. Mainly using it for hosting this website and [Nextcloud](https://nextcloud.com/). - diff --git a/src/blog/nix.md b/src/blog/nix.md deleted file mode 100644 index 22ac1b3..0000000 --- a/src/blog/nix.md +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: "Nix" -date: "2025-06-25" -excerpt: "A deep dive into my fully declarative system setup using Nix, from desktop to VPS." -cover: "/images/nixsnowflake.webp" ---- - -# My Nix Setup: Why I Switched Everything - -I've used Linux for years, mostly in dual boot with Windows. But a few months ago, I switched completely to [NixOS](https://nixos.org/), and honestly, I love it. The idea of configuring everything on my system **declaratively**—just by writing code—is what really got me hooked. - -## Sway with Nix - -I use [Sway](https://github.com/swaywm/sway) as my window manager, and setting it up with Nix has been super easy. Instead of messing with dotfiles or installing things manually, I just describe what I want in a config file and let Nix handle it. - -## Neovim + NVF - -One of my favorite changes has been switching my Neovim setup to use [NVF](https://github.com/NotAShelf/nvf). It makes managing plugins and settings way easier. - -Here's a peek at my `nvf.nix` config: - -```nix -{ config, lib, pkgs, ... }: - -{ - programs.nvf = { - enable = true; - settings = { - vim = { - theme.enable = true; - theme.name = "gruvbox"; - - options = { - clipboard = "unnamedplus"; - tabstop = 2; - shiftwidth = 2; - expandtab = true; - autoindent = true; - mouse = "a"; - }; - - telescope.enable = true; - autocomplete.nvim-cmp.enable = true; - - autopairs.nvim-autopairs.enable = true; - - git.enable = true; - - lsp = { - enable = true; - formatOnSave = true; - lspkind.enable = true; - lspSignature.enable = true; - }; - - languages = { - enableTreesitter = true; - - nix.enable = true; - markdown.enable = true; - - clang.enable = true; - css.enable = true; - html.enable = true; - java.enable = true; - ts.enable = true; - go.enable = true; - lua.enable = true; - python.enable = true; - typst.enable = true; - }; - - formatter.conform-nvim.enable = true; - - visuals = { - nvim-web-devicons.enable = true; - }; - - snippets.luasnip.enable = true; - - binds = { - whichKey.enable = true; - cheatsheet.enable = true; - }; - - statusline.lualine.enable = true; - }; - }; - }; - }; -} -``` - -You can check out my full Nix configuration [here on GitHub](https://github.com/schererleander/nix). - -## Firefox, Configured by Code - -I even manage **Firefox** through Nix! From settings to extensions, everything is set up declaratively. It’s cool to see a browser this customizable through config files. - -## Using nix-darwin on macOS - -I’ve got a **MacBook Air**, and I’m using [nix-darwin](https://github.com/LnL7/nix-darwin) on it. It’s not quite as deep as NixOS, but I can still manage most of my tools and configs declaratively. Works great for development stuff. - -## VPS with nix-infect - -My VPS is running Nix too. I used [nix-infect](https://github.com/elitak/nix-infect) to get started, and now I manage things like **[nginx](https://nginx.org/)** and **[Nextcloud](https://nextcloud.com/)** with Nix. It’s super easy to maintain and back up. diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx deleted file mode 100644 index 2b3be02..0000000 --- a/src/components/Footer.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Github, Mail } from "lucide-react"; - -export default function Footer() { - const year = new Date().getFullYear(); - return ( - <footer className="flex flex-col items-center gap-2 py-4"> - <div className="flex gap-6"> - <a href="https://github.com/schererleander" target="_blank" rel="noopener noreferrer" aria-label="Leander Scherer's GitHub"> - <Github className="w-4 h-4" strokeWidth={3} /> - </a> - - <a href="mailto:leander@schererleander.de" aria-label="Send email to Leander Scherer"> - <Mail className="w-4 h-4" strokeWidth={3} /> - </a> - </div> - - <span className="text-sm">© {year} Leander Scherer — ʕっ•ᴥ•ʔっ</span> - </footer> - ); -} diff --git a/src/components/Gallery.tsx b/src/components/Gallery.tsx deleted file mode 100644 index ffcf0e1..0000000 --- a/src/components/Gallery.tsx +++ /dev/null @@ -1,100 +0,0 @@ -import React, { useState, useEffect } from 'react'; - -interface ImageItems { - images: Array<{ - src: string; - alt?: string; - id?: string | number; - }>; -} - -export default function Gallery({ images }: ImageItems) { - const [selectedIndex, setSelectedIndex] = useState<number | null>(null); - const closeModal = () => setSelectedIndex(null); - const prev = (e?: React.MouseEvent) => { - e?.stopPropagation(); - setSelectedIndex((i) => (i !== null && i > 0 ? i - 1 : i)); - }; - const next = (e?: React.MouseEvent) => { - e?.stopPropagation(); - setSelectedIndex((i) => (i !== null && i < images.length - 1 ? i + 1 : i)); - }; - - useEffect(() => { - function handleKeyDown(event: KeyboardEvent) { - if (selectedIndex === null) return; - if (event.key === 'ArrowLeft') { - prev(); - } else if (event.key === 'ArrowRight') { - next(); - } - } - - window.addEventListener('keydown', handleKeyDown); - return () => window.removeEventListener('keydown', handleKeyDown); - }, [selectedIndex, images.length]); - - return ( - <> - <div className="grid gap-2 grid-cols-4 max-sm:grid-cols-3 p-4"> - {images.map((image, idx) => ( - <button - key={image.id ?? image.src} - className="relative overflow-hidden rounded-xl shadow-lg aspect-square group cursor-pointer" - onClick={() => setSelectedIndex(idx)} - > - <img - src={image.src} - alt={image.alt || 'Gallery image'} - className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" - /> - </button> - ))} - </div> - - {/* Overlay */} - {selectedIndex !== null && ( - <div - className="fixed inset-0 bg-white/50 dark:bg-black/50 backdrop-blur-sm flex items-center justify-center z-50" - onClick={closeModal} - role="dialog" - aria-modal="true" - > - {/* Prev arrow */} - <button - onClick={prev} - className="absolute left-4 text-black dark:text-white text-4xl disabled:opacity-30" - disabled={selectedIndex === 0} - > - ← - </button> - - <img - src={images[selectedIndex].src} - alt={images[selectedIndex].alt || 'Enlarged gallery'} - className="w-2/3 h-2/3 object-contain rounded-lg" - /> - - {/* Next arrow */} - <button - onClick={next} - className="absolute right-4 text-black dark:text-white text-4xl disabled:opacity-30" - disabled={selectedIndex === images.length - 1} - aria-label='Next image' - > - → - </button> - <button - onClick={(e) => { - e.stopPropagation(); - closeModal(); - }} - className="absolute top-4 right-4 text-black dark:text-white text-3xl" - > - × - </button> - </div> - )} - </> - ); -} diff --git a/src/components/Map.tsx b/src/components/Map.tsx deleted file mode 100644 index 1e4038f..0000000 --- a/src/components/Map.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import { useEffect } from "react"; -import { MapContainer, TileLayer, Marker, useMap } from "react-leaflet"; -import L from "leaflet"; -import "leaflet/dist/leaflet.css"; - -const KARLSRUHE: [number, number] = [49.0069, 8.4037]; - -const icon = L.divIcon({ - html: `<div style="width:12px;height:12px;background:var(--background);border-radius:50%;border:2px solid var(--foreground);box-shadow:0 0 10px rgba(0,0,0,0.5)"></div>`, - className: "", - iconSize: [12, 12], -}); - -function MapEffects() { - const map = useMap(); - useEffect(() => { - map.flyTo(KARLSRUHE, 14, { duration: 3 }); - }, [map]); - return null; -} - -export default function Map() { - const tileUrl = "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png"; - - return ( - <div - className="relative h-[450px] w-full overflow-hidden rounded-xl border border-border z-0" - style={{ - maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)", - WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)", - }} - > - <MapContainer center={[49, 8.4]} zoom={10} scrollWheelZoom={false} className="h-full w-full" zoomControl={false} attributionControl={false}> - <TileLayer url={tileUrl} /> - <Marker position={KARLSRUHE} icon={icon} /> - <MapEffects /> - </MapContainer> - </div> - ); -} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx deleted file mode 100644 index 720274b..0000000 --- a/src/components/Navbar.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Button } from "./ui/button"; -import ThemeToggle from "./ThemeToggle"; - -const navItems = [ - { label: "Home", href: "/" }, - { label: "Blog", href: "/blog" }, -]; - -export default function Navbar() { - return ( - <nav className="sticky top-0 z-50 w-full backdrop-blur"> - <div className="max-w-2xl mx-auto flex items-center justify-between px-4 py-4"> - <div className="flex gap-4 justify-center flex-1"> - {navItems.map(({ label, href }) => ( - <Button key={label} variant="ghost" asChild> - <a href={href}>{label}</a> - </Button> - ))} - </div> - <ThemeToggle /> - </div> - </nav> - ); -} diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx deleted file mode 100644 index fa269b1..0000000 --- a/src/components/ThemeToggle.tsx +++ /dev/null @@ -1,29 +0,0 @@ -// components/ThemeToggle.tsx -import { useTheme } from "../hooks/theme"; - -export default function ThemeToggle() { - const { theme, toggleTheme } = useTheme(); - return ( - <button type="button" aria-label="Toogle dark mode" onClick={toggleTheme}> - {theme === 'dark' ? ( - <svg - className="w-5 h-5" - fill="none" - viewBox="0 0 24 24" - stroke="currentColor" - aria-hidden="true" - > - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"></path></svg> - ) : ( - <svg - className="w-5 h-5" - fill="none" - viewBox="0 0 24 24" - stroke="currentColor" - aria-hidden="true" - > - <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"></path></svg> - )} - </button> - ); -}
\ No newline at end of file diff --git a/src/components/ui/aspect-ratio.tsx b/src/components/ui/aspect-ratio.tsx deleted file mode 100644 index 3df3fd0..0000000 --- a/src/components/ui/aspect-ratio.tsx +++ /dev/null @@ -1,11 +0,0 @@ -"use client" - -import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" - -function AspectRatio({ - ...props -}: React.ComponentProps<typeof AspectRatioPrimitive.Root>) { - return <AspectRatioPrimitive.Root data-slot="aspect-ratio" {...props} /> -} - -export { AspectRatio } diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx deleted file mode 100644 index ec8d7f7..0000000 --- a/src/components/ui/badge.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -function Badge({ - className, - variant, - asChild = false, - ...props -}: React.ComponentProps<"span"> & - VariantProps<typeof badgeVariants> & { asChild?: boolean }) { - const Comp = asChild ? Slot : "span" - - return ( - <Comp - data-slot="badge" - className={cn(badgeVariants({ variant }), className)} - {...props} - /> - ) -} -export { Badge, badgeVariants } - diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx deleted file mode 100644 index a2df8dc..0000000 --- a/src/components/ui/button.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: - "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", - destructive: - "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - -function Button({ - className, - variant, - size, - asChild = false, - ...props -}: React.ComponentProps<"button"> & - VariantProps<typeof buttonVariants> & { - asChild?: boolean - }) { - const Comp = asChild ? Slot : "button" - - return ( - <Comp - data-slot="button" - className={cn(buttonVariants({ variant, size, className }))} - {...props} - /> - ) -} - -export { Button, buttonVariants } diff --git a/src/components/ui/card.tsx b/src/components/ui/card.tsx deleted file mode 100644 index d05bbc6..0000000 --- a/src/components/ui/card.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -function Card({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card" - className={cn( - "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm", - className - )} - {...props} - /> - ) -} - -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-header" - className={cn( - "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6", - className - )} - {...props} - /> - ) -} - -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-title" - className={cn("leading-none font-semibold", className)} - {...props} - /> - ) -} - -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-description" - className={cn("text-muted-foreground text-sm", className)} - {...props} - /> - ) -} - -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-action" - className={cn( - "col-start-2 row-span-2 row-start-1 self-start justify-self-end", - className - )} - {...props} - /> - ) -} - -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-content" - className={cn("px-6", className)} - {...props} - /> - ) -} - -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-footer" - className={cn("flex items-center px-6 [.border-t]:pt-6", className)} - {...props} - /> - ) -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -} diff --git a/src/components/ui/navigation-menu.tsx b/src/components/ui/navigation-menu.tsx deleted file mode 100644 index 1199945..0000000 --- a/src/components/ui/navigation-menu.tsx +++ /dev/null @@ -1,168 +0,0 @@ -import * as React from "react" -import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" -import { cva } from "class-variance-authority" -import { ChevronDownIcon } from "lucide-react" - -import { cn } from "@/lib/utils" - -function NavigationMenu({ - className, - children, - viewport = true, - ...props -}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & { - viewport?: boolean -}) { - return ( - <NavigationMenuPrimitive.Root - data-slot="navigation-menu" - data-viewport={viewport} - className={cn( - "group/navigation-menu relative flex max-w-max flex-1 items-center justify-center", - className - )} - {...props} - > - {children} - {viewport && <NavigationMenuViewport />} - </NavigationMenuPrimitive.Root> - ) -} - -function NavigationMenuList({ - className, - ...props -}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) { - return ( - <NavigationMenuPrimitive.List - data-slot="navigation-menu-list" - className={cn( - "group flex flex-1 list-none items-center justify-center gap-1", - className - )} - {...props} - /> - ) -} - -function NavigationMenuItem({ - className, - ...props -}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) { - return ( - <NavigationMenuPrimitive.Item - data-slot="navigation-menu-item" - className={cn("relative", className)} - {...props} - /> - ) -} - -const navigationMenuTriggerStyle = cva( - "group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1" -) - -function NavigationMenuTrigger({ - className, - children, - ...props -}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) { - return ( - <NavigationMenuPrimitive.Trigger - data-slot="navigation-menu-trigger" - className={cn(navigationMenuTriggerStyle(), "group", className)} - {...props} - > - {children}{" "} - <ChevronDownIcon - className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180" - aria-hidden="true" - /> - </NavigationMenuPrimitive.Trigger> - ) -} - -function NavigationMenuContent({ - className, - ...props -}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) { - return ( - <NavigationMenuPrimitive.Content - data-slot="navigation-menu-content" - className={cn( - "data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto", - "group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none", - className - )} - {...props} - /> - ) -} - -function NavigationMenuViewport({ - className, - ...props -}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) { - return ( - <div - className={cn( - "absolute top-full left-0 isolate z-50 flex justify-center" - )} - > - <NavigationMenuPrimitive.Viewport - data-slot="navigation-menu-viewport" - className={cn( - "origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]", - className - )} - {...props} - /> - </div> - ) -} - -function NavigationMenuLink({ - className, - ...props -}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) { - return ( - <NavigationMenuPrimitive.Link - data-slot="navigation-menu-link" - className={cn( - "data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4", - className - )} - {...props} - /> - ) -} - -function NavigationMenuIndicator({ - className, - ...props -}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) { - return ( - <NavigationMenuPrimitive.Indicator - data-slot="navigation-menu-indicator" - className={cn( - "data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden", - className - )} - {...props} - > - <div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" /> - </NavigationMenuPrimitive.Indicator> - ) -} - -export { - NavigationMenu, - NavigationMenuList, - NavigationMenuItem, - NavigationMenuContent, - NavigationMenuTrigger, - NavigationMenuLink, - NavigationMenuIndicator, - NavigationMenuViewport, - navigationMenuTriggerStyle, -} diff --git a/src/components/ui/separator.tsx b/src/components/ui/separator.tsx deleted file mode 100644 index bb3ad74..0000000 --- a/src/components/ui/separator.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" - -import { cn } from "@/lib/utils" - -function Separator({ - className, - orientation = "horizontal", - decorative = true, - ...props -}: React.ComponentProps<typeof SeparatorPrimitive.Root>) { - return ( - <SeparatorPrimitive.Root - data-slot="separator" - decorative={decorative} - orientation={orientation} - className={cn( - "bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px", - className - )} - {...props} - /> - ) -} - -export { Separator } diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx deleted file mode 100644 index 6906f5b..0000000 --- a/src/components/ui/sheet.tsx +++ /dev/null @@ -1,137 +0,0 @@ -import * as React from "react" -import * as SheetPrimitive from "@radix-ui/react-dialog" -import { XIcon } from "lucide-react" - -import { cn } from "@/lib/utils" - -function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive.Root>) { - return <SheetPrimitive.Root data-slot="sheet" {...props} /> -} - -function SheetTrigger({ - ...props -}: React.ComponentProps<typeof SheetPrimitive.Trigger>) { - return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} /> -} - -function SheetClose({ - ...props -}: React.ComponentProps<typeof SheetPrimitive.Close>) { - return <SheetPrimitive.Close data-slot="sheet-close" {...props} /> -} - -function SheetPortal({ - ...props -}: React.ComponentProps<typeof SheetPrimitive.Portal>) { - return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} /> -} - -function SheetOverlay({ - className, - ...props -}: React.ComponentProps<typeof SheetPrimitive.Overlay>) { - return ( - <SheetPrimitive.Overlay - data-slot="sheet-overlay" - className={cn( - "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50", - className - )} - {...props} - /> - ) -} - -function SheetContent({ - className, - children, - side = "right", - ...props -}: React.ComponentProps<typeof SheetPrimitive.Content> & { - side?: "top" | "right" | "bottom" | "left" -}) { - return ( - <SheetPortal> - <SheetOverlay /> - <SheetPrimitive.Content - data-slot="sheet-content" - className={cn( - "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500", - side === "right" && - "data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm", - side === "left" && - "data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm", - side === "top" && - "data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b", - side === "bottom" && - "data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t", - className - )} - {...props} - > - {children} - <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"> - <XIcon className="size-4" /> - <span className="sr-only">Close</span> - </SheetPrimitive.Close> - </SheetPrimitive.Content> - </SheetPortal> - ) -} - -function SheetHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="sheet-header" - className={cn("flex flex-col gap-1.5 p-4", className)} - {...props} - /> - ) -} - -function SheetFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="sheet-footer" - className={cn("mt-auto flex flex-col gap-2 p-4", className)} - {...props} - /> - ) -} - -function SheetTitle({ - className, - ...props -}: React.ComponentProps<typeof SheetPrimitive.Title>) { - return ( - <SheetPrimitive.Title - data-slot="sheet-title" - className={cn("text-foreground font-semibold", className)} - {...props} - /> - ) -} - -function SheetDescription({ - className, - ...props -}: React.ComponentProps<typeof SheetPrimitive.Description>) { - return ( - <SheetPrimitive.Description - data-slot="sheet-description" - className={cn("text-muted-foreground text-sm", className)} - {...props} - /> - ) -} - -export { - Sheet, - SheetTrigger, - SheetClose, - SheetContent, - SheetHeader, - SheetFooter, - SheetTitle, - SheetDescription, -} diff --git a/src/data/projects.ts b/src/data/projects.ts deleted file mode 100644 index dcee782..0000000 --- a/src/data/projects.ts +++ /dev/null @@ -1,31 +0,0 @@ -export const Projects = [ - { - name: "Boilerplate", - description: "A boilerplate for a web application", - image: "/images/boilerplate.webp", - link: "https://github.com/schererleander/boilerplate", - badges: ["Next.js", "Tailwind CSS", "TypeScript", "MongoDB", "MinIO"], - }, - { - name: "Quiz Website", - description: "A quiz website", - image: "/images/quiz.webp", - link: "https://github.com/schererleander/quiz", - badges: ["Express", "PostgreSQL", "Tailwind CSS", "JavaScript"], - }, - { - name: "Space Invaders", - description: "A space invaders clone written in Python", - image: "/images/spaceinvaders.webp", - link: "https://github.com/schererleander/spaceinvaders", - badges: ["Python", "Pygame"], - }, - { - name: "Specula", - description: "A minimal TUI for file metadata", - image: "/images/specula.webp", - link: "https://github.com/schererleander/specula", - badges: ["Go", "Bubble Tea", "TUI"], - }, - -];
\ No newline at end of file diff --git a/src/data/tools.ts b/src/data/tools.ts deleted file mode 100644 index 0f753f0..0000000 --- a/src/data/tools.ts +++ /dev/null @@ -1,38 +0,0 @@ -export const Tools = [ - { - name: "Linux", - color: "bg-neutral-500/20", - description: "Server and desktop OS", - image: "/icons/linux.svg", - }, - { - name: "Git", - color: "bg-orange-500/20", - description: "Version control", - image: "/icons/git.svg", - }, - { - name: "Docker", - color: "bg-blue-500/20", - description: "Containerization", - image: "/icons/docker.svg", - }, - { - name: "Python", - color: "bg-yellow-500/20", - description: "Scripting lang", - image: "/icons/python.svg", - }, - { - name: "Java", - color: "bg-orange-500/20", - description: "Object-oriented programming", - image: "/icons/java.svg", - }, - { - name: "Nix", - color: "bg-blue-500/20", - description: "Declarative & reproducible", - image: "/icons/nix.svg" - }, -]; diff --git a/src/hooks/theme.tsx b/src/hooks/theme.tsx deleted file mode 100644 index d65b18c..0000000 --- a/src/hooks/theme.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// hooks/theme.ts -import { useState, useEffect, useCallback } from 'react'; - -type Theme = 'light' | 'dark'; - -const STORAGE_KEY = 'theme'; - -export function useTheme() { - const [theme, setTheme] = useState<Theme>(() => { - const stored = typeof window !== 'undefined' ? localStorage.getItem(STORAGE_KEY) : null; - if (stored === 'light' || stored === 'dark') { - return stored; - } - - if (typeof window !== 'undefined' && window.matchMedia('(prefers-color-scheme: dark)').matches) { - return 'dark'; - } - return 'light'; - }); - - useEffect(() => { - const root = document.documentElement; - root.setAttribute('data-theme', theme); - root.classList.toggle('dark', theme === 'dark'); - localStorage.setItem(STORAGE_KEY, theme); - }, [theme]); - - const toggleTheme = useCallback(() => { - setTheme(prev => (prev === 'dark' ? 'light' : 'dark')); - }, []); - - return { theme, toggleTheme }; -} diff --git a/src/index.css b/src/index.css deleted file mode 100644 index bad19d3..0000000 --- a/src/index.css +++ /dev/null @@ -1,118 +0,0 @@ -@import "tailwindcss"; -@import "tw-animate-css"; -@plugin "@tailwindcss/typography"; - -@custom-variant dark (&:where(.dark, .dark *)); - -@layer base { - body { - @apply bg-background text-foreground; - } -} - -@theme inline { - --radius-sm: calc(var(--radius) - 4px); - --radius-md: calc(var(--radius) - 2px); - --radius-lg: var(--radius); - --radius-xl: calc(var(--radius) + 4px); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-destructive: var(--destructive); - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-chart-1: var(--chart-1); - --color-chart-2: var(--chart-2); - --color-chart-3: var(--chart-3); - --color-chart-4: var(--chart-4); - --color-chart-5: var(--chart-5); - --color-sidebar: var(--sidebar); - --color-sidebar-foreground: var(--sidebar-foreground); - --color-sidebar-primary: var(--sidebar-primary); - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); - --color-sidebar-accent: var(--sidebar-accent); - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); - --color-sidebar-border: var(--sidebar-border); - --color-sidebar-ring: var(--sidebar-ring); -} - -:root { - --radius: 0.625rem; - --background: oklch(1 0 0); - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); - --popover-foreground: oklch(0.145 0 0); - --primary: oklch(0.205 0 0); - --primary-foreground: oklch(0.985 0 0); - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); -} - -.dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.205 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.205 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.922 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.704 0.191 22.216); - --border: oklch(1 0 0 / 10%); - --input: oklch(1 0 0 / 15%); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(1 0 0 / 10%); - --sidebar-ring: oklch(0.556 0 0); -}
\ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts deleted file mode 100644 index bd0c391..0000000 --- a/src/lib/utils.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" - -export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) -} diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index 32ac14c..0000000 --- a/src/main.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { BrowserRouter } from 'react-router-dom'; -import ReactDOM from 'react-dom/client'; -import './index.css' -import App from './App.tsx' - -ReactDOM.createRoot(document.getElementById('root')!).render( - <BrowserRouter> - <App /> - </BrowserRouter> -); diff --git a/src/pages/404.tsx b/src/pages/404.tsx deleted file mode 100644 index 5a8da53..0000000 --- a/src/pages/404.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useNavigate } from "react-router-dom"; -import { useEffect } from "react"; - -export default function NotFound() { - const navigate = useNavigate(); - - useEffect(() => { - const timer = setTimeout(() => { - navigate('/', { replace: true }); - }, 4000); - - return () => clearTimeout(timer); - }, [navigate]); - - return ( - <> - <title>߸ 404</title> - <h1 className="text-2xl font-semibold pt-4">404 - Not found</h1> - <img src="/images/404.webp" className="rounded-lg" /> - </> - ); -} diff --git a/src/pages/Blog.tsx b/src/pages/Blog.tsx deleted file mode 100644 index 1854f47..0000000 --- a/src/pages/Blog.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import { ArrowRight } from "lucide-react"; -import { - Card, - CardContent, - CardHeader, - CardTitle, - CardDescription, -} from "@/components/ui/card"; -import Gallery from "@/components/Gallery"; - -interface Meta { - slug: string; - title: string; - date: string; - excerpt: string; - cover?: string; -} - -const images =[ - { src: "/images/3ds.webp", alt: "Nintendo 3DS", id: 1 }, - { src: "/images/esp32.webp", alt: "ESP 32", id: 2 }, - { src: "/images/manga.webp", alt: "Manga", id: 4 }, - { src: "/images/ocarinaoftime.webp", alt: "Ocarina of Time", id: 6 }, - { src: "/images/hellsparadise.webp", alt: "Hells paradise", id: 7 } -] - -const postFiles = import.meta.glob("../blog/*.md", { eager: true }) as Record< - string, - { attributes: Omit<Meta, "slug"> } ->; - -const posts: Meta[] = Object.entries(postFiles) - .map(([path, mod]) => ({ - slug: path.split("/").pop()!.replace(".md", ""), - ...(mod.attributes as Omit<Meta, "slug">), - })) - .sort( - (a, b) => new Date(b.date).getTime() - new Date(a.date).getTime() - ); - -export default function Blog() { - return ( - <> - <title>߸ blog</title> - <main className="container mx-auto py-12"> - <h1 className="text-3xl font-semibold mb-4"> - Blog Posts - </h1> - <div className="grid gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 lg:gap-8"> - {posts.map((post) => ( - <Card className="bg-secondary border border-border p-0" key={post.slug}> - <CardHeader className="p-0"> - <a - href={`/blog/${post.slug}`} - className="block rounded-t-xl overflow-hidden" - > - <img - src={post.cover} - alt={post.title} - className="h-48 w-full object-cover object-center" - style={{ marginTop: 0, paddingTop: 0 }} - /> - </a> - </CardHeader> - <CardContent className="pb-4 px-6"> - <CardTitle className="text-lg font-semibold hover:underline md:text-xl mb-2"> - <a href={`/blog/${post.slug}`}>{post.title}</a> - </CardTitle> - <CardDescription className="mb-4">{post.excerpt}</CardDescription> - <a - href={`/blog/${post.slug}`} - className="inline-flex items-center text-foreground font-medium hover:underline text-sm" - > - Read more - <ArrowRight className="ml-2 size-4" /> - </a> - </CardContent> - </Card> - ))} - </div> - - <section className="mt-12"> - <h2 className="text-2xl font-semibold mb-6">Some images</h2> - <Gallery images={images} /> - </section> - - </main> - </> - ); -} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx deleted file mode 100644 index de32cda..0000000 --- a/src/pages/Home.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { useEffect, useState } from "react"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Card, CardTitle, CardDescription, CardContent } from "@/components/ui/card"; -import { Projects } from "@/data/projects"; -import { Tools } from "@/data/tools"; -import { Github, Mail } from "lucide-react"; - -import { MapContainer, TileLayer, Marker, useMap } from "react-leaflet"; -import L from "leaflet"; -import "leaflet/dist/leaflet.css"; - -const KARLSRUHE: [number, number] = [49.0069, 8.4037]; - -const icon = L.divIcon({ - html: `<div style="width:12px;height:12px;background:#fff;border-radius:50%;border:2px solid #000;box-shadow:0 0 10px rgba(255,255,255,0.5)"></div>`, - className: "", - iconSize: [12, 12], -}); - -function MapEffects() { - const map = useMap(); - useEffect(() => { - map.flyTo(KARLSRUHE, 14, { duration: 3 }); - }, [map]); - return null; -} - -export default function Home() { - const [mounted, setMounted] = useState(false); - const [isDark, setIsDark] = useState(false); - - useEffect(() => { - setMounted(true); - const checkDark = () => setIsDark(document.documentElement.classList.contains("dark")); - checkDark(); - const obs = new MutationObserver(checkDark); - obs.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] }); - return () => obs.disconnect(); - }, []); - - if (!mounted) return null; - - const tileUrl = isDark - ? "https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png" - : "https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png"; - - return ( - <main className="container mx-auto py-12"> - <section className="relative"> - <div - className="relative h-[450px] w-full overflow-hidden rounded-xl border border-border z-0" - style={{ - maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)", - WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)" - }} - > - <MapContainer center={[49, 8.4]} zoom={10} scrollWheelZoom={false} className="h-full w-full" zoomControl={false} attributionControl={false}> - <TileLayer key={tileUrl} url={tileUrl} /> - <Marker position={KARLSRUHE} icon={icon} /> - <MapEffects /> - </MapContainer> - </div> - - <div className="relative -mt-32 z-10 text-center"> - <h1 className="text-4xl font-bold mb-4">Hi, <span className="text-blue-500 dark:text-purple-500">I'm Leander.</span></h1> - <p className="text-lg max-w-2xl mx-auto mb-4">Passionate about hardware & software, pursuing computer science studies. Currently building 3D-printing projects and exploring homelabing.</p> - <div className="flex justify-center gap-4"> - <Button asChild className="mt-4"><a href="https://github.com/schererleander" target="_blank" rel="noopener noreferrer"><Github className="mr-2 h-4 w-4" /> Github</a></Button> - <Button asChild className="mt-4 bg-blue-500 dark:bg-purple-500"><a href="mailto:leander@schererleander.de" target="_blank" rel="noopener noreferrer"><Mail className="mr-2 h-4 w-4" /> Mail</a></Button> - </div> - </div> - </section> - - <section className="mt-24"> - <h2 className="text-2xl font-semibold mb-6">Tools & Technologies</h2> - <div className="grid grid-cols-1 md:grid-cols-2 gap-6"> - {Tools.map((t, i) => ( - <Card key={i} className="bg-secondary border border-border p-2"> - <div className="flex items-center px-4 py-2"> - <div className={`p-2 rounded-sm ${t.color}`}><img src={t.image} alt={t.name} className="w-6 h-6" /></div> - <div className="ml-4"><CardTitle>{t.name}</CardTitle><CardDescription>{t.description}</CardDescription></div> - </div> - </Card> - ))} - </div> - </section> - - <section className="mt-24"> - <h2 className="text-2xl font-semibold mb-6">Recent Projects</h2> - <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> - {Projects.map((p, i) => ( - <a key={i} href={p.link} target="_blank" rel="noopener noreferrer"> - <Card className="bg-secondary border border-border py-2 h-full"> - <CardContent> - <img src={p.image} alt={p.name} className="w-full h-48 rounded-lg mb-4 object-cover mx-auto" /> - <CardTitle>{p.name}</CardTitle> - <CardDescription>{p.description}</CardDescription> - <div className="flex flex-wrap gap-2 my-4">{p.badges.map(b => <Badge key={b}>{b}</Badge>)}</div> - </CardContent> - </Card> - </a> - ))} - </div> - </section> - </main> - ); -} diff --git a/src/pages/Post.tsx b/src/pages/Post.tsx deleted file mode 100644 index 83a4c20..0000000 --- a/src/pages/Post.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { useParams, Navigate } from "react-router-dom"; -import { useEffect, type FC } from "react"; - -interface Meta { - title: string; - date: string; - cover?: string; -} - -interface Post { - attributes: Meta; - ReactComponent: FC; -} - -const posts = import.meta.glob<Post>("../blog/*.md", { eager: true }); - -const formDate = new Intl.DateTimeFormat("de-DE", { dateStyle: "medium" }); - -export default function Post() { - const { slug } = useParams(); - - const post = posts[`../blog/${slug}.md`]; - const title = post?.attributes.title ?? "߸ blog post" - - useEffect(() => { - document.title = `߸ ${meta.title}`; - }, [title]) - - if (!post) return <Navigate to="/404" replace />; - - const { attributes: meta, ReactComponent: Content } = post; - - - - return ( - <> - <main> - <a href="/blog" className="no-underline hover:underline"> - ← Back - </a> - - {meta.cover && ( - <img - src={meta.cover} - alt={meta.title} - className="w-full h-60 object-cover rounded-lg my-6" - /> - )} - - <h1 className="text-2xl font-bold mb-4">{meta.title}</h1> - <p className="text-sm text-muted-foreground mb-8">{formDate.format(new Date(meta.date))}</p> - <div className="prose prose-neutral max-w-none prose-img:mx-auto prose-img:w-1/2 prose-img:rounded-lg prose-img:shadow-lg dark:prose-invert"> - <Content /> - </div> - </main> - </> - ); -} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts deleted file mode 100644 index 9f74198..0000000 --- a/src/vite-env.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/// <reference types="vite/client" /> -declare module "*.md" { - import { FC } from "react"; - const attributes: Record<string, unknown>; - const markdown: string; - const ReactComponent: FC; - export { attributes, markdown, ReactComponent }; - export default ReactComponent; -} |
