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