feat: initial commit

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

View file

@ -0,0 +1,26 @@
---
import NavLink from "@components/nav/NavLink.astro";
import { fetchGlobalData } from "@utils/data/global/index.js";
const { nav } = await fetchGlobalData(Astro);
---
<footer>
<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,21 @@
---
import Menu from "@components/nav/Menu.astro";
const { siteName, url, nav } = 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,42 @@
---
import icons from "@cdransf/astro-tabler-icons";
const {
IconArticle,
IconHeadphones,
IconDeviceTvOld,
IconBooks,
IconLink,
IconInfoCircle,
IconSearch,
IconRss,
IconBrandMastodon,
IconMail,
IconBrandGithub,
IconBrandNpm,
IconCoffee,
IconDeviceWatch,
IconHeartHandshake,
} = icons;
const { icon, className } = 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 ? <div set:html={SelectedIcon({ size: 24, className })} /> : null}

View file

@ -0,0 +1,105 @@
---
import { parseISO, format } from "date-fns";
import IconMapper from "@components/IconMapper.astro";
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}>
<div class="media-title">
<IconMapper icon={icon} /> {label}
</div>
<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>({format(parseISO(item.date), "PPPP")})</span>
)}
</li>
))}
</ul>
</section>
);
})
}
</div>

View file

@ -0,0 +1,63 @@
---
import { fetchAllPosts } from "@data/posts.js";
import { fetchAnalyticsData } from "@data/analytics.js";
import { fetchLinks } from "@data/links.js";
import AddonLinks from "@components/blocks/links/AddonLinks.astro";
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 Npm from "@components/blocks/banners/Npm.astro";
import Rss from "@components/blocks/banners/Rss.astro";
import YouTubePlayer from "@components/blocks//YouTubePlayer.astro";
import { md } from "@utils/helpers/general.js";
import { getPopularPosts } from "@utils/getPopularPosts.js";
const [analytics, links, posts] = await Promise.all([
fetchAnalyticsData(),
fetchLinks(),
fetchAllPosts(),
]);
const popularPosts = getPopularPosts(posts, analytics);
const { blocks } = Astro.props;
---
<div>
{
blocks.map((block) => (
<>
{block.type === "addon_links" && (
<AddonLinks popularPosts={popularPosts} links={links} />
)}
{block.type === "associated_media" && (
<AssociatedMedia media={block.media} />
)}
{block.type === "divider" && <div set:html={md(block.markup)} />}
{block.type === "github_banner" && <GitHub url={block.url} />}
{block.type === "hero" && <Hero image={block.image} alt={block.alt} />}
{block.type === "markdown" && (
<div set:html={md(block.text)} />
)}
{block.type === "npm_banner" && (
<Npm url={block.url} command={block.command} />
)}
{block.type === "modal" && <Modal content={block.content} />}
{block.type === "rss_banner" && (
<Rss url={block.url} text={block.text} />
)}
{block.type === "youtube_player" && <YouTubePlayer url={block.url} />}
</>
))
}
</div>

View file

@ -0,0 +1,26 @@
---
import { fetchGlobalData } from "@utils/data/global/index.js";
const { image, alt } = Astro.props;
const { globals } = await fetchGlobalData(Astro);
---
<div class="hero">
<img
srcset={`
${globals.cdn_url}${image}?class=bannersm&type=webp 256w,
${globals.cdn_url}${image}?class=bannermd&type=webp 512w,
${globals.cdn_url}${image}?class=bannerbase&type=webp 1024w
`}
sizes="(max-width: 450px) 256px,
(max-width: 850px) 512px,
1024px"
src={`${globals.cdn_url}${image}?class=bannersm&type=webp`}
alt={alt}
class="image-banner"
loading="lazy"
decoding="async"
width="720"
height="480"
/>
</div>

View file

@ -0,0 +1,26 @@
---
import { md } from "@utils/helpers/general.js";
import icons from "@cdransf/astro-tabler-icons";
const { IconCircleX, IconInfoCircle } = icons;
const { content, id } = Astro.props;
---
<>
<input
class="modal-input"
id={id}
type="checkbox"
tabindex="0"
/>
<label class="modal-toggle" for={id}>
<div set:html={IconInfoCircle({ size: 24 })}/>
</label>
<div class="modal-wrapper">
<div class="modal-body">
<label class="modal-close" for={id}>
<div set:html={IconCircleX({ size: 24 })}/>
</label>
<div set:html={md(content)}/>
</div>
</div>
</>

View file

@ -0,0 +1,7 @@
---
import { YouTube } from 'astro-embed';
const { url } = Astro.props;
---
<YouTube id={url} />

View file

@ -0,0 +1,14 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconCoffee } = icons;
---
<div class="banner coffee">
<p>
<span set:html={IconCoffee({ size: 24 })} />
<a class="coffee" href="https://buymeacoffee.com/cory">
If you found this post helpful, you can buy me a coffee.
</a>
</p>
</div>

View file

@ -0,0 +1,10 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconAlertCircle } = icons;
const { text } = Astro.props;
---
<div class="banner error">
<p><span set:html={IconAlertCircle({ size: 24 })}/> {text}</p>
</div>

View file

@ -0,0 +1,14 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconBrandGithub } = icons;
const { url } = Astro.props;
---
<div class="banner github">
<p>
<span set:html={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,13 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconBrandMastodon } = icons;
const { url } = Astro.props;
---
<div class="banner mastodon">
<p>
<span set:html={IconBrandMastodon({ size: 24 })} />
<a class="mastodon" href={url}> Discuss this post on Mastodon. </a>
</p>
</div>

View file

