diff options
| author | schererleander <leander@schererleander.de> | 2025-12-25 23:33:25 +0000 |
|---|---|---|
| committer | schererleander <leander@schererleander.de> | 2025-12-25 23:33:25 +0000 |
| commit | d82fb3b552d20a279efdd9408042183cfa02fb48 (patch) | |
| tree | 4ffe818e591e54da71f7592506c873abf0d9d481 /components | |
| parent | d7edbf05ab0e90eedcb99e4462e3a61793b2eff9 (diff) | |
initial commit
Diffstat (limited to 'components')
| -rw-r--r-- | components/footer.tsx | 20 | ||||
| -rw-r--r-- | components/header.tsx | 42 | ||||
| -rw-r--r-- | components/map-wrapper.tsx | 10 | ||||
| -rw-r--r-- | components/map.tsx | 70 | ||||
| -rw-r--r-- | components/mdx-content.tsx | 12 | ||||
| -rw-r--r-- | components/noise.tsx | 8 | ||||
| -rw-r--r-- | components/panda.tsx | 9 | ||||
| -rw-r--r-- | components/post-card.tsx | 20 | ||||
| -rw-r--r-- | components/projects-grid.tsx | 51 | ||||
| -rw-r--r-- | components/theme-provider.tsx | 11 | ||||
| -rw-r--r-- | components/tools-grid.tsx | 56 | ||||
| -rw-r--r-- | components/ui/button.tsx | 58 | ||||
| -rw-r--r-- | components/ui/card.tsx | 92 |
13 files changed, 459 insertions, 0 deletions
diff --git a/components/footer.tsx b/components/footer.tsx new file mode 100644 index 0000000..b78b8b3 --- /dev/null +++ b/components/footer.tsx @@ -0,0 +1,20 @@ +import Link from 'next/link' +import { Panda } from './panda' + +export function Footer() { + return ( + <footer className="mx-auto max-w-[var(--site-width)] py-4"> + <div className="flex items-center justify-between"> + <div className="flex gap-4"> + <Link + href="/privacy" + className="text-muted-foreground/60 transition-colors hover:text-foreground" + > + Privacy + </Link> + </div> + <Panda /> + </div> + </footer> + ) +} diff --git a/components/header.tsx b/components/header.tsx new file mode 100644 index 0000000..79cb0c0 --- /dev/null +++ b/components/header.tsx @@ -0,0 +1,42 @@ +import Link from "next/link" +import { PawPrint, Github, Mail } from "lucide-react" +import { Button } from "@/components/ui/button" + +export function Header() { + return ( + <header className="mx-auto flex w-full max-w-[var(--site-width)] items-center justify-between gap-5 py-4"> + <div className="flex items-center gap-6"> + <Link href="/" className="font-bold text-xl"> + <PawPrint className="hover:rotate-12 transition-transform" /> + </Link> + <nav className="flex gap-6"> + <Link + href="/blog" + className="text-muted-foreground hover:text-foreground transition-colors" + > + Blog + </Link> + <Link + href="/projects" + className="text-muted-foreground hover:text-foreground transition-colors" + > + Projects + </Link> + </nav> + </div> + + <div className="flex items-center gap-2"> + <Button asChild variant="ghost" size="icon"> + <Link href="https://github.com/schererleander" target="_blank" aria-label="Github"> + <Github className="h-4 w-4" /> + </Link> + </Button> + <Button asChild variant="ghost" size="icon"> + <Link href="mailto:leander@schererleander.de" aria-label="Contact"> + <Mail className="h-4 w-4" /> + </Link> + </Button> + </div> + </header> + ) +} diff --git a/components/map-wrapper.tsx b/components/map-wrapper.tsx new file mode 100644 index 0000000..2407702 --- /dev/null +++ b/components/map-wrapper.tsx @@ -0,0 +1,10 @@ + +"use client" + +import dynamic from 'next/dynamic' + +const Map = dynamic(() => import('./map')) + +export default function MapWrapper() { + return <Map /> +} diff --git a/components/map.tsx b/components/map.tsx new file mode 100644 index 0000000..8584c76 --- /dev/null +++ b/components/map.tsx @@ -0,0 +1,70 @@ + +"use client" + +import { useRef } from 'react'; +import MapGL, { Marker, MapRef } from 'react-map-gl/maplibre'; +import maplibregl from 'maplibre-gl'; +import 'maplibre-gl/dist/maplibre-gl.css'; + +const KARLSRUHE: { longitude: number, latitude: number } = { longitude: 8.4037, latitude: 49.0069 }; + +export default function Map() { + const mapRef = useRef<MapRef>(null); + + const onMapLoad = () => { + mapRef.current?.flyTo({ + center: [KARLSRUHE.longitude, KARLSRUHE.latitude], + zoom: 10, + speed: 0.5, + curve: 1, + essential: true + }); + }; + + return ( + <div + className="relative h-[350px] rounded-xl overflow-hidden border border-border z-0 bg-background" + style={{ + maskImage: "linear-gradient(to bottom, black 50%, transparent 100%)", + WebkitMaskImage: "linear-gradient(to bottom, black 50%, transparent 100%)", + }} + > + <MapGL + ref={mapRef} + mapLib={maplibregl} + onLoad={onMapLoad} + initialViewState={{ + longitude: KARLSRUHE.longitude, + latitude: KARLSRUHE.latitude, + zoom: 2 + }} + style={{ width: "100%", height: "100%" }} + mapStyle={{ + version: 8, + sources: { + "carto-dark": { + type: "raster", + tiles: ["https://a.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}.png"], + tileSize: 256, + attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>, © <a href="https://carto.com/attributions">CARTO</a>', + }, + }, + layers: [ + { + id: "carto-dark-layer", + type: "raster", + source: "carto-dark", + minzoom: 0, + maxzoom: 22, + }, + ], + }} + attributionControl={false} + > + <Marker longitude={KARLSRUHE.longitude} latitude={KARLSRUHE.latitude} color="red"> + <div style={{width:'12px',height:'12px',background:'var(--background)',borderRadius:'50%',border:'2px solid var(--foreground)',boxShadow:'0 0 10px rgba(0,0,0,0.5)'}} /> + </Marker> + </MapGL> + </div> + ); +} diff --git a/components/mdx-content.tsx b/components/mdx-content.tsx new file mode 100644 index 0000000..d93ba3d --- /dev/null +++ b/components/mdx-content.tsx @@ -0,0 +1,12 @@ +'use client' + +import { useMDXComponent } from 'next-contentlayer2/hooks' + +interface MDXContentProps { + code: string +} + +export function MDXContent({ code }: MDXContentProps) { + const Component = useMDXComponent(code) + return <Component /> +} diff --git a/components/noise.tsx b/components/noise.tsx new file mode 100644 index 0000000..3e29b66 --- /dev/null +++ b/components/noise.tsx @@ -0,0 +1,8 @@ + +export function Noise() { + return ( + <div + className="fixed top-0 left-0 w-full h-full pointer-events-none z-50 noise" + /> + ); +} diff --git a/components/panda.tsx b/components/panda.tsx new file mode 100644 index 0000000..ee9328c --- /dev/null +++ b/components/panda.tsx @@ -0,0 +1,9 @@ +export function Panda() { + return ( + <label className="text-sm text-muted-foreground/60 transition-colors hover:text-foreground cursor-pointer select-none"> + <input type="checkbox" className="peer sr-only" /> + <span className="peer-checked:hidden">ʕっ•ᴥ•ʔっ</span> + <span className="hidden peer-checked:inline">ʕ•ᴥ•ʔ</span> + </label> + ) +} diff --git a/components/post-card.tsx b/components/post-card.tsx new file mode 100644 index 0000000..4990188 --- /dev/null +++ b/components/post-card.tsx @@ -0,0 +1,20 @@ +import { type Post } from "contentlayer/generated" +import { format, parseISO } from "date-fns" +import Link from "next/link" + +export function PostCard(post: Post) { + return ( + <div className="mb-8"> + <Link href={post.url} className="no-underline"> + <h2 className="mb-1.5 mt-3 text-xl font-bold text-foreground"> + {post.title} + </h2> + </Link> + <div className="flex gap-1.5 font-medium text-muted-foreground/60"> + <time dateTime={post.date} title={format(parseISO(post.date), 'yyyy/MM/dd')}> + {format(parseISO(post.date), 'do MMM yyyy')} + </time> + </div> + </div> + ) +} diff --git a/components/projects-grid.tsx b/components/projects-grid.tsx new file mode 100644 index 0000000..9ccb0fb --- /dev/null +++ b/components/projects-grid.tsx @@ -0,0 +1,51 @@ +import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; +import Link from "next/link"; + +interface Project { + title: string; + description: string; + githubUrl: string; +} + +const projects: Project[] = [ + { + title: "Boilerplate", + description: "A comprehensive starter template for modern web development projects.", + githubUrl: "https://github.com/schererleander/boilerplate", + }, + { + title: "Quiz Website", + description: "An interactive quiz platform built to test your knowledge.", + githubUrl: "https://github.com/schererleander/quiz", + }, + { + title: "Space Invaders", + description: "A classic arcade game clone recreated with modern web technologies.", + githubUrl: "https://github.com/schererleander/spaceinvaders", + }, + { + title: "Specula", + description: "A mirror project or reflection tool (Description inferred from name).", + githubUrl: "https://github.com/schererleander/specula", + }, +]; + +export function ProjectsGrid() { + return ( + <section> + <div className="grid grid-cols-2 md:grid-cols-2 gap-6"> + {projects.map((project) => ( + <Link href={project.githubUrl} key={project.title} target="_blank" className="block group h-full"> + <Card className="h-full transition-all duration-300 bg-card/50 hover:bg-card/80 shadow-none p-0 gap-0 overflow-hidden"> + <div className="w-full h-40 bg-muted animate-pulse" /> + <CardHeader className="p-6"> + <CardTitle className="group-hover:text-primary transition-colors">{project.title}</CardTitle> + <CardDescription className="text-muted-foreground/60 group-hover:text-muted-foreground transition-colors">{project.description}</CardDescription> + </CardHeader> + </Card> + </Link> + ))} + </div> + </section> + ); +} diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx new file mode 100644 index 0000000..6a1ffe4 --- /dev/null +++ b/components/theme-provider.tsx @@ -0,0 +1,11 @@ +"use client" + +import * as React from "react" +import { ThemeProvider as NextThemesProvider } from "next-themes" + +export function ThemeProvider({ + children, + ...props +}: React.ComponentProps<typeof NextThemesProvider>) { + return <NextThemesProvider {...props}>{children}</NextThemesProvider> +} diff --git a/components/tools-grid.tsx b/components/tools-grid.tsx new file mode 100644 index 0000000..b0cdfa6 --- /dev/null +++ b/components/tools-grid.tsx @@ -0,0 +1,56 @@ +import { Card } from "@/components/ui/card"; +import { GitGraph, Coffee, Terminal, Box, Snowflake, Code } from 'lucide-react'; + +const tools = [ + { + name: 'Git', + icon: GitGraph, + color: 'group-hover:text-[#F05032]' + }, + { + name: 'Java', + icon: Coffee, + color: 'group-hover:text-[#5382a1]' + }, + { + name: 'Python', + icon: Code, + color: 'group-hover:text-[#FFE873]' + }, + { + name: 'Nix', + icon: Snowflake, + color: 'group-hover:text-[#5277C3]' + }, + { + name: 'Docker', + icon: Box, + color: 'group-hover:text-[#2496ED]' + }, + { + name: 'Linux', + icon: Terminal, + color: 'group-hover:text-[#FCC624]' + }, +]; + +export function ToolsGrid() { + return ( + <div className="grid grid-cols-3 md:grid-cols-6 gap-4 w-full max-w-4xl mx-auto"> + {tools.map((tool) => ( + <Card + key={tool.name} + className="group flex flex-col items-center justify-center gap-2 p-4 bg-card/50 hover:bg-card/80 transition-all duration-300 shadow-none" + > + <tool.icon + className={`w-6 h-6 text-muted-foreground transition-colors duration-300 ${tool.color}`} + strokeWidth={1.5} + /> + <span className="text-xs font-medium text-muted-foreground/60 group-hover:text-muted-foreground transition-colors"> + {tool.name} + </span> + </Card> + ))} + </div> + ); +} diff --git a/components/ui/button.tsx b/components/ui/button.tsx new file mode 100644 index 0000000..3ffbfed --- /dev/null +++ b/components/ui/button.tsx @@ -0,0 +1,58 @@ +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-[color,box-shadow] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 [&_svg]:shrink-0 ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 focus-visible:ring-4 focus-visible:outline-1 aria-invalid:focus-visible:ring-0", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-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/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..681ad98 --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,92 @@ +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-2 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, +} |
