From afdc982863b6cca573f1db58e1795aa8c45fabca Mon Sep 17 00:00:00 2001 From: schererleander Date: Fri, 30 May 2025 01:01:17 +0200 Subject: rewrite site --- src/components/CardLink.tsx | 29 +++++++++++ src/components/CodeSnippet.tsx | 94 ++++++++++++++++++++++++++++++++++++ src/components/ExternalLink.tsx | 3 ++ src/components/Footer.tsx | 21 ++++++++ src/components/GitHubIcon.tsx | 12 +++++ src/components/ImageGalleryGrid.tsx | 96 +++++++++++++++++++++++++++++++++++++ src/components/LinkWithIcon.tsx | 25 ++++++++++ src/components/MailIcon.tsx | 12 +++++ src/components/Navbar.tsx | 25 ++++++++++ src/components/ThemeToggle.tsx | 15 ++++++ 10 files changed, 332 insertions(+) create mode 100644 src/components/CardLink.tsx create mode 100644 src/components/CodeSnippet.tsx create mode 100644 src/components/ExternalLink.tsx create mode 100644 src/components/Footer.tsx create mode 100644 src/components/GitHubIcon.tsx create mode 100644 src/components/ImageGalleryGrid.tsx create mode 100644 src/components/LinkWithIcon.tsx create mode 100644 src/components/MailIcon.tsx create mode 100644 src/components/Navbar.tsx create mode 100644 src/components/ThemeToggle.tsx (limited to 'src/components') diff --git a/src/components/CardLink.tsx b/src/components/CardLink.tsx new file mode 100644 index 0000000..3492038 --- /dev/null +++ b/src/components/CardLink.tsx @@ -0,0 +1,29 @@ +import ExternalLinkIcon from "./ExternalLink"; + +interface Props { + title: string; + body: string; + href?: string; + imgSrc?: string; +} + +export default function CardLink({ title, body, href, imgSrc }: Props) { + const Wrapper = href ? 'a' : 'div'; + return ( + + +
+

{title}

+

{body}

+
+ {href && } +
+ ); +} \ No newline at end of file diff --git a/src/components/CodeSnippet.tsx b/src/components/CodeSnippet.tsx new file mode 100644 index 0000000..73ac1e7 --- /dev/null +++ b/src/components/CodeSnippet.tsx @@ -0,0 +1,94 @@ +import React, { useState, useMemo, useCallback } from 'react'; + +interface CodeSnippetProps { + code: string; + initialLines?: number; +} + +const ExpandIcon = () => ( + + + +); + +const CollapseIcon = () => ( + + + +); + +const CopyIcon = () => ( + + + +); + +export const CodeSnippet: React.FC = React.memo( + ({ code, initialLines = 5 }) => { + const [expanded, setExpanded] = useState(false); + const [copied, setCopied] = useState(false); + + const lines = useMemo(() => code.split('\n'), [code]); + const shouldTruncate = useMemo( + () => lines.length > initialLines, + [lines.length, initialLines] + ); + const displayedLines = useMemo( + () => (expanded || !shouldTruncate ? lines : lines.slice(0, initialLines)), + [expanded, shouldTruncate, lines, initialLines] + ); + + const toggleExpanded = useCallback( + () => setExpanded(prev => !prev), + [] + ); + + const handleCopy = useCallback(() => { + navigator.clipboard.writeText(code) + .then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }) + .catch(() => {}); + }, [code]); + + return ( +
+ {shouldTruncate && ( +
+ + +
+ )} + +
+          {displayedLines.map((line, idx) => (
+            
+ + {idx + 1} + + {line} +
+ ))} + {shouldTruncate && !expanded && ( +
...
+ )} +
+
+ ); + } +); + +export default CodeSnippet; \ No newline at end of file diff --git a/src/components/ExternalLink.tsx b/src/components/ExternalLink.tsx new file mode 100644 index 0000000..2b35a12 --- /dev/null +++ b/src/components/ExternalLink.tsx @@ -0,0 +1,3 @@ +export default function ExternalLinkIcon() { + return ; +} \ No newline at end of file diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx new file mode 100644 index 0000000..13ba637 --- /dev/null +++ b/src/components/Footer.tsx @@ -0,0 +1,21 @@ +import GitHubIcon from "./GitHubIcon"; +import MailIcon from "./MailIcon"; + +export default function Footer() { + const year = new Date().getFullYear(); + return ( +
+
+ + + + + + + +
+ + © {year} Leander Scherer — ʕっ•ᴥ•ʔっ +
+ ); +} \ No newline at end of file diff --git a/src/components/GitHubIcon.tsx b/src/components/GitHubIcon.tsx new file mode 100644 index 0000000..c93dbc7 --- /dev/null +++ b/src/components/GitHubIcon.tsx @@ -0,0 +1,12 @@ +export default function GitHubIcon() { + return ( + + ); +} \ No newline at end of file diff --git a/src/components/ImageGalleryGrid.tsx b/src/components/ImageGalleryGrid.tsx new file mode 100644 index 0000000..fc651bf --- /dev/null +++ b/src/components/ImageGalleryGrid.tsx @@ -0,0 +1,96 @@ +import React, { useState, useEffect } from 'react'; + +interface Props { + images: Array<{ + src: string; + alt?: string; + id?: string | number; + }>; +} + +export default function ImageGalleryGrid({ images }: Props) { + const [selectedIndex, setSelectedIndex] = useState(null); + const closeModal = () => setSelectedIndex(null); + const showPrev = (e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedIndex((prev) => (prev !== null && prev > 0 ? prev - 1 : prev)); + }; + const showNext = (e: React.MouseEvent) => { + e.stopPropagation(); + setSelectedIndex((prev) => + prev !== null && prev < images.length - 1 ? prev + 1 : prev + ); + }; + + useEffect(() => { + function handleKeyDown(event: KeyboardEvent) { + if (selectedIndex === null) return; + if (event.key === 'Escape') { + closeModal; + } else if (event.key === 'ArrowLeft') { + setSelectedIndex((prev) => (prev && prev > 0 ? prev - 1 : prev)); + } else if (event.key === 'ArrowRight') { + setSelectedIndex((prev) => + prev !== null && prev < images.length - 1 ? prev + 1 : prev + ); + } + } + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [selectedIndex, images.length]); + + return ( + <> +
+ {images.map((image, idx) => ( +
setSelectedIndex(idx)} + > + {image.alt +
+ ))} +
+ + {/* Overlay */} + {selectedIndex !== null && ( +
+ {/* Prev arrow */} + + + {images[selectedIndex].alt + + {/* Next arrow */} + +
+ )} + + ); +} \ No newline at end of file diff --git a/src/components/LinkWithIcon.tsx b/src/components/LinkWithIcon.tsx new file mode 100644 index 0000000..0046125 --- /dev/null +++ b/src/components/LinkWithIcon.tsx @@ -0,0 +1,25 @@ +import ExternalLinkIcon from "./ExternalLink"; + +export default function LinkWithIcon({ + href, + children, + className = 'inline-flex items-center gap-1 underline text-blue-400', + target = '_blank', +}: { + href: string; + children: React.ReactNode; + className?: string; + target?: React.HTMLAttributeAnchorTarget; +}) { + return ( + + {children} + + + ); +} diff --git a/src/components/MailIcon.tsx b/src/components/MailIcon.tsx new file mode 100644 index 0000000..cff91e1 --- /dev/null +++ b/src/components/MailIcon.tsx @@ -0,0 +1,12 @@ +export default function MailIcon() { + return ( + + ); +} diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..624f9f1 --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,25 @@ +import { Link } from "react-router-dom"; +import ThemeToggle from "./ThemeToggle"; + +export default function Navbar() { + const navItems = [ + { label: 'Home', href: '/' }, + { label: 'Gear', href: '/gear' }, + { label: 'Projects', href: '/projects' }, + { label: 'Homelab', href: '/homelab' }, + { label: '3D Printing', href: '/printing' } + ]; + + return ( + + ); +} \ No newline at end of file diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx new file mode 100644 index 0000000..74dbf57 --- /dev/null +++ b/src/components/ThemeToggle.tsx @@ -0,0 +1,15 @@ +// components/ThemeToggle.tsx +import { useTheme } from "../hooks/theme"; + +export default function ThemeToggle() { + const { theme, toggleTheme } = useTheme(); + return ( + + ); +} -- cgit v1.3.1