@ -0,0 +1,14 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconBrandNpm } = icons;
const { url, command } = Astro.props;
---
<div class="banner npm">
<p>
<span set:html={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,18 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconClockX } = icons;
const { isOldPost } = Astro.props;
---
{
isOldPost && (
<div class="banner old-post">
<p>
<span set:html={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,13 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconRss } = icons;
const { url, text } = Astro.props;
---
<div class="banner rss">
<p>
<span set:html={IconRss({ size: 24 })}/>
<a href={url}>{text}</a>.
</p>
</div>

View file

@ -0,0 +1,13 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconAlertTriangle } = icons;
const { text } = Astro.props;
---
<div class="banner warning">
<p>
<span set:html={IconAlertTriangle({ size: 24 })}/>
{text}
</p>
</div>

View file

@ -0,0 +1,10 @@
---
import PopularPosts from './PopularPosts.astro';
import RecentLinks from './RecentLinks.astro';
const { popularPosts, links } = Astro.props;
---
<div class="addon-links">
<PopularPosts popularPosts={popularPosts} />
<RecentLinks links={links} />
</div>

View file

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

View file

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

View file

@ -0,0 +1,7 @@
---
const { intro } = Astro.props;
---
<article class="intro">
<div set:html={intro} />
</article>

View file

@ -0,0 +1,76 @@
---
import { fetchBooks } from "@utils/data/books.js";
import { fetchLinks } from "@utils/data/links.js";
import { fetchMovies } from "@utils/data/movies.js";
import { fetchMusicWeek } from "@utils/data/music/week.js";
import { fetchShows } from "@utils/data/tv.js";
import icons from "@cdransf/astro-tabler-icons";
import Rss from "@components/blocks/banners/Rss.astro";
const { IconActivity } = icons;
const [music, tv, movies, books, links] = await Promise.all([
fetchMusicWeek(),
fetchShows(),
fetchMovies(),
fetchBooks(),
fetchLinks(),
]);
const artist = music.week?.artists[0];
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>
<div set:html={IconActivity({ size: 24 })}/>
Recent activity
</h2>
<ul>
<li>
<span class="music">Top artist this week:</span>
<a href={artist.artist_url}>{artist.artist_name}</a>
</li>
<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,36 @@
---
import icons from "@cdransf/astro-tabler-icons";
import { fetchAllPosts } from "@utils/data/posts.js";
import { md } from "@utils/helpers/general.js";
const { IconClock, IconStar, IconArrowRight } = icons;
const posts = await fetchAllPosts();
---
<h2>
<div set:html={IconClock({ size: 24 })}/>
Recent posts
</h2>
{
posts.slice(0, 5).map((post) => (
<article key={post.url}>
<div class="post-meta">
{post.featured && <div set:html={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)} />
</article>
))
}
<a class="icon-link" href="/posts">
View all posts <div set:html={IconArrowRight({ size: 16 })}/>
</a>

View file

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

View file

@ -0,0 +1,9 @@
---
const { percentage } = Astro.props;
---
{percentage && (
<div class="progress-bar-wrapper" title={percentage}>
<div style={`width: ${percentage}`} class="progress-bar"/>
</div>
)}

View file

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

View file

@ -0,0 +1,48 @@
---
import { fetchGlobalData } from "@utils/data/global/index.js";
const { data } = Astro.props;
const { globals } = await fetchGlobalData(Astro);
---
<div class="music-chart">
{
data.slice(0, 10).map((item) => (
<div class="item">
<div class="meta">
<a href={item.chart.url}>
<img
srcset={`
${globals.cdn_url}${item.chart.image}?class=w50&type=webp 50w,
${globals.cdn_url}${item.chart.image}?class=w100&type=webp 100w
`}
sizes="(max-width: 450px) 50px, 100px"
src={`${globals.cdn_url}${item.chart.image}?class=w50&type=webp`}
alt={item.chart.alt.replace(/['"]/g, "")}
loading="lazy"
decoding="async"
width="64"
height="64"
/>
</a>
<div class="meta-text">
<a class="title" href={item.chart.url}>
{item.chart.title}
</a>
<span class="subtext">{item.chart.subtext}</span>
</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>

View file

@ -0,0 +1,18 @@
---
import Hero from "@components/blocks/Hero.astro";
const { movie } = Astro.props;
---
<a href={movie.url}>
<div class="watching media-overlay hero">
<div class="meta-text">
<div class="header">{movie.title}</div>
<div class="subheader">
{movie.rating && <span class="rating">{movie.rating} </span>}
({movie.year})
</div>
</div>
<Hero image={movie.backdrop} alt={movie.title} />
</div>
</a>

View file

@ -0,0 +1,33 @@
---
import icons from "@cdransf/astro-tabler-icons";
import NavLink from "@components/nav/NavLink.astro";
const { IconMenu2, IconX } = icons
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">
<div set:html={IconMenu2({ size: 24 })}/>
</div>
<div class="menu-open" aria-hidden="true">
<div set:html={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,27 @@
---
import IconMapper from "@components/IconMapper.astro";
import { removeTrailingSlash } from "@utils/helpers/general.js";
const { url, title, icon } = Astro.props;
const isHttp = url?.startsWith("http");
const isActive = Astro.url.pathname === removeTrailingSlash(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,50 @@
---
import icons from "@cdransf/astro-tabler-icons";
const { IconArrowLeft, IconArrowRight } = icons;
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"}
>
<div set:html={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"}
>
<div set:html={IconArrowRight({ size: 24 })}/>
</a>
</nav>

View file

@ -0,0 +1,8 @@
---
const { content } = Astro.props;
---
<div data-toggle-content class="text-toggle-hidden">
<div set:html={content} />
</div>
<button data-toggle-button>Show more</button>