chore: myriad fixes + artist pages

This commit is contained in:
Cory Dransfeldt 2024-11-17 13:21:45 -08:00
parent aec8471b06
commit ff00020b70
No known key found for this signature in database
53 changed files with 753 additions and 486 deletions

View file

@ -1,36 +1,36 @@
--- ---
import NavLink from '@components/nav/NavLink.astro'; import NavLink from "@components/nav/NavLink.astro";
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
const { updated } = Astro.props; const { updated } = Astro.props;
const { nav } = await fetchGlobalData(Astro); const { nav } = await fetchGlobalData(Astro);
--- ---
<footer style={updated ? undefined : 'margin-top: var(--spacing-3xl)'}> <footer style={updated ? undefined : "margin-top: var(--spacing-3xl)"}>
{updated && ( {
<p class="updated"> updated && (
<em>This page was last updated on {new Date(updated).toLocaleDateString()}</em> <p class="updated">
</p> <em>
)} This page was last updated on {new Date(updated).toLocaleDateString()}
</em>
</p>
)
}
<nav aria-label="Social icons" class="social"> <nav aria-label="Social icons" class="social">
{nav.footer_icons.map(link => ( {
<NavLink nav.footer_icons.map((link) => (
url={link.permalink} <NavLink url={link.permalink} title={link.title} icon={link.icon} />
title={link.title} ))
icon={link.icon} }
/>
))}
</nav> </nav>
<nav aria-label="Secondary site navigation" class="sub-pages"> <nav aria-label="Secondary site navigation" class="sub-pages">
{nav.footer_text.map((link, index) => ( {
<> nav.footer_text.map((link, index) => (
<NavLink <>
url={link.permalink} <NavLink url={link.permalink} title={link.title} icon={link.icon} />
title={link.title} {index < nav.footer_text.length - 1 && <span>/</span>}
icon={link.icon} </>
/> ))
{index < nav.footer_text.length - 1 && <span>/</span>} }
</>
))}
</nav> </nav>
</footer> </footer>

View file

@ -1,19 +1,23 @@
--- ---
import Menu from '@components/nav/Menu.astro'; import Menu from "@components/nav/Menu.astro";
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
const { siteName, url } = Astro.props; const { siteName, url } = Astro.props;
const { nav } = await fetchGlobalData(Astro); const { nav } = await fetchGlobalData(Astro);
const isHomePage = url === '/'; const isHomePage = url === "/";
--- ---
<section class="main-title"> <section class="main-title">
<h1> <h1>
{isHomePage ? ( {
siteName isHomePage ? (
) : ( siteName
<a href="/" tabindex="0">{siteName}</a> ) : (
)} <a href="/" tabindex="0">
{siteName}
</a>
)
}
</h1> </h1>
<Menu nav={nav} /> <Menu nav={nav} />
</section> </section>

View file

@ -15,29 +15,29 @@ import {
IconCoffee, IconCoffee,
IconDeviceWatch, IconDeviceWatch,
IconHeartHandshake, IconHeartHandshake,
} from '@tabler/icons-react'; } from "@tabler/icons-react";
const { icon } = Astro.props; const { icon } = Astro.props;
const iconComponents = { const iconComponents = {
article: IconArticle, article: IconArticle,
headphones: IconHeadphones, headphones: IconHeadphones,
'device-tv-old': IconDeviceTvOld, "device-tv-old": IconDeviceTvOld,
books: IconBooks, books: IconBooks,
link: IconLink, link: IconLink,
'info-circle': IconInfoCircle, "info-circle": IconInfoCircle,
search: IconSearch, search: IconSearch,
rss: IconRss, rss: IconRss,
'brand-mastodon': IconBrandMastodon, "brand-mastodon": IconBrandMastodon,
mail: IconMail, mail: IconMail,
'brand-github': IconBrandGithub, "brand-github": IconBrandGithub,
'brand-npm': IconBrandNpm, "brand-npm": IconBrandNpm,
coffee: IconCoffee, coffee: IconCoffee,
'device-watch': IconDeviceWatch, "device-watch": IconDeviceWatch,
'heart-handshake': IconHeartHandshake, "heart-handshake": IconHeartHandshake,
}; };
const SelectedIcon = iconComponents[icon?.toLowerCase()] || null; const SelectedIcon = iconComponents[icon?.toLowerCase()] || null;
--- ---
{SelectedIcon ? <SelectedIcon size={24} /> : null} {SelectedIcon ? <SelectedIcon size={24} /> : null}

View file

