timconspicuous.neocities.org

Rewrite in Preact tsx

+361 -229
+3 -1
deno.json
··· 11 11 "compilerOptions": { 12 12 "types": [ 13 13 "lume/types.ts" 14 - ] 14 + ], 15 + "jsx": "react-jsx", 16 + "jsxImportSource": "npm:preact" 15 17 }, 16 18 "exclude": [ 17 19 "./_site"
+3 -1
plugins.ts
··· 6 6 import metas from "lume/plugins/metas.ts"; 7 7 import postcss from "lume/plugins/postcss.ts"; 8 8 import transformImages from "lume/plugins/transform_images.ts"; 9 + import jsx from "lume/plugins/jsx_preact.ts"; 9 10 10 11 /** Configure the site */ 11 12 export default function () { ··· 16 17 .use(basePath()) 17 18 .mergeKey("extra_head", "stringArray") 18 19 .use(transformImages()) 19 - .use(simpleIcons()); 20 + .use(simpleIcons()) 21 + .use(jsx()); 20 22 21 23 site.data("textColor", (hex: string) => { 22 24 const color = new Color(`#${hex}`);
+67
src/_components/Button.css
··· 1 + .link-list { 2 + list-style: none; 3 + margin: 0; 4 + padding: 0; 5 + display: grid; 6 + row-gap: 10px; 7 + 8 + .button { 9 + display: flex; 10 + font: var(--font-body-bold); 11 + transition: transform 200ms; 12 + border: solid 1px #00000022; 13 + 14 + &:hover { 15 + transform: scale(1.05); 16 + box-shadow: 0 2px 10px -8px #0009; 17 + } 18 + } 19 + 20 + .button:not(.is-primary) { 21 + background: var(--bg-color); 22 + color: var(--text-color); 23 + } 24 + 25 + svg { 26 + width: 20px; 27 + height: 20px; 28 + fill: currentColor; 29 + } 30 + } 31 + 32 + [data-theme="dark"] { 33 + .link-list .button { 34 + border: solid 1px #FFFFFF16; 35 + } 36 + } 37 + 38 + .icon-list { 39 + list-style: none; 40 + margin: 0 0 min(5vh, 100px); 41 + padding: 0; 42 + display: flex; 43 + gap: 10px; 44 + justify-content: center; 45 + 46 + svg { 47 + width: 20px; 48 + height: 20px; 49 + fill: currentColor; 50 + } 51 + 52 + .button { 53 + display: flex; 54 + font: var(--font-body-bold); 55 + transition: transform 200ms; 56 + 57 + &:hover { 58 + transform: scale(1.05); 59 + box-shadow: 0 2px 10px -8px #0009; 60 + } 61 + } 62 + 63 + .button:not(.is-primary) { 64 + background: var(--bg-color); 65 + color: var(--text-color); 66 + } 67 + }
+58
src/_components/Button.tsx
··· 1 + import * as si from "npm:simple-icons@13.10.0"; 2 + import type { SimpleIcon } from "npm:simple-icons@13.10.0"; 3 + 4 + export default function ({ 5 + link, 6 + }: { 7 + link: { 8 + type: string; 9 + text: string; 10 + href: string; 11 + hex?: string; 12 + textColor?: string; 13 + only_icon?: boolean; 14 + }; 15 + }) { 16 + // Get the icon information directly from simple-icons 17 + const icons = Object.values(si) as SimpleIcon[]; 18 + const icon = icons.find((icon) => icon.slug === link.type); 19 + 20 + // Use the specified hex or get it from the icon (or default to white) 21 + const hex = link.hex || (icon ? `#${icon.hex}` : "#fff"); 22 + 23 + // Function to determine text color based on background color brightness 24 + const getTextColor = (backgroundColor: string) => { 25 + // Remove the # if present and pad to 6 characters if needed 26 + const color = (backgroundColor.startsWith("#") 27 + ? backgroundColor.slice(1) 28 + : backgroundColor 29 + ).padEnd(6, backgroundColor.length <= 4 ? backgroundColor.slice(-1) : ""); 30 + 31 + // Convert hex to RGB 32 + const r = parseInt(color.substring(0, 2), 16); 33 + const g = parseInt(color.substring(2, 4), 16); 34 + const b = parseInt(color.substring(4, 6), 16); 35 + 36 + // Calculate luminance to determine perceived brightness 37 + const luminance = (0.2126 * r + 0.7152 * g + 0.0722 * b); 38 + 39 + // Return black for light backgrounds, white for dark ones 40 + return luminance > 128 ? "#000000" : "#ffffff"; 41 + }; 42 + 43 + const textColor = link.textColor || getTextColor(hex); 44 + 45 + return ( 46 + <a 47 + href={link.href} 48 + class="button" 49 + style={`--bg-color:${hex}; --text-color:${textColor}`} 50 + title={link.only_icon ? link.text : undefined} 51 + dangerouslySetInnerHTML={{ 52 + __html: `${icon ? icon.svg : ""}${ 53 + !link.only_icon ? link.text : "" 54 + }`, 55 + }} 56 + /> 57 + ); 58 + }
+39
src/_components/Linktree.tsx
··· 1 + import Button from "./Button.tsx"; 2 + 3 + export default function ({ links }: { 4 + links: Array<{ 5 + type: string; 6 + text: string; 7 + href: string; 8 + hex?: string; 9 + textColor?: string; 10 + only_icon?: boolean; 11 + }>; 12 + }, { comp }: { comp: Lume.Data }) { 13 + const iconLinks = links.filter((link) => link.only_icon); 14 + const regularLinks = links.filter((link) => !link.only_icon); 15 + 16 + return ( 17 + <> 18 + {iconLinks.length > 0 && ( 19 + <ul class="icon-list"> 20 + {iconLinks.map((link) => ( 21 + <li key={link.href}> 22 + <Button link={link} /> 23 + </li> 24 + ))} 25 + </ul> 26 + )} 27 + 28 + <ul class="link-list"> 29 + {regularLinks.map((link) => ( 30 + <li key={link.href}> 31 + <Button link={link} /> 32 + </li> 33 + ))} 34 + </ul> 35 + </> 36 + ); 37 + } 38 + 39 + export const css = "@import './_components/Button.css';";
-36
src/_includes/css/header.css
··· 1 - .header { 2 - font: var(--font-body); 3 - margin-bottom: min(5vh, 100px); 4 - color: var(--color-text); 5 - 6 - p { 7 - margin: 0; 8 - text-wrap: balance; 9 - 10 - + p { 11 - margin-top: .5em; 12 - } 13 - } 14 - } 15 - 16 - .header-avatar { 17 - border-radius: 50%; 18 - aspect-ratio: 1; 19 - object-fit: cover; 20 - object-position: center center; 21 - width: 200px; 22 - max-width: 50vw; 23 - } 24 - 25 - .header-title { 26 - font: var(--font-title); 27 - letter-spacing: var(--font-title-spacing); 28 - margin: .5em 0 0; 29 - color: var(--color-base); 30 - } 31 - 32 - .header-theme { 33 - position: absolute; 34 - top: 1rem; 35 - right: 1.5rem; 36 - }
-65
src/_includes/css/link.css
··· 1 - .link-list { 2 - list-style: none; 3 - margin: 0; 4 - padding: 0; 5 - display: grid; 6 - row-gap: 10px; 7 - 8 - .button { 9 - display: flex; 10 - font: var(--font-body-bold); 11 - transition: transform 200ms; 12 - border: solid 1px #00000022; 13 - 14 - &:hover { 15 - transform: scale(1.05); 16 - box-shadow: 0 2px 10px -8px #0009; 17 - } 18 - } 19 - .button:not(.is-primary) { 20 - background: var(--bg-color); 21 - color: var(--text-color); 22 - } 23 - 24 - svg { 25 - width: 20px; 26 - height: 20px; 27 - fill: currentColor; 28 - } 29 - } 30 - 31 - [data-theme="dark"] { 32 - .link-list .button { 33 - border: solid 1px #FFFFFF16; 34 - } 35 - } 36 - 37 - .icon-list { 38 - list-style: none; 39 - margin: 0 0 min(5vh, 100px); 40 - padding: 0; 41 - display: flex; 42 - gap: 10px; 43 - justify-content: center; 44 - 45 - svg { 46 - width: 20px; 47 - height: 20px; 48 - fill: currentColor; 49 - } 50 - 51 - .button { 52 - display: flex; 53 - font: var(--font-body-bold); 54 - transition: transform 200ms; 55 - 56 - &:hover { 57 - transform: scale(1.05); 58 - box-shadow: 0 2px 10px -8px #0009; 59 - } 60 - } 61 - .button:not(.is-primary) { 62 - background: var(--bg-color); 63 - color: var(--text-color); 64 - } 65 - }
+92
src/_includes/layout.tsx
··· 1 + export default function Layout(data: Lume.Data) { 2 + const title = data.header?.title || data.title || "timconspicuous"; 3 + const description = data.header?.description || data.description || ""; 4 + const avatar = data.header?.avatar || "/avatar.jpg"; 5 + const footer = data.footer || ""; 6 + const links = data.links || []; 7 + 8 + return ( 9 + <html lang={data.lang || "en"}> 10 + <head> 11 + <meta charset="utf-8" /> 12 + <meta 13 + name="viewport" 14 + content="width=device-width, initial-scale=1.0" 15 + /> 16 + <title>{title}</title> 17 + <meta name="supported-color-schemes" content="light dark" /> 18 + <meta 19 + name="theme-color" 20 + content="hsl(220, 20%, 100%)" 21 + media="(prefers-color-scheme: light)" 22 + /> 23 + <meta 24 + name="theme-color" 25 + content="hsl(220, 20%, 10%)" 26 + media="(prefers-color-scheme: dark)" 27 + /> 28 + <link rel="stylesheet" href="/styles.css" /> 29 + <link rel="stylesheet" href="/components.css" /> 30 + <link 31 + rel="icon" 32 + type="image/png" 33 + sizes="32x32" 34 + href="/favicon.png" 35 + /> 36 + <link rel="canonical" href={data.url} /> 37 + {data.extra_head?.map((item: string) => ( 38 + <div dangerouslySetInnerHTML={{ __html: item }} /> 39 + ))} 40 + </head> 41 + <body> 42 + <main> 43 + <header class="header"> 44 + <script 45 + dangerouslySetInnerHTML={{ 46 + __html: ` 47 + let theme = localStorage.getItem("theme") || (window.matchMedia("(prefers-color-scheme: dark)").matches 48 + ? "dark" 49 + : "light"); 50 + document.documentElement.dataset.theme = theme; 51 + function changeTheme() { 52 + theme = theme === "dark" ? "light" : "dark"; 53 + localStorage.setItem("theme", theme); 54 + document.documentElement.dataset.theme = theme; 55 + } 56 + `, 57 + }} 58 + /> 59 + <button 60 + class="button header-theme" 61 + onclick="changeTheme()" 62 + > 63 + <span class="icon">◐</span> 64 + </button> 65 + {avatar && ( 66 + <img 67 + class="header-avatar" 68 + src={avatar} 69 + alt="Avatar" 70 + data-lume-transform-images="webp avif 200@2" 71 + /> 72 + )} 73 + <h1 class="header-title">{title}</h1> 74 + {description && ( 75 + <div 76 + dangerouslySetInnerHTML={{ 77 + __html: description, 78 + }} 79 + /> 80 + )} 81 + </header> 82 + 83 + {data.children} 84 + </main> 85 + 86 + {footer && ( 87 + <footer dangerouslySetInnerHTML={{ __html: footer }} /> 88 + )} 89 + </body> 90 + </html> 91 + ); 92 + }
-84
src/_includes/layouts/base.vto
··· 1 - <!DOCTYPE html> 2 - 3 - <html lang="{{ it.lang }}"> 4 - <head> 5 - <meta charset="utf-8"> 6 - <meta name="viewport" content="width=device-width, initial-scale=1.0"> 7 - <title>{{ header.title }}</title> 8 - 9 - <meta name="supported-color-schemes" content="light dark"> 10 - <meta name="theme-color" content="hsl(220, 20%, 100%)" media="(prefers-color-scheme: light)"> 11 - <meta name="theme-color" content="hsl(220, 20%, 10%)" media="(prefers-color-scheme: dark)"> 12 - 13 - <link rel="stylesheet" href="/styles.css"> 14 - <link rel="icon" type="image/png" sizes="32x32" href="/favicon.png"> 15 - <link rel="canonical" href="{{ url |> url(true) }}"> 16 - 17 - {{ it.extra_head?.join("\n") }} 18 - </head> 19 - <body> 20 - <main> 21 - <header class="header"> 22 - <script> 23 - let theme = localStorage.getItem("theme") || (window.matchMedia("(prefers-color-scheme: dark)").matches 24 - ? "dark" 25 - : "light"); 26 - document.documentElement.dataset.theme = theme; 27 - function changeTheme() { 28 - theme = theme === "dark" ? "light" : "dark"; 29 - localStorage.setItem("theme", theme); 30 - document.documentElement.dataset.theme = theme; 31 - } 32 - </script> 33 - <button class="button header-theme" onclick="changeTheme()"> 34 - <span class="icon">◐</span> 35 - </button> 36 - 37 - <img class="header-avatar" src="{{ header.avatar }}" alt="Avatar" transform-images="webp avif 200@2"> 38 - <h1 class="header-title">{{ header.title }}</h1> 39 - {{ header.description |> md }} 40 - </header> 41 - 42 - {{> const icons = links.filter((link) => link.only_icon) }} 43 - 44 - {{ if icons.length }} 45 - <ul class="icon-list"> 46 - {{ for link of icons }} 47 - {{ set hex = link.type |> simpleicons("hex") }} 48 - <li> 49 - <a 50 - href="{{ link.href }}" 51 - class="button" 52 - style='--bg-color:{{ link.hex || `#${hex || "fff" }` }}; --text-color:{{ link.textColor || textColor(hex || "fff") }}' 53 - title="{{ link.text }}" 54 - > 55 - {{ link.type |> simpleicons }} 56 - </a> 57 - </li> 58 - {{ /for }} 59 - </ul> 60 - {{ /if }} 61 - 62 - <ul class="link-list"> 63 - {{ for link of links.filter((link) => !link.only_icon) }} 64 - {{ set hex = link.type |> simpleicons("hex") }} 65 - <li> 66 - <a 67 - href="{{ link.href }}" 68 - class="button" 69 - style='--bg-color:{{ link.hex || `#${hex || "fff" }` }}; --text-color:{{ link.textColor || textColor(hex || "fff") }}' 70 - > 71 - {{ link.type |> simpleicons }} 72 - {{ link.text }} 73 - </a> 74 - </li> 75 - {{ /for }} 76 - </ul> 77 - </main> 78 - {{ if footer }} 79 - <footer> 80 - {{ footer |> md }} 81 - </footer> 82 - {{ /if }} 83 - </body> 84 - </html>
+59
src/index.tsx
··· 1 + import { marked } from "npm:marked"; 2 + 3 + export const title = "timconspicuous"; 4 + export const header = { 5 + title: "timconspicuous", 6 + description: "", 7 + avatar: "/avatar.jpg", 8 + }; 9 + 10 + export const links = [ 11 + { 12 + type: "bluesky", 13 + text: "Bluesky", 14 + href: "https://bsky.app/profile/timconspicuous.neocities.org", 15 + }, 16 + { 17 + type: "letterboxd", 18 + text: "Letterboxd", 19 + href: "https://letterboxd.com/timconspicuous", 20 + }, 21 + { 22 + type: "bookwyrm", 23 + text: "📚 BookWyrm", 24 + href: "https://bookwyrm.social/user/timconspicuous", 25 + }, 26 + { 27 + type: "lichess", 28 + text: "Lichess", 29 + href: "https://lichess.org/@/timconspicuous", 30 + }, 31 + { 32 + type: "discord", 33 + text: "Discord", 34 + href: "https://discordapp.com/users/timconspicuous", 35 + }, 36 + { 37 + type: "spotify", 38 + text: "Spotify", 39 + href: "https://open.spotify.com/user/iafsfv7j85qcxqhnygkl8xuds", 40 + }, 41 + { 42 + type: "tumblr", 43 + text: "Tumblr", 44 + href: "https://www.tumblr.com/timconspicuous", 45 + }, 46 + ]; 47 + 48 + export const footer = marked.parseInline( 49 + "Powered by [Lume](https://lume.land)", 50 + ); 51 + 52 + // Layout to use for this page 53 + export const layout = "layout.tsx"; 54 + 55 + export default ({ comp }: Lume.Data) => { 56 + return ( 57 + <comp.Linktree links={links} /> 58 + ); 59 + };
-35
src/index.yml
··· 1 - layout: layouts/base.vto 2 - header: 3 - title: timconspicuous 4 - description: 5 - avatar: /avatar.jpg 6 - metas: 7 - title: =header.title 8 - description: =header.description 9 - image: =header.avatar 10 - generator: true 11 - twitter: '' 12 - links: 13 - - type: bluesky 14 - text: Bluesky 15 - href: 'https://bsky.app/profile/timconspicuous.neocities.org' 16 - - type: letterboxd 17 - text: 'Letterboxd' 18 - href: 'https://letterboxd.com/timconspicuous' 19 - - type: '' 20 - text: BookWyrm 21 - href: 'https://bookwyrm.social/user/timconspicuous' 22 - - type: lichess 23 - text: Lichess 24 - href: 'https://lichess.org/@/timconspicuous' 25 - - type: discord 26 - text: Discord 27 - href: 'https://discordapp.com/users/timconspicuous' 28 - - type: spotify 29 - text: Spotify 30 - href: 'https://open.spotify.com/user/iafsfv7j85qcxqhnygkl8xuds' 31 - - type: tumblr 32 - text: Tumblr 33 - href: 'https://www.tumblr.com/timconspicuous' 34 - footer: "Powered by [Lume](https://lume.land) & [SimpleMe](https://github.com/lumeland/theme-simple-me) theme" 35 - extra_head: ''
+40 -7
src/styles.css
··· 1 1 /* Lume's design system */ 2 2 @import "https://unpkg.com/@lumeland/ds@0.5.2/ds.css"; 3 3 4 - /* Custom components */ 5 - @import "css/header.css"; 6 - @import "css/link.css"; 7 - 8 4 body { 9 5 display: grid; 10 6 grid-template-columns: minmax(0, 500px); ··· 21 17 align-self: center; 22 18 } 23 19 20 + .header { 21 + font: var(--font-body); 22 + margin-bottom: min(5vh, 100px); 23 + color: var(--color-text); 24 + 25 + p { 26 + margin: 0; 27 + text-wrap: balance; 28 + 29 + +p { 30 + margin-top: .5em; 31 + } 32 + } 33 + } 34 + 35 + .header-avatar { 36 + border-radius: 50%; 37 + aspect-ratio: 1; 38 + object-fit: cover; 39 + object-position: center center; 40 + width: 200px; 41 + max-width: 50vw; 42 + } 43 + 44 + .header-title { 45 + font: var(--font-title); 46 + letter-spacing: var(--font-title-spacing); 47 + margin: .5em 0 0; 48 + color: var(--color-base); 49 + } 50 + 51 + .header-theme { 52 + position: absolute; 53 + top: 1rem; 54 + right: 1.5rem; 55 + } 56 + 24 57 footer { 25 58 font: var(--font-small); 26 59 color: var(--color-dim); 27 60 28 - > * { 61 + >* { 29 62 margin: 0; 30 63 } 31 64 32 - > * + * { 65 + >*+* { 33 66 margin-top: 1em; 34 67 } 35 68 36 69 a { 37 70 color: inherit; 38 71 } 39 - } 72 + }