initial commit
This commit is contained in:
commit
c70fc72952
143 changed files with 13594 additions and 0 deletions
35
src/components/BlockRenderer.astro
Normal file
35
src/components/BlockRenderer.astro
Normal file
|
@ -0,0 +1,35 @@
|
|||
---
|
||||
import AssociatedMedia from '@components/blocks//AssociatedMedia.astro';
|
||||
import GitHub from '@components/blocks/banners/GitHub.astro';
|
||||
import Hero from '@components/blocks//Hero.astro';
|
||||
import Modal from '@components/blocks//Modal.astro';
|
||||
import YouTubePlayer from '@components/blocks//YouTubePlayer.astro';
|
||||
import { md } from '@utils/helpers.js';
|
||||
|
||||
const { block } = Astro.props;
|
||||
const htmlContent = block.type === 'markdown' ? md(block.text) : '';
|
||||
---
|
||||
|
||||
{block.type === 'youtube_player' && (
|
||||
<YouTubePlayer url={block.url} />
|
||||
)}
|
||||
|
||||
{block.type === 'hero' && (
|
||||
<Hero image={block.image} alt={block.alt} />
|
||||
)}
|
||||
|
||||
{block.type === 'markdown' && (
|
||||
<div set:html={htmlContent}></div>
|
||||
)}
|
||||
|
||||
{block.type === 'modal' && (
|
||||
<Modal content={block.content} />
|
||||
)}
|
||||
|
||||
{block.type === 'associated_media' && (
|
||||
<AssociatedMedia media={block.media} />
|
||||
)}
|
||||
|
||||
{block.type === 'github_banner' && (
|
||||
<GitHub url={block.url} />
|
||||
)}
|
34
src/components/Footer.astro
Normal file
34
src/components/Footer.astro
Normal file
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
import NavLink from './nav/NavLink.astro';
|
||||
|
||||
const { nav, updated } = Astro.props;
|
||||
---
|
||||
|
||||
<footer style={updated ? undefined : 'margin-top: var(--spacing-3xl)'}>
|
||||
{updated && (
|
||||
<p class="updated">
|
||||
<em>This page was last updated on {new Date(updated).toLocaleDateString()}</em>
|
||||
</p>
|
||||
)}
|
||||
<nav aria-label="Social icons" class="social">
|
||||
{nav.footer_icons.map(link => (
|
||||
<NavLink
|
||||
url={link.permalink}
|
||||
title={link.title}
|
||||
icon={link.icon}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
<nav aria-label="Secondary site navigation" class="sub-pages">
|
||||
{nav.footer_text.map((link, index) => (
|
||||
<>
|
||||
<NavLink
|
||||
url={link.permalink}
|
||||
title={link.title}
|
||||
icon={link.icon}
|
||||
/>
|
||||
{index < nav.footer_text.length - 1 && <span>/</span>}
|
||||
</>
|
||||
))}
|
||||
</nav>
|
||||
</footer>
|
17
src/components/Header.astro
Normal file
17
src/components/Header.astro
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
import Menu from './nav/Menu.astro';
|
||||
|
||||
const { nav, siteName, url } = Astro.props;
|
||||
const isHomePage = url === '/';
|
||||
---
|
||||
|
||||
<section class="main-title">
|
||||
<h1>
|
||||
{isHomePage ? (
|
||||
siteName
|
||||
) : (
|
||||
<a href="/" tabindex="0">{siteName}</a>
|
||||
)}
|
||||
</h1>
|
||||
<Menu nav={nav} />
|
||||
</section>
|
43
src/components/IconMapper.astro
Normal file
43
src/components/IconMapper.astro
Normal file
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
import {
|
||||
IconArticle,
|
||||
IconHeadphones,
|
||||
IconDeviceTvOld,
|
||||
IconBooks,
|
||||
IconLink,
|
||||
IconInfoCircle,
|
||||
IconSearch,
|
||||
IconRss,
|
||||
IconBrandMastodon,
|
||||
IconMail,
|
||||
IconBrandGithub,
|
||||
IconBrandNpm,
|
||||
IconCoffee,
|
||||
IconDeviceWatch,
|
||||
IconHeartHandshake,
|
||||
} from '@tabler/icons-react';
|
||||
|
||||
const { icon } = Astro.props;
|
||||
|
||||
const iconComponents = {
|
||||
article: IconArticle,
|
||||
headphones: IconHeadphones,
|
||||
'device-tv-old': IconDeviceTvOld,
|
||||
books: IconBooks,
|
||||
link: IconLink,
|
||||
'info-circle': IconInfoCircle,
|
||||
search: IconSearch,
|
||||
rss: IconRss,
|
||||
'brand-mastodon': IconBrandMastodon,
|
||||
mail: IconMail,
|
||||
'brand-github': IconBrandGithub,
|
||||
'brand-npm': IconBrandNpm,
|
||||
coffee: IconCoffee,
|
||||
'device-watch': IconDeviceWatch,
|
||||
'heart-handshake': IconHeartHandshake,
|
||||
};
|
||||
|
||||
const SelectedIcon = iconComponents[icon?.toLowerCase()] || null;
|
||||
---
|
||||
|
||||
{SelectedIcon ? <SelectedIcon size={24} /> : null}
|
10
src/components/Intro.astro
Normal file
10
src/components/Intro.astro
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
import NowPlaying from "@components/blocks/NowPlaying.astro";
|
||||
|
||||
const { intro } = Astro.props;
|
||||
---
|
||||
|
||||
<article class="intro">
|
||||
<div set:html={intro} />
|
||||
<p class="music"><NowPlaying /></p>
|
||||
</article>
|
43
src/components/RecentActivity.astro
Normal file
43
src/components/RecentActivity.astro
Normal file
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
import { fetchMusicData } from '@utils/data/music.js';
|
||||
import { fetchShows } from '@utils/data/tv.js';
|
||||
import { fetchMovies } from '@utils/data/movies.js';
|
||||
import { fetchBooks } from '@utils/data/books.js';
|
||||
import { fetchLinks } from '@utils/data/links.js';
|
||||
import { IconActivity } from '@tabler/icons-react';
|
||||
|
||||
import Rss from '@components/blocks/banners/Rss.astro';
|
||||
|
||||
const music = await fetchMusicData();
|
||||
const tv = await fetchShows();
|
||||
const movies = await fetchMovies();
|
||||
const books = await fetchBooks();
|
||||
const links = await fetchLinks();
|
||||
|
||||
const track = music.week?.tracks[0];
|
||||
const show = tv.recentlyWatched[0];
|
||||
const movie = movies.recentlyWatched[0];
|
||||
const book = books.currentYear[0];
|
||||
const link = links[0];
|
||||
---
|
||||
|
||||
<article>
|
||||
<h2>
|
||||
<IconActivity size={24} />
|
||||
Recent activity
|
||||
</h2>
|
||||
<ul>
|
||||
<li><span class="music">Top track this week:</span> <a href={track.artist_url}>{track.track_name} by {track.artist_name}</a></li>
|
||||
<li><span class="tv">Last episode watched:</span> <strong class="highlight-text">{show.formatted_episode}</strong> of <a href={show.url}>{show.title}</a></li>
|
||||
<li><span class="movies">Last movie watched:</span> <a href={movie.url}>{movie.title}</a>{movie.rating ? ` (${movie.rating})` : ''}</li>
|
||||
<li><span class="books">Last book finished:</span> <a href={book.url}>{book.title}</a> by {book.author}{book.rating ? ` (${book.rating})` : ''}</li>
|
||||
<li>
|
||||
<span class="link">Last link shared:</span>
|
||||
<a href={link.link}>{link.title}</a>
|
||||
{link.author && (
|
||||
<span> via <a href={link.author.url}>{link.author.name}</a></span>
|
||||
)}
|
||||
</li>
|
||||
</ul>
|
||||
<Rss url="/feeds" text="Subscribe to my movies, books, links or activity feed(s)" />
|
||||
</article>
|
33
src/components/RecentPosts.astro
Normal file
33
src/components/RecentPosts.astro
Normal file
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import { IconClock, IconStar, IconArrowRight } from '@tabler/icons-react';
|
||||
import { fetchAllPosts } from '../utils/data/posts.js';
|
||||
import { md } from '@utils/helpers.js';
|
||||
|
||||
const posts = await fetchAllPosts();
|
||||
---
|
||||
|
||||
<h2>
|
||||
<IconClock size={24} />
|
||||
Recent posts
|
||||
</h2>
|
||||
{posts.slice(0, 5).map(post => (
|
||||
<article key={post.url}>
|
||||
<div class="post-meta">
|
||||
{post.featured && <IconStar size={16} />}
|
||||
<time datetime={post.date}>
|
||||
{new Date(post.date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
<h3>
|
||||
<a href={post.url}>{post.title}</a>
|
||||
</h3>
|
||||
<p set:html={md(post.description)}></p>
|
||||
</article>
|
||||
))}
|
||||
<a class="icon-link" href="/posts">
|
||||
View all posts <IconArrowRight size={16} />
|
||||
</a>
|
110
src/components/blocks/AssociatedMedia.astro
Normal file
110
src/components/blocks/AssociatedMedia.astro
Normal file
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
const {
|
||||
artists = [],
|
||||
books = [],
|
||||
genres = [],
|
||||
movies = [],
|
||||
posts = [],
|
||||
shows = [],
|
||||
} = Astro.props;
|
||||
|
||||
const media = [
|
||||
...(artists || []),
|
||||
...(books || []),
|
||||
...(genres || []),
|
||||
...(movies || []),
|
||||
...(posts || []),
|
||||
...(shows || []),
|
||||
];
|
||||
|
||||
if (media.length === 0) return null;
|
||||
|
||||
const sections = [
|
||||
{
|
||||
key: "artists",
|
||||
icon: "headphones",
|
||||
cssClass: "music",
|
||||
label: "Related artist(s)",
|
||||
items: artists || [],
|
||||
},
|
||||
{
|
||||
key: "books",
|
||||
icon: "books",
|
||||
cssClass: "books",
|
||||
label: "Related book(s)",
|
||||
items: books || [],
|
||||
},
|
||||
{
|
||||
key: "genres",
|
||||
icon: "headphones",
|
||||
cssClass: "music",
|
||||
label: "Related genre(s)",
|
||||
items: genres || [],
|
||||
},
|
||||
{
|
||||
key: "movies",
|
||||
icon: "movie",
|
||||
cssClass: "movies",
|
||||
label: "Related movie(s)",
|
||||
items: movies || [],
|
||||
},
|
||||
{
|
||||
key: "posts",
|
||||
icon: "article",
|
||||
cssClass: "article",
|
||||
label: "Related post(s)",
|
||||
items: posts || [],
|
||||
},
|
||||
{
|
||||
key: "shows",
|
||||
icon: "device-tv-old",
|
||||
cssClass: "tv",
|
||||
label: "Related show(s)",
|
||||
items: shows || [],
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<div class="associated-media">
|
||||
{
|
||||
sections.map(({ key, icon, cssClass, label, items }) => {
|
||||
if (!items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={key} class={cssClass}>
|
||||
<p>
|
||||
<IconMapper icon={icon} /> {label}
|
||||
</p>
|
||||
<ul>
|
||||
{items.map((item) => (
|
||||
<li>
|
||||
<a href={item.url}>{item.title || item.name}</a>
|
||||
{key === "artists" && item.total_plays > 0 && (
|
||||
<strong class="highlight-text">
|
||||
{item.total_plays}{" "}
|
||||
{item.total_plays === 1 ? "play" : "plays"}
|
||||
</strong>
|
||||
)}
|
||||
{key === "books" && <span>by {item.author}</span>}
|
||||
{(key === "movies" || key === "shows") && (
|
||||
<span>({item.year})</span>
|
||||
)}
|
||||
{key === "posts" && (
|
||||
<span>
|
||||
(
|
||||
{new Date(item.date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
)
|
||||
</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
7
src/components/blocks/Hero.astro
Normal file
7
src/components/blocks/Hero.astro
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
const { image, alt } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="hero">
|
||||
<img src={image} alt={alt} />
|
||||
</div>
|
7
src/components/blocks/MastodonPost.astro
Normal file
7
src/components/blocks/MastodonPost.astro
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
const { post } = Astro.props;
|
||||
---
|
||||
|
||||
<article class="mastodon-post">
|
||||
<p>{post.content}</p>
|
||||
</article>
|
8
src/components/blocks/Modal.astro
Normal file
8
src/components/blocks/Modal.astro
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="modal">
|
||||
<button class="close">Close</button>
|
||||
<div class="content">{content}</div>
|
||||
</div>
|
24
src/components/blocks/NowPlaying.astro
Normal file
24
src/components/blocks/NowPlaying.astro
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import { fetchNowPlaying } from '../../utils/data/nowPlaying.js';
|
||||
|
||||
const isProduction = import.meta.env.MODE === 'production';
|
||||
const nowPlayingData = await fetchNowPlaying();
|
||||
---
|
||||
|
||||
<span class="loading client-side" id="now-playing-content" set:html={nowPlayingData.content}></span>
|
||||
<noscript>
|
||||
<p>{nowPlayingData.content}</p>
|
||||
</noscript>
|
||||
|
||||
{isProduction && (<script type="module">
|
||||
async function updateNowPlaying() {
|
||||
const response = await fetch('/api/now-playing');
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
document.getElementById('now-playing-content').innerHTML = data.content;
|
||||
}
|
||||
}
|
||||
|
||||
setInterval(updateNowPlaying, 180000);
|
||||
updateNowPlaying();
|
||||
</script>)}
|
10
src/components/blocks/YouTubePlayer.astro
Normal file
10
src/components/blocks/YouTubePlayer.astro
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
const { url } = Astro.props;
|
||||
---
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
src={url}
|
||||
allowfullscreen>
|
||||
</iframe>
|
13
src/components/blocks/banners/Coffee.astro
Normal file
13
src/components/blocks/banners/Coffee.astro
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
import { IconCoffee } from "@tabler/icons-react";
|
||||
---
|
||||
<div class="banner coffee">
|
||||
<p>
|
||||
<a
|
||||
class="coffee plausible-event-name=Buy+Me+a+Coffee+post+footer"
|
||||
href="https://buymeacoffee.com/cory"
|
||||
>
|
||||
<IconCoffee size={24} /> If you found this post helpful, you can buy me a coffee.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
8
src/components/blocks/banners/Error.astro
Normal file
8
src/components/blocks/banners/Error.astro
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
import { IconAlertCircle } from '@tabler/icons-react';
|
||||
|
||||
const { text } = Astro.props
|
||||
---
|
||||
<div class="banner error">
|
||||
<p><IconAlertCircle size={24} />{ text }</p>
|
||||
</div>
|
9
src/components/blocks/banners/GitHub.astro
Normal file
9
src/components/blocks/banners/GitHub.astro
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
import { IconBrandGithub } from '@tabler/icons-react';
|
||||
|
||||
const { url } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner github">
|
||||
<p><IconBrandGithub size={24} /> Take a look at <a href={url}>the GitHub repository for this project</a>. (Give it a star if you feel like it.)</p>
|
||||
</div>
|
12
src/components/blocks/banners/Npm.astro
Normal file
12
src/components/blocks/banners/Npm.astro
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
import { IconBrandNpm } from '@tabler/icons-react';
|
||||
|
||||
const { url, command } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner npm">
|
||||
<p>
|
||||
<IconBrandNpm size={24} />
|
||||
<a href={url}>You can take a look at this package on NPM</a> or install it by running <code>{command}</code>.
|
||||
</p>
|
||||
</div>
|
14
src/components/blocks/banners/OldPost.astro
Normal file
14
src/components/blocks/banners/OldPost.astro
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
import { IconClockX } from '@tabler/icons-react';
|
||||
|
||||
const { isOldPost } = Astro.props;
|
||||
---
|
||||
|
||||
{isOldPost && (
|
||||
<div class="banner old-post">
|
||||
<p>
|
||||
<IconClockX size={24} />
|
||||
This post is over 3 years old. I've probably changed my mind since it was written and it <em>could</em> be out of date.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
12
src/components/blocks/banners/Rss.astro
Normal file
12
src/components/blocks/banners/Rss.astro
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
import { IconRss } from '@tabler/icons-react';
|
||||
|
||||
const { url, text } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner rss">
|
||||
<p>
|
||||
<IconRss size={24} />
|
||||
<a href={url}>{text}</a>.
|
||||
</p>
|
||||
</div>
|
12
src/components/blocks/banners/Warning.astro
Normal file
12
src/components/blocks/banners/Warning.astro
Normal file
|
@ -0,0 +1,12 @@
|
|||
---
|
||||
import { IconAlertTriangle } from '@tabler/icons-react';
|
||||
|
||||
const { text } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner warning">
|
||||
<p>
|
||||
<IconAlertTriangle size={24} />
|
||||
{text}
|
||||
</p>
|
||||
</div>
|
30
src/components/nav/Menu.astro
Normal file
30
src/components/nav/Menu.astro
Normal file
|
@ -0,0 +1,30 @@
|
|||
---
|
||||
import { IconMenu2, IconX } from '@tabler/icons-react';
|
||||
import NavLink from './NavLink.astro';
|
||||
|
||||
const { nav } = Astro.props;
|
||||
---
|
||||
|
||||
<menu>
|
||||
<input id="menu-toggle" type="checkbox" aria-hidden="true" />
|
||||
<label class="menu-button-container" for="menu-toggle" tabindex="0">
|
||||
<div class="menu-closed" aria-hidden="true">
|
||||
<IconMenu2 size={24} />
|
||||
</div>
|
||||
<div class="menu-open" aria-hidden="true">
|
||||
<IconX size={24} />
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<ul class="menu-primary" aria-label="Primary site navigation" id="primary-navigation">
|
||||
{nav.primary.map((link) => (
|
||||
<li>
|
||||
<NavLink
|
||||
url={link.permalink}
|
||||
title={link.title}
|
||||
icon={link.icon}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</menu>
|
24
src/components/nav/NavLink.astro
Normal file
24
src/components/nav/NavLink.astro
Normal file
|
@ -0,0 +1,24 @@
|
|||
---
|
||||
import IconMapper from '@components/IconMapper.astro';
|
||||
|
||||
const { url, title, icon } = Astro.props;
|
||||
const isHttp = url.startsWith('http');
|
||||
const isActive = Astro.url.pathname === url;
|
||||
---
|
||||
|
||||
{isActive ? (
|
||||
<span class={`active icon ${icon?.toLowerCase()}`} aria-current="page">
|
||||
<IconMapper icon={icon} />
|
||||
<span>{title}</span>
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
class={`icon ${icon}`}
|
||||
href={url}
|
||||
rel={isHttp ? 'me' : undefined}
|
||||
aria-label={title}
|
||||
>
|
||||
<IconMapper icon={icon} />
|
||||
<span>{title}</span>
|
||||
</a>
|
||||
)}
|
49
src/components/nav/Paginator.astro
Normal file
49
src/components/nav/Paginator.astro
Normal file
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react";
|
||||
|
||||
const { pagination } = Astro.props;
|
||||
const {
|
||||
currentPage,
|
||||
totalPages,
|
||||
hasPrevious,
|
||||
hasNext,
|
||||
previousPage,
|
||||
nextPage,
|
||||
pages,
|
||||
} = pagination;
|
||||
---
|
||||
|
||||
<nav aria-label="Pagination" class="pagination">
|
||||
<a
|
||||
href={hasPrevious ? previousPage : "#"}
|
||||
aria-label="Previous page"
|
||||
class={hasPrevious ? "" : "disabled"}
|
||||
>
|
||||
<IconArrowLeft size={24} />
|
||||
</a>
|
||||
|
||||
<select class="client-side" aria-label="Page selection">
|
||||
{pages.map((page, index) => (
|
||||
<option
|
||||
value={index}
|
||||
data-href={page.href}
|
||||
selected={page.number === currentPage}
|
||||
>
|
||||
{page.number} of {totalPages}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<noscript>
|
||||
<p>
|
||||
<span aria-current="page">{currentPage}</span> of {totalPages}
|
||||
</p>
|
||||
</noscript>
|
||||
|
||||
<a
|
||||
href={hasNext ? nextPage : "#"}
|
||||
aria-label="Next page"
|
||||
class={hasNext ? "" : "disabled"}
|
||||
>
|
||||
<IconArrowRight size={24} />
|
||||
</a>
|
||||
</nav>
|
1
src/env.d.ts
vendored
Normal file
1
src/env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference path="../.astro/types.d.ts" />
|
96
src/layouts/Layout.astro
Normal file
96
src/layouts/Layout.astro
Normal file
|
@ -0,0 +1,96 @@
|
|||
---
|
||||
import "@styles/index.css";
|
||||
import Header from "../components/Header.astro";
|
||||
import Footer from "../components/Footer.astro";
|
||||
import { fetchNavigation } from "../utils/data/nav.js";
|
||||
|
||||
const currentUrl = Astro.url.pathname;
|
||||
const nav = await fetchNavigation();
|
||||
const { globals, pageTitle, description, ogImage, fullUrl, schema } =
|
||||
Astro.props;
|
||||
const isProduction = import.meta.env.MODE === "production";
|
||||
---
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<title>{globals.site_name}</title>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/ml.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<link
|
||||
rel="preload"
|
||||
href="/fonts/mlb.woff2"
|
||||
as="font"
|
||||
type="font/woff2"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content={description || globals?.site_description}
|
||||
/>
|
||||
<meta property="og:title" content={pageTitle || globals?.site_name} />
|
||||
<meta
|
||||
property="og:description"
|
||||
content={description || globals?.site_description}
|
||||
/>
|
||||
<meta property="og:type" content={schema || "website"} />
|
||||
<meta property="og:url" content={fullUrl} />
|
||||
<meta property="og:image" content={`${ogImage}?class=w800`} />
|
||||
<meta name="theme-color" content={globals?.theme_color} />
|
||||
<meta name="fediverse:creator" content={globals?.mastodon} />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href={`${globals?.cdn_url}${globals?.avatar_transparent}?class=w200`}
|
||||
/>
|
||||
<script defer src="/scripts/index.js"></script>
|
||||
{
|
||||
isProduction && (
|
||||
<>
|
||||
<script defer data-domain="coryd.dev" src="/js/script.js" />
|
||||
<script>
|
||||
window.plausible = window.plausible || function(){" "}
|
||||
{(window.plausible.q = window.plausible.q || []).push(arguments)}
|
||||
</script>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<noscript>
|
||||
<style>.client-side {display:none}</style>
|
||||
</noscript>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
(() => {
|
||||
const currentTheme = sessionStorage.getItem("theme");
|
||||
const metaColorScheme = document.querySelector(
|
||||
'meta[name="color-scheme"]'
|
||||
);
|
||||
const prefersDarkScheme = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)"
|
||||
).matches;
|
||||
const themeToSet =
|
||||
currentTheme || (prefersDarkScheme ? "dark" : "light");
|
||||
if (!currentTheme) sessionStorage.setItem("theme", themeToSet);
|
||||
metaColorScheme?.setAttribute("content", themeToSet);
|
||||
})();
|
||||
</script>
|
||||
<div class="main-wrapper">
|
||||
<main>
|
||||
<Header nav={nav} siteName={globals?.site_name} url={currentUrl} />
|
||||
<div class="default-wrapper">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
<Footer nav={nav} />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
33
src/pages/[permalink].astro
Normal file
33
src/pages/[permalink].astro
Normal file
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import Layout from '@layouts/Layout.astro';
|
||||
import BlockRenderer from '@components/BlockRenderer.astro';
|
||||
import { fetchGlobals } from '@utils/data/globals.js';
|
||||
import { fetchPages } from '@utils/data/pages';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const pages = await fetchPages();
|
||||
return pages.map((page) => ({
|
||||
params: { permalink: page.permalink },
|
||||
props: { page },
|
||||
}));
|
||||
}
|
||||
|
||||
const globals = await fetchGlobals();
|
||||
const { page } = Astro.props;
|
||||
const currentUrl = Astro.url.pathname;
|
||||
---
|
||||
|
||||
<Layout
|
||||
globals={globals}
|
||||
pageTitle={page.title}
|
||||
description={page.description}
|
||||
ogImage={page.open_graph_image}
|
||||
updated={page.updated}
|
||||
currentUrl={currentUrl}
|
||||
>
|
||||
{page.blocks.map((block) => (
|
||||
<BlockRenderer block={block} />
|
||||
))}
|
||||
</Layout>
|
64
src/pages/feeds/syndication.xml.js
Normal file
64
src/pages/feeds/syndication.xml.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
import fetchSyndication from '../../utils/data/syndication.js';
|
||||
import { fetchGlobals } from '../../utils/data/globals.js';
|
||||
|
||||
export async function GET() {
|
||||
const globals = await fetchGlobals();
|
||||
const entries = await fetchSyndication();
|
||||
|
||||
if (!entries.length) return new Response('No feed entries found.', { status: 404 });
|
||||
|
||||
const title = globals.site_name || 'Syndicated content / Cory Dransfeldt';
|
||||
const permalink = '/feeds/syndication.xml';
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<atom:link href="${globals.url}${permalink}" rel="self" type="application/rss+xml" />
|
||||
<title><![CDATA[${title}]]></title>
|
||||
<description><![CDATA[${globals.site_description || ''}]]></description>
|
||||
<link>${globals.url}${permalink}</link>
|
||||
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate>
|
||||
<image>
|
||||
<title><![CDATA[${title}]]></title>
|
||||
<link>${globals.url}${permalink}</link>
|
||||
<url>${globals.cdn_url}${globals.avatar}?class=w200</url>
|
||||
<width>144</width>
|
||||
<height>144</height>
|
||||
</image>
|
||||
${entries
|
||||
.slice(0, 20)
|
||||
.map(
|
||||
(entry) => `
|
||||
<item>
|
||||
<title><![CDATA[${entry.syndication.title}]]></title>
|
||||
<link>${encodeAmp(entry.syndication.url)}</link>
|
||||
<pubDate>${new Date(entry.syndication.date).toUTCString()}</pubDate>
|
||||
<guid isPermaLink="false">${encodeAmp(entry.syndication.url)}</guid>
|
||||
<description><![CDATA[${escapeHTML(entry.syndication.description)}]]></description>
|
||||
</item>`
|
||||
)
|
||||
.join('')}
|
||||
</channel>
|
||||
</rss>`;
|
||||
|
||||
return new Response(xml, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'application/rss+xml',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function encodeAmp(url) {
|
||||
return url.replace(/&/g, '&');
|
||||
}
|
||||
|
||||
function escapeHTML(str) {
|
||||
if (!str) return '';
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
29
src/pages/index.astro
Normal file
29
src/pages/index.astro
Normal file
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
import Layout from '../layouts/Layout.astro';
|
||||
import Intro from '../components/Intro.astro';
|
||||
import { fetchGlobals } from '../utils/data/globals';
|
||||
import RecentActivity from '../components/RecentActivity.astro';
|
||||
import RecentPosts from '../components/RecentPosts.astro';
|
||||
|
||||
const globals = await fetchGlobals();
|
||||
const schema = 'blog';
|
||||
const pageTitle = globals.site_name;
|
||||
const description = 'This is a blog post description';
|
||||
const ogImage = globals.cdn_url + globals.avatar;
|
||||
const fullUrl = globals.url + '/blog/my-post';
|
||||
const themeColor = globals.theme_color;
|
||||
---
|
||||
<Layout
|
||||
globals={globals}
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
ogImage={ogImage}
|
||||
fullUrl={fullUrl}
|
||||
themeColor={themeColor}
|
||||
schema={schema}
|
||||
globals={globals}
|
||||
>
|
||||
<Intro intro={globals.intro} />
|
||||
<RecentActivity />
|
||||
<RecentPosts />
|
||||
</Layout>
|
59
src/pages/posts/[...page].astro
Normal file
59
src/pages/posts/[...page].astro
Normal file
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import { IconStar } from '@tabler/icons-react';
|
||||
import { fetchGlobals } from "@data/globals.js";
|
||||
import { fetchAllPosts } from "@data/posts.js";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Paginator from '@components/nav/Paginator.astro';
|
||||
import { md } from '@utils/helpers.js';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const globals = await fetchGlobals();
|
||||
const posts = await fetchAllPosts();
|
||||
const { page } = Astro.props;
|
||||
const currentUrl = Astro.url.pathname;
|
||||
|
||||
const currentPage = Astro.params.page ? parseInt(Astro.params.page, 10) : 1;
|
||||
const pageSize = 15;
|
||||
const totalPosts = posts.length;
|
||||
const totalPages = Math.ceil(totalPosts / pageSize);
|
||||
const start = (currentPage - 1) * pageSize;
|
||||
const end = start + pageSize;
|
||||
const paginatedPosts = posts.slice(start, end);
|
||||
|
||||
const pagination = {
|
||||
currentPage,
|
||||
totalPages,
|
||||
hasPrevious: currentPage > 1,
|
||||
hasNext: currentPage < totalPages,
|
||||
previousPage: currentPage > 1 ? `/posts/${currentPage - 1}` : null,
|
||||
nextPage: currentPage < totalPages ? `/posts/${currentPage + 1}` : null,
|
||||
pages: Array.from({ length: totalPages }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href: `/posts/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
---
|
||||
|
||||
<Layout
|
||||
globals={globals}
|
||||
pageTitle="All posts"
|
||||
currentUrl={currentUrl}
|
||||
>
|
||||
{paginatedPosts.map((post) => (
|
||||
<article>
|
||||
<div class="post-meta">
|
||||
{post.featured && <IconStar size={16} />}
|
||||
<time datetime={post.date}>
|
||||
{DateTime.fromISO(post.date).toLocaleString(DateTime.DATE_FULL)}
|
||||
</time>
|
||||
</div>
|
||||
<h3>
|
||||
<a href={post.slug}>{post.title}</a>
|
||||
</h3>
|
||||
<p set:html={md(post.description)}></p>
|
||||
</article>
|
||||
))}
|
||||
|
||||
<Paginator pagination={pagination} appVersion="1.0.0" />
|
||||
</Layout>
|
102
src/pages/posts/[year]/[title].astro
Normal file
102
src/pages/posts/[year]/[title].astro
Normal file
|
@ -0,0 +1,102 @@
|
|||
---
|
||||
import { IconStar } from "@tabler/icons-react";
|
||||
import { fetchAllPosts } from "@data/posts.js";
|
||||
import { fetchGlobals } from "@data/globals.js";
|
||||
import { md } from '@utils/helpers.js';
|
||||
import OldPost from "@components/blocks/banners/OldPost.astro";
|
||||
import BlockRenderer from "@components/BlockRenderer.astro";
|
||||
import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
|
||||
import MastodonPost from "@components/blocks/MastodonPost.astro";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Coffee from "@components/blocks/banners/Coffee.astro";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await fetchAllPosts();
|
||||
|
||||
return posts.map((post) => {
|
||||
const match = post.url.match(/^\/posts\/(\d{4})\/(.+)$/);
|
||||
if (!match) throw new Error(`Invalid post URL: ${post.url}`);
|
||||
|
||||
const [, year, title] = match;
|
||||
|
||||
return {
|
||||
params: { year, title },
|
||||
props: { post },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
const { year, title } = Astro.params;
|
||||
const globals = await fetchGlobals();
|
||||
const currentUrl = Astro.url.pathname;
|
||||
const htmlContent = md(post.content);
|
||||
---
|
||||
|
||||
<Layout
|
||||
globals={globals}
|
||||
pageTitle={post.title}
|
||||
description={post.description}
|
||||
ogImage={post.open_graph_image}
|
||||
updated={post.updated}
|
||||
currentUrl={currentUrl}
|
||||
>
|
||||
<article class="standalone">
|
||||
<div class="post-meta">
|
||||
{post.featured && <IconStar size={16} />}
|
||||
<time datetime={post.date}>
|
||||
{
|
||||
new Date(post.date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})
|
||||
}
|
||||
</time>
|
||||
</div>
|
||||
<h3>{post.title}</h3>
|
||||
<div>
|
||||
{post.old_post && <OldPost />}
|
||||
{
|
||||
post.image && (
|
||||
<img
|
||||
srcset={`
|
||||
${globals.cdn_url}${post.image}?class=w200&type=webp 200w,
|
||||
${globals.cdn_url}${post.image}?class=w400&type=webp 400w,
|
||||
${globals.cdn_url}${post.image}?class=w800&type=webp 800w,
|
||||
${globals.cdn_url}${post.image}?class=w1600&type=webp 1600w
|
||||
`}
|
||||
sizes="(max-width: 450px) 200px,
|
||||
(max-width: 850px) 400px,
|
||||
(max-width: 1000px) 800px,
|
||||
1200px"
|
||||
src={`${globals.cdn_url}${post.image}?class=w200`}
|
||||
alt={post.image_alt?.replace(/['"]/g, "")}
|
||||
class="image-banner"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
width="200"
|
||||
height="auto"
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div set:html={htmlContent} />
|
||||
{
|
||||
post.blocks &&
|
||||
post.blocks.map((block) => <BlockRenderer block={block} />)
|
||||
}
|
||||
<!-- {post.mastodon_url && <MastodonPost url={post.mastodon_url} />} -->
|
||||
<AssociatedMedia
|
||||
artists={post.artists}
|
||||
books={post.books}
|
||||
genres={post.genres}
|
||||
movies={post.movies}
|
||||
posts={post.posts}
|
||||
shows={post.shows}
|
||||
/>
|
||||
<Coffee />
|
||||
</div>
|
||||
</article>
|
||||
</Layout>
|
26
src/pages/robots.txt.js
Normal file
26
src/pages/robots.txt.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { fetchAllRobots } from '../utils//data/robots.js';
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const robots = await fetchAllRobots();
|
||||
|
||||
const sitemap = `Sitemap: https://coryd.dev/sitemap.xml\n\n`;
|
||||
const allowAll = `User-agent: *\nDisallow:\n\n`;
|
||||
const disallowedBots = robots
|
||||
.map((userAgent) => `User-agent: ${userAgent}`)
|
||||
.join('\n');
|
||||
const disallowAll = `\nDisallow: /`;
|
||||
|
||||
const robotsTxt = `${sitemap}${allowAll}${disallowedBots}${disallowAll}`;
|
||||
|
||||
return new Response(robotsTxt, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating robots.txt:', error);
|
||||
return new Response('Error generating robots.txt', { status: 500 });
|
||||
}
|
||||
}
|
31
src/styles/base/fonts.css
Normal file
31
src/styles/base/fonts.css
Normal file
|
@ -0,0 +1,31 @@
|
|||
@font-face {
|
||||
font-family: MonoLisa;
|
||||
src: url("/fonts/ml.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: MonoLisa;
|
||||
src: url("/fonts/mlb.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: MonoLisa;
|
||||
src: url("/fonts/mli.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: MonoLisa;
|
||||
src: url("/fonts/mlbi.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
487
src/styles/base/index.css
Normal file
487
src/styles/base/index.css
Normal file
|
@ -0,0 +1,487 @@
|
|||
html,
|
||||
body {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--text-color);
|
||||
background: var(--background-color);
|
||||
}
|
||||
|
||||
html {
|
||||
scrollbar-color: var(--accent-color) var(--gray-light);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: var(--sizing-md);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--gray-light);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--accent-color);
|
||||
border-radius: var(--border-radius-full);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--accent-color-hover);
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: var(--color-lightest);
|
||||
background: var(--accent-color);
|
||||
}
|
||||
|
||||
p {
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
||||
|
||||
.highlight-text {
|
||||
color: var(--text-color-inverted);
|
||||
background-color: var(--accent-color);
|
||||
padding: var(--spacing-xs);
|
||||
border-radius: var(--border-radius-slight);
|
||||
}
|
||||
|
||||
details > summary {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-size: var(--font-size-lg);
|
||||
color: var(--gray-dark);
|
||||
padding-left: var(--spacing-lg);
|
||||
border-left: var(--sizing-xs) solid var(--gray-dark);
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
||||
|
||||
:is(h1, h2, h3) svg {
|
||||
stroke-width: var(--stroke-width-bold);
|
||||
}
|
||||
|
||||
strong,
|
||||
blockquote {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
em,
|
||||
blockquote {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
svg {
|
||||
width: var(--sizing-svg-base);
|
||||
height: var(--sizing-svg-base);
|
||||
stroke-width: var(--stroke-width-default);
|
||||
}
|
||||
|
||||
/* images */
|
||||
img {
|
||||
border-radius: var(--border-radius-slight);
|
||||
|
||||
&.image-banner {
|
||||
border: var(--border-default);
|
||||
height: auto;
|
||||
width: 100%;
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
||||
}
|
||||
|
||||
/* lists */
|
||||
ul,
|
||||
ol {
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
padding-left: var(--spacing-base);
|
||||
|
||||
& li:not(:last-child) {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
}
|
||||
|
||||
/* brand + section colors */
|
||||
.article,
|
||||
.books,
|
||||
.brand-github,
|
||||
.brand-mastodon,
|
||||
.brand-npm,
|
||||
.coffee,
|
||||
.collected,
|
||||
.concerts,
|
||||
.country,
|
||||
.device-tv-old,
|
||||
.device-watch,
|
||||
.favorite,
|
||||
.headphones,
|
||||
.heart-handshake,
|
||||
.info-circle,
|
||||
.link,
|
||||
.mail,
|
||||
.mail-plus,
|
||||
.movies,
|
||||
.music,
|
||||
.rss,
|
||||
.search,
|
||||
.tattoo,
|
||||
.tv {
|
||||
&.article {
|
||||
--section-color: var(--article);
|
||||
}
|
||||
&.books {
|
||||
--section-color: var(--books);
|
||||
}
|
||||
&.brand-github {
|
||||
--section-color: var(--brand-github);
|
||||
}
|
||||
&.brand-mastodon {
|
||||
--section-color: var(--brand-mastodon);
|
||||
}
|
||||
&.brand-npm {
|
||||
--section-color: var(--brand-npm);
|
||||
}
|
||||
&.coffee {
|
||||
--section-color: var(--brand-buy-me-a-coffee);
|
||||
}
|
||||
&.collected {
|
||||
--section-color: var(--collected);
|
||||
}
|
||||
&.concerts {
|
||||
--section-color: var(--concerts);
|
||||
}
|
||||
&.country {
|
||||
--section-color: var(--country);
|
||||
}
|
||||
&.device-tv-old {
|
||||
--section-color: var(--tv);
|
||||
}
|
||||
&.device-watch {
|
||||
--section-color: var(--now);
|
||||
}
|
||||
&.favorite {
|
||||
--section-color: var(--favorite);
|
||||
}
|
||||
&.headphones {
|
||||
--section-color: var(--music);
|
||||
}
|
||||
&.heart-handshake {
|
||||
--section-color: var(--webrings);
|
||||
}
|
||||
&.info-circle {
|
||||
--section-color: var(--about);
|
||||
}
|
||||
&.link {
|
||||
--section-color: var(--link);
|
||||
}
|
||||
&.mail {
|
||||
--section-color: var(--brand-fastmail);
|
||||
}
|
||||
&.mail-plus {
|
||||
--section-color: var(--newsletter);
|
||||
}
|
||||
&.movies,
|
||||
&.tv {
|
||||
--section-color: var(--tv);
|
||||
}
|
||||
&.music {
|
||||
--section-color: var(--music);
|
||||
}
|
||||
&.rss {
|
||||
--section-color: var(--brand-rss);
|
||||
}
|
||||
&.search {
|
||||
--section-color: var(--search);
|
||||
}
|
||||
&.tattoo {
|
||||
--section-color: var(--tattoo);
|
||||
}
|
||||
|
||||
color: var(--section-color);
|
||||
|
||||
& svg {
|
||||
stroke: var(--section-color);
|
||||
}
|
||||
}
|
||||
|
||||
/* links */
|
||||
a {
|
||||
color: var(--accent-color);
|
||||
|
||||
&.back-link {
|
||||
margin-bottom: var(--spacing-base);
|
||||
}
|
||||
|
||||
& > img {
|
||||
border: var(--border-default);
|
||||
}
|
||||
|
||||
& svg {
|
||||
stroke: var(--accent-color);
|
||||
}
|
||||
|
||||
&:is(:hover, :focus, :active) {
|
||||
color: var(--accent-color-hover);
|
||||
transition: color var(--transition-duration-default)
|
||||
var(--transition-ease-in-out);
|
||||
|
||||
& > img {
|
||||
border: var(--border-default-hover);
|
||||
transition: border var(--transition-duration-default)
|
||||
var(--transition-ease-in-out);
|
||||
}
|
||||
|
||||
& > svg {
|
||||
stroke: var(--accent-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:is(h1, h2, h3, a, p:not(.banner p), span, th, td, .post-meta):has(svg) {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* headers */
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--line-height-md);
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-xl);
|
||||
|
||||
&.page-title {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
h1 {
|
||||
font-size: var(--font-size-3xl);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: var(--font-size-2xl);
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: var(--font-size-xl);
|
||||
}
|
||||
}
|
||||
|
||||
/* dividers */
|
||||
hr {
|
||||
color: var(--gray-light);
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
||||
|
||||
/* articles */
|
||||
article {
|
||||
margin-bottom: var(--spacing-base);
|
||||
|
||||
&:not([class], :last-of-type) {
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
|
||||
&.intro {
|
||||
border-bottom: var(--border-gray);
|
||||
|
||||
& p {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
& h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
& .post-meta {
|
||||
& svg {
|
||||
stroke: var(--gray-dark);
|
||||
width: var(--sizing-svg-sm);
|
||||
height: var(--sizing-svg-sm);
|
||||
}
|
||||
|
||||
& time {
|
||||
color: var(--gray-dark);
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sup.footnote-ref {
|
||||
line-height: var(--line-height-xs);
|
||||
padding: var(--spacing-xs);
|
||||
}
|
||||
|
||||
/* tables */
|
||||
table {
|
||||
display: block;
|
||||
border: var(--border-gray);
|
||||
border-radius: var(--border-radius-slight);
|
||||
overflow-x: scroll;
|
||||
white-space: nowrap;
|
||||
caption-side: bottom;
|
||||
overscroll-behavior: none;
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
||||
|
||||
table,
|
||||
th,
|
||||
td {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
:is(th, td):not(:first-child, :last-child) {
|
||||
border-right: var(--border-gray);
|
||||
}
|
||||
|
||||
th,
|
||||
tr:not(:last-child) {
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: var(--spacing-sm);
|
||||
word-break: break-word;
|
||||
|
||||
&:first-child {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
max-width: 200px;
|
||||
border-inline-end: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-block-start: 0;
|
||||
inset-inline-end: 0;
|
||||
width: 1px;
|
||||
height: 100%;
|
||||
background: var(--gray-light);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
th {
|
||||
font-weight: var(--font-weight-bold);
|
||||
background-color: var(--gray-lighter);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
td {
|
||||
min-width: calc(var(--spacing-3xl) * 2);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
&:first-child {
|
||||
background: var(--background-color);
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
td:first-of-type,
|
||||
:where(thead, tfoot) th:nth-child(2) {
|
||||
border-inline-start: none;
|
||||
}
|
||||
|
||||
/* header */
|
||||
.main-title {
|
||||
width: 100%;
|
||||
padding-top: var(--spacing-3xl);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
& h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* nav */
|
||||
.active,
|
||||
.active svg {
|
||||
cursor: not-allowed;
|
||||
color: var(--accent-color-active);
|
||||
stroke: var(--accent-color-active);
|
||||
}
|
||||
|
||||
/* layout */
|
||||
.default-wrapper {
|
||||
padding-top: var(--spacing-2xl);
|
||||
}
|
||||
|
||||
.main-wrapper {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
main {
|
||||
flex: 1 1 0%;
|
||||
}
|
||||
|
||||
main,
|
||||
footer {
|
||||
width: 80%;
|
||||
margin: 0 auto;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
& .updated {
|
||||
text-align: center;
|
||||
font-size: var(--font-size-sm);
|
||||
margin: var(--spacing-3xl) 0 var(--spacing-lg);
|
||||
}
|
||||
|
||||
& nav {
|
||||
&.social,
|
||||
&.sub-pages {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&.social {
|
||||
gap: var(--spacing-md);
|
||||
margin-bottom: var(--spacing-lg);
|
||||
width: 100%;
|
||||
|
||||
& .icon > span,
|
||||
& .active > span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .active {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
&.sub-pages {
|
||||
font-size: var(--font-size-sm);
|
||||
padding-bottom: var(--spacing-3xl);
|
||||
gap: var(--sizing-sm);
|
||||
}
|
||||
}
|
||||
}
|
131
src/styles/base/reset.css
Normal file
131
src/styles/base/reset.css
Normal file
|
@ -0,0 +1,131 @@
|
|||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:where([hidden]:not([hidden='until-found'])) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:where(html) {
|
||||
font-size: 100%;
|
||||
-webkit-text-size-adjust: none;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-gutter: stable;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
:where(html:has(dialog:modal[open])) {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:where(html:focus-within) {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
:where(body) {
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
:where(button) {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
:where(input, button, textarea, select) {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:where(textarea) {
|
||||
resize: vertical;
|
||||
resize: block;
|
||||
}
|
||||
|
||||
:where(button, label, select, summary, [role='button'], [role='option']) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:where(:disabled) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:where(label:has(> input:disabled), label:has(+ input:disabled)) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:where(a) {
|
||||
color: inherit;
|
||||
text-underline-offset: var(--spacing-xs);
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: number;
|
||||
}
|
||||
|
||||
:where(ul, ol) {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
:where(img, svg, video, canvas, audio, iframe, embed, object) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:where(p, h1, h2, h3) {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
:where(hr) {
|
||||
border: none;
|
||||
border-block-start: 1px solid;
|
||||
border-block-start-color: currentColor;
|
||||
color: inherit;
|
||||
block-size: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
:where(dialog, [popover]) {
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
inset: unset;
|
||||
max-width: unset;
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
:where(dialog:not([open], [popover]), [popover]:not(:popover-open)) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:where(:focus-visible) {
|
||||
outline: var(--border-default);
|
||||
outline-offset: 1px;
|
||||
border-radius: var(--border-radius-slight);
|
||||
box-shadow: 0 0 0 1px var(--accent-color);
|
||||
}
|
||||
|
||||
:where(:focus-visible, :target) {
|
||||
scroll-margin-block: 8vh;
|
||||
}
|
||||
|
||||
:where(.visually-hidden:not(:focus-within, :active)) {
|
||||
clip-path: inset(50%) !important;
|
||||
height: 1px !important;
|
||||
width: 1px !important;
|
||||
overflow: hidden !important;
|
||||
position: absolute !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
user-select: none !important;
|
||||
}
|
162
src/styles/base/vars.css
Normal file
162
src/styles/base/vars.css
Normal file
|
@ -0,0 +1,162 @@
|
|||
:root {
|
||||
/* colors */
|
||||
--blue-100: #a2c4ff;
|
||||
--blue-200: #6b9eff;
|
||||
--blue-300: #4a78ff;
|
||||
--blue-400: #3364ff;
|
||||
--blue-500: #2553e6;
|
||||
--blue-600: #1e42c7;
|
||||
|
||||
--gray-100: #f9fafb;
|
||||
--gray-200: #eceef1;
|
||||
--gray-300: #dfe3e8;
|
||||
--gray-400: #959eae;
|
||||
--gray-500: #7f899b;
|
||||
--gray-600: #626d7f;
|
||||
--gray-700: #545e71;
|
||||
--gray-800: #4a5365;
|
||||
--gray-900: #14161a;
|
||||
|
||||
--gray-lighter: light-dark(var(--gray-200), var(--gray-700));
|
||||
--gray-light: light-dark(var(--gray-300), var(--gray-600));
|
||||
--gray-medium: var(--gray-400);
|
||||
--gray-dark: light-dark(var(--gray-800), var(--gray-300));
|
||||
|
||||
/* base theme */
|
||||
--color-lightest: var(--gray-100);
|
||||
--color-darkest: var(--gray-900);
|
||||
--text-color: light-dark(var(--color-darkest), var(--color-lightest));
|
||||
--background-color: light-dark(var(--color-lightest), var(--color-darkest));
|
||||
--text-color-inverted: light-dark(
|
||||
var(--color-lightest),
|
||||
var(--color-darkest)
|
||||
);
|
||||
--background-color-inverted: light-dark(
|
||||
var(--color-darkest),
|
||||
var(--color-lightest)
|
||||
);
|
||||
--accent-color: light-dark(var(--blue-400), var(--blue-200));
|
||||
--accent-color-hover: light-dark(var(--blue-600), var(--blue-100));
|
||||
--accent-color-active: light-dark(var(--blue-600), var(--blue-100));
|
||||
|
||||
--brand-buy-me-a-coffee: light-dark(#0d0c22, #ffdd00);
|
||||
--brand-github: light-dark(#333, #f5f5f5);
|
||||
--brand-fastmail: light-dark(#0067b9, #ffc107);
|
||||
--brand-mastodon: light-dark(#563acc, #858afa);
|
||||
--brand-npm: #cb3837;
|
||||
--brand-rss: #f26522;
|
||||
|
||||
--article: light-dark(#007272, #00ffff);
|
||||
--about: light-dark(#e4513a, #ff967d);
|
||||
--books: light-dark(#1a7b1a, #6fff6f);
|
||||
--collected: light-dark(#9a501a, #ffae73);
|
||||
--concerts: light-dark(#cb426e, #ff82aa);
|
||||
--country: light-dark(#146a67, #80dcdc);
|
||||
--error: light-dark(#b81f1f, #ff8b8b);
|
||||
--favorite: light-dark(#b03c72, #ff9ccd);
|
||||
--link: light-dark(#7b5cba, #e2b8ff);
|
||||
--music: light-dark(#3d7099, #76b8cc);
|
||||
--newsletter: light-dark(#37b0b0, #91fffa);
|
||||
--now: light-dark(#cc1076, #ff82d5);
|
||||
--search: light-dark(#6b5e3a, #c0b594);
|
||||
--tattoo: light-dark(#951b1b, #ff7373);
|
||||
--tv: light-dark(#cc3600, #ff8f66);
|
||||
--warning: light-dark(#cc6f00, #ffbf66);
|
||||
--webrings: light-dark(#b054b0, #ffb3ff);
|
||||
|
||||
/* borders */
|
||||
--border-default: 1px solid var(--accent-color);
|
||||
--border-default-hover: 1px solid var(--accent-color-hover);
|
||||
--border-gray: 1px solid var(--gray-light);
|
||||
|
||||
/* fonts */
|
||||
--font-mono: MonoLisa, Menlo, Consolas, Monaco, Liberation Mono,
|
||||
Lucida Console, ui-monospace, monospace;
|
||||
|
||||
/* text */
|
||||
--font-size-xs: 0.7rem;
|
||||
--font-size-sm: 0.85rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.15rem;
|
||||
--font-size-xl: 1.3rem;
|
||||
--font-size-2xl: 1.45rem;
|
||||
--font-size-3xl: 1.6rem;
|
||||
|
||||
--font-weight-base: 400;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--line-height-sm: 1;
|
||||
--line-height-md: 1.5;
|
||||
--line-height-base: 2;
|
||||
|
||||
/* sizing */
|
||||
--sizing-xs: 0.25rem;
|
||||
--sizing-sm: 0.5rem;
|
||||
--sizing-md: 0.75rem;
|
||||
--sizing-lg: 1rem;
|
||||
--sizing-base: 1.5rem;
|
||||
--sizing-xl: 1.75rem;
|
||||
--sizing-2xl: 2rem;
|
||||
--sizing-3xl: 2.25rem;
|
||||
|
||||
--sizing-svg-sm: 18px;
|
||||
--sizing-svg-base: 24px;
|
||||
|
||||
/* spacing */
|
||||
--spacing-xs: var(--sizing-xs);
|
||||
--spacing-sm: var(--sizing-sm);
|
||||
--spacing-md: var(--sizing-md);
|
||||
--spacing-lg: var(--sizing-lg);
|
||||
--spacing-base: var(--sizing-base);
|
||||
--spacing-xl: var(--sizing-xl);
|
||||
--spacing-2xl: var(--sizing-2xl);
|
||||
--spacing-3xl: var(--sizing-3xl);
|
||||
|
||||
--margin-vertical-base-horizontal-zero: var(--spacing-base) 0;
|
||||
|
||||
/* radii */
|
||||
--border-radius-slight: var(--sizing-xs);
|
||||
--border-radius-full: 9999px;
|
||||
|
||||
/* aspect ratios */
|
||||
--aspect-ratio-square: 1/1;
|
||||
--aspect-ratio-vertical: 2/3;
|
||||
--aspect-ratio-banner: 3/2;
|
||||
|
||||
/* grid columns */
|
||||
--grid-columns-one: repeat(1, minmax(0, 1fr));
|
||||
--grid-columns-two: repeat(2, minmax(0, 1fr));
|
||||
--grid-columns-three: repeat(3, minmax(0, 1fr));
|
||||
--grid-columns-four: repeat(4, minmax(0, 1fr));
|
||||
--grid-columns-six: repeat(6, minmax(0, 1fr));
|
||||
|
||||
--grid-poster: var(--grid-columns-two);
|
||||
--grid-square: var(--grid-columns-two);
|
||||
--grid-vertical: var(--grid-columns-three);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
--grid-poster: var(--grid-columns-three);
|
||||
--grid-square: var(--grid-columns-four);
|
||||
--grid-vertical: var(--grid-columns-six);
|
||||
}
|
||||
|
||||
/* transitions */
|
||||
--transition-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-duration-default: 300ms;
|
||||
|
||||
/* svgs */
|
||||
--stroke-width-default: 1.4;
|
||||
--stroke-width-bold: 2;
|
||||
--inline-margin-bottom: -5px;
|
||||
|
||||
/* shadows */
|
||||
--box-shadow-media: inset 0 -85px 60px -40px var(--color-darkest);
|
||||
--box-shadow-text-toggle: inset 0 -120px 60px -60px var(--background-color);
|
||||
--text-shadow-default: rgba(0, 0, 0, 0.7) 0px 0px 10px;
|
||||
|
||||
/* modals */
|
||||
--modal-overlay-background: light-dark(#ffffffbf, #000000bf);
|
||||
|
||||
/* input accent color */
|
||||
accent-color: var(--accent-color);
|
||||
}
|
14
src/styles/components/addon-links.css
Normal file
14
src/styles/components/addon-links.css
Normal file
|
@ -0,0 +1,14 @@
|
|||
.addon-links {
|
||||
display: grid;
|
||||
gap: var(--sizing-base);
|
||||
grid-template-columns: var(--grid-columns-one);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-template-columns: var(--grid-columns-two);
|
||||
}
|
||||
|
||||
& article {
|
||||
border-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
20
src/styles/components/badge-grid.css
Normal file
20
src/styles/components/badge-grid.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
.badge-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-md);
|
||||
grid-template-columns: var(--grid-columns-three);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-template-columns: var(--grid-columns-four);
|
||||
}
|
||||
|
||||
& > a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
& img {
|
||||
image-rendering: pixelated;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
63
src/styles/components/banners.css
Normal file
63
src/styles/components/banners.css
Normal file
|
@ -0,0 +1,63 @@
|
|||
.banner {
|
||||
padding: var(--spacing-md);
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
border: 1px solid;
|
||||
border-radius: var(--border-radius-slight);
|
||||
|
||||
& p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
& svg {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
height: var(--sizing-lg);
|
||||
width: var(--sizing-lg);
|
||||
}
|
||||
}
|
||||
|
||||
&.coffee,
|
||||
&.error,
|
||||
&.github,
|
||||
&.npm,
|
||||
&.old-post,
|
||||
&.rss,
|
||||
&.warning {
|
||||
&.coffee {
|
||||
--banner-accent-color: var(--brand-buy-me-a-coffee);
|
||||
}
|
||||
&.error {
|
||||
--banner-accent-color: var(--error);
|
||||
}
|
||||
&.github {
|
||||
--banner-accent-color: var(--brand-github);
|
||||
}
|
||||
&.npm {
|
||||
--banner-accent-color: var(--brand-npm);
|
||||
}
|
||||
&.old-post {
|
||||
--banner-accent-color: var(--gray-dark);
|
||||
}
|
||||
&.rss {
|
||||
--banner-accent-color: var(--brand-rss);
|
||||
}
|
||||
&.warning {
|
||||
--banner-accent-color: var(--warning);
|
||||
}
|
||||
|
||||
border-color: var(--banner-accent-color);
|
||||
|
||||
& p a:is(:hover, :active, :focus) {
|
||||
color: var(--banner-accent-color);
|
||||
}
|
||||
|
||||
& svg {
|
||||
stroke: var(--banner-accent-color);
|
||||
}
|
||||
}
|
||||
}
|
28
src/styles/components/buttons.css
Normal file
28
src/styles/components/buttons.css
Normal file
|
@ -0,0 +1,28 @@
|
|||
@import url("./tab-buttons.css");
|
||||
@import url("./text-toggle.css");
|
||||
|
||||
button,
|
||||
.button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
border: 2px solid var(--accent-color);
|
||||
border-radius: var(--border-radius-full);
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--line-height-base);
|
||||
white-space: nowrap;
|
||||
color: var(--text-color-inverted);
|
||||
background-color: var(--accent-color);
|
||||
transition: color var(--transition-duration-default)
|
||||
var(--transition-ease-in-out);
|
||||
|
||||
&:not(.active):is(:hover, :active, :focus, :focus-within) {
|
||||
background-color: var(--accent-color-hover);
|
||||
border: 2px solid var(--accent-color-hover);
|
||||
transition: background-color var(--transition-duration-default)
|
||||
var(--transition-ease-in-out),
|
||||
border var(--transition-duration-default) var(--transition-ease-in-out),
|
||||
color var(--transition-duration-default) var(--transition-ease-in-out);
|
||||
}
|
||||
}
|
65
src/styles/components/forms.css
Normal file
65
src/styles/components/forms.css
Normal file
|
@ -0,0 +1,65 @@
|
|||
::placeholder {
|
||||
color: var(--text-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="checkbox"]),
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="checkbox"]),
|
||||
textarea,
|
||||
select {
|
||||
color: var(--text-color);
|
||||
border-radius: var(--border-radius-slight);
|
||||
background-color: var(--background-color);
|
||||
padding: var(--spacing-sm);
|
||||
border: var(--border-gray);
|
||||
}
|
||||
|
||||
form,
|
||||
input:not([type="button"]):not([type="submit"]):not([type="reset"]):not([type="checkbox"]),
|
||||
textarea {
|
||||
margin-bottom: var(--spacing-base);
|
||||
}
|
||||
|
||||
textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.search__form {
|
||||
margin-top: 0;
|
||||
|
||||
& .search__form--input::-webkit-search-cancel-button {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.search__form--type {
|
||||
display: flex;
|
||||
gap: var(--spacing-md);
|
||||
margin-top: var(--sizing-md);
|
||||
border: none;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
}
|
||||
}
|
||||
|
||||
.search__results {
|
||||
margin: 0 0 var(--spacing-base);
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: none;
|
||||
|
||||
& li {
|
||||
margin: var(--spacing-sm) 0;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-bottom: var(--sizing-base);
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
}
|
||||
}
|
22
src/styles/components/mastodon-post.css
Normal file
22
src/styles/components/mastodon-post.css
Normal file
|
@ -0,0 +1,22 @@
|
|||
mastodon-post {
|
||||
width: 100%;
|
||||
|
||||
.mastodon-post-wrapper {
|
||||
& dl,
|
||||
& dt {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
& dl {
|
||||
align-items: center;
|
||||
|
||||
& dd {
|
||||
margin-left: var(--spacing-xs);
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: var(--spacing-lg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
67
src/styles/components/media-grid.css
Normal file
67
src/styles/components/media-grid.css
Normal file
|
@ -0,0 +1,67 @@
|
|||
.media-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-sm);
|
||||
|
||||
& ~ .pagination {
|
||||
margin-top: var(--spacing-base);
|
||||
}
|
||||
|
||||
&.poster {
|
||||
grid-template-columns: var(--grid-poster);
|
||||
|
||||
& a {
|
||||
aspect-ratio: var(--aspect-ratio-banner);
|
||||
}
|
||||
}
|
||||
|
||||
&.square {
|
||||
grid-template-columns: var(--grid-square);
|
||||
|
||||
& a {
|
||||
aspect-ratio: var(--aspect-ratio-square);
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
grid-template-columns: var(--grid-vertical);
|
||||
|
||||
& a {
|
||||
aspect-ratio: var(--aspect-ratio-vertical);
|
||||
}
|
||||
}
|
||||
|
||||
&:is(.poster, .square, .vertical) img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
& .item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& .meta-text {
|
||||
color: var(--color-lightest);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
padding: 0 var(--spacing-sm);
|
||||
bottom: var(--spacing-sm);
|
||||
|
||||
& .header,
|
||||
& .subheader {
|
||||
color: var(--color-lightest);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-md);
|
||||
text-shadow: var(--text-shadow-default);
|
||||
}
|
||||
|
||||
& .header {
|
||||
font-weight: var(--font-weight-bold);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 6;
|
||||
line-clamp: 6;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
136
src/styles/components/menu.css
Normal file
136
src/styles/components/menu.css
Normal file
|
@ -0,0 +1,136 @@
|
|||
menu {
|
||||
& .menu-primary {
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
top: calc(var(--spacing-3xl) * 1.75);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 3;
|
||||
|
||||
& > li {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: var(--spacing-sm) 0;
|
||||
width: 100%;
|
||||
background: var(--background-color);
|
||||
|
||||
& a,
|
||||
& .active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
& a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
& .active {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:is(.icon, .active) > svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:is(.icon, .active) > span {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#menu-toggle {
|
||||
display: none;
|
||||
|
||||
&:checked + .menu-button-container {
|
||||
& .menu-closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .menu-open {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:checked) + .menu-button-container {
|
||||
& .menu-closed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& .menu-open {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& ~ .menu-primary li {
|
||||
height: 0;
|
||||
padding: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
&:checked ~ .menu-primary li {
|
||||
height: calc(var(--sizing-3xl) * 1.5);
|
||||
|
||||
@media (max-width: 767px) {
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@media (max-width: 767px) {
|
||||
border-top: var(--border-gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-button-container {
|
||||
display: unset;
|
||||
|
||||
& svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.menu-primary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
gap: var(--spacing-md);
|
||||
position: relative;
|
||||
top: unset;
|
||||
left: unset;
|
||||
width: auto;
|
||||
|
||||
& > li {
|
||||
background: none;
|
||||
|
||||
& a {
|
||||
width: var(--sizing-svg-base);
|
||||
height: var(--sizing-svg-base);
|
||||
}
|
||||
|
||||
:is(.icon, .active) > svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:is(.icon, .active) > span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#menu-toggle ~ .menu-primary li,
|
||||
#menu-toggle:checked ~ .menu-primary li {
|
||||
height: unset;
|
||||
}
|
||||
|
||||
.menu-button-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
63
src/styles/components/modal.css
Normal file
63
src/styles/components/modal.css
Normal file
|
@ -0,0 +1,63 @@
|
|||
.modal-wrapper,
|
||||
.modal-body {
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
background: var(--modal-overlay-background);
|
||||
z-index: 3;
|
||||
|
||||
.modal-body {
|
||||
background: var(--background-color);
|
||||
padding: var(--spacing-lg) var(--spacing-base);
|
||||
overflow-y: auto;
|
||||
border-radius: var(--border-radius-slight);
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-width: 75%;
|
||||
max-height: 75%;
|
||||
inset: 12.5%;
|
||||
border: var(--border-gray);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
display: none;
|
||||
|
||||
&:checked ~ .modal-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:not(:checked) ~ .modal-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-toggle,
|
||||
.modal-close {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
|
||||
svg {
|
||||
stroke: var(--accent-color);
|
||||
|
||||
&:is(:hover, :focus, :active) {
|
||||
stroke: var(--accent-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
103
src/styles/components/music-chart.css
Normal file
103
src/styles/components/music-chart.css
Normal file
|
@ -0,0 +1,103 @@
|
|||
.music-chart {
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
|
||||
& ol {
|
||||
padding-left: 0;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
list-style-position: outside;
|
||||
}
|
||||
}
|
||||
|
||||
& .item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
& .progress-bar-wrapper {
|
||||
max-width: 40%;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
& img {
|
||||
width: calc(var(--sizing-3xl) * 1.5);
|
||||
height: calc(var(--sizing-3xl) * 1.5);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
width: calc(var(--sizing-3xl) * 2);
|
||||
height: calc(var(--sizing-3xl) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
& .info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: calc(75% - var(--sizing-lg));
|
||||
}
|
||||
}
|
||||
|
||||
& .meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
width: calc(75% - var(--sizing-lg));
|
||||
}
|
||||
}
|
||||
|
||||
& .meta-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
gap: var(--spacing-xs);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
|
||||
& .title {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
& .title,
|
||||
& .subtext,
|
||||
& time {
|
||||
line-height: var(--line-height-md);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
& .subtext,
|
||||
& time {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
& time {
|
||||
margin-top: var(--spacing-sm);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
src/styles/components/paginator.css
Normal file
25
src/styles/components/paginator.css
Normal file
|
@ -0,0 +1,25 @@
|
|||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-base);
|
||||
|
||||
& button {
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
& a {
|
||||
display: flex;
|
||||
|
||||
&.disabled svg[data-tablericon-name^="arrow-"] {
|
||||
cursor: not-allowed;
|
||||
stroke: var(--gray-medium);
|
||||
stroke-width: var(--stroke-width-default);
|
||||
}
|
||||
}
|
||||
|
||||
& p {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
13
src/styles/components/progress-bar.css
Normal file
13
src/styles/components/progress-bar.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
.progress-bar-wrapper {
|
||||
display: flex;
|
||||
height: var(--sizing-lg);
|
||||
width: 100%;
|
||||
background-color: var(--gray-light);
|
||||
border-radius: var(--border-radius-full);
|
||||
overflow: hidden;
|
||||
|
||||
& .progress-bar {
|
||||
background-color: var(--accent-color);
|
||||
border-radius: var(--border-radius-full);
|
||||
}
|
||||
}
|
38
src/styles/components/tab-buttons.css
Normal file
38
src/styles/components/tab-buttons.css
Normal file
|
@ -0,0 +1,38 @@
|
|||
#tracks-recent,
|
||||
#tracks-chart,
|
||||
.tracks-recent,
|
||||
.tracks-chart {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tracks-recent:checked ~ .tracks-recent,
|
||||
#tracks-chart:checked ~ .tracks-chart {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[id="tracks-recent"] ~ .tracks-recent,
|
||||
input[id="tracks-chart"] ~ .tracks-chart {
|
||||
margin-top: var(--spacing-base);
|
||||
}
|
||||
|
||||
[for="tracks-recent"] {
|
||||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
#tracks-recent:checked ~ [for="tracks-recent"],
|
||||
#tracks-chart:checked ~ [for="tracks-chart"] {
|
||||
cursor: not-allowed;
|
||||
border-color: var(--accent-color);
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
|
||||
#tracks-recent:not(:checked) ~ [for="tracks-recent"],
|
||||
#tracks-chart:not(:checked) ~ [for="tracks-chart"] {
|
||||
color: var(--accent-color);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#tracks-recent:not(:checked) ~ [for="tracks-recent"]:is(:hover, :active),
|
||||
#tracks-chart:not(:checked) ~ [for="tracks-chart"]:is(:hover, :active) {
|
||||
color: var(--accent-color-hover);
|
||||
}
|
23
src/styles/components/text-toggle.css
Normal file
23
src/styles/components/text-toggle.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
[data-toggle-content] {
|
||||
&.text-toggle-hidden {
|
||||
position: relative;
|
||||
height: 500px;
|
||||
overflow: hidden;
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
|
||||
& p:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
content: "";
|
||||
box-shadow: var(--box-shadow-text-toggle);
|
||||
width: 100%;
|
||||
height: 20%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
15
src/styles/components/youtube-player.css
Normal file
15
src/styles/components/youtube-player.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
youtube-video {
|
||||
aspect-ratio: 16/9;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
border: var(--border-default);
|
||||
border-radius: var(--border-radius-slight);
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
|
||||
&:hover {
|
||||
border: var(--border-default-hover);
|
||||
transition: border var(--transition-duration-default)
|
||||
var(--transition-ease-in-out);
|
||||
}
|
||||
}
|
81
src/styles/feed.liquid
Normal file
81
src/styles/feed.liquid
Normal file
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
permalink: /assets/styles/feed.xsl
|
||||
---
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<xsl:stylesheet version="3.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||
xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<xsl:output method="html" version="1.0" encoding="UTF-8" indent="yes" />
|
||||
<xsl:template match="/">
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
|
||||
<head>
|
||||
<title>
|
||||
<xsl:value-of select="/rss/channel/title" /> / {{ globals.site_name }}
|
||||
</title>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="{{ globals.theme_color }}" />
|
||||
<meta name="fediverse:creator" content="{{ globals.mastodon }}" />
|
||||
<meta name="generator" content="Eleventy" />
|
||||
<meta name="robots" content="noai, noimageai" />
|
||||
<link rel="preload" href="/assets/fonts/ml.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<link rel="preload" href="/assets/fonts/mlb.woff2" as="font" type="font/woff2" crossorigin="anonymous" />
|
||||
<link rel="stylesheet" href="/assets/styles/index.css?v={% appVersion %}" type="text/css" />
|
||||
</head>
|
||||
<body class="feed">
|
||||
<div class="main-wrapper">
|
||||
<main>
|
||||
{% render "header.liquid", globals:globals, page:page, nav:nav %}
|
||||
<div class="default-wrapper">
|
||||
<h2 class="page-title">
|
||||
<xsl:value-of select="/rss/channel/title" />
|
||||
</h2>
|
||||
<article class="intro">
|
||||
<p>
|
||||
<xsl:value-of select="/rss/channel/description" />
|
||||
</p>
|
||||
<p>
|
||||
<strong class="highlight-text">Subscribe by adding the URL below to your feed reader
|
||||
of choice.</strong>
|
||||
</p>
|
||||
<p>
|
||||
<pre class="small">
|
||||
<code><xsl:value-of select="rss/channel/atom:link/@href"/></code>
|
||||
</pre>
|
||||
</p>
|
||||
<p>
|
||||
<a href="/feeds">View more of the feeds from my site.</a>
|
||||
</p>
|
||||
</article>
|
||||
<section>
|
||||
<xsl:for-each select="/rss/channel/item">
|
||||
<article>
|
||||
<time>Published: <xsl:value-of select="pubDate" /></time>
|
||||
<h3>
|
||||
<a>
|
||||
<xsl:attribute name="href">
|
||||
<xsl:value-of select="link" />
|
||||
</xsl:attribute>
|
||||
<xsl:value-of select="title" />
|
||||
</a>
|
||||
</h3>
|
||||
<xsl:value-of select="description" disable-output-escaping="yes" />
|
||||
<xsl:if test="enclosure">
|
||||
<img class="image-banner" src="{enclosure/@url}" alt="{title}" />
|
||||
</xsl:if>
|
||||
</article>
|
||||
</xsl:for-each>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
{% render "footer.liquid",
|
||||
page:page,
|
||||
nav:nav,
|
||||
updated:updated,
|
||||
pageUpdated:page.updated
|
||||
%}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</xsl:template>
|
||||
</xsl:stylesheet>
|
39
src/styles/index.css
Normal file
39
src/styles/index.css
Normal file
|
@ -0,0 +1,39 @@
|
|||
@layer reset, defaults, base, page, components, plugins;
|
||||
|
||||
/* style resets */
|
||||
@import url("./base/reset.css") layer(reset);
|
||||
|
||||
/* core defaults */
|
||||
@import url("./base/fonts.css") layer(defaults);
|
||||
@import url("./base/vars.css") layer(defaults);
|
||||
|
||||
/* base styles */
|
||||
@import url("./base/index.css") layer(base);
|
||||
|
||||
/* plugins */
|
||||
@import url("./plugins/prism.css") layer(plugins);
|
||||
|
||||
/* page styles */
|
||||
@import url("./pages/about.css") layer(page);
|
||||
@import url("./pages/books.css") layer(page);
|
||||
@import url("./pages/contact.css") layer(page);
|
||||
@import url("./pages/links.css") layer(page);
|
||||
@import url("./pages/media.css") layer(page);
|
||||
@import url("./pages/music.css") layer(page);
|
||||
@import url("./pages/watching.css") layer(page);
|
||||
@import url("./pages/webrings.css") layer(page);
|
||||
|
||||
/* component styles */
|
||||
@import url("./components/addon-links.css") layer(components);
|
||||
@import url("./components/badge-grid.css") layer(components);
|
||||
@import url("./components/banners.css") layer(components);
|
||||
@import url("./components/buttons.css") layer(components);
|
||||
@import url("./components/forms.css") layer(components);
|
||||
@import url("./components/mastodon-post.css") layer(components);
|
||||
@import url("./components/media-grid.css") layer(components);
|
||||
@import url("./components/menu.css") layer(components);
|
||||
@import url("./components/modal.css") layer(components);
|
||||
@import url("./components/music-chart.css") layer(components);
|
||||
@import url("./components/paginator.css") layer(components);
|
||||
@import url("./components/progress-bar.css") layer(components);
|
||||
@import url("./components/youtube-player.css") layer(components);
|
24
src/styles/pages/about.css
Normal file
24
src/styles/pages/about.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
:root {
|
||||
--avatar-size: 16rem;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
--avatar-size: 24rem;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
& img {
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
}
|
||||
|
||||
.about-title {
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
text-align: center;
|
||||
}
|
87
src/styles/pages/books.css
Normal file
87
src/styles/pages/books.css
Normal file
|
@ -0,0 +1,87 @@
|
|||
:is(.book-entry, .book-focus) img {
|
||||
height: auto;
|
||||
aspect-ratio: var(--aspect-ratio-vertical);
|
||||
}
|
||||
|
||||
.book-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
|
||||
&:not(:last-of-type) {
|
||||
padding-bottom: var(--spacing-base);
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-base);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
& img {
|
||||
max-width: calc(var(--sizing-3xl) * 4);
|
||||
}
|
||||
|
||||
& .media-meta {
|
||||
margin-top: var(--sizing-base);
|
||||
align-items: center;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
margin-top: 0;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
& .description p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
& .progress-bar-wrapper {
|
||||
max-width: 75%;
|
||||
margin-bottom: 0;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
margin-top: 0;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.book-focus {
|
||||
& .book-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-base);
|
||||
margin-bottom: var(--spacing-base);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
& img {
|
||||
border: var(--border-default);
|
||||
}
|
||||
|
||||
& .media-meta {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
width: auto;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
& .progress-bar-wrapper {
|
||||
max-width: 50%;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
src/styles/pages/contact.css
Normal file
36
src/styles/pages/contact.css
Normal file
|
@ -0,0 +1,36 @@
|
|||
.contact-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: var(--grid-columns-one);
|
||||
gap: var(--spacing-base);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-template-columns: var(--grid-columns-two);
|
||||
}
|
||||
|
||||
& .hp,
|
||||
& label > span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& textarea {
|
||||
height: calc(var(--sizing-3xl) * 5);
|
||||
}
|
||||
|
||||
& .column.description {
|
||||
& p:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact-success-wrapper {
|
||||
text-align: center;
|
||||
|
||||
& h2 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
15
src/styles/pages/links.css
Normal file
15
src/styles/pages/links.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
.link-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-sm);
|
||||
grid-template-columns: var(--grid-columns-one);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-template-columns: var(--grid-columns-two);
|
||||
}
|
||||
|
||||
& .link-box {
|
||||
border: var(--border-gray);
|
||||
border-radius: var(--border-radius-slight);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
}
|
||||
}
|
42
src/styles/pages/media.css
Normal file
42
src/styles/pages/media.css
Normal file
|
@ -0,0 +1,42 @@
|
|||
.media-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-sm);
|
||||
|
||||
& .title {
|
||||
font-size: var(--font-size-xl);
|
||||
line-height: var(--line-height-md);
|
||||
}
|
||||
|
||||
& .sub-meta {
|
||||
font-size: var(--font-size-sm);
|
||||
|
||||
& svg {
|
||||
width: var(--sizing-svg-sm);
|
||||
height: var(--sizing-svg-sm);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
a:is(:hover, :active, :focus) .media-overlay::after {
|
||||
border: var(--border-default-hover);
|
||||
transition: border-color var(--transition-duration-default)
|
||||
var(--transition-ease-in-out);
|
||||
}
|
||||
|
||||
.media-overlay::after {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: var(--border-default);
|
||||
box-shadow: var(--box-shadow-media);
|
||||
border-radius: var(--border-radius-slight);
|
||||
}
|
||||
|
||||
.associated-media {
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
}
|
37
src/styles/pages/music.css
Normal file
37
src/styles/pages/music.css
Normal file
|
@ -0,0 +1,37 @@
|
|||
.artist-focus {
|
||||
& img {
|
||||
border: var(--border-default);
|
||||
aspect-ratio: var(--aspect-ratio-square);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: calc(var(--sizing-3xl) * 6.75);
|
||||
}
|
||||
}
|
||||
|
||||
& .artist-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
margin-bottom: var(--spacing-base);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
& .media-meta {
|
||||
margin-top: var(--spacing-base);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& table + p {
|
||||
font-size: var(--font-size-sm);
|
||||
margin: var(--spacing-base) 0 0;
|
||||
}
|
||||
}
|
29
src/styles/pages/watching.css
Normal file
29
src/styles/pages/watching.css
Normal file
|
@ -0,0 +1,29 @@
|
|||
.watching.hero {
|
||||
position: relative;
|
||||
|
||||
& img {
|
||||
aspect-ratio: var(--aspect-ratio-banner);
|
||||
}
|
||||
|
||||
& .meta-text {
|
||||
color: var(--color-lightest);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: var(--spacing-sm);
|
||||
bottom: var(--spacing-sm);
|
||||
|
||||
& .header {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
& .subheader {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
& .header,
|
||||
& .subheader {
|
||||
line-height: var(--line-height-md);
|
||||
text-shadow: var(--text-shadow-default);
|
||||
}
|
||||
}
|
||||
}
|
20
src/styles/pages/webrings.css
Normal file
20
src/styles/pages/webrings.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
.webring-wrapper,
|
||||
.webring-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.webring-wrapper {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
|
||||
& p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& .webring-navigation {
|
||||
justify-content: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
110
src/styles/plugins/prism.css
Normal file
110
src/styles/plugins/prism.css
Normal file
|
@ -0,0 +1,110 @@
|
|||
code,
|
||||
pre {
|
||||
color: var(--blue-100);
|
||||
background: none;
|
||||
border-radius: var(--border-radius-slight);
|
||||
font-family: var(--font-mono);
|
||||
text-align: left;
|
||||
white-space: pre;
|
||||
word-spacing: normal;
|
||||
word-break: normal;
|
||||
word-wrap: normal;
|
||||
line-height: var(--line-height-md);
|
||||
tab-size: 2;
|
||||
hyphens: none;
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: var(--spacing-lg);
|
||||
margin: var(--sizing-xl) 0;
|
||||
overflow: auto;
|
||||
|
||||
& > code {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
:not(pre) > code {
|
||||
padding: var(--spacing-xs);
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
pre,
|
||||
:not(pre) > code {
|
||||
background: var(--color-darkest);
|
||||
border: var(--border-gray);
|
||||
}
|
||||
|
||||
.namespace {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.language-css .token.string,
|
||||
.style .token.string {
|
||||
color: #99ccff;
|
||||
}
|
||||
|
||||
.token {
|
||||
color: var(--blue-200);
|
||||
|
||||
&.comment,
|
||||
&.prolog,
|
||||
&.doctype,
|
||||
&.cdata {
|
||||
color: var(--gray-500);
|
||||
}
|
||||
|
||||
&.punctuation {
|
||||
color: var(--gray-300);
|
||||
}
|
||||
|
||||
&.boolean,
|
||||
&.number {
|
||||
color: var(--blue-400);
|
||||
}
|
||||
|
||||
&.selector,
|
||||
&.attr-name,
|
||||
&.string,
|
||||
&.char,
|
||||
&.builtin,
|
||||
&.inserted {
|
||||
color: #6fff6f;
|
||||
}
|
||||
|
||||
&.operator,
|
||||
&.entity,
|
||||
&.url,
|
||||
&.variable {
|
||||
color: #99ccff;
|
||||
}
|
||||
|
||||
&.atrule,
|
||||
&.attr-value,
|
||||
&.function,
|
||||
&.class-name {
|
||||
color: #ff8f66;
|
||||
}
|
||||
|
||||
&.keyword {
|
||||
color: #00ffff;
|
||||
}
|
||||
|
||||
&.regex,
|
||||
&.important {
|
||||
color: #ff7373;
|
||||
}
|
||||
|
||||
&.italic {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.entity {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
&.important,
|
||||
&.bold {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
}
|
3
src/styles/styles.json
Normal file
3
src/styles/styles.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"eleventyExcludeFromCollections": true
|
||||
}
|
24
src/utils/data/activity.js
Normal file
24
src/utils/data/activity.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
|
||||
export async function fetchActivity() {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_all_activity')
|
||||
.select('feed');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching activity data:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
const [{ feed } = {}] = data || [];
|
||||
return feed?.filter((item) => item.feed !== null) || [];
|
||||
} catch (error) {
|
||||
console.error('Unexpected error fetching activity data:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
43
src/utils/data/albumReleases.js
Normal file
43
src/utils/data/albumReleases.js
Normal file
|
@ -0,0 +1,43 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
|
||||
export async function fetchAlbumReleases() {
|
||||
try {
|
||||
const today = DateTime.utc().startOf('day').toSeconds();
|
||||
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_album_releases')
|
||||
.select('*');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching album releases:', error);
|
||||
return { all: [], upcoming: [] };
|
||||
}
|
||||
|
||||
const all = data
|
||||
.map((album) => {
|
||||
const releaseDate = DateTime.fromSeconds(album.release_timestamp)
|
||||
.toUTC()
|
||||
.startOf('day');
|
||||
|
||||
return {
|
||||
...album,
|
||||
description: album.artist.description,
|
||||
date: releaseDate.toLocaleString(DateTime.DATE_FULL),
|
||||
timestamp: releaseDate.toSeconds(),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.timestamp - b.timestamp);
|
||||
|
||||
const upcoming = all.filter((album) => album.release_timestamp > today);
|
||||
|
||||
return { all, upcoming };
|
||||
} catch (error) {
|
||||
console.error('Unexpected error processing album releases:', error);
|
||||
return { all: [], upcoming: [] };
|
||||
}
|
||||
}
|
38
src/utils/data/artists.js
Normal file
38
src/utils/data/artists.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
import { parseCountryField } from '@utils/helpers.js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
const PAGE_SIZE = 1000;
|
||||
|
||||
export async function fetchArtists(){
|
||||
let artists = [];
|
||||
let rangeStart = 0;
|
||||
|
||||
while (true) {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_artists')
|
||||
.select('*')
|
||||
.range(rangeStart, rangeStart + PAGE_SIZE - 1);
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching artists:', error);
|
||||
break;
|
||||
}
|
||||
|
||||
// Process and concatenate artists data
|
||||
artists = artists.concat(
|
||||
data.map((artist) => ({
|
||||
...artist,
|
||||
country: parseCountryField(artist['country']),
|
||||
}))
|
||||
);
|
||||
|
||||
// Break if no more data
|
||||
if (data.length < PAGE_SIZE) break;
|
||||
rangeStart += PAGE_SIZE;
|
||||
}
|
||||
|
||||
return artists;
|
||||
};
|
22
src/utils/data/blogroll.js
Normal file
22
src/utils/data/blogroll.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
|
||||
export async function fetchBlogroll() {
|
||||
const { data, error } = await supabase
|
||||
.from('authors')
|
||||
.select('*')
|
||||
.eq('blogroll', true)
|
||||
.order('name', { ascending: true });
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching authors for the blogroll:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data.sort((a, b) =>
|
||||
a.name.toLowerCase().localeCompare(b.name.toLowerCase())
|
||||
);
|
||||
};
|
50
src/utils/data/books.js
Normal file
50
src/utils/data/books.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
const PAGE_SIZE = 1000;
|
||||
|
||||
export async function fetchBooks() {
|
||||
let books = [];
|
||||
let rangeStart = 0;
|
||||
|
||||
while (true) {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_books')
|
||||
.select('*')
|
||||
.order('date_finished', { ascending: false })
|
||||
.range(rangeStart, rangeStart + PAGE_SIZE - 1);
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching books:', error);
|
||||
break;
|
||||
}
|
||||
|
||||
books = books.concat(data);
|
||||
if (data.length < PAGE_SIZE) break;
|
||||
rangeStart += PAGE_SIZE;
|
||||
}
|
||||
|
||||
const years = {};
|
||||
books.forEach((book) => {
|
||||
const year = book.year;
|
||||
if (!years[year]) {
|
||||
years[year] = { value: year, data: [book] };
|
||||
} else {
|
||||
years[year].data.push(book);
|
||||
}
|
||||
});
|
||||
|
||||
const sortedByYear = Object.values(years).filter((year) => year.value > 2017);
|
||||
const currentYear = new Date().getFullYear();
|
||||
const booksForCurrentYear =
|
||||
sortedByYear.find((yearGroup) => yearGroup.value === currentYear)?.data || [];
|
||||
|
||||
return {
|
||||
all: books,
|
||||
years: sortedByYear,
|
||||
currentYear: booksForCurrentYear,
|
||||
feed: books.filter((book) => book.feed),
|
||||
};
|
||||
};
|
32
src/utils/data/concerts.js
Normal file
32
src/utils/data/concerts.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
const PAGE_SIZE = 1000;
|
||||
|
||||
export async function fetchConcertsData() {
|
||||
let concerts = [];
|
||||
let rangeStart = 0;
|
||||
|
||||
while (true) {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_concerts')
|
||||
.select('*')
|
||||
.range(rangeStart, rangeStart + PAGE_SIZE - 1);
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching concerts:', error);
|
||||
break;
|
||||
}
|
||||
|
||||
concerts = concerts.concat(data);
|
||||
if (data.length < PAGE_SIZE) break;
|
||||
rangeStart += PAGE_SIZE;
|
||||
}
|
||||
|
||||
return concerts.map((concert) => ({
|
||||
...concert,
|
||||
artist: concert.artist || { name: concert.artist_name_string, url: null },
|
||||
}));
|
||||
};
|
18
src/utils/data/genres.js
Normal file
18
src/utils/data/genres.js
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
|
||||
export async function fetchGenres() {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_genres')
|
||||
.select('*');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching genres with artists:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
19
src/utils/data/globals.js
Normal file
19
src/utils/data/globals.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
|
||||
export async function fetchGlobals() {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_globals')
|
||||
.select('*')
|
||||
.single();
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching globals:', error);
|
||||
return {};
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
31
src/utils/data/links.js
Normal file
31
src/utils/data/links.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
const PAGE_SIZE = 1000;
|
||||
|
||||
export async function fetchLinks() {
|
||||
let links = [];
|
||||
let page = 0;
|
||||
let fetchMore = true;
|
||||
|
||||
while (fetchMore) {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_links')
|
||||
.select('*')
|
||||
.range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1);
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching links:', error);
|
||||
return links;
|
||||
}
|
||||
|
||||
if (data.length < PAGE_SIZE) fetchMore = false;
|
||||
|
||||
links = links.concat(data);
|
||||
page++;
|
||||
}
|
||||
|
||||
return links;
|
||||
};
|
42
src/utils/data/movies.js
Normal file
42
src/utils/data/movies.js
Normal file
|
@ -0,0 +1,42 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
const PAGE_SIZE = 1000;
|
||||
|
||||
export async function fetchMovies() {
|
||||
let movies = [];
|
||||
let rangeStart = 0;
|
||||
|
||||
while (true) {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_movies')
|
||||
.select('*')
|
||||
.range(rangeStart, rangeStart + PAGE_SIZE - 1);
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching movies:', error);
|
||||
break;
|
||||
}
|
||||
|
||||
movies = movies.concat(data);
|
||||
if (data.length < PAGE_SIZE) break;
|
||||
rangeStart += PAGE_SIZE;
|
||||
}
|
||||
|
||||
const year = DateTime.now().year;
|
||||
const favoriteMovies = movies.filter(movie => movie.favorite);
|
||||
const recentlyWatchedMovies = movies.filter(
|
||||
movie => movie.last_watched && year - DateTime.fromISO(movie.last_watched).year <= 3
|
||||
);
|
||||
|
||||
return {
|
||||
movies,
|
||||
watchHistory: movies.filter(movie => movie.last_watched),
|
||||
recentlyWatched: recentlyWatchedMovies,
|
||||
favorites: favoriteMovies.sort((a, b) => a.title.localeCompare(b.title)),
|
||||
feed: movies.filter(movie => movie.feed),
|
||||
};
|
||||
};
|
79
src/utils/data/music.js
Normal file
79
src/utils/data/music.js
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
const PAGE_SIZE = 1000;
|
||||
|
||||
export async function fetchMusicData() {
|
||||
const fetchDataFromView = async (viewName) => {
|
||||
let rows = [];
|
||||
let rangeStart = 0;
|
||||
|
||||
while (true) {
|
||||
const { data, error } = await supabase
|
||||
.from(viewName)
|
||||
.select('*')
|
||||
.range(rangeStart, rangeStart + PAGE_SIZE - 1);
|
||||
|
||||
if (error) {
|
||||
console.error(`Error fetching data from view ${viewName}:`, error);
|
||||
break;
|
||||
}
|
||||
|
||||
if (data.length === 0) break;
|
||||
|
||||
rows = [...rows, ...data];
|
||||
|
||||
if (data.length < PAGE_SIZE) break;
|
||||
rangeStart += PAGE_SIZE;
|
||||
}
|
||||
|
||||
return rows;
|
||||
};
|
||||
|
||||
try {
|
||||
const [
|
||||
recentTracks,
|
||||
weekTracks,
|
||||
weekArtists,
|
||||
weekAlbums,
|
||||
weekGenres,
|
||||
monthTracks,
|
||||
monthArtists,
|
||||
monthAlbums,
|
||||
monthGenres,
|
||||
] = await Promise.all([
|
||||
fetchDataFromView('recent_tracks'),
|
||||
fetchDataFromView('week_tracks'),
|
||||
fetchDataFromView('week_artists'),
|
||||
fetchDataFromView('week_albums'),
|
||||
fetchDataFromView('week_genres'),
|
||||
fetchDataFromView('month_tracks'),
|
||||
fetchDataFromView('month_artists'),
|
||||
fetchDataFromView('month_albums'),
|
||||
fetchDataFromView('month_genres'),
|
||||
]);
|
||||
|
||||
return {
|
||||
recent: recentTracks,
|
||||
week: {
|
||||
tracks: weekTracks,
|
||||
artists: weekArtists,
|
||||
albums: weekAlbums,
|
||||
genres: weekGenres,
|
||||
totalTracks: weekTracks.reduce((acc, track) => acc + track.plays, 0).toLocaleString('en-US'),
|
||||
},
|
||||
month: {
|
||||
tracks: monthTracks,
|
||||
artists: monthArtists,
|
||||
albums: monthAlbums,
|
||||
genres: monthGenres,
|
||||
totalTracks: monthTracks.reduce((acc, track) => acc + track.plays, 0).toLocaleString('en-US'),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error fetching and processing music data:', error);
|
||||
return {};
|
||||
}
|
||||
};
|
39
src/utils/data/nav.js
Normal file
39
src/utils/data/nav.js
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
|
||||
export async function fetchNavigation() {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_navigation')
|
||||
.select('*')
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching navigation data:', error)
|
||||
return {}
|
||||
}
|
||||
|
||||
const menu = data.reduce((acc, item) => {
|
||||
const menuItem = {
|
||||
title: item['title'] || item['page_title'],
|
||||
permalink: item['permalink'] || item ['page_permalink'],
|
||||
icon: item['icon'],
|
||||
sort: item['sort']
|
||||
}
|
||||
|
||||
if (!acc[item['menu_location']]) {
|
||||
acc[item['menu_location']] = [menuItem]
|
||||
} else {
|
||||
acc[item['menu_location']].push(menuItem)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
Object.keys(menu).forEach(location => {
|
||||
menu[location].sort((a, b) => a['sort'] - b['sort'])
|
||||
})
|
||||
|
||||
return menu
|
||||
}
|
24
src/utils/data/nowPlaying.js
Normal file
24
src/utils/data/nowPlaying.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
|
||||
export async function fetchNowPlaying() {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_latest_listen')
|
||||
.select('*')
|
||||
.single()
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching the latest track:', error)
|
||||
return {}
|
||||
}
|
||||
|
||||
const genreEmoji = data.genre_emoji
|
||||
const emoji = data.artist_emoji || genreEmoji
|
||||
|
||||
return {
|
||||
content: `${emoji || '🎧'} ${data.track_name} by <a href="https://coryd.dev${data.url}">${data.artist_name}</a>`,
|
||||
}
|
||||
}
|
16
src/utils/data/pages.js
Normal file
16
src/utils/data/pages.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
|
||||
export async function fetchPages() {
|
||||
const { data, error } = await supabase.from('optimized_pages').select('*');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching pages:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
32
src/utils/data/posts.js
Normal file
32
src/utils/data/posts.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const SUPABASE_URL = import.meta.env['SUPABASE_URL']
|
||||
const SUPABASE_KEY = import.meta.env['SUPABASE_KEY']
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
const PAGE_SIZE = 1000
|
||||
|
||||
export async function fetchAllPosts() {
|
||||
let posts = []
|
||||
let page = 0
|
||||
let fetchMore = true
|
||||
|
||||
while (fetchMore) {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_posts')
|
||||
.select('*')
|
||||
.order('date', { ascending: false })
|
||||
.range(page * PAGE_SIZE, (page + 1) * PAGE_SIZE - 1)
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching posts:', error)
|
||||
return posts
|
||||
}
|
||||
|
||||
if (data.length < PAGE_SIZE) fetchMore = false
|
||||
|
||||
posts = posts.concat(data)
|
||||
page++
|
||||
}
|
||||
|
||||
return posts
|
||||
}
|
29
src/utils/data/robots.js
Normal file
29
src/utils/data/robots.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
const PAGE_SIZE = 500;
|
||||
|
||||
export async function fetchAllRobots() {
|
||||
let robots = [];
|
||||
let from = 0;
|
||||
|
||||
while (true) {
|
||||
const { data, error } = await supabase
|
||||
.from('robots')
|
||||
.select('user_agent')
|
||||
.range(from, from + PAGE_SIZE - 1);
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching robot data:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
robots = robots.concat(data);
|
||||
if (data.length < PAGE_SIZE) break;
|
||||
from += PAGE_SIZE;
|
||||
}
|
||||
|
||||
return robots.map((robot) => robot['user_agent']).sort();
|
||||
};
|
19
src/utils/data/search.js
Normal file
19
src/utils/data/search.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { createClient } from '@supabase/supabase-js'
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY)
|
||||
|
||||
export async function fetchSearchIndex() {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_search_index')
|
||||
.select('search_index')
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching search index data:', error)
|
||||
return []
|
||||
}
|
||||
|
||||
const [{ search_index } = {}] = data
|
||||
return search_index || []
|
||||
}
|
20
src/utils/data/syndication.js
Normal file
20
src/utils/data/syndication.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
|
||||
export default async function fetchSyndication() {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_syndication')
|
||||
.select('syndication');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching syndication data:', error);
|
||||
return [];
|
||||
}
|
||||
|
||||
const [{ syndication } = {}] = data;
|
||||
|
||||
return syndication?.filter((item) => item.syndication !== null) || [];
|
||||
}
|
46
src/utils/data/tv.js
Normal file
46
src/utils/data/tv.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { createClient } from '@supabase/supabase-js';
|
||||
|
||||
const SUPABASE_URL = import.meta.env.SUPABASE_URL;
|
||||
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
|
||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
|
||||
const PAGE_SIZE = 1000;
|
||||
|
||||
export const fetchShows = async () => {
|
||||
let shows = [];
|
||||
let rangeStart = 0;
|
||||
|
||||
while (true) {
|
||||
const { data, error } = await supabase
|
||||
.from('optimized_shows')
|
||||
.select('*')
|
||||
.range(rangeStart, rangeStart + PAGE_SIZE - 1);
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching shows:', error);
|
||||
break;
|
||||
}
|
||||
|
||||
shows = shows.concat(data);
|
||||
if (data.length < PAGE_SIZE) break;
|
||||
rangeStart += PAGE_SIZE;
|
||||
}
|
||||
|
||||
const watchedShows = shows.filter(show => show['last_watched_at'] !== null);
|
||||
const episodes = watchedShows.map(show => ({
|
||||
title: show['episode']['title'],
|
||||
year: show['year'],
|
||||
formatted_episode: show['episode']['formatted_episode'],
|
||||
url: show['episode']['url'],
|
||||
image: show['episode']['image'],
|
||||
backdrop: show['episode']['backdrop'],
|
||||
last_watched_at: show['episode']['last_watched_at'],
|
||||
grid: show['grid'],
|
||||
type: 'tv'
|
||||
}));
|
||||
|
||||
return {
|
||||
shows,
|
||||
recentlyWatched: episodes.slice(0, 225),
|
||||
favorites: shows.filter(show => show.favorite).sort((a, b) => a.title.localeCompare(b.title)),
|
||||
};
|
||||
};
|
45
src/utils/helpers.js
Normal file
45
src/utils/helpers.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import markdownIt from "markdown-it";
|
||||
import markdownItAnchor from "markdown-it-anchor";
|
||||
import markdownItFootnote from "markdown-it-footnote";
|
||||
import markdownItPrism from "markdown-it-prism";
|
||||
|
||||
const markdown = markdownIt({ html: true, linkify: true });
|
||||
markdown.use(markdownItAnchor, {
|
||||
level: [1, 2],
|
||||
permalink: markdownItAnchor.permalink.headerLink({
|
||||
safariReaderFix: true,
|
||||
}),
|
||||
});
|
||||
markdown.use(markdownItFootnote);
|
||||
markdown.use(markdownItPrism);
|
||||
|
||||
export const shuffleArray = (array) => {
|
||||
const shuffled = [...array];
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
let j = Math.floor(Math.random() * (i + 1));
|
||||
let temp = shuffled[i];
|
||||
shuffled[i] = shuffled[j];
|
||||
shuffled[j] = temp;
|
||||
}
|
||||
return shuffled;
|
||||
};
|
||||
|
||||
export const regionNames = new Intl.DisplayNames(["en"], { type: "region" });
|
||||
|
||||
export const getCountryName = (countryCode) =>
|
||||
regionNames.of(countryCode.trim()) || countryCode.trim();
|
||||
|
||||
export const parseCountryField = (countryField) => {
|
||||
if (!countryField) return null;
|
||||
|
||||
const delimiters = [",", "/", "&", "and"];
|
||||
let countries = [countryField];
|
||||
|
||||
delimiters.forEach((delimiter) => {
|
||||
countries = countries.flatMap((country) => country.split(delimiter));
|
||||
});
|
||||
|
||||
return countries.map(getCountryName).join(", ");
|
||||
};
|
||||
|
||||
export const md = (string) => markdown.render(string);
|
Reference in a new issue