initial commit

This commit is contained in:
Cory Dransfeldt 2024-11-16 16:43:07 -08:00
commit c70fc72952
No known key found for this signature in database
143 changed files with 13594 additions and 0 deletions

View 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} />
)}

View 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>

View 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>

View 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}

View 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>

View 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>

View 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>

View 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>

View file

@ -0,0 +1,7 @@
---
const { image, alt } = Astro.props;
---
<div class="hero">
<img src={image} alt={alt} />
</div>

View file

@ -0,0 +1,7 @@
---
const { post } = Astro.props;
---
<article class="mastodon-post">
<p>{post.content}</p>
</article>

View file

@ -0,0 +1,8 @@
---
const { content } = Astro.props;
---
<div class="modal">
<button class="close">Close</button>
<div class="content">{content}</div>
</div>

View 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>)}

View file

@ -0,0 +1,10 @@
---
const { url } = Astro.props;
---
<iframe
width="560"
height="315"
src={url}
allowfullscreen>
</iframe>

View 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>

View 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>

View 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>

View 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>

View 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>
)}

View 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>

View 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>

View 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>

View 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>
)}

View 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
View file

@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />

96
src/layouts/Layout.astro Normal file
View 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>

View 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>

View 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, '&amp;');
}
function escapeHTML(str) {
if (!str) return '';
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}

29
src/pages/index.astro Normal file
View 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>

View 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>

View 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
View 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
View 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
View 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
View 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
View 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);
}

View 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;
}
}

View 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;
}
}

View 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);
}
}
}

View 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);
}
}

View 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);
}
}
}

View 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);
}
}
}
}
}

View 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;
}
}
}

View 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;
}
}
}

View 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);
}
}
}

View 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;
}
}
}
}

View 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;
}
}

View 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);
}
}

View 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);
}

View 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;
}
}
}

View 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
View 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
View 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);

View 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;
}

View 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;
}
}
}
}
}

View 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;
}
}

View 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);
}
}

View 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);
}

View 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;
}
}

View 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);
}
}
}

View 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);
}
}

View 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
View file

@ -0,0 +1,3 @@
{
"eleventyExcludeFromCollections": true
}

View 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 [];
}
}

View 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
View 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;
};

View 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
View 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),
};
};

View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
View 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
View 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
View 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
View 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 || []
}

View 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
View 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
View 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);