@ -1,6 +1,6 @@
--- ---
import { escapeHtml } from "@utils/helpers/general.js"; import { escapeHtml } from "@utils/helpers/general.js";
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
const { const {
schema = "page", schema = "page",
@ -107,7 +107,8 @@ if (titleOverride) pageTitle = titleOverride;
if (descriptionOverride) pageDescription = descriptionOverride; if (descriptionOverride) pageDescription = descriptionOverride;
if (ogImageOverride) ogImage = ogImageOverride; if (ogImageOverride) ogImage = ogImageOverride;
if (pageTitle !== globals.site_name && schema !== "blog") pageTitle = `${pageTitle} / ${globals.site_name}`; if (pageTitle !== globals.site_name && schema !== "blog")
pageTitle = `${pageTitle} / ${globals.site_name}`;
const escapedPageDescription = escapeHtml(pageDescription); const escapedPageDescription = escapeHtml(pageDescription);
--- ---

View file

@ -1,4 +1,6 @@
--- ---
import IconMapper from "@components/IconMapper.astro";
const { const {
artists = [], artists = [],
books = [], books = [],

View file

@ -3,17 +3,17 @@ import { fetchAllPosts } from "@data/posts.js";
import { fetchAnalyticsData } from "@data/analytics.js"; import { fetchAnalyticsData } from "@data/analytics.js";
import { fetchLinks } from "@data/links.js"; import { fetchLinks } from "@data/links.js";
import AddonLinks from '@components/blocks/links/AddonLinks.astro'; import AddonLinks from "@components/blocks/links/AddonLinks.astro";
import AssociatedMedia from '@components/blocks//AssociatedMedia.astro'; import AssociatedMedia from "@components/blocks//AssociatedMedia.astro";
import GitHub from '@components/blocks/banners/GitHub.astro'; import GitHub from "@components/blocks/banners/GitHub.astro";
import Hero from '@components/blocks//Hero.astro'; import Hero from "@components/blocks//Hero.astro";
import Modal from '@components/blocks//Modal.astro'; import Modal from "@components/blocks//Modal.astro";
import Npm from '@components/blocks/banners/Npm.astro'; import Npm from "@components/blocks/banners/Npm.astro";
import Rss from '@components/blocks/banners/Rss.astro'; import Rss from "@components/blocks/banners/Rss.astro";
import YouTubePlayer from '@components/blocks//YouTubePlayer.astro'; import YouTubePlayer from "@components/blocks//YouTubePlayer.astro";
import { md } from '@utils/helpers/general.js'; import { md } from "@utils/helpers/general.js";
import { getPopularPosts } from '@utils/getPopularPosts.js'; import { getPopularPosts } from "@utils/getPopularPosts.js";
const analytics = await fetchAnalyticsData(); const analytics = await fetchAnalyticsData();
const links = await fetchLinks(); const links = await fetchLinks();
@ -22,42 +22,26 @@ const popularPosts = getPopularPosts(posts, analytics);
const { block } = Astro.props; const { block } = Astro.props;
--- ---
{block.type === 'addon_links' && ( {
<AddonLinks popularPosts={popularPosts} links={links} /> block.type === "addon_links" && (
)} <AddonLinks popularPosts={popularPosts} links={links} />
)
}
{block.type === 'associated_media' && ( {block.type === "associated_media" && <AssociatedMedia media={block.media} />}
<AssociatedMedia media={block.media} />
)}
{block.type === 'divider' && ( {block.type === "divider" && <div set:html={md(block.markup)} />}
<div set:html={md(block.markup)}></div>
)}
{block.type === 'github_banner' && ( {block.type === "github_banner" && <GitHub url={block.url} />}
<GitHub url={block.url} />
)}
{block.type === 'hero' && ( {block.type === "hero" && <Hero image={block.image} alt={block.alt} />}
<Hero image={block.image} alt={block.alt} />
)}
{block.type === 'markdown' && ( {block.type === "markdown" && <div set:html={md(block.text)} />}
<div set:html={md(block.text)}></div>
)}
{block.type === 'npm_banner' && ( {block.type === "npm_banner" && <Npm url={block.url} command={block.command} />}
<Npm url={block.url} command={block.command} />
)}
{block.type === 'modal' && ( {block.type === "modal" && <Modal content={block.content} />}
<Modal content={block.content} />
)}
{block.type === 'rss_banner' && ( {block.type === "rss_banner" && <Rss url={block.url} text={block.text} />}
<Rss url={block.url} text={block.text} />
)}
{block.type === 'youtube_player' && ( {block.type === "youtube_player" && <YouTubePlayer url={block.url} />}
<YouTubePlayer url={block.url} />
)}

View file

@ -1,8 +1,8 @@
--- ---
import { fetchGlobals } from "@utils/data/globals.js"; import { fetchGlobalData } from "@utils/data/global/index.js";
const { image, alt } = Astro.props; const { image, alt } = Astro.props;
const globals = await fetchGlobals(Astro); const { globals } = await fetchGlobalData(Astro);
--- ---
<div class="hero"> <div class="hero">

View file

@ -1,7 +1,7 @@
--- ---
import { fetchNowPlaying } from '@utils/data/nowPlaying.js'; import { fetchNowPlaying } from "@utils/data/nowPlaying.js";
const isProduction = import.meta.env.MODE === 'production'; const isProduction = import.meta.env.MODE === "production";
const nowPlayingData = await fetchNowPlaying(); const nowPlayingData = await fetchNowPlaying();
--- ---
@ -10,12 +10,12 @@ const nowPlayingData = await fetchNowPlaying();
<p>{nowPlayingData.content}</p> <p>{nowPlayingData.content}</p>
</noscript> </noscript>
{isProduction && (<script type="module"> {isProduction && (<script type="module" is:inline>
async function updateNowPlaying() { async function updateNowPlaying() {
const response = await fetch('/api/now-playing'); const response = await fetch("/api/now-playing");
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
document.getElementById('now-playing-content').innerHTML = data.content; document.getElementById("now-playing-content").innerHTML = data.content;
} }
} }

View file

@ -1,7 +1,7 @@
--- ---
import YoutubeVideo from '@npm/youtube-video-element/dist/react.js'; import YoutubeVideo from "@npm/youtube-video-element/dist/react.js";
const { url } = Astro.props; const { url } = Astro.props;
--- ---
<YoutubeVideo src={url} controls /> <YoutubeVideo src={url} controls />

View file

@ -1,8 +1,9 @@
--- ---
import { IconAlertCircle } from '@tabler/icons-react'; import { IconAlertCircle } from "@tabler/icons-react";
const { text } = Astro.props const { text } = Astro.props;
--- ---
<div class="banner error"> <div class="banner error">
<p><IconAlertCircle size={24} />{ text }</p> <p><IconAlertCircle size={24} />{text}</p>
</div> </div>

View file

@ -1,9 +1,13 @@
--- ---
import { IconBrandGithub } from '@tabler/icons-react'; import { IconBrandGithub } from "@tabler/icons-react";
const { url } = Astro.props; const { url } = Astro.props;
--- ---
<div class="banner github"> <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> <p>
</div> <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

@ -1,5 +1,5 @@
--- ---
import { IconBrandNpm } from '@tabler/icons-react'; import { IconBrandNpm } from "@tabler/icons-react";
const { url, command } = Astro.props; const { url, command } = Astro.props;
--- ---
@ -7,6 +7,7 @@ const { url, command } = Astro.props;
<div class="banner npm"> <div class="banner npm">
<p> <p>
<IconBrandNpm size={24} /> <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>. <a href={url}>You can take a look at this package on NPM</a> or install it by
running <code>{command}</code>.
</p> </p>
</div> </div>

View file

@ -1,14 +1,17 @@
--- ---
import { IconClockX } from '@tabler/icons-react'; import { IconClockX } from "@tabler/icons-react";
const { isOldPost } = Astro.props; const { isOldPost } = Astro.props;
--- ---
{isOldPost && ( {
<div class="banner old-post"> isOldPost && (
<p> <div class="banner old-post">
<IconClockX size={24} /> <p>
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. <IconClockX size={24} />
</p> This post is over 3 years old. I've probably changed my mind since it
</div> was written and it <em>could</em> be out of date.
)} </p>
</div>
)
}

View file

@ -1,5 +1,5 @@
--- ---
import { IconRss } from '@tabler/icons-react'; import { IconRss } from "@tabler/icons-react";
const { url, text } = Astro.props; const { url, text } = Astro.props;
--- ---
@ -9,4 +9,4 @@ const { url, text } = Astro.props;
<IconRss size={24} /> <IconRss size={24} />
<a href={url}>{text}</a>. <a href={url}>{text}</a>.
</p> </p>
</div> </div>

View file

@ -1,5 +1,5 @@
--- ---
import { IconAlertTriangle } from '@tabler/icons-react'; import { IconAlertTriangle } from "@tabler/icons-react";
const { text } = Astro.props; const { text } = Astro.props;
--- ---
@ -9,4 +9,4 @@ const { text } = Astro.props;
<IconAlertTriangle size={24} /> <IconAlertTriangle size={24} />
{text} {text}
</p> </p>
</div> </div>

View file

@ -1,21 +1,24 @@
--- ---
const { popularPosts } = Astro.props; const { popularPosts } = Astro.props;
import { IconChartBarPopular } from '@tabler/icons-react'; import { IconChartBarPopular } from "@tabler/icons-react";
--- ---
{popularPosts && popularPosts.length > 0 && (
<article> {
<h3> popularPosts && popularPosts.length > 0 && (
<a class="article" href="/posts"> <article>
<IconChartBarPopular size={24} /> <h3>
Popular posts <a class="article" href="/posts">
</a> <IconChartBarPopular size={24} />
</h3> Popular posts
<ol type="1"> </a>
{popularPosts.slice(0, 5).map((post) => ( </h3>
<li> <ol type="1">
<a href={post.url}>{post.title}</a> {popularPosts.slice(0, 5).map((post) => (
</li> <li>
))} <a href={post.url}>{post.title}</a>
</ol> </li>
</article> ))}
)} </ol>
</article>
)
}

View file

@ -1,26 +1,32 @@
--- ---
const { links } = Astro.props; const { links } = Astro.props;
import { IconLink } from '@tabler/icons-react'; import { IconLink } from "@tabler/icons-react";
--- ---
{links && links.length > 0 && (
<article> {
<h3> links && links.length > 0 && (
<a class="link" href="/links"> <article>
<IconLink size={24} /> <h3>
Recent links <a class="link" href="/links">
</a> <IconLink size={24} />
</h3> Recent links
<ul> </a>
{links.slice(0, 5).map((link) => ( </h3>
<li> <ul>
<a href={link.link} title={link.title}> {links.slice(0, 5).map((link) => (
{link.title} <li>
</a> <a href={link.link} title={link.title}>
{link.author && ( {link.title}
<> via <a href={link.author.url}>{link.author.name}</a></> </a>
)} {link.author && (
</li> <>
))} {" "}
</ul> via <a href={link.author.url}>{link.author.name}</a>
</article> </>
)} )}
</li>
))}
</ul>
</article>
)
}

View file

@ -1,12 +1,12 @@
--- ---
import { fetchMusicData } from '@utils/data/music.js'; import { fetchMusicData } from "@utils/data/music.js";
import { fetchShows } from '@utils/data/tv.js'; import { fetchShows } from "@utils/data/tv.js";
import { fetchMovies } from '@utils/data/movies.js'; import { fetchMovies } from "@utils/data/movies.js";
import { fetchBooks } from '@utils/data/books.js'; import { fetchBooks } from "@utils/data/books.js";
import { fetchLinks } from '@utils/data/links.js'; import { fetchLinks } from "@utils/data/links.js";
import { IconActivity } from '@tabler/icons-react'; import { IconActivity } from "@tabler/icons-react";
import Rss from '@components/blocks/banners/Rss.astro'; import Rss from "@components/blocks/banners/Rss.astro";
const music = await fetchMusicData(); const music = await fetchMusicData();
const tv = await fetchShows(); const tv = await fetchShows();
@ -27,17 +27,42 @@ const link = links[0];
Recent activity Recent activity
</h2> </h2>
<ul> <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>
<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> <span class="music">Top track this week:</span>
<li><span class="movies">Last movie watched:</span> <a href={movie.url}>{movie.title}</a>{movie.rating ? ` (${movie.rating})` : ''}</li> <a href={track.artist_url}>{track.track_name} by {track.artist_name}</a>
<li><span class="books">Last book finished:</span> <a href={book.url}>{book.title}</a> by {book.author}{book.rating ? ` (${book.rating})` : ''}</li> </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> <li>
<span class="link">Last link shared:</span> <span class="link">Last link shared:</span>
<a href={link.link}>{link.title}</a> <a href={link.link}>{link.title}</a>
{link.author && ( {
<span> via <a href={link.author.url}>{link.author.name}</a></span> link.author && (
)} <span>
{" "}
via <a href={link.author.url}>{link.author.name}</a>
</span>
)
}
</li> </li>
</ul> </ul>
<Rss url="/feeds" text="Subscribe to my movies, books, links or activity feed(s)" /> <Rss
</article> url="/feeds"
text="Subscribe to my movies, books, links or activity feed(s)"
/>
</article>

View file

@ -1,7 +1,7 @@
--- ---
import { IconClock, IconStar, IconArrowRight } from '@tabler/icons-react'; import { IconClock, IconStar, IconArrowRight } from "@tabler/icons-react";
import { fetchAllPosts } from '@utils/data/posts.js'; import { fetchAllPosts } from "@utils/data/posts.js";
import { md } from '@utils/helpers/general.js'; import { md } from "@utils/helpers/general.js";
const posts = await fetchAllPosts(); const posts = await fetchAllPosts();
--- ---
@ -10,24 +10,26 @@ const posts = await fetchAllPosts();
<IconClock size={24} /> <IconClock size={24} />
Recent posts Recent posts
</h2> </h2>
{posts.slice(0, 5).map(post => ( {
<article key={post.url}> posts.slice(0, 5).map((post) => (
<div class="post-meta"> <article key={post.url}>
{post.featured && <IconStar size={16} />} <div class="post-meta">
<time datetime={post.date}> {post.featured && <IconStar size={16} />}
{new Date(post.date).toLocaleDateString('en-US', { <time datetime={post.date}>
year: 'numeric', {new Date(post.date).toLocaleDateString("en-US", {
month: 'long', year: "numeric",
day: 'numeric', month: "long",
})} day: "numeric",
</time> })}
</div> </time>
<h3> </div>
<a href={post.url}>{post.title}</a> <h3>
</h3> <a href={post.url}>{post.title}</a>
<p set:html={md(post.description)}></p> </h3>
</article> <p set:html={md(post.description)} />
))} </article>
))
}
<a class="icon-link" href="/posts"> <a class="icon-link" href="/posts">
View all posts <IconArrowRight size={16} /> View all posts <IconArrowRight size={16} />
</a> </a>

View file

@ -1,6 +1,6 @@
--- ---
import Paginator from '@components/nav/Paginator.astro'; import Paginator from "@components/nav/Paginator.astro";
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
const { data, count, shape, pagination, loading = "lazy" } = Astro.props; const { data, count, shape, pagination, loading = "lazy" } = Astro.props;
const { globals } = await fetchGlobalData(Astro); const { globals } = await fetchGlobalData(Astro);
@ -9,24 +9,24 @@ const hidePagination = pageCount <= 1;
function getImageAttributes(item, shape) { function getImageAttributes(item, shape) {
let imageUrl = item.grid.image; let imageUrl = item.grid.image;
let imageClass = ''; let imageClass = "";
let width = 0; let width = 0;
let height = 0; let height = 0;
switch (shape) { switch (shape) {
case 'poster': case "poster":
imageUrl = item.grid.backdrop; imageUrl = item.grid.backdrop;
imageClass = 'banner'; imageClass = "banner";
width = 256; width = 256;
height = 170; height = 170;
break; break;
case 'square': case "square":
imageClass = 'square'; imageClass = "square";
width = 200; width = 200;
height = 200; height = 200;
break; break;
case 'vertical': case "vertical":
imageClass = 'vertical'; imageClass = "vertical";
width = 200; width = 200;
height = 307; height = 307;
break; break;
@ -37,36 +37,39 @@ function getImageAttributes(item, shape) {
--- ---
<div class={`media-grid ${shape}`}> <div class={`media-grid ${shape}`}>
{data.slice(0, count).map((item) => { {
const alt = item.grid.alt?.replace(/['"]/g, ''); data.slice(0, count).map((item) => {
const { imageUrl, imageClass, width, height } = getImageAttributes(item, shape); const alt = item.grid.alt?.replace(/['"]/g, "");
const { imageUrl, imageClass, width, height } = getImageAttributes(
item,
shape
);
return ( return (
<a href={item.grid.url} title={alt}> <a href={item.grid.url} title={alt}>
<div class="item media-overlay"> <div class="item media-overlay">
<div class="meta-text"> <div class="meta-text">
<div class="header">{item.grid.title}</div> <div class="header">{item.grid.title}</div>
<div class="subheader">{item.grid.subtext}</div> <div class="subheader">{item.grid.subtext}</div>
</div> </div>
<img <img
srcSet={` srcSet={`
${globals.cdn_url}${imageUrl}?class=${imageClass}sm&type=webp ${width}w, ${globals.cdn_url}${imageUrl}?class=${imageClass}sm&type=webp ${width}w,
${globals.cdn_url}${imageUrl}?class=${imageClass}md&type=webp ${width * 2}w ${globals.cdn_url}${imageUrl}?class=${imageClass}md&type=webp ${width * 2}w
`} `}
sizes={`(max-width: 450px) ${width}px, ${width * 2}px`} sizes={`(max-width: 450px) ${width}px, ${width * 2}px`}
src={`${globals.cdn_url}${imageUrl}?class=${imageClass}sm&type=webp`} src={`${globals.cdn_url}${imageUrl}?class=${imageClass}sm&type=webp`}
alt={alt} alt={alt}
loading={loading} loading={loading}
decoding="async" decoding="async"
width={width} width={width}
height={height} height={height}
/> />
</div> </div>
</a> </a>
); );
})} })
}
</div> </div>
{!hidePagination && ( {!hidePagination && <Paginator pagination={pagination} />}
<Paginator pagination={pagination} />
)}

View file

@ -1,29 +1,33 @@
--- ---
import ProgressBar from '@components/media/ProgressBar.astro'; import ProgressBar from "@components/media/ProgressBar.astro";
const { data, count } = Astro.props; const { data, count } = Astro.props;
--- ---
<div class="music-chart"> <div class="music-chart">
<ol type="1"> <ol type="1">
{data.slice(0, count).map((item) => { {
const percentage = `${item.chart.percentage}%`; data.slice(0, count).map((item) => {
const playsLabel = item.chart.plays === 1 ? 'play' : 'plays'; const percentage = `${item.chart.percentage}%`;
const playsLabel = item.chart.plays === 1 ? "play" : "plays";
return ( return (
<li value={item.chart.rank}> <li value={item.chart.rank}>
<div class="item"> <div class="item">
<div class="info"> <div class="info">
<a class="title" href={item.chart.url}>{item.chart.title}</a> <a class="title" href={item.chart.url}>
<span class="subtext">{item.chart.artist}</span> {item.chart.title}
<span class="subtext"> </a>
{item.chart.plays} {playsLabel} <span class="subtext">{item.chart.artist}</span>
</span> <span class="subtext">
{item.chart.plays} {playsLabel}
</span>
</div>
<ProgressBar percentage={percentage} />
</div> </div>
<ProgressBar percentage={percentage} /> </li>
</div> );
</li> })
); }
})}
</ol> </ol>
</div> </div>

View file

@ -1,44 +1,48 @@
--- ---
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
const { data } = Astro.props; const { data } = Astro.props;
const { globals } = await fetchGlobalData(Astro); const { globals } = await fetchGlobalData(Astro);
--- ---
<div class="music-chart"> <div class="music-chart">
{data.slice(0, 10).map((item) => ( {
<div class="item"> data.slice(0, 10).map((item) => (
<div class="meta"> <div class="item">
<a href={item.chart.url}> <div class="meta">
<img <a href={item.chart.url}>
srcSet={` <img
srcSet={`
${globals.cdn_url}${item.chart.image}?class=w50&type=webp 50w, ${globals.cdn_url}${item.chart.image}?class=w50&type=webp 50w,
${globals.cdn_url}${item.chart.image}?class=w100&type=webp 100w ${globals.cdn_url}${item.chart.image}?class=w100&type=webp 100w
`} `}
sizes="(max-width: 450px) 50px, 100px" sizes="(max-width: 450px) 50px, 100px"
src={`${globals.cdn_url}${item.chart.image}?class=w50&type=webp`} src={`${globals.cdn_url}${item.chart.image}?class=w50&type=webp`}
alt={item.chart.alt.replace(/['"]/g, '')} alt={item.chart.alt.replace(/['"]/g, "")}
loading="lazy" loading="lazy"
decoding="async" decoding="async"
width="64" width="64"
height="64" height="64"
/> />
</a> </a>
<div class="meta-text"> <div class="meta-text">
<a class="title" href={item.chart.url}>{item.chart.title}</a> <a class="title" href={item.chart.url}>
<span class="subtext">{item.chart.subtext}</span> {item.chart.title}
</a>
<span class="subtext">{item.chart.subtext}</span>
</div>
</div> </div>
<time dateTime={item.chart.played_at}>
{new Date(item.chart.played_at).toLocaleString("en-US", {
timeZone: "America/Los_Angeles",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
hour12: true,
})}
</time>
</div> </div>
<time dateTime={item.chart.played_at}> ))
{new Date(item.chart.played_at).toLocaleString("en-US", { }
timeZone: "America/Los_Angeles",
month: "long",
day: "numeric",
hour: "numeric",
minute: "numeric",
hour12: true,
})}
</time>
</div>
))}
</div> </div>

View file

@ -1,5 +1,5 @@
--- ---
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
import Hero from "@components/blocks/Hero.astro"; import Hero from "@components/blocks/Hero.astro";
const { movie } = Astro.props; const { movie } = Astro.props;

View file

@ -1,6 +1,6 @@
--- ---
import { IconMenu2, IconX } from '@tabler/icons-react'; import { IconMenu2, IconX } from "@tabler/icons-react";
import NavLink from './NavLink.astro'; import NavLink from "./NavLink.astro";
const { nav } = Astro.props; const { nav } = Astro.props;
--- ---
@ -16,15 +16,17 @@ const { nav } = Astro.props;
</div> </div>
</label> </label>
<ul class="menu-primary" aria-label="Primary site navigation" id="primary-navigation"> <ul
{nav.primary.map((link) => ( class="menu-primary"
<li> aria-label="Primary site navigation"
<NavLink id="primary-navigation"
url={link.permalink} >
title={link.title} {
icon={link.icon} nav.primary.map((link) => (
/> <li>
</li> <NavLink url={link.permalink} title={link.title} icon={link.icon} />
))} </li>
))
}
</ul> </ul>
</menu> </menu>

View file

@ -1,24 +1,26 @@
--- ---
import IconMapper from '@components/IconMapper.astro'; import IconMapper from "@components/IconMapper.astro";
const { url, title, icon } = Astro.props; const { url, title, icon } = Astro.props;
const isHttp = url.startsWith('http'); const isHttp = url.startsWith("http");
const isActive = Astro.url.pathname === url; const isActive = Astro.url.pathname === url;
--- ---
{isActive ? ( {
<span class={`active icon ${icon?.toLowerCase()}`} aria-current="page"> isActive ? (
<IconMapper icon={icon} /> <span class={`active icon ${icon?.toLowerCase()}`} aria-current="page">
<span>{title}</span> <IconMapper icon={icon} />
</span> <span>{title}</span>
) : ( </span>
<a ) : (
class={`icon ${icon}`} <a
href={url} class={`icon ${icon}`}
rel={isHttp ? 'me' : undefined} href={url}
aria-label={title} rel={isHttp ? "me" : undefined}
> aria-label={title}
<IconMapper icon={icon} /> >
<span>{title}</span> <IconMapper icon={icon} />
</a> <span>{title}</span>
)} </a>
)
}

View file

@ -0,0 +1,10 @@
---
import { md } from "@utils/helpers/general.js";
const { content } = Astro.props;
---
<div data-toggle-content class="text-toggle-hidden">
<div set:html={md(content)} />
</div>
<button data-toggle-button>Show more</button>

View file

@ -3,7 +3,7 @@ import "@styles/index.css";
import Header from "@components/Header.astro"; import Header from "@components/Header.astro";
import Footer from "@components/Footer.astro"; import Footer from "@components/Footer.astro";
import Metadata from "@components/Metadata.astro"; import Metadata from "@components/Metadata.astro";
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
const { const {
schema = "page", schema = "page",
@ -61,7 +61,11 @@ const isProduction = import.meta.env.MODE === "production";
) )
} }
<noscript> <noscript>
<style>.client-side {display:none}</style> <style>
.client-side {
display: none;
}
</style>
</noscript> </noscript>
</head> </head>
<body> <body>
@ -74,5 +78,6 @@ const isProduction = import.meta.env.MODE === "production";
</main> </main>
<Footer updated={updated} /> <Footer updated={updated} />
</div> </div>
</body> </body>
</html> </html>

View file

@ -1,4 +1,4 @@
import { fetchGlobals } from "@utils/data/globals"; import { fetchGlobals } from "@utils/data/globals.js";
export async function GET() { export async function GET() {
try { try {

View file

@ -1,8 +1,8 @@
--- ---
import Layout from '@layouts/Layout.astro'; import Layout from "@layouts/Layout.astro";
import BlockRenderer from '@components/blocks/BlockRenderer.astro'; import BlockRenderer from "@components/blocks/BlockRenderer.astro";
import { fetchPages } from '@utils/data/pages'; import { fetchPages } from "@utils/data/pages.js";
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
export const prerender = true; export const prerender = true;
@ -26,7 +26,5 @@ const currentUrl = Astro.url.pathname;
updated={page.updated} updated={page.updated}
currentUrl={currentUrl} currentUrl={currentUrl}
> >
{page.blocks.map((block) => ( {page.blocks.map((block) => <BlockRenderer block={block} />)}
<BlockRenderer block={block} />
))}
</Layout> </Layout>

View file

@ -1,21 +1,28 @@
--- ---
import Layout from '@layouts/Layout.astro'; import Layout from "@layouts/Layout.astro";
import { IconRss, IconJson, IconMailPlus, IconBrandMastodon } from "@tabler/icons-react"; import {
import { fetchBlogroll } from '@utils/data/blogroll.js'; IconRss,
IconJson,
IconMailPlus,
IconBrandMastodon,
} from "@tabler/icons-react";
import { fetchBlogroll } from "@utils/data/blogroll.js";
const blogroll = await fetchBlogroll(); const blogroll = await fetchBlogroll();
const currentUrl = Astro.url.pathname; const currentUrl = Astro.url.pathname;
const title = "Blogroll"; const title = "Blogroll";
const description = "These are awesome blogs that I enjoy and you may enjoy too."; const description =
"These are awesome blogs that I enjoy and you may enjoy too.";
--- ---
<Layout
pageTitle={title} <Layout pageTitle={title} description={description} currentUrl={currentUrl}>
description={description}
currentUrl={currentUrl}
>
<h2 class="page-title">{title}</h2> <h2 class="page-title">{title}</h2>
<p> <p>
You can <a href="/blogroll.opml" class="plausible-event-name=Blogroll+OPML+download">download an OPML file</a> containing all of these feeds and import them into your RSS reader. You can <a
href="/blogroll.opml"
class="plausible-event-name=Blogroll+OPML+download"
>download an OPML file</a
> containing all of these feeds and import them into your RSS reader.
</p> </p>
<table> <table>
<thead> <thead>
@ -26,39 +33,60 @@ const description = "These are awesome blogs that I enjoy and you may enjoy too.
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{blogroll.map((blog) => ( {
<tr> blogroll.map((blog) => (
<td>{blog.name}</td> <tr>
<td> <td>{blog.name}</td>
<a href={blog.url}>{blog.url.replace("https://", "")}</a> <td>
</td> <a href={blog.url}>{blog.url.replace("https://", "")}</a>
<td class="blog-roll-icons"> </td>
{blog.rss_feed && ( <td class="blog-roll-icons">
<a class="rss" href={blog.rss_feed} aria-label={`RSS feed for ${blog.name}`}> {blog.rss_feed && (
<IconRss size={16} /> <a
</a> class="rss"
)} href={blog.rss_feed}
{blog.json_feed && ( aria-label={`RSS feed for ${blog.name}`}
<a class="json" href={blog.json_feed} aria-label={`JSON feed for ${blog.name}`}> >
<IconJson size={16} /> <IconRss size={16} />
</a> </a>
)} )}
{blog.newsletter && ( {blog.json_feed && (
<a class="mail-plus" href={blog.newsletter} aria-label={`Subscribe to ${blog.name}'s newsletter`}> <a
<IconMailPlus size={16} /> class="json"
</a> href={blog.json_feed}
)} aria-label={`JSON feed for ${blog.name}`}
{blog.mastodon && ( >
<a class="brand-mastodon" href={blog.mastodon} aria-label={`Follow ${blog.name} on Mastodon`}> <IconJson size={16} />
<IconBrandMastodon size={16} /> </a>
</a> )}
)} {blog.newsletter && (
</td> <a
</tr> class="mail-plus"
))} href={blog.newsletter}
aria-label={`Subscribe to ${blog.name}'s newsletter`}
>
<IconMailPlus size={16} />
</a>
)}
{blog.mastodon && (
<a
class="brand-mastodon"
href={blog.mastodon}
aria-label={`Follow ${blog.name} on Mastodon`}
>
<IconBrandMastodon size={16} />
</a>
)}
</td>
</tr>
))
}
</tbody> </tbody>
</table> </table>
<p> <p>
Head on over to <a href="https://blogroll.org">blogroll.org</a> to find more blogs to follow or search for feeds using <a href="https://feedle.world">feedle</a>. Head on over to <a href="https://blogroll.org">blogroll.org</a> to find more
blogs to follow or search for feeds using <a href="https://feedle.world"
>feedle</a
>.
</p> </p>
</Layout> </Layout>

View file

@ -1,5 +1,5 @@
import { fetchBlogroll } from "@utils/data/blogroll"; import { fetchBlogroll } from "@utils/data/blogroll.js";
import { fetchGlobals } from "@utils/data/globals"; import { fetchGlobals } from "@utils/data/global/index.js";
export async function GET() { export async function GET() {
try { try {

View file

@ -5,7 +5,7 @@ import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
import ProgressBar from "@components/media/ProgressBar.astro"; import ProgressBar from "@components/media/ProgressBar.astro";
import { IconArrowLeft, IconHeart, IconNeedle } from "@tabler/icons-react"; import { IconArrowLeft, IconHeart, IconNeedle } from "@tabler/icons-react";
import { fetchBookByUrl } from "@utils/data/dynamic/bookByUrl.js"; import { fetchBookByUrl } from "@utils/data/dynamic/bookByUrl.js";
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
const { isbn } = Astro.params; const { isbn } = Astro.params;

View file

@ -3,7 +3,7 @@ import Layout from "@layouts/Layout.astro";
import Rss from "@components/blocks/banners/Rss.astro"; import Rss from "@components/blocks/banners/Rss.astro";
import ProgressBar from "@components/media/ProgressBar.astro"; import ProgressBar from "@components/media/ProgressBar.astro";
import { fetchBooks } from "@utils/data/books.js"; import { fetchBooks } from "@utils/data/books.js";
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
import { md, htmlTruncate } from "@utils/helpers/general.js"; import { md, htmlTruncate } from "@utils/helpers/general.js";
const books = await fetchBooks(); const books = await fetchBooks();
@ -30,7 +30,6 @@ const bookYearLinks = (years) =>
--- ---
<Layout <Layout
globals={globals}
pageTitle={title} pageTitle={title}
description={description} description={description}
updated={updated} updated={updated}
@ -43,7 +42,7 @@ const bookYearLinks = (years) =>
>{currentBookCount} books</strong >{currentBookCount} books</strong
> this year. > this year.
</p> </p>
<p set:html={bookYearLinks(books.years)}></p> <p set:html={bookYearLinks(books.years)} />
<Rss <Rss
url="/feeds/books" url="/feeds/books"
text="Subscribe to my books feed or follow along on this page" text="Subscribe to my books feed or follow along on this page"

View file

@ -2,8 +2,12 @@
import Layout from "@layouts/Layout.astro"; import Layout from "@layouts/Layout.astro";
import Grid from "@components/media/Grid.astro"; import Grid from "@components/media/Grid.astro";
import { IconArrowLeft } from "@tabler/icons-react"; import { IconArrowLeft } from "@tabler/icons-react";
import { filterBooksByStatus, findFavoriteBooks, mediaLinks } from "@utils/helpers/media.js"; import {
import { fetchGlobalData } from '@utils/data/global/index.js'; filterBooksByStatus,
findFavoriteBooks,
mediaLinks,
} from "@utils/helpers/media.js";
import { fetchGlobalData } from "@utils/data/global/index.js";
import { fetchBooks } from "@utils/data/books.js"; import { fetchBooks } from "@utils/data/books.js";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
@ -26,21 +30,30 @@ const description = isCurrentYear
const intro = isCurrentYear const intro = isCurrentYear
? ` ? `
I've finished <strong class="highlight-text">${bookData.length} books</strong> this year. I've finished <strong class="highlight-text">${bookData.length} books</strong> this year.
${favoriteBooks ? ` Among my favorites are ${favoriteBooks}.` : ''} ${favoriteBooks ? ` Among my favorites are ${favoriteBooks}.` : ""}
` `
: ` : `
I finished <strong class="highlight-text">${bookData.length} books</strong> in I finished <strong class="highlight-text">${bookData.length} books</strong> in
<strong class="highlight-text">${year}</strong>. <strong class="highlight-text">${year}</strong>.
${favoriteBooks ? ` Among my favorites were ${favoriteBooks}.` : ''} ${favoriteBooks ? ` Among my favorites were ${favoriteBooks}.` : ""}
`; `;
--- ---
<Layout globals={globals} pageTitle={pageTitle} description={description} schema="books-year"> <Layout
pageTitle={pageTitle}
description={description}
schema="books-year"
>
<a href="/books" class="back-link"> <a href="/books" class="back-link">
<IconArrowLeft size={18} /> Back to books <IconArrowLeft size={18} /> Back to books
</a> </a>
<h2 class="page-title">{year} / Books</h2> <h2 class="page-title">{year} / Books</h2>
<div set:html={intro}></div> <div set:html={intro} />
<hr /> <hr />
<Grid globals={globals} data={bookData} shape="vertical" count={200} loading="eager" /> <Grid
data={bookData}
shape="vertical"
count={200}
loading="eager"
/>
</Layout> </Layout>

View file

@ -1,8 +1,8 @@
import { generateJsonFeed } from '@utils/generateJsonFeed'; import { generateJsonFeed } from "@utils/generateJsonFeed.js";
import { fetchGlobals } from '@utils/data/globals'; import { fetchGlobals } from "@utils/data/globals.js";
import { fetchActivity } from '@utils/data/activity'; import { fetchActivity } from "@utils/data/activity.js";
import fs from 'fs/promises'; import fs from "fs/promises";
import path from 'path'; import path from "path";
export async function getStaticPaths() { export async function getStaticPaths() {
const globals = await fetchGlobals(); const globals = await fetchGlobals();
@ -15,7 +15,7 @@ export async function getStaticPaths() {
data: activity, data: activity,
}); });
const filePath = path.resolve('public/feeds/all.json'); const filePath = path.resolve("public/feeds/all.json");
await fs.mkdir(path.dirname(filePath), { recursive: true }); await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, feed); await fs.writeFile(filePath, feed);

View file

@ -1,6 +1,6 @@
import { generateRssFeed } from "@utils/generateRssFeed"; import { generateRssFeed } from "@utils/generateRssFeed";
import { fetchGlobals } from "@utils/data/globals"; import { fetchGlobals } from "@utils/data/globals.js";
import { fetchActivity } from "@utils/data/activity"; import { fetchActivity } from "@utils/data/activity.js";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";

View file

@ -1,6 +1,6 @@
import { generateJsonFeed } from "@utils/generateJsonFeed"; import { generateJsonFeed } from "@utils/generateJsonFeed.js";
import { fetchGlobals } from "@utils/data/globals"; import { fetchGlobals } from "@utils/data/globals.js";
import { fetchBooks } from "@utils/data/books"; import { fetchBooks } from "@utils/data/books.js";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";

View file

@ -1,6 +1,6 @@
import { generateRssFeed } from "@utils/generateRssFeed"; import { generateRssFeed } from "@utils/generateRssFeed";
import { fetchGlobals } from "@utils/data/globals"; import { fetchGlobals } from "@utils/data/globals.js";
import { fetchBooks } from "@utils/data/books"; import { fetchBooks } from "@utils/data/books.js";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";

View file

@ -1,6 +1,6 @@
import { generateJsonFeed } from "@utils/generateJsonFeed"; import { generateJsonFeed } from "@utils/generateJsonFeed.js";
import { fetchGlobals } from "@utils/data/globals"; import { fetchGlobals } from "@utils/data/globals.js";
import { fetchLinks } from "@utils/data/links"; import { fetchLinks } from "@utils/data/links.js";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";

View file

@ -1,5 +1,5 @@
import { generateRssFeed } from "@utils/generateRssFeed"; import { generateRssFeed } from "@utils/generateRssFeed";
import { fetchGlobals } from "@utils/data/globals"; import { fetchGlobals } from "@utils/data/globals.js";
import { fetchLinks } from "@utils/data/links"; import { fetchLinks } from "@utils/data/links";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";

View file

@ -1,5 +1,5 @@
import { generateJsonFeed } from '@utils/generateJsonFeed'; import { generateJsonFeed } from '@utils/generateJsonFeed.js';
import { fetchGlobals } from '@utils/data/globals'; import { fetchGlobals } from '@utils/data/globals.js';
import { fetchMovies } from '@utils/data/movies'; import { fetchMovies } from '@utils/data/movies';
import fs from 'fs/promises'; import fs from 'fs/promises';
import path from 'path'; import path from 'path';

View file

@ -1,6 +1,6 @@
import { generateRssFeed } from "@utils/generateRssFeed"; import { generateRssFeed } from "@utils/generateRssFeed";
import { fetchGlobals } from "@utils/data/globals"; import { fetchGlobals } from "@utils/data/globals.js";
import { fetchMovies } from "@utils/data/movies"; import { fetchMovies } from "@utils/data/movies.js";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";

View file

@ -1,6 +1,6 @@
import { generateJsonFeed } from "@utils/generateJsonFeed"; import { generateJsonFeed } from "@utils/generateJsonFeed.js";
import { fetchGlobals } from "@utils/data/globals"; import { fetchGlobals } from "@utils/data/globals.js";
import { fetchAllPosts } from "@utils/data/posts"; import { fetchAllPosts } from "@utils/data/posts.js";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";

View file

@ -1,6 +1,6 @@
import { generateRssFeed } from "@utils/generateRssFeed"; import { generateRssFeed } from "@utils/generateRssFeed";
import { fetchGlobals } from "@utils/data/globals"; import { fetchGlobals } from "@utils/data/globals.js";
import { fetchAllPosts } from "@utils/data/posts"; import { fetchAllPosts } from "@utils/data/posts.js";
import fs from "fs/promises"; import fs from "fs/promises";
import path from "path"; import path from "path";

View file

@ -1,4 +1,4 @@
import { fetchGlobals } from '@utils/data/globals'; import { fetchGlobals } from '@utils/data/globals.js';
export async function GET() { export async function GET() {
try { try {

View file

@ -1,18 +1,19 @@
--- ---
import Layout from '@layouts/Layout.astro'; import Layout from "@layouts/Layout.astro";
import Intro from '@components/home/Intro.astro'; import Intro from "@components/home/Intro.astro";
import RecentActivity from '@components/home/RecentActivity.astro'; import RecentActivity from "@components/home/RecentActivity.astro";
import RecentPosts from '@components/home/RecentPosts.astro'; import RecentPosts from "@components/home/RecentPosts.astro";
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
const { globals } = await fetchGlobalData(Astro); const { globals } = await fetchGlobalData(Astro);
const schema = 'blog'; const schema = "blog";
const pageTitle = globals.site_name; const pageTitle = globals.site_name;
const description = 'This is a blog post description'; const description = "This is a blog post description";
const ogImage = globals.cdn_url + globals.avatar; const ogImage = globals.cdn_url + globals.avatar;
const fullUrl = globals.url + '/blog/my-post'; const fullUrl = globals.url + "/blog/my-post";
const themeColor = globals.theme_color; const themeColor = globals.theme_color;
--- ---
<Layout <Layout
pageTitle={pageTitle} pageTitle={pageTitle}
description={description} description={description}
@ -24,4 +25,4 @@ const themeColor = globals.theme_color;
<Intro intro={globals.intro} /> <Intro intro={globals.intro} />
<RecentActivity /> <RecentActivity />
<RecentPosts /> <RecentPosts />
</Layout> </Layout>

View file

@ -3,16 +3,20 @@ import Layout from "@layouts/Layout.astro";
import Paginator from "@components/nav/Paginator.astro"; import Paginator from "@components/nav/Paginator.astro";
import RssBanner from "@components/blocks/banners/Rss.astro"; import RssBanner from "@components/blocks/banners/Rss.astro";
import { fetchLinks } from "@utils/data/links.js"; import { fetchLinks } from "@utils/data/links.js";
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
const { globals } = await fetchGlobalData(Astro); const { globals } = await fetchGlobalData(Astro);
const links = await fetchLinks(); const links = await fetchLinks();
const title = "Links"; const title = "Links";
const description = "These are links I've liked or otherwise found interesting. They're all added manually, after having been read and, I suppose, properly considered."; const description =
"These are links I've liked or otherwise found interesting. They're all added manually, after having been read and, I suppose, properly considered.";
const pageSize = 30; const pageSize = 30;
const currentPage = parseInt(Astro.url.searchParams.get("page") || "1", 10); const currentPage = parseInt(Astro.url.searchParams.get("page") || "1", 10);
const totalPages = Math.ceil(links.length / pageSize); const totalPages = Math.ceil(links.length / pageSize);
const paginatedLinks = links.slice((currentPage - 1) * pageSize, currentPage * pageSize); const paginatedLinks = links.slice(
(currentPage - 1) * pageSize,
currentPage * pageSize
);
const pagination = { const pagination = {
currentPage, currentPage,
@ -29,37 +33,40 @@ const pagination = {
--- ---
<Layout <Layout
globals={globals}
pageTitle={title} pageTitle={title}
description={description} description={description}
currentUrl={Astro.url.pathname} currentUrl={Astro.url.pathname}
> >
{currentPage === 1 && ( {
<> currentPage === 1 && (
<h2 class="page-title">{title}</h2> <>
<p>{description}</p> <h2 class="page-title">{title}</h2>
<RssBanner <p>{description}</p>
url="/feeds/links" <RssBanner
text="Subscribe to my links feed or follow along on this page" url="/feeds/links"
/> text="Subscribe to my links feed or follow along on this page"
<hr /> />
</> <hr />
)} </>
)
}
<div class="link-grid"> <div class="link-grid">
{paginatedLinks.map((link) => ( {
<div class="link-box"> paginatedLinks.map((link) => (
<a href={link.link} title={link.title}> <div class="link-box">
<strong>{link.title}</strong> <a href={link.link} title={link.title}>
</a> <strong>{link.title}</strong>
{link.author && ( </a>
<> {link.author && (
{" via "} <>
<a href={link.author.url}>{link.author.name}</a> {" via "}
</> <a href={link.author.url}>{link.author.name}</a>
)} </>
</div> )}
))} </div>
))
}
</div> </div>
<Paginator pagination={pagination} /> <Paginator pagination={pagination} />

View file

@ -0,0 +1,133 @@
---
import Layout from "@layouts/Layout.astro";
import ToggleContent from "@components/utils/ToggleContent.astro";
import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
import { IconArrowLeft, IconHeart, IconNeedle, IconMapPin, IconDeviceSpeaker } from "@tabler/icons-react";
import { fetchGlobalData } from "@utils/data/global/index.js";
import { fetchArtistByUrl } from "@utils/data/dynamic/artistByUrl.js";
const { globals } = await fetchGlobalData(Astro);
const artist = await fetchArtistByUrl(Astro.url.pathname);
if (!artist) return Astro.redirect("/404", 404);
const pageTitle = `${artist.name} / Music`;
const description = artist.description || `Learn more about ${artist.name}`;
const alt = `${artist.name} / ${artist.country}`;
const playLabel = artist.total_plays === 1 ? "play" : "plays";
---
<Layout pageTitle={pageTitle} description={description} schema="artist">
<a href="/music" class="back-link">
<IconArrowLeft size={18} /> Back to music
</a>
<article class="artist-focus">
<div class="artist-display">
<img
srcset={`
${globals.cdn_url}${artist.image}?class=w200&type=webp 200w,
${globals.cdn_url}${artist.image}?class=w600&type=webp 400w,
${globals.cdn_url}${artist.image}?class=w800&type=webp 800w
`}
sizes="(max-width: 450px) 200px,
(max-width: 850px) 400px,
800px"
src={`${globals.cdn_url}${artist.image}?class=w200&type=webp`}
alt={alt}
loading="eager"
decoding="async"
width="200"
height="200"
/>
<div class="media-meta">
<span class="title"><strong>{artist.name}</strong></span>
<span class="sub-meta country">
<IconMapPin size={18} /> {artist.country}
</span>
{artist.favorite && (
<span class="sub-meta favorite">
<IconHeart size={18} /> This is one of my favorite artists!
</span>
)}
{artist.tattoo && (
<span class="sub-meta tattoo">
<IconNeedle size={18} /> I have a tattoo inspired by this artist!
</span>
)}
{artist.total_plays > 0 && (
<span class="sub-meta">
<strong class="highlight-text">{artist.total_plays} {playLabel}</strong>
</span>
)}
<span class="sub-meta">
<a href={artist.genre.url} title={`Learn more about ${artist.genre.name}`}>
{artist.genre.name}
</a>
</span>
</div>
</div>
<AssociatedMedia
artists={artist.related_artists}
books={artist.books}
genres={artist.genres}
movies={artist.movies}
posts={artist.posts}
shows={artist.shows}
/>
{artist.description && (
<>
<h2>Overview</h2>
<ToggleContent content={artist.description} />
</>
)}
{artist.concerts && (
<>
<p id="concerts" class="concerts">
<IconDeviceSpeaker size={18} /> I've seen this artist live!
</p>
<ul>
{artist.concerts.map((concert) => {
const venue = concert.venue_latitude && concert.venue_longitude
? `<a href="https://www.openstreetmap.org/?mlat=${concert.venue_latitude}&mlon=${concert.venue_longitude}#map=18/${concert.venue_latitude}/${concert.venue_longitude}">${concert.venue_name_short}</a>`
: concert.venue_name_short;
return (
<li>
On <strong class="highlight-text">{concert.date.toLocaleString(DateTime.DATE_MED)}</strong>
{venue && <> at {venue}</>}
{concert.notes && (
<span> — {concert.notes}</span>
)}
</li>
);
})}
</ul>
</>
)}
{artist.albums && (
<>
<table>
<tr>
<th>Album</th>
<th>Plays</th>
<th>Year</th>
</tr>
{artist.albums.map((album) => (
<tr>
<td>{album.name}</td>
<td>{album.total_plays}</td>
<td>{album.release_year}</td>
</tr>
))}
</table>
<p>
<em>These are the albums by this artist that are in my collection, not necessarily a comprehensive discography.</em>
</p>
</>
)}
</article>
</Layout>

View file

@ -1,23 +1,24 @@
import { albumReleasesCalendar } from '@utils/albumReleasesCalendar'; import { albumReleasesCalendar } from "@utils/albumReleasesCalendar.js";
import { fetchAlbumReleases } from '@utils/data/albumReleases'; import { fetchAlbumReleases } from "@utils/data/albumReleases.js";
export async function GET() { export async function GET() {
try { try {
const { all: albumReleases } = await fetchAlbumReleases(); const { all: albumReleases } = await fetchAlbumReleases();
const icsContent = await albumReleasesCalendar(albumReleases); const icsContent = await albumReleasesCalendar(albumReleases);
if (!icsContent) return new Response('Error generating ICS file', { status: 500 }); if (!icsContent)
return new Response("Error generating ICS file", { status: 500 });
return new Response(icsContent, { return new Response(icsContent, {
status: 200, status: 200,
headers: { headers: {
'Content-Type': 'text/calendar', "Content-Type": "text/calendar",
'Content-Disposition': 'attachment; filename="releases.ics"', "Content-Disposition": 'attachment; filename="releases.ics"',
}, },
}); });
} catch (error) { } catch (error) {
console.error('Error generating album releases ICS file:', error); console.error("Error generating album releases ICS file:", error);
return new Response('Error generating album releases ICS file', { return new Response("Error generating album releases ICS file", {
status: 500, status: 500,
}); });
} }

View file

@ -1,12 +1,12 @@
--- ---
import { getCollection } from 'astro:content'; import { getCollection } from "astro:content";
import { IconStar } from '@tabler/icons-react'; import { IconStar } from "@tabler/icons-react";
import { fetchAllPosts } from "@data/posts.js"; import { fetchAllPosts } from "@data/posts.js";
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
import Layout from "@layouts/Layout.astro"; import Layout from "@layouts/Layout.astro";
import Paginator from '@components/nav/Paginator.astro'; import Paginator from "@components/nav/Paginator.astro";
import { md } from '@utils/helpers/general.js'; import { md } from "@utils/helpers/general.js";
import { DateTime } from 'luxon'; import { DateTime } from "luxon";
const posts = await fetchAllPosts(); const posts = await fetchAllPosts();
const { page } = Astro.props; const { page } = Astro.props;
@ -35,25 +35,23 @@ const pagination = {
}; };
--- ---
<Layout <Layout pageTitle="All posts" currentUrl={currentUrl}>
globals={globals} {
pageTitle="All posts" paginatedPosts.map((post) => (
currentUrl={currentUrl} <article>
> <div class="post-meta">
{paginatedPosts.map((post) => ( {post.featured && <IconStar size={16} />}
<article> <time datetime={post.date}>
<div class="post-meta"> {DateTime.fromISO(post.date).toLocaleString(DateTime.DATE_FULL)}
{post.featured && <IconStar size={16} />} </time>
<time datetime={post.date}> </div>
{DateTime.fromISO(post.date).toLocaleString(DateTime.DATE_FULL)} <h3>
</time> <a href={post.url}>{post.title}</a>
</div> </h3>
<h3> <p set:html={md(post.description)} />
<a href={post.url}>{post.title}</a> </article>
</h3> ))
<p set:html={md(post.description)}></p> }
</article>
))}
<Paginator pagination={pagination} /> <Paginator pagination={pagination} />
</Layout> </Layout>

View file

@ -3,16 +3,16 @@ import { IconStar } from "@tabler/icons-react";
import { fetchAllPosts } from "@data/posts.js"; import { fetchAllPosts } from "@data/posts.js";
import { fetchAnalyticsData } from "@data/analytics.js"; import { fetchAnalyticsData } from "@data/analytics.js";
import { fetchLinks } from "@data/links.js"; import { fetchLinks } from "@data/links.js";
import { fetchGlobalData } from '@utils/data/global/index.js'; import { fetchGlobalData } from "@utils/data/global/index.js";
import { md } from '@utils/helpers/general.js'; import { md } from "@utils/helpers/general.js";
import { getPopularPosts } from '@utils/getPopularPosts.js'; import { getPopularPosts } from "@utils/getPopularPosts.js";
const analytics = await fetchAnalyticsData(); const analytics = await fetchAnalyticsData();
const links = await fetchLinks(); const links = await fetchLinks();
const posts = await fetchAllPosts(); const posts = await fetchAllPosts();
const popularPosts = getPopularPosts(posts, analytics); const popularPosts = getPopularPosts(posts, analytics);
import AddonLinks from '@components/blocks/links/AddonLinks.astro'; import AddonLinks from "@components/blocks/links/AddonLinks.astro";
import AssociatedMedia from "@components/blocks/AssociatedMedia.astro"; import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
import BlockRenderer from "@components/blocks/BlockRenderer.astro"; import BlockRenderer from "@components/blocks/BlockRenderer.astro";
import Coffee from "@components/blocks/banners/Coffee.astro"; import Coffee from "@components/blocks/banners/Coffee.astro";

View file

@ -0,0 +1,25 @@
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 artistCache = {};
export async function fetchArtistByUrl(url) {
if (import.meta.env.MODE === "development" && artistCache[url]) return artistCache[url];
const { data: artist, error } = await supabase
.from("optimized_artists")
.select("*")
.eq("url", url)
.limit(1);
if (error) {
console.error("Error fetching artist:", error);
return null;
}
if (import.meta.env.MODE === "development") artistCache[url] = artist[0];
return artist[0];
}

View file

@ -3,11 +3,10 @@ import { createClient } from "@supabase/supabase-js";
const SUPABASE_URL = import.meta.env.SUPABASE_URL; const SUPABASE_URL = import.meta.env.SUPABASE_URL;
const SUPABASE_KEY = import.meta.env.SUPABASE_KEY; const SUPABASE_KEY = import.meta.env.SUPABASE_KEY;
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY); const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
const bookCache = {}; const bookCache = {};
export async function fetchBookByUrl(url) { export async function fetchBookByUrl(url) {
if (bookCache[url]) return bookCache[url]; if (import.meta.env.MODE === "development" && bookCache[url]) return bookCache[url];
const { data: book, error } = await supabase const { data: book, error } = await supabase
.from("optimized_books") .from("optimized_books")
@ -15,12 +14,11 @@ export async function fetchBookByUrl(url) {
.eq("url", url) .eq("url", url)
.limit(1); .limit(1);
if (error || !book) { if (error || !book || book.length === 0) {
console.error(`Error fetching book with URL ${url}:`, error); console.error(`Error fetching book with URL ${url}:`, error);
return null; return null;
} }
if (import.meta.env.MODE === "development") bookCache[url] = book[0];
bookCache[url] = book[0];
return book[0]; return book[0];
} }