feat: initial commit
This commit is contained in:
commit
0ff7457679
192 changed files with 24379 additions and 0 deletions
26
src/components/Footer.astro
Normal file
26
src/components/Footer.astro
Normal 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>
|
21
src/components/Header.astro
Normal file
21
src/components/Header.astro
Normal 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>
|
42
src/components/IconMapper.astro
Normal file
42
src/components/IconMapper.astro
Normal 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}
|
105
src/components/blocks/AssociatedMedia.astro
Normal file
105
src/components/blocks/AssociatedMedia.astro
Normal 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>
|
63
src/components/blocks/BlockRenderer.astro
Normal file
63
src/components/blocks/BlockRenderer.astro
Normal 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>
|
26
src/components/blocks/Hero.astro
Normal file
26
src/components/blocks/Hero.astro
Normal 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>
|
26
src/components/blocks/Modal.astro
Normal file
26
src/components/blocks/Modal.astro
Normal 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>
|
||||
</>
|
7
src/components/blocks/YouTubePlayer.astro
Normal file
7
src/components/blocks/YouTubePlayer.astro
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
import { YouTube } from 'astro-embed';
|
||||
|
||||
const { url } = Astro.props;
|
||||
---
|
||||
|
||||
<YouTube id={url} />
|
14
src/components/blocks/banners/Coffee.astro
Normal file
14
src/components/blocks/banners/Coffee.astro
Normal 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>
|
10
src/components/blocks/banners/Error.astro
Normal file
10
src/components/blocks/banners/Error.astro
Normal 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>
|
14
src/components/blocks/banners/GitHub.astro
Normal file
14
src/components/blocks/banners/GitHub.astro
Normal 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>
|
13
src/components/blocks/banners/Mastodon.astro
Normal file
13
src/components/blocks/banners/Mastodon.astro
Normal 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>
|
14
src/components/blocks/banners/Npm.astro
Normal file
14
src/components/blocks/banners/Npm.astro
Normal 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>
|
18
src/components/blocks/banners/OldPost.astro
Normal file
18
src/components/blocks/banners/OldPost.astro
Normal 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>
|
||||
)
|
||||
}
|
13
src/components/blocks/banners/Rss.astro
Normal file
13
src/components/blocks/banners/Rss.astro
Normal 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>
|
13
src/components/blocks/banners/Warning.astro
Normal file
13
src/components/blocks/banners/Warning.astro
Normal 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>
|
10
src/components/blocks/links/AddonLinks.astro
Normal file
10
src/components/blocks/links/AddonLinks.astro
Normal 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>
|
26
src/components/blocks/links/PopularPosts.astro
Normal file
26
src/components/blocks/links/PopularPosts.astro
Normal 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>
|
||||
)
|
||||
}
|
34
src/components/blocks/links/RecentLinks.astro
Normal file
34
src/components/blocks/links/RecentLinks.astro
Normal 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>
|
||||
)
|
||||
}
|
7
src/components/home/Intro.astro
Normal file
7
src/components/home/Intro.astro
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
const { intro } = Astro.props;
|
||||
---
|
||||
|
||||
<article class="intro">
|
||||
<div set:html={intro} />
|
||||
</article>
|
76
src/components/home/RecentActivity.astro
Normal file
76
src/components/home/RecentActivity.astro
Normal 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>
|
36
src/components/home/RecentPosts.astro
Normal file
36
src/components/home/RecentPosts.astro
Normal 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>
|
75
src/components/media/Grid.astro
Normal file
75
src/components/media/Grid.astro
Normal 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} />}
|
9
src/components/media/ProgressBar.astro
Normal file
9
src/components/media/ProgressBar.astro
Normal 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>
|
||||
)}
|
33
src/components/media/music/Chart.astro
Normal file
33
src/components/media/music/Chart.astro
Normal 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>
|
48
src/components/media/music/Recent.astro
Normal file
48
src/components/media/music/Recent.astro
Normal 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>
|
18
src/components/media/watching/Hero.astro
Normal file
18
src/components/media/watching/Hero.astro
Normal 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>
|
33
src/components/nav/Menu.astro
Normal file
33
src/components/nav/Menu.astro
Normal 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>
|
27
src/components/nav/NavLink.astro
Normal file
27
src/components/nav/NavLink.astro
Normal 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>
|
||||
)
|
||||
}
|
50
src/components/nav/Paginator.astro
Normal file
50
src/components/nav/Paginator.astro
Normal 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>
|
8
src/components/utils/ToggleContent.astro
Normal file
8
src/components/utils/ToggleContent.astro
Normal 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>
|
1
src/env.d.ts
vendored
Normal file
1
src/env.d.ts
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/// <reference path="../.astro/types.d.ts" />
|
120
src/layouts/Layout.astro
Normal file
120
src/layouts/Layout.astro
Normal file
|
@ -0,0 +1,120 @@
|
|||
---
|
||||
import "@styles/index.css";
|
||||
import Header from "@components/Header.astro";
|
||||
import Footer from "@components/Footer.astro";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
import { md, htmlToText, htmlTruncate } from "@utils/helpers/general.js";
|
||||
|
||||
const { globals, nav } = await fetchGlobalData(Astro);
|
||||
const currentUrl = Astro.url.pathname;
|
||||
const isProduction = import.meta.env.MODE === "production";
|
||||
const {
|
||||
schema = "page",
|
||||
pageTitle = globals.site_name,
|
||||
description = globals.site_description,
|
||||
ogImage = globals.avatar,
|
||||
fullUrl = currentUrl,
|
||||
} = Astro.props;
|
||||
|
||||
const pageDescription = md(description);
|
||||
---
|
||||
|
||||
<!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>
|
||||
{
|
||||
pageTitle !== globals.site_name
|
||||
? `${pageTitle} / ${globals.site_name}`
|
||||
: pageTitle
|
||||
}
|
||||
</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"
|
||||
/>
|
||||
<link rel="canonical" href={`${globals.url}${fullUrl}`} />
|
||||
<meta
|
||||
property="og:title"
|
||||
content={pageTitle !== globals.site_name
|
||||
? `${pageTitle} / ${globals.site_name}`
|
||||
: pageTitle}
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content={htmlToText(htmlTruncate(pageDescription))}
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content={htmlToText(htmlTruncate(pageDescription))}
|
||||
/>
|
||||
<meta property="og:type" content={schema || "page"} />
|
||||
<meta property="og:url" content={fullUrl} />
|
||||
<meta
|
||||
property="og:image"
|
||||
content={`${globals.cdn_url}${ogImage}?class=w800`}
|
||||
/>
|
||||
<meta name="theme-color" content={globals.theme_color} />
|
||||
<meta name="fediverse:creator" content={globals.mastodon} />
|
||||
<meta name="generator" content="Astro" />
|
||||
<meta name="robots" content="noai, noimageai" />
|
||||
<link
|
||||
href={`${globals.cdn_url}${globals.avatar_transparent}?class=w50`}
|
||||
rel="icon"
|
||||
sizes="any"
|
||||
/>
|
||||
<link
|
||||
href={`${globals.cdn_url}${globals.avatar_transparent}?class=w50&type=svg`}
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
/>
|
||||
<link
|
||||
href={`${globals.cdn_url}${globals.avatar}?class=w800`}
|
||||
rel="apple-touch-icon"
|
||||
/>
|
||||
<link
|
||||
type="application/atom+xml"
|
||||
rel="alternate"
|
||||
title={`Posts / ${globals.site_name}`}
|
||||
href={`${globals.url}/feeds/posts.xml`}
|
||||
/>
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<script defer src="/scripts/index.js" is:inline></script>
|
||||
{
|
||||
isProduction && (
|
||||
<script defer data-domain="coryd.dev" src="/js/script.js" />
|
||||
)
|
||||
}
|
||||
<noscript>
|
||||
<style>
|
||||
.client-side {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</noscript>
|
||||
</head>
|
||||
<body>
|
||||
<div class="main-wrapper">
|
||||
<main>
|
||||
<Header siteName={globals.site_name} url={currentUrl} nav={nav} />
|
||||
<div class="default-wrapper">
|
||||
<slot />
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
108
src/middleware.js
Normal file
108
src/middleware.js
Normal file
|
@ -0,0 +1,108 @@
|
|||
import { fetchGlobals } from "@utils/data/globals.js";
|
||||
import { fetchNavigation } from "@utils/data/nav.js";
|
||||
import { fetchArtistByUrl } from "@utils/data/dynamic/artistByUrl.js";
|
||||
import { fetchBookByUrl } from "@utils/data/dynamic/bookByUrl.js";
|
||||
import { fetchGenreByUrl } from "@utils/data/dynamic/genreByUrl.js";
|
||||
import { fetchMovieByUrl } from "@utils/data/dynamic/movieByUrl.js";
|
||||
import { fetchShowByUrl } from "@utils/data/dynamic/showByUrl.js";
|
||||
import { isbnRegex } from "@utils/helpers/media.js";
|
||||
import { isExcludedPath } from "@utils/helpers/general.js";
|
||||
import { CACHE_DURATION } from "@utils/constants/index.js";
|
||||
|
||||
let cachedGlobals = null;
|
||||
let cachedNav = null;
|
||||
let cachedByType = {};
|
||||
let lastFetchTimeGlobalsNav = 0;
|
||||
let lastFetchTimeByType = {};
|
||||
|
||||
export async function onRequest(context, next) {
|
||||
const now = Date.now();
|
||||
const { request, locals } = context;
|
||||
|
||||
try {
|
||||
const runtimeEnv = locals.runtime?.env;
|
||||
|
||||
if (!runtimeEnv)
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
|
||||
const urlPath = new URL(request.url).pathname;
|
||||
|
||||
if (
|
||||
!cachedGlobals ||
|
||||
!cachedNav ||
|
||||
now - lastFetchTimeGlobalsNav > CACHE_DURATION
|
||||
) {
|
||||
const [globals, nav] = await Promise.all([
|
||||
fetchGlobals(runtimeEnv),
|
||||
fetchNavigation(runtimeEnv),
|
||||
]);
|
||||
cachedGlobals = globals;
|
||||
cachedNav = nav;
|
||||
lastFetchTimeGlobalsNav = now;
|
||||
}
|
||||
|
||||
let resourceType = null;
|
||||
|
||||
if (urlPath.startsWith("/music/artists/")) resourceType = "artist";
|
||||
else if (isbnRegex.test(urlPath)) resourceType = "book";
|
||||
else if (urlPath.startsWith("/music/genres/")) resourceType = "genre";
|
||||
else if (
|
||||
urlPath.startsWith("/watching/movies/") &&
|
||||
!isExcludedPath(urlPath, ["/favorites", "/recent"])
|
||||
)
|
||||
resourceType = "movie";
|
||||
else if (
|
||||
urlPath.startsWith("/watching/shows/") &&
|
||||
!isExcludedPath(urlPath, ["/favorites", "/recent"])
|
||||
)
|
||||
resourceType = "show";
|
||||
|
||||
if (resourceType) {
|
||||
if (
|
||||
!cachedByType[urlPath] ||
|
||||
now - (lastFetchTimeByType[urlPath] || 0) > CACHE_DURATION
|
||||
) {
|
||||
let fetchFunction = null;
|
||||
|
||||
switch (resourceType) {
|
||||
case "artist":
|
||||
fetchFunction = fetchArtistByUrl;
|
||||
break;
|
||||
case "book":
|
||||
fetchFunction = fetchBookByUrl;
|
||||
break;
|
||||
case "genre":
|
||||
fetchFunction = fetchGenreByUrl;
|
||||
break;
|
||||
case "movie":
|
||||
fetchFunction = fetchMovieByUrl;
|
||||
break;
|
||||
case "show":
|
||||
fetchFunction = fetchShowByUrl;
|
||||
break;
|
||||
}
|
||||
|
||||
const data = await fetchFunction(runtimeEnv, urlPath);
|
||||
|
||||
if (!data)
|
||||
return new Response(
|
||||
`${resourceType.charAt(0).toUpperCase() + resourceType.slice(1)} Not Found`,
|
||||
{ status: 404 }
|
||||
);
|
||||
|
||||
cachedByType[urlPath] = data;
|
||||
lastFetchTimeByType[urlPath] = now;
|
||||
}
|
||||
|
||||
locals[resourceType] = cachedByType[urlPath];
|
||||
}
|
||||
|
||||
locals.globals = cachedGlobals;
|
||||
locals.nav = cachedNav;
|
||||
} catch (error) {
|
||||
console.error("Error in middleware:", error);
|
||||
return new Response("Internal Server Error", { status: 500 });
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
48
src/pages/.well-known/webfinger.js
Normal file
48
src/pages/.well-known/webfinger.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { fetchGlobals } from "@utils/data/globals.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const globals = await fetchGlobals();
|
||||
|
||||
const webfingerResponse = {
|
||||
subject: `acct:${globals.webfinger_username}@${globals.webfinger_hostname}`,
|
||||
aliases: [
|
||||
`https://${globals.webfinger_hostname}/@${globals.webfinger_username}`,
|
||||
`https://${globals.webfinger_hostname}/users/${globals.webfinger_username}`,
|
||||
],
|
||||
links: [
|
||||
{
|
||||
rel: "http://webfinger.net/rel/profile-page",
|
||||
type: "text/html",
|
||||
href: `https://${globals.webfinger_hostname}/@${globals.webfinger_username}`,
|
||||
},
|
||||
{
|
||||
rel: "self",
|
||||
type: "application/activity+json",
|
||||
href: `https://${globals.webfinger_hostname}/users/${globals.webfinger_username}`,
|
||||
},
|
||||
{
|
||||
rel: "http://ostatus.org/schema/1.0/subscribe",
|
||||
template: `https://${globals.webfinger_hostname}/authorize_interaction?uri={uri}`,
|
||||
},
|
||||
{
|
||||
rel: "http://webfinger.net/rel/avatar",
|
||||
type: "image/png",
|
||||
href: `${globals.cdn_url}${globals.avatar}?class=squarebase`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return new Response(JSON.stringify(webfingerResponse), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/jrd+json",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating WebFinger response:", error);
|
||||
return new Response("Error generating WebFinger response", { status: 500 });
|
||||
}
|
||||
}
|
60
src/pages/404.astro
Normal file
60
src/pages/404.astro
Normal file
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import AddonLinks from "@components/blocks/links/AddonLinks.astro";
|
||||
import { getPopularPosts } from "@utils/getPopularPosts.js";
|
||||
import { fetchAllPosts } from "@data/posts.js";
|
||||
import { fetchAnalyticsData } from "@data/analytics.js";
|
||||
import { fetchLinks } from "@data/links.js";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
const [analytics, links, posts] = await Promise.all([
|
||||
fetchAnalyticsData(),
|
||||
fetchLinks(),
|
||||
fetchAllPosts(),
|
||||
]);
|
||||
const popularPosts = getPopularPosts(posts, analytics);
|
||||
const { globals } = await fetchGlobalData(Astro);
|
||||
|
||||
const pageTitle = "404";
|
||||
const description = "What kind of idiots do you have working here?";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
currentUrl={Astro.url.pathname}
|
||||
>
|
||||
<div class="hero">
|
||||
<img
|
||||
srcset="
|
||||
https://cdn.coryd.dev/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannersm&type=webp 256w,
|
||||
https://cdn.coryd.dev/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannermd&type=webp 512w,
|
||||
https://cdn.coryd.dev/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannerbase&type=webp 1024w
|
||||
"
|
||||
srcset={`
|
||||
${globals.cdn_url}/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannersm&type=webp 256w,
|
||||
${globals.cdn_url}/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannermd&type=webp 512w,
|
||||
${globals.cdn_url}/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannerbase&type=webp 1024w,
|
||||
`}
|
||||
sizes="(max-width: 450px) 256px,
|
||||
(max-width: 850px) 512px,
|
||||
1024px"
|
||||
src={`${globals.cdn_url}/daac2a48-455f-4b7b-a727-b33868cbb2fc.jpg?class=bannersm&type=webp`}
|
||||
alt={description}
|
||||
class="image-banner"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
width="720"
|
||||
height="480"
|
||||
/>
|
||||
</div>
|
||||
<div style="text-align:center">
|
||||
<h2>{pageTitle}</h2>
|
||||
<p>{description}</p>
|
||||
<p><a href="/">Hurry up and skip out on the room service bill!</a></p>
|
||||
</div>
|
||||
<hr />
|
||||
<AddonLinks popularPosts={popularPosts} links={links} />
|
||||
</Layout>
|
27
src/pages/[permalink].astro
Normal file
27
src/pages/[permalink].astro
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import BlockRenderer from "@components/blocks/BlockRenderer.astro";
|
||||
import { fetchPages } from "@utils/data/pages.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const pages = await fetchPages();
|
||||
return pages.map((page) => ({
|
||||
params: { permalink: page.permalink },
|
||||
props: { page },
|
||||
}));
|
||||
}
|
||||
|
||||
const { page } = Astro.props;
|
||||
const currentUrl = Astro.url.pathname;
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={page.title}
|
||||
description={page.description}
|
||||
ogImage={page.open_graph_image}
|
||||
currentUrl={currentUrl}
|
||||
>
|
||||
<BlockRenderer blocks={page.blocks} />
|
||||
</Layout>
|
97
src/pages/blogroll.astro
Normal file
97
src/pages/blogroll.astro
Normal file
|
@ -0,0 +1,97 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
import { fetchBlogroll } from "@utils/data/blogroll.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
const {
|
||||
IconRss,
|
||||
IconJson,
|
||||
IconMailPlus,
|
||||
IconBrandMastodon,
|
||||
} = icons;
|
||||
const blogroll = await fetchBlogroll();
|
||||
const title = "Blogroll";
|
||||
const description =
|
||||
"These are awesome blogs that I enjoy and you may enjoy too.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={title}
|
||||
description={description}
|
||||
currentUrl={Astro.url.pathname}
|
||||
>
|
||||
<h2 class="page-title">{title}</h2>
|
||||
<p>
|
||||
You can <a
|
||||
href="/blogroll.opml"
|
||||
>download an OPML file</a
|
||||
> containing all of these feeds and import them into your RSS reader.
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Link</th>
|
||||
<th>Subscribe</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
blogroll.map((blog) => (
|
||||
<tr>
|
||||
<td>{blog.name}</td>
|
||||
<td>
|
||||
<a href={blog.url}>{blog.url.replace("https://", "")}</a>
|
||||
</td>
|
||||
<td class="blog-roll-icons">
|
||||
{blog.rss_feed && (
|
||||
<a
|
||||
class="rss"
|
||||
href={blog.rss_feed}
|
||||
aria-label={`RSS feed for ${blog.name}`}
|
||||
>
|
||||
<div set:html={IconRss({ size: 16 })}/>
|
||||
</a>
|
||||
)}
|
||||
{blog.json_feed && (
|
||||
<a
|
||||
class="json"
|
||||
href={blog.json_feed}
|
||||
aria-label={`JSON feed for ${blog.name}`}
|
||||
>
|
||||
<div set:html={IconJson({ size: 16 })}/>
|
||||
</a>
|
||||
)}
|
||||
{blog.newsletter && (
|
||||
<a
|
||||
class="mail-plus"
|
||||
href={blog.newsletter}
|
||||
aria-label={`Subscribe to ${blog.name}'s newsletter`}
|
||||
>
|
||||
<div set:html={IconMailPlus({ size: 16 })}/>
|
||||
</a>
|
||||
)}
|
||||
{blog.mastodon && (
|
||||
<a
|
||||
class="brand-mastodon"
|
||||
href={blog.mastodon}
|
||||
aria-label={`Follow ${blog.name} on Mastodon`}
|
||||
>
|
||||
<div set:html={IconBrandMastodon({ size: 16 })}/>
|
||||
</a>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<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
|
||||
>.
|
||||
</p>
|
||||
</Layout>
|
46
src/pages/blogroll.opml.js
Normal file
46
src/pages/blogroll.opml.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { fetchBlogroll } from "@utils/data/blogroll.js";
|
||||
import { fetchGlobals } from '@utils/data/globals.js';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const blogroll = await fetchBlogroll();
|
||||
const globals = await fetchGlobals();
|
||||
const dateCreated = new Date().toUTCString();
|
||||
|
||||
const opmlContent = `
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<opml version="1.0">
|
||||
<head>
|
||||
<title>OPML for all feeds in ${globals.site_name}'s blogroll</title>
|
||||
<dateCreated>${dateCreated}</dateCreated>
|
||||
</head>
|
||||
<body>
|
||||
${blogroll
|
||||
.map(
|
||||
(blog) => `
|
||||
<outline
|
||||
text="${blog.name}"
|
||||
title="${blog.name}"
|
||||
type="rss"
|
||||
xmlUrl="${blog.rss_feed}"
|
||||
htmlUrl="${blog.url}"
|
||||
/>`
|
||||
)
|
||||
.join("\n")}
|
||||
</body>
|
||||
</opml>
|
||||
`.trim();
|
||||
|
||||
return new Response(opmlContent, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "application/xml",
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating blogroll OPML:", error);
|
||||
return new Response("Error generating blogroll OPML", { status: 500 });
|
||||
}
|
||||
}
|
114
src/pages/books/[isbn].astro
Normal file
114
src/pages/books/[isbn].astro
Normal file
|
@ -0,0 +1,114 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Warning from "@components/blocks/banners/Warning.astro";
|
||||
import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
|
||||
import ProgressBar from "@components/media/ProgressBar.astro";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
import { md } from "@utils/helpers/general.js";
|
||||
|
||||
const { IconArrowLeft, IconHeart, IconNeedle } = icons;
|
||||
const { book, globals } = await fetchGlobalData(Astro, Astro.url.pathname);
|
||||
|
||||
if (!book) return Astro.redirect("/404", 404);
|
||||
|
||||
const alt = `${book.title}${book.author ? ` by ${book.author}` : ""}`;
|
||||
const pageTitle = `${book.title} by ${book.author} / Books`;
|
||||
const description =
|
||||
book.description || `Details about ${book.title} by ${book.author}.`;
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={book.image}
|
||||
>
|
||||
<a class="back-link" href="/books" title="Go back to the books index page">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to books
|
||||
</a>
|
||||
<article class="book-focus">
|
||||
<div class="book-display">
|
||||
<img
|
||||
srcset={`
|
||||
${globals.cdn_url}${book.image}?class=verticalsm&type=webp 200w,
|
||||
${globals.cdn_url}${book.image}?class=verticalmd&type=webp 400w,
|
||||
${globals.cdn_url}${book.image}?class=verticalbase&type=webp 800w
|
||||
`}
|
||||
sizes="(max-width: 450px) 203px,
|
||||
(max-width: 850px) 406px,
|
||||
(max-width: 1000px) 812px,
|
||||
812px"
|
||||
src={`${globals.cdn_url}${book.image}?class=verticalsm&type=webp`}
|
||||
alt={alt}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
width="200"
|
||||
height="307"
|
||||
/>
|
||||
<div class="media-meta">
|
||||
<span class="title"><strong>{book.title}</strong></span>
|
||||
{book.rating && <span>{book.rating}</span>}
|
||||
{book.author && <span class="sub-meta">By {book.author}</span>}
|
||||
{
|
||||
book.favorite && (
|
||||
<span class="sub-meta favorite">
|
||||
<div set:html={IconHeart({ size: 18 })}/> This is one of my favorite books!
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
book.tattoo && (
|
||||
<span class="sub-meta tattoo">
|
||||
<div set:html={IconNeedle({ size: 18 })}/> I have a tattoo inspired by this book!
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
book.status === "finished" && (
|
||||
<span class="sub-meta">
|
||||
Finished on:{" "}
|
||||
<strong class="highlight-text">
|
||||
{new Date(book.date_finished).toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
})}
|
||||
</strong>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
book.status === "started" && (
|
||||
<ProgressBar percentage={`${book.progress}%`} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
book.review && (
|
||||
<>
|
||||
<Warning text="There are probably spoilers after this banner — this is a warning about them." />
|
||||
<h2>My thoughts</h2>
|
||||
<div set:html={md(book.review)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<AssociatedMedia
|
||||
artists={book.artists}
|
||||
books={book.related_books}
|
||||
genres={book.genres}
|
||||
movies={book.movies}
|
||||
posts={book.posts}
|
||||
shows={book.shows}
|
||||
/>
|
||||
{
|
||||
book.description && (
|
||||
<>
|
||||
<h2>Overview</h2>
|
||||
<div set:html={md(book.description)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
</article>
|
||||
</Layout>
|
78
src/pages/books/index.astro
Normal file
78
src/pages/books/index.astro
Normal file
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Rss from "@components/blocks/banners/Rss.astro";
|
||||
import ProgressBar from "@components/media/ProgressBar.astro";
|
||||
import { fetchBooks } from "@utils/data/books.js";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
import { md, htmlTruncate } from "@utils/helpers/general.js";
|
||||
import { bookYearLinks } from "@utils/helpers/media.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
const books = await fetchBooks();
|
||||
const currentBookCount = books.currentYear.length;
|
||||
const bookData = books.all
|
||||
.filter((book) => book.status === "started")
|
||||
.reverse();
|
||||
const { globals } = await fetchGlobalData(Astro);
|
||||
const title = "Currently reading";
|
||||
const description = "Here's what I'm reading at the moment.";
|
||||
const updated = new Date().toISOString();
|
||||
const currentYear = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={title}
|
||||
description={description}
|
||||
currentUrl={Astro.url.pathname}
|
||||
ogImage={bookData[0].image}
|
||||
>
|
||||
<h2 class="page-title">{title}</h2>
|
||||
<p>
|
||||
{description} I've finished <strong class="highlight-text"
|
||||
>{currentBookCount} books</strong
|
||||
> this year.
|
||||
</p>
|
||||
<p set:html={bookYearLinks(books.years)} />
|
||||
<Rss
|
||||
url="/feeds/books.xml"
|
||||
text="Subscribe to my books feed or follow along on this page"
|
||||
/>
|
||||
<hr />
|
||||
{
|
||||
bookData.map((book) => (
|
||||
<article class="book-entry" key={book.url}>
|
||||
<a href={book.url}>
|
||||
<img
|
||||
srcset={`
|
||||
${globals.cdn_url}${book.image}?class=verticalsm&type=webp 200w,
|
||||
${globals.cdn_url}${book.image}?class=verticalmd&type=webp 400w
|
||||
`}
|
||||
sizes="(max-width: 450px) 200px, 400px"
|
||||
src={`${globals.cdn_url}${book.image}?class=verticalsm&type=webp`}
|
||||
alt={`${book.title} by ${book.authors}`}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
width="200"
|
||||
height="307"
|
||||
/>
|
||||
</a>
|
||||
<div class="media-meta">
|
||||
<a href={book.url}>
|
||||
<span class="title">
|
||||
<strong>{book.title}</strong>
|
||||
</span>
|
||||
</a>
|
||||
{book.author && <span class="sub-meta">By {book.author}</span>}
|
||||
{book.progress && <ProgressBar percentage={`${book.progress}%`} />}
|
||||
{book.description && (
|
||||
<div
|
||||
class="description"
|
||||
set:html={htmlTruncate(md(book.description))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</Layout>
|
63
src/pages/books/years/[year].astro
Normal file
63
src/pages/books/years/[year].astro
Normal file
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Grid from "@components/media/Grid.astro";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
import {
|
||||
filterBooksByStatus,
|
||||
findFavoriteBooks,
|
||||
mediaLinks,
|
||||
} from "@utils/helpers/media.js";
|
||||
import { fetchBooks } from "@utils/data/books.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const books = await fetchBooks();
|
||||
|
||||
return books.years.map((yearData) => ({
|
||||
params: { year: String(yearData.value) },
|
||||
}));
|
||||
}
|
||||
|
||||
const { IconArrowLeft } = icons;
|
||||
const books = await fetchBooks();
|
||||
const { year } = Astro.params;
|
||||
const yearData = books.years.find((y) => y.value === parseInt(year, 10));
|
||||
|
||||
if (!yearData) return Astro.redirect("/404", 404);
|
||||
|
||||
const bookData = filterBooksByStatus(yearData.data, "finished");
|
||||
const bookDataFavorites = findFavoriteBooks(bookData);
|
||||
const favoriteBooks = mediaLinks(bookDataFavorites, "book", 5);
|
||||
const currentYear = new Date().getFullYear();
|
||||
const isCurrentYear = parseInt(year, 10) === currentYear;
|
||||
const pageTitle = `${year} / Books`;
|
||||
const description = isCurrentYear
|
||||
? `I've finished ${bookData.length} books this year.`
|
||||
: `I finished ${bookData.length} books in ${year}.`;
|
||||
const intro = isCurrentYear
|
||||
? `
|
||||
I've finished <strong class="highlight-text">${bookData.length} books</strong> this year.
|
||||
${favoriteBooks ? ` Among my favorites are ${favoriteBooks}.` : ""}
|
||||
`
|
||||
: `
|
||||
I finished <strong class="highlight-text">${bookData.length} books</strong> in
|
||||
<strong class="highlight-text">${year}</strong>.
|
||||
${favoriteBooks ? ` Among my favorites were ${favoriteBooks}.` : ""}
|
||||
`;
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={bookData[0]?.image || ""}
|
||||
>
|
||||
<a href="/books" class="back-link">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to books
|
||||
</a>
|
||||
<h2 class="page-title">{year} / Books</h2>
|
||||
<div set:html={intro} />
|
||||
<hr />
|
||||
<Grid data={bookData} shape="vertical" count={200} loading="eager" />
|
||||
</Layout>
|
23
src/pages/feeds/all.xml.js
Normal file
23
src/pages/feeds/all.xml.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { fetchGlobals } from "@utils/data/globals.js";
|
||||
import { fetchActivity } from "@utils/data/activity.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
const globals = await fetchGlobals();
|
||||
const activity = await fetchActivity();
|
||||
|
||||
return rss({
|
||||
title: "coryd.dev activity feed",
|
||||
description: "The latest activity from Cory Dransfeldt.",
|
||||
site: globals.url,
|
||||
stylesheet: `${globals.url}/feeds/style.xsl`,
|
||||
items: activity.map((item) => ({
|
||||
title: item.feed.title,
|
||||
pubDate: item.feed.date,
|
||||
link: item.feed.url,
|
||||
description: item.feed.description,
|
||||
})),
|
||||
});
|
||||
}
|
24
src/pages/feeds/books.xml.js
Normal file
24
src/pages/feeds/books.xml.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { fetchGlobals } from "@utils/data/globals.js";
|
||||
import { fetchBooks } from "@utils/data/books.js";
|
||||
import { escapeHtml, md } from "@utils/helpers/general.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
const globals = await fetchGlobals();
|
||||
const books = await fetchBooks();
|
||||
|
||||
return rss({
|
||||
title: "coryd.dev books feed",
|
||||
description: "The latest books Cory Dransfeldt has read.",
|
||||
site: globals.url,
|
||||
stylesheet: `${globals.url}/feeds/style.xsl`,
|
||||
items: books.feed.slice(0, 20).map((book) => ({
|
||||
title: book.feed.title,
|
||||
pubDate: book.feed.date,
|
||||
link: book.feed.url,
|
||||
description: escapeHtml(md(book.feed.description)),
|
||||
})),
|
||||
});
|
||||
}
|
24
src/pages/feeds/links.xml.js
Normal file
24
src/pages/feeds/links.xml.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { fetchGlobals } from "@utils/data/globals.js";
|
||||
import { fetchLinks } from "@utils/data/links.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
const globals = await fetchGlobals();
|
||||
const links = await fetchLinks();
|
||||
|
||||
return rss({
|
||||
title: "coryd.dev links feed",
|
||||
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.",
|
||||
site: globals.url,
|
||||
stylesheet: `${globals.url}/feeds/style.xsl`,
|
||||
items: links.slice(0, 20).map((link) => ({
|
||||
title: link.feed.title,
|
||||
pubDate: link.feed.date,
|
||||
link: link.feed.url,
|
||||
description: link.feed.description,
|
||||
})),
|
||||
});
|
||||
}
|
24
src/pages/feeds/movies.xml.js
Normal file
24
src/pages/feeds/movies.xml.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { fetchGlobals } from "@utils/data/globals.js";
|
||||
import { fetchMovies } from "@utils/data/movies.js";
|
||||
import { escapeHtml, md } from "@utils/helpers/general.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
const globals = await fetchGlobals();
|
||||
const movies = await fetchMovies();
|
||||
|
||||
return rss({
|
||||
title: "coryd.dev movies feed",
|
||||
description: "The latest movie Cory Dransfeldt has watched.",
|
||||
site: globals.url,
|
||||
stylesheet: `${globals.url}/feeds/style.xsl`,
|
||||
items: movies.feed.slice(0, 20).map((movie) => ({
|
||||
title: movie.feed.title,
|
||||
pubDate: movie.feed.date,
|
||||
link: movie.feed.url,
|
||||
description: escapeHtml(md(movie.feed.description)),
|
||||
})),
|
||||
});
|
||||
}
|
24
src/pages/feeds/posts.xml.js
Normal file
24
src/pages/feeds/posts.xml.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { fetchGlobals } from "@utils/data/globals.js";
|
||||
import { fetchAllPosts } from "@utils/data/posts.js";
|
||||
import { escapeHtml, md } from "@utils/helpers/general.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
const globals = await fetchGlobals();
|
||||
const posts = await fetchAllPosts();
|
||||
|
||||
return rss({
|
||||
title: "coryd.dev posts feed",
|
||||
description: "The latest posts from Cory Dransfeldt.",
|
||||
site: globals.url,
|
||||
stylesheet: `${globals.url}/feeds/style.xsl`,
|
||||
items: posts.slice(0, 20).map((post) => ({
|
||||
title: post.feed.title,
|
||||
pubDate: post.feed.date,
|
||||
link: post.feed.url,
|
||||
description: escapeHtml(md(post.feed.description)),
|
||||
})),
|
||||
});
|
||||
}
|
22
src/pages/feeds/syndication.xml.js
Normal file
22
src/pages/feeds/syndication.xml.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import rss from "@astrojs/rss";
|
||||
import { fetchGlobals } from "@utils/data/globals.js";
|
||||
import { fetchSyndication } from "@utils/data/syndication.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
const globals = await fetchGlobals();
|
||||
const syndication = await fetchSyndication();
|
||||
|
||||
return rss({
|
||||
title: "coryd.dev syndication feed",
|
||||
description: "The feed that gets syndicated out to Mastodon",
|
||||
site: globals.url,
|
||||
items: syndication.map((item) => ({
|
||||
title: item.syndication.title,
|
||||
pubDate: item.syndication.date,
|
||||
link: item.syndication.url,
|
||||
description: item.syndication.description,
|
||||
})),
|
||||
});
|
||||
}
|
31
src/pages/humans.txt.js
Normal file
31
src/pages/humans.txt.js
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { fetchGlobals } from '@utils/data/globals.js';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const globals = await fetchGlobals();
|
||||
|
||||
const humansTxt = `
|
||||
## team
|
||||
|
||||
${globals.site_name}
|
||||
${globals.url}
|
||||
${globals.mastodon}
|
||||
|
||||
## colophon
|
||||
|
||||
${globals.url}/colophon
|
||||
`.trim();
|
||||
|
||||
return new Response(humansTxt, {
|
||||
status: 200,
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error generating humans.txt:', error);
|
||||
return new Response('Error generating humans.txt', { status: 500 });
|
||||
}
|
||||
}
|
17
src/pages/index.astro
Normal file
17
src/pages/index.astro
Normal file
|
@ -0,0 +1,17 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Intro from "@components/home/Intro.astro";
|
||||
import RecentActivity from "@components/home/RecentActivity.astro";
|
||||
import RecentPosts from "@components/home/RecentPosts.astro";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
const { globals } = await fetchGlobalData(Astro);
|
||||
---
|
||||
|
||||
<Layout>
|
||||
<Intro intro={globals.intro} />
|
||||
<RecentActivity />
|
||||
<RecentPosts />
|
||||
</Layout>
|
76
src/pages/links/[...page].astro
Normal file
76
src/pages/links/[...page].astro
Normal file
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Paginator from "@components/nav/Paginator.astro";
|
||||
import Rss from "@components/blocks/banners/Rss.astro";
|
||||
import { fetchLinks } from "@utils/data/links.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const getStaticPaths = async ({ paginate }) => {
|
||||
const links = await fetchLinks();
|
||||
return paginate(links, {
|
||||
pageSize: 30,
|
||||
});
|
||||
};
|
||||
|
||||
const { page } = Astro.props;
|
||||
|
||||
const paginatedLinks = page.data;
|
||||
const pagination = {
|
||||
currentPage: page.currentPage,
|
||||
totalPages: page.lastPage,
|
||||
hasPrevious: page.currentPage > 1,
|
||||
hasNext: page.currentPage < page.lastPage,
|
||||
previousPage: page.url.prev || null,
|
||||
nextPage: page.url.next || null,
|
||||
pages: Array.from({ length: page.lastPage }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href: i === 0 ? `/links` : `/links/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
|
||||
const pageTitle =
|
||||
pagination.currentPage === 1
|
||||
? "Links"
|
||||
: `Links / page ${pagination.currentPage}`;
|
||||
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.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
currentUrl={Astro.url.pathname}
|
||||
>
|
||||
{
|
||||
pagination.currentPage === 1 && (
|
||||
<>
|
||||
<h2 class="page-title">{pageTitle}</h2>
|
||||
<p>{description}</p>
|
||||
<Rss
|
||||
url="/feeds/links.xml"
|
||||
text="Subscribe to my links feed or follow along on this page"
|
||||
/>
|
||||
<hr />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div class="link-grid">
|
||||
{
|
||||
paginatedLinks.map((link) => (
|
||||
<div class="link-box">
|
||||
<a href={link.link} title={link.title}>
|
||||
<strong>{link.title}</strong>
|
||||
</a>
|
||||
{link.author && (
|
||||
<>
|
||||
{" via "}
|
||||
<a href={link.author.url}>{link.author.name}</a>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<Paginator pagination={pagination} />
|
||||
</Layout>
|
180
src/pages/music/artists/[slug].astro
Normal file
180
src/pages/music/artists/[slug].astro
Normal file
|
@ -0,0 +1,180 @@
|
|||
---
|
||||
import { parseISO, format } from "date-fns";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Modal from "@components/blocks/Modal.astro";
|
||||
import ToggleContent from "@components/utils/ToggleContent.astro";
|
||||
import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
import { parseCountries } from "@utils/helpers/media.js";
|
||||
import { md } from "@utils/helpers/general.js";
|
||||
|
||||
const { artist, globals } = await fetchGlobalData(Astro, Astro.url.pathname);
|
||||
|
||||
if (!artist) return Astro.redirect("/404", 404);
|
||||
|
||||
const {
|
||||
IconArrowLeft,
|
||||
IconHeart,
|
||||
IconNeedle,
|
||||
IconMapPin,
|
||||
IconDeviceSpeaker,
|
||||
} = icons;
|
||||
const pageTitle = `${artist.name} / Music`;
|
||||
const description = artist.description || `Learn more about ${artist.name}`;
|
||||
const alt = `${artist.name} / ${parseCountries(artist.country)}`;
|
||||
const playLabel = artist.total_plays === 1 ? "play" : "plays";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
{artist.image}
|
||||
>
|
||||
<a href="/music" class="back-link">
|
||||
<div set:html={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=w400&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">
|
||||
<div set:html={IconMapPin({ size: 18 })}/>
|
||||
{parseCountries(artist.country)}
|
||||
</span>
|
||||
{
|
||||
artist.favorite && (
|
||||
<span class="sub-meta favorite">
|
||||
<div set:html={IconHeart({ size: 18 })}/> This is one of my favorite artists!
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
artist.tattoo && (
|
||||
<span class="sub-meta tattoo">
|
||||
<div set:html={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">
|
||||
{artist.emoji || artist.genre_emoji}
|
||||
<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={md(artist.description)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
artist.concerts && (
|
||||
<>
|
||||
<div id="concerts" class="concerts media-title">
|
||||
<div set:html={IconDeviceSpeaker({ size: 18 })}/> I've seen this artist live!
|
||||
</div>
|
||||
<ul>
|
||||
{artist.concerts.map((concert, index) => (
|
||||
<li key={index}>
|
||||
On{" "}
|
||||
<strong class="highlight-text">{format(parseISO(concert.date), "MMM d, yyyy")}</strong>
|
||||
{concert.venue_name_short && (
|
||||
<>
|
||||
{" "}
|
||||
at{" "}
|
||||
{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>
|
||||
) : (
|
||||
<span>{concert.venue_name_short}</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{concert.notes && (
|
||||
<>
|
||||
{" "}
|
||||
<Modal
|
||||
id={`modal-${index}`}
|
||||
content={`### Notes\n${concert.notes}`}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</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>
|
90
src/pages/music/concerts/[...page].astro
Normal file
90
src/pages/music/concerts/[...page].astro
Normal file
|
@ -0,0 +1,90 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Paginator from "@components/nav/Paginator.astro";
|
||||
import Modal from "@components/blocks/Modal.astro";
|
||||
import { fetchConcerts } from "@utils/data/concerts.js";
|
||||
import { parseISO, format } from "date-fns";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const getStaticPaths = async ({ paginate }) => {
|
||||
const concerts = await fetchConcerts();
|
||||
return paginate(concerts, {
|
||||
pageSize: 30,
|
||||
});
|
||||
};
|
||||
|
||||
const { page } = Astro.props;
|
||||
|
||||
const paginatedConcerts = page.data;
|
||||
const pagination = {
|
||||
currentPage: page.currentPage,
|
||||
totalPages: page.lastPage,
|
||||
hasPrevious: page.currentPage > 1,
|
||||
hasNext: page.currentPage < page.lastPage,
|
||||
previousPage: page.url.prev || null,
|
||||
nextPage: page.url.next || null,
|
||||
pages: Array.from({ length: page.lastPage }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href: i === 0 ? `/links` : `/links/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
|
||||
const pageTitle =
|
||||
pagination.currentPage === 1
|
||||
? "Concerts"
|
||||
: `Concerts / page ${pagination.currentPage}`;
|
||||
const description =
|
||||
"These are concerts I've attended (not all of them — just the ones I could remember or glean from emails, photo metadata et al). I've been to at least " +
|
||||
page.data.length +
|
||||
" shows.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
currentUrl={Astro.url.pathname}
|
||||
>
|
||||
{page.currentPage === 1 && (
|
||||
<>
|
||||
<h2 class="page-title">{pageTitle}</h2>
|
||||
<p>These are concerts I've attended (not all of them — just the ones I could remember or glean from emails, photo metadata et al). I've been to at least <strong class="highlight-text">{page.total}</strong> shows.</p>
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
|
||||
<ul class="concert-list">
|
||||
{paginatedConcerts.map((concert) => (
|
||||
<li>
|
||||
{concert.artist.url ? (
|
||||
<a href={concert.artist.url}>{concert.artist.name}</a>
|
||||
) : (
|
||||
<span>{concert.artist.name}</span>
|
||||
)}{" "}
|
||||
on{" "}
|
||||
<strong class="highlight-text">{format(parseISO(concert.date), "PPPP")}</strong>
|
||||
{concert.venue?.name && (
|
||||
<>
|
||||
{" at "}
|
||||
{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 || concert.venue.name}</a>
|
||||
) : (
|
||||
<span>{concert.venue.name_short || concert.venue.name}</span>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{concert.notes && (
|
||||
<Modal
|
||||
id={`modal-${concert.id}`}
|
||||
icon="info-circle"
|
||||
content={`### Notes\n${concert.notes}`}
|
||||
/>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
<Paginator pagination={pagination} />
|
||||
</Layout>
|
72
src/pages/music/genres/[slug].astro
Normal file
72
src/pages/music/genres/[slug].astro
Normal file
|
@ -0,0 +1,72 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
import { mediaLinks } from "@utils/helpers/media.js";
|
||||
import { md } from "@utils/helpers/general.js";
|
||||
|
||||
const { genre } = await fetchGlobalData(Astro, Astro.url.pathname);
|
||||
if (!genre) return Astro.redirect("/404", 404);
|
||||
|
||||
const { IconArrowLeft } = icons;
|
||||
const artistCount = genre.artists?.length || 0;
|
||||
const connectingWords = artistCount > 1 ? "artists are" : "artist is";
|
||||
const genreMediaLinks = mediaLinks(genre.artists, "artist", 5);
|
||||
const pageTitle = `${genre.name} / Music`;
|
||||
const description = `Discover the music genre ${genre.name}, featuring ${artistCount} artists and ${genre.total_plays} total track plays.`;
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={genre.artists[0].image}
|
||||
>
|
||||
<a class="back-link" href="/music" title="Go back to the music index page">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to music
|
||||
</a>
|
||||
<h2>{genre.name}</h2>
|
||||
<article class="genre-focus">
|
||||
{
|
||||
genreMediaLinks && (
|
||||
<>
|
||||
<p>
|
||||
My top <strong class="highlight-text">{genre.name}</strong>{" "}
|
||||
{connectingWords} <span set:html={genreMediaLinks}></span> I've listened to{" "}
|
||||
<strong class="highlight-text">{genre.total_plays}</strong>{" "}
|
||||
tracks from this genre.
|
||||
</p>
|
||||
<hr />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<AssociatedMedia
|
||||
books={genre.books}
|
||||
movies={genre.movies}
|
||||
posts={genre.posts}
|
||||
/>
|
||||
{
|
||||
genre.description && (
|
||||
<>
|
||||
<h3>Overview</h3>
|
||||
<div data-toggle-content class="text-toggle-hidden">
|
||||
<div set:html={md(genre.description)} />
|
||||
<p>
|
||||
<a href={genre.wiki_link}>Continue reading at Wikipedia.</a>
|
||||
</p>
|
||||
<p>
|
||||
<em>
|
||||
Wikipedia content provided under the terms of the{" "}
|
||||
<a href="https://creativecommons.org/licenses/by-sa/3.0/">
|
||||
Creative Commons BY-SA license
|
||||
</a>
|
||||
</em>
|
||||
</p>
|
||||
</div>
|
||||
<button data-toggle-button>Show more</button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</article>
|
||||
</Layout>
|
124
src/pages/music/index.astro
Normal file
124
src/pages/music/index.astro
Normal file
|
@ -0,0 +1,124 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Grid from "@components/media/Grid.astro";
|
||||
import Recent from "@components/media/music/Recent.astro";
|
||||
import Chart from "@components/media/music/Chart.astro";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
import { fetchMusicWeek } from "@utils/data/music/week.js";
|
||||
import { fetchAlbumReleases } from "@utils/data/albumReleases.js";
|
||||
import { mediaLinks } from "@utils/helpers/media.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
const {
|
||||
IconMicrophone2,
|
||||
IconVinyl,
|
||||
IconPlaylist,
|
||||
IconCalendarTime,
|
||||
} = icons;
|
||||
const { globals } = await fetchGlobalData(Astro);
|
||||
const [music, albumReleases ] = await Promise.all([
|
||||
fetchMusicWeek(),
|
||||
fetchAlbumReleases(),
|
||||
]);
|
||||
|
||||
const title = "Music";
|
||||
const description =
|
||||
"This is everything I've been listening to recently — it's collected in a database as I listen to it and displayed here.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={title}
|
||||
description={description}
|
||||
currentUrl={Astro.url.pathname}
|
||||
ogImage={music.week.artists[0].artist_art}
|
||||
>
|
||||
<h2 class="page-title">{title}</h2>
|
||||
<p>
|
||||
I've listened to{" "}
|
||||
<strong class="highlight-text">
|
||||
{music.week.artists.length} artists
|
||||
</strong>
|
||||
,{" "}
|
||||
<strong class="highlight-text">
|
||||
{music.week.albums.length} albums
|
||||
</strong>
|
||||
, and{" "}
|
||||
<strong class="highlight-text">
|
||||
{music.week.totalTracks} tracks
|
||||
</strong>{" "}
|
||||
this week. Most of that has been{" "}
|
||||
<span set:html={mediaLinks(music.week.genres, "genre", 5)}></span>.
|
||||
</p>
|
||||
<p>
|
||||
<strong class="highlight-text">Take a look at what I've listened to</strong
|
||||
>{" "}
|
||||
<a href="/music/this-month">this month</a> or{" "}
|
||||
<a href="/music/concerts">check out the concerts I've been to</a>.
|
||||
</p>
|
||||
<hr />
|
||||
<h3 id="artists">
|
||||
<a href="/music/this-week/artists">
|
||||
<div set:html={IconMicrophone2({ size: 24 })}/> Artists
|
||||
</a>
|
||||
</h3>
|
||||
<Grid
|
||||
globals={globals}
|
||||
data={music.week.artists}
|
||||
shape="square"
|
||||
count={8}
|
||||
loading="eager"
|
||||
/>
|
||||
<h3 id="albums">
|
||||
<a href="/music/this-week/artists">
|
||||
<div set:html={IconVinyl({ size: 24 })}/> Albums
|
||||
</a>
|
||||
</h3>
|
||||
<Grid globals={globals} data={music.week.albums} shape="square" count={8} />
|
||||
<h3 id="tracks">
|
||||
<div set:html={IconPlaylist({ size: 24 })}/> Tracks
|
||||
</h3>
|
||||
<div>
|
||||
<input
|
||||
id="tracks-recent"
|
||||
name="track-options"
|
||||
type="radio"
|
||||
aria-hidden="true"
|
||||
checked
|
||||
/>
|
||||
<input
|
||||
id="tracks-chart"
|
||||
name="track-options"
|
||||
type="radio"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<label for="tracks-recent" class="button" data-toggle="tracks-recent">
|
||||
Recent
|
||||
</label>
|
||||
<label for="tracks-chart" class="button" data-toggle="tracks-chart">
|
||||
This week
|
||||
</label>
|
||||
<div class="tracks-recent">
|
||||
<Recent globals={globals} data={music.recent} />
|
||||
</div>
|
||||
<div class="tracks-chart">
|
||||
<Chart data={music.week.tracks} count={10} />
|
||||
</div>
|
||||
</div>
|
||||
{
|
||||
albumReleases.upcoming.length > 0 && (
|
||||
<>
|
||||
<h3 id="album-releases">
|
||||
<div set:html={IconCalendarTime({ size: 24 })}/> Anticipated albums
|
||||
</h3>
|
||||
<Grid
|
||||
globals={globals}
|
||||
data={albumReleases.upcoming}
|
||||
shape="square"
|
||||
count={8}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</Layout>
|
27
src/pages/music/releases.ics.js
Normal file
27
src/pages/music/releases.ics.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
import { albumReleasesCalendar } from "@utils/albumReleasesCalendar.js";
|
||||
import { fetchAlbumReleases } from "@utils/data/albumReleases.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const { all: albumReleases } = await fetchAlbumReleases();
|
||||
const icsContent = await albumReleasesCalendar(albumReleases);
|
||||
|
||||
if (!icsContent)
|
||||
return new Response("Error generating ICS file", { status: 500 });
|
||||
|
||||
return new Response(icsContent, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/calendar",
|
||||
"Content-Disposition": 'attachment; filename="releases.ics"',
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error generating album releases ICS file:", error);
|
||||
return new Response("Error generating album releases ICS file", {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
71
src/pages/music/this-month/albums/[...page].astro
Normal file
71
src/pages/music/this-month/albums/[...page].astro
Normal file
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Grid from "@components/media/Grid.astro";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
import { fetchMusicMonth } from "@utils/data/music/month.js";
|
||||
import { shuffleArray } from "@utils/helpers/general.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const getStaticPaths = async ({ paginate }) => {
|
||||
const music = await fetchMusicMonth();
|
||||
const monthlyAlbums = music.month.albums;
|
||||
|
||||
return paginate(monthlyAlbums, {
|
||||
pageSize: 16,
|
||||
});
|
||||
};
|
||||
|
||||
const { IconArrowLeft } = icons;
|
||||
const { page } = Astro.props;
|
||||
const paginatedAlbums = page.data;
|
||||
const pagination = {
|
||||
currentPage: page.currentPage,
|
||||
totalPages: page.lastPage,
|
||||
hasPrevious: page.currentPage > 1,
|
||||
hasNext: page.currentPage < page.lastPage,
|
||||
previousPage: page.url.prev || null,
|
||||
nextPage: page.url.next || null,
|
||||
pages: Array.from({ length: page.lastPage }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href:
|
||||
i === 0 ? `/music/this-month/albums` : `/music/this-month/albums/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
|
||||
const pageTitle =
|
||||
pagination.currentPage === 1
|
||||
? "Albums I've listened to this month"
|
||||
: `Albums I've listened to this month / page ${pagination.currentPage}`;
|
||||
const description =
|
||||
"These are the albums I've been listening to this month. All of them are awesome.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={shuffleArray(page.data)[0]?.album_art}
|
||||
>
|
||||
<a href="/music" class="back-link">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to Music
|
||||
</a>
|
||||
{
|
||||
pagination.currentPage === 1 && (
|
||||
<>
|
||||
<h2 class="page-title">{pageTitle}</h2>
|
||||
<p>{description} Listed in descending order from most plays to least.</p>
|
||||
<p><strong class="highlight-text">You can also take a look at</strong> the <a href="/music/this-month/artists">artists I've listened to this month</a>, <a href="/music/this-week/artists">the artists I've listened to this week</a> or <a href="/music/this-week/albums">the albums I've listened to this week</a>.</p>
|
||||
<p><a href="/music/concerts">I keep track of the concerts I've been to too</a>.</p>
|
||||
<hr />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Grid
|
||||
data={paginatedAlbums}
|
||||
pagination={pagination}
|
||||
shape="square"
|
||||
count={16}
|
||||
loading="eager"
|
||||
/>
|
||||
</Layout>
|
69
src/pages/music/this-month/artists/[...page].astro
Normal file
69
src/pages/music/this-month/artists/[...page].astro
Normal file
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Grid from "@components/media/Grid.astro";
|
||||
import { fetchMusicMonth } from "@utils/data/music/month.js";
|
||||
import { shuffleArray } from "@utils/helpers/general.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const getStaticPaths = async ({ paginate }) => {
|
||||
const music = await fetchMusicMonth();
|
||||
const monthlyArtists = music.month.artists;
|
||||
|
||||
return paginate(monthlyArtists, {
|
||||
pageSize: 16,
|
||||
});
|
||||
};
|
||||
|
||||
const { IconArrowLeft } = icons;
|
||||
const { page } = Astro.props;
|
||||
const paginatedArtists = page.data;
|
||||
const pagination = {
|
||||
currentPage: page.currentPage,
|
||||
totalPages: page.lastPage,
|
||||
hasPrevious: page.currentPage > 1,
|
||||
hasNext: page.currentPage < page.lastPage,
|
||||
previousPage: page.url.prev || null,
|
||||
nextPage: page.url.next || null,
|
||||
pages: Array.from({ length: page.lastPage }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href:
|
||||
i === 0
|
||||
? `/music/this-month/artists`
|
||||
: `/music/this-month/artists/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
const pageTitle =
|
||||
pagination.currentPage === 1
|
||||
? "Artists I've listened to this month"
|
||||
: `Artists I've listened to this month / page ${pagination.currentPage}`;
|
||||
const description = "These are the artists I've been listening to this month. All of them are awesome.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={shuffleArray(page.data)[0]?.artist_art}
|
||||
>
|
||||
<a href="/music" class="back-link">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to Music
|
||||
</a>
|
||||
{pagination.currentPage === 1 && (
|
||||
<>
|
||||
<h2 class="page-title">{pageTitle}</h2>
|
||||
<p>{description} Listed in descending order from most plays to least.</p>
|
||||
<p><strong class="highlight-text">You can also take a look at</strong> the <a href="/music/this-month/albums">albums I've listened to this month</a>, <a href="/music/this-week/artists">the artists I've listened to this week</a> or <a href="/music/this-week/albums">the albums I've listened to this week</a>.</p>
|
||||
<p><a href="/music/concerts">I keep track of the concerts I've been to too</a>.</p>
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
<Grid
|
||||
data={paginatedArtists}
|
||||
pagination={pagination}
|
||||
shape="square"
|
||||
count={16}
|
||||
loading="eager"
|
||||
/>
|
||||
</Layout>
|
71
src/pages/music/this-month/index.astro
Normal file
71
src/pages/music/this-month/index.astro
Normal file
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Grid from "@components/media/Grid.astro";
|
||||
import Chart from "@components/media/music/Chart.astro";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
import { fetchMusicMonth } from "@utils/data/music/month.js";
|
||||
import { mediaLinks } from "@utils/helpers/media.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
const { IconMicrophone2, IconVinyl, IconPlaylist } = icons;
|
||||
const { globals } = await fetchGlobalData(Astro);
|
||||
const music = await fetchMusicMonth();
|
||||
|
||||
const title = "This month / music";
|
||||
const description =
|
||||
"This is everything I've been listening to this month — it's collected in a database as I listen to it and displayed here.";
|
||||
|
||||
const artistCount = music.month.artists.length;
|
||||
const albumCount = music.month.albums.length;
|
||||
const totalTracks = music.month.totalTracks;
|
||||
const topGenres = mediaLinks(music.month.genres, "genre", 5);
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={title}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={music.month.artists[0].image}
|
||||
>
|
||||
<h2 class="page-title">{title}</h2>
|
||||
<p>
|
||||
I've listened to <strong class="highlight-text"
|
||||
>{artistCount} artists</strong
|
||||
>,
|
||||
<strong class="highlight-text">{albumCount} albums</strong> and
|
||||
<strong class="highlight-text">{totalTracks} tracks</strong> this month. Most
|
||||
of that has been <span set:html={topGenres} />.
|
||||
</p>
|
||||
<p>
|
||||
<strong class="highlight-text">Take a look at what I've listened to</strong>
|
||||
<a href="/music">this week</a> or <a href="/music/concerts"
|
||||
>check out the concerts I've been to</a
|
||||
>.
|
||||
</p>
|
||||
<hr />
|
||||
<h3 id="artists">
|
||||
<a href="/music/this-month/artists">
|
||||
<div set:html={IconMicrophone2({ size: 18 })}/> Artists
|
||||
</a>
|
||||
</h3>
|
||||
<Grid
|
||||
globals={globals}
|
||||
data={music.month.artists}
|
||||
shape="square"
|
||||
count={8}
|
||||
loading="eager"
|
||||
/>
|
||||
<h3 id="albums">
|
||||
<a href="/music/this-month/albums">
|
||||
<div set:html={IconVinyl({ size: 18 })}/> Albums
|
||||
</a>
|
||||
</h3>
|
||||
<Grid globals={globals} data={music.month.albums} shape="square" count={8} />
|
||||
|
||||
<h3 id="tracks">
|
||||
<div set:html={IconPlaylist({ size: 18 })}/> Tracks
|
||||
</h3>
|
||||
<Chart data={music.month.tracks} count={10} />
|
||||
</Layout>
|
71
src/pages/music/this-week/albums/[...page].astro
Normal file
71
src/pages/music/this-week/albums/[...page].astro
Normal file
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Grid from "@components/media/Grid.astro";
|
||||
import { fetchMusicWeek } from "@utils/data/music/week.js";
|
||||
import { shuffleArray } from "@utils/helpers/general.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const getStaticPaths = async ({ paginate }) => {
|
||||
const music = await fetchMusicWeek();
|
||||
const weeklyAlbums = music.week.albums;
|
||||
|
||||
return paginate(weeklyAlbums, {
|
||||
pageSize: 16,
|
||||
});
|
||||
};
|
||||
|
||||
const { IconArrowLeft } = icons;
|
||||
const { page } = Astro.props;
|
||||
const paginatedAlbums = page.data;
|
||||
const pagination = {
|
||||
currentPage: page.currentPage,
|
||||
totalPages: page.lastPage,
|
||||
hasPrevious: page.currentPage > 1,
|
||||
hasNext: page.currentPage < page.lastPage,
|
||||
previousPage: page.url.prev || null,
|
||||
nextPage: page.url.next || null,
|
||||
pages: Array.from({ length: page.lastPage }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href:
|
||||
i === 0 ? `/music/this-week/albums` : `/music/this-week/albums/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
|
||||
const pageTitle =
|
||||
pagination.currentPage === 1
|
||||
? "Albums I've listened to this week"
|
||||
: `Albums I've listened to this week / page ${pagination.currentPage}`;
|
||||
const description =
|
||||
"These are the albums I've been listening to this week. All of them are awesome.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={shuffleArray(page.data)[0]?.album_art}
|
||||
>
|
||||
<a href="/music" class="back-link">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to Music
|
||||
</a>
|
||||
{
|
||||
pagination.currentPage === 1 && (
|
||||
<>
|
||||
<h2 class="page-title">{pageTitle}</h2>
|
||||
<p>{description} Listed in descending order from most plays to least.</p>
|
||||
<p><strong class="highlight-text">You can also take a look at</strong> the <a href="/music/this-week/artists">artists I've listened to this week</a>, <a href="/music/this-month/artists">the artists I've listened to this month</a> or <a href="/music/this-month/albums">the albums I've listened to this month</a>.</p>
|
||||
<p><a href="/music/concerts">I keep track of the concerts I've been to too</a>.</p>
|
||||
<hr />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Grid
|
||||
data={paginatedAlbums}
|
||||
pagination={pagination}
|
||||
shape="square"
|
||||
count={16}
|
||||
loading="eager"
|
||||
/>
|
||||
</Layout>
|
69
src/pages/music/this-week/artists/[...page].astro
Normal file
69
src/pages/music/this-week/artists/[...page].astro
Normal file
|
@ -0,0 +1,69 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Grid from "@components/media/Grid.astro";
|
||||
import { fetchMusicWeek } from "@utils/data/music/week.js";
|
||||
import { shuffleArray } from "@utils/helpers/general.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const getStaticPaths = async ({ paginate }) => {
|
||||
const music = await fetchMusicWeek();
|
||||
const weeklyArtists = music.week.artists;
|
||||
|
||||
return paginate(weeklyArtists, {
|
||||
pageSize: 16,
|
||||
});
|
||||
};
|
||||
|
||||
const { IconArrowLeft } = icons;
|
||||
const { page } = Astro.props;
|
||||
const paginatedArtists = page.data;
|
||||
const pagination = {
|
||||
currentPage: page.currentPage,
|
||||
totalPages: page.lastPage,
|
||||
hasPrevious: page.currentPage > 1,
|
||||
hasNext: page.currentPage < page.lastPage,
|
||||
previousPage: page.url.prev || null,
|
||||
nextPage: page.url.next || null,
|
||||
pages: Array.from({ length: page.lastPage }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href:
|
||||
i === 0
|
||||
? `/music/this-week/artists`
|
||||
: `/music/this-week/artists/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
const pageTitle =
|
||||
pagination.currentPage === 1
|
||||
? "Artists I've listened to this week"
|
||||
: `Artists I've listened to this week / page ${pagination.currentPage}`;
|
||||
const description = "These are the artists I've been listening to this week. All of them are awesome.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={shuffleArray(page.data)[0]?.artist_art}
|
||||
>
|
||||
<a href="/music" class="back-link">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to Music
|
||||
</a>
|
||||
{pagination.currentPage === 1 && (
|
||||
<>
|
||||
<h2 class="page-title">{pageTitle}</h2>
|
||||
<p>{description} Listed in descending order from most plays to least.</p>
|
||||
<p><strong class="highlight-text">You can also take a look at</strong> the <a href="/music/this-week/albums">albums I've listened to this week</a>, <a href="/music/this-month/artists">the artists I've listened to this month</a> or <a href="/music/this-month/albums">the albums I've listened to this month</a>.</p>
|
||||
<p><a href="/music/concerts">I keep track of the concerts I've been to too</a>.</p>
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
<Grid
|
||||
data={paginatedArtists}
|
||||
pagination={pagination}
|
||||
shape="square"
|
||||
count={16}
|
||||
loading="eager"
|
||||
/>
|
||||
</Layout>
|
64
src/pages/posts/[...page].astro
Normal file
64
src/pages/posts/[...page].astro
Normal file
|
@ -0,0 +1,64 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
import { fetchAllPosts } from "@data/posts.js";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Paginator from "@components/nav/Paginator.astro";
|
||||
import { md } from "@utils/helpers/general.js";
|
||||
import { parseISO, format } from "date-fns";
|
||||
|
||||
export const prerender = true;
|
||||
export const getStaticPaths = async ({ paginate }) => {
|
||||
const posts = await fetchAllPosts();
|
||||
const sortedPosts = posts.sort((a, b) => new Date(b.date) - new Date(a.date));
|
||||
|
||||
return paginate(sortedPosts, {
|
||||
pageSize: 15,
|
||||
});
|
||||
};
|
||||
|
||||
const { IconStar } = icons;
|
||||
const { page } = Astro.props;
|
||||
const paginatedPosts = page.data;
|
||||
const pagination = {
|
||||
currentPage: page.currentPage,
|
||||
totalPages: page.lastPage,
|
||||
hasPrevious: page.currentPage > 1,
|
||||
hasNext: page.currentPage < page.lastPage,
|
||||
previousPage: page.url.prev || null,
|
||||
nextPage: page.url.next || null,
|
||||
pages: Array.from({ length: page.lastPage }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href: i === 0 ? `/posts` : `/posts/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
const pageTitle =
|
||||
pagination.currentPage === 1
|
||||
? "Posts"
|
||||
: `Posts / page ${pagination.currentPage}`;
|
||||
const description =
|
||||
"These are posts I've written. They're all added manually, after having been written and, I suppose, properly considered.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
currentUrl={Astro.url.pathname}
|
||||
>
|
||||
{
|
||||
paginatedPosts.map((post) => (
|
||||
<article>
|
||||
<div class="post-meta">
|
||||
{post.featured && <div set:html={IconStar({ size: 16 })}/>}
|
||||
<time datetime={post.date}>
|
||||
{format(parseISO(post.date), "PPPP")}
|
||||
</time>
|
||||
</div>
|
||||
<h3>
|
||||
<a href={post.url}>{post.title}</a>
|
||||
</h3>
|
||||
<p set:html={md(post.description)} />
|
||||
</article>
|
||||
))
|
||||
}
|
||||
<Paginator pagination={pagination} />
|
||||
</Layout>
|
110
src/pages/posts/[year]/[title].astro
Normal file
110
src/pages/posts/[year]/[title].astro
Normal file
|
@ -0,0 +1,110 @@
|
|||
---
|
||||
import { fetchAllPosts } from "@data/posts.js";
|
||||
import { fetchAnalyticsData } from "@data/analytics.js";
|
||||
import { fetchLinks } from "@data/links.js";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
import { md } from "@utils/helpers/general.js";
|
||||
import { getPopularPosts } from "@utils/getPopularPosts.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconStar } = icons;
|
||||
const [analytics, links, posts] = await Promise.all([
|
||||
fetchAnalyticsData(),
|
||||
fetchLinks(),
|
||||
fetchAllPosts(),
|
||||
]);
|
||||
const popularPosts = getPopularPosts(posts, analytics);
|
||||
|
||||
import AddonLinks from "@components/blocks/links/AddonLinks.astro";
|
||||
import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
|
||||
import BlockRenderer from "@components/blocks/BlockRenderer.astro";
|
||||
import Coffee from "@components/blocks/banners/Coffee.astro";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Mastodon from "@components/blocks/banners/Mastodon.astro";
|
||||
import OldPost from "@components/blocks/banners/OldPost.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 { globals } = await fetchGlobalData(Astro);
|
||||
const currentUrl = Astro.url.pathname;
|
||||
const htmlContent = md(post.content);
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={post.title}
|
||||
description={post.description}
|
||||
ogImage={post.open_graph_image}
|
||||
currentUrl={currentUrl}
|
||||
>
|
||||
<article class="standalone">
|
||||
<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>{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 && <BlockRenderer blocks={post.blocks} />}
|
||||
{post.mastodon_url && <Mastodon url={post.mastodon_url} />}
|
||||
<AssociatedMedia
|
||||
artists={post.artists}
|
||||
books={post.books}
|
||||
genres={post.genres}
|
||||
movies={post.movies}
|
||||
posts={post.posts}
|
||||
shows={post.shows}
|
||||
/>
|
||||
<Coffee />
|
||||
<AddonLinks popularPosts={popularPosts} links={links} />
|
||||
</div>
|
||||
</article>
|
||||
</Layout>
|
26
src/pages/robots.txt.js
Normal file
26
src/pages/robots.txt.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { fetchAllRobots } from '@utils//data/robots.js';
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export async function GET() {
|
||||
try {
|
||||
const robots = await fetchAllRobots();
|
||||
const sitemap = `Sitemap: https://coryd.dev/sitemap-index.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 });
|
||||
}
|
||||
}
|
278
src/pages/search.astro
Normal file
278
src/pages/search.astro
Normal file
|
@ -0,0 +1,278 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import AddonLinks from "@components/blocks/links/AddonLinks.astro";
|
||||
import { getPopularPosts } from "@utils/getPopularPosts.js";
|
||||
import { fetchAllPosts } from "@data/posts.js";
|
||||
import { fetchAnalyticsData } from "@data/analytics.js";
|
||||
import { fetchLinks } from "@data/links.js";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
const [analytics, links, posts] = await Promise.all([
|
||||
fetchAnalyticsData(),
|
||||
fetchLinks(),
|
||||
fetchAllPosts(),
|
||||
]);
|
||||
const popularPosts = getPopularPosts(posts, analytics);
|
||||
const title = "Search";
|
||||
const description =
|
||||
"Search for posts, links, artists, genres, movies, shows and books on my site.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={title}
|
||||
description={description}
|
||||
currentUrl={Astro.url.pathname}
|
||||
>
|
||||
<h2 class="page-title">Search</h2>
|
||||
<p>
|
||||
You can find <a href="/posts">posts</a>, <a href="/links">links</a>, <a
|
||||
href="/music/#artists">artists</a
|
||||
>, genres, <a href="/watching#movies">movies</a>, <a href="/watching#tv"
|
||||
>shows</a
|
||||
> and <a href="/books">books</a> via the field below (though it only surfaces
|
||||
movies and shows I've watched and books I've written something about).
|
||||
</p>
|
||||
<noscript>
|
||||
<p>
|
||||
<strong class="highlight-text"
|
||||
>If you're seeing this it means that you've (quite likely) disabled
|
||||
JavaScript (that's a totally valid choice!).</strong
|
||||
> You can search for anything on my site using the form below, but your query
|
||||
will be routed through <a href="https://duckduckgo.com">DuckDuckGo</a>.
|
||||
</p>
|
||||
<p>
|
||||
<strong class="highlight-text">Type something in and hit enter.</strong>
|
||||
</p>
|
||||
</noscript>
|
||||
<form class="search__form" action="https://duckduckgo.com" method="get">
|
||||
<input
|
||||
class="search__form--input"
|
||||
placeholder="Search"
|
||||
type="search"
|
||||
name="q"
|
||||
autocomplete="off"
|
||||
autofocus
|
||||
/>
|
||||
<details>
|
||||
<summary class="highlight-text">Filter by type</summary>
|
||||
<fieldset class="search__form--type">
|
||||
<label
|
||||
><input type="checkbox" name="type" value="post" checked /> Posts</label
|
||||
>
|
||||
<label
|
||||
><input type="checkbox" name="type" value="link" checked /> Links</label
|
||||
>
|
||||
<label
|
||||
><input type="checkbox" name="type" value="artist" checked /> Artists</label
|
||||
>
|
||||
<label
|
||||
><input type="checkbox" name="type" value="genre" checked /> Genres</label
|
||||
>
|
||||
<label
|
||||
><input type="checkbox" name="type" value="book" checked /> Books</label
|
||||
>
|
||||
<label
|
||||
><input type="checkbox" name="type" value="movie" checked /> Movies</label
|
||||
>
|
||||
<label
|
||||
><input type="checkbox" name="type" value="show" checked /> Shows</label
|
||||
>
|
||||
</fieldset>
|
||||
</details>
|
||||
<input
|
||||
class="search__form--fallback"
|
||||
type="hidden"
|
||||
name="sites"
|
||||
value="coryd.dev"
|
||||
/>
|
||||
</form>
|
||||
<ul class="search__results"></ul>
|
||||
<button class="search__load-more" style="display:none">Load More</button>
|
||||
<AddonLinks popularPosts={popularPosts} links={links} />
|
||||
</Layout>
|
||||
<script>
|
||||
import MiniSearch from "minisearch";
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
(() => {
|
||||
const miniSearch = new MiniSearch({
|
||||
fields: ["title", "description", "tags", "type"],
|
||||
idField: "id",
|
||||
storeFields: [
|
||||
"id",
|
||||
"title",
|
||||
"url",
|
||||
"description",
|
||||
"type",
|
||||
"tags",
|
||||
"total_plays",
|
||||
],
|
||||
searchOptions: {
|
||||
fields: ["title", "tags"],
|
||||
prefix: true,
|
||||
fuzzy: 0.1,
|
||||
boost: { title: 5, tags: 2, description: 1 },
|
||||
},
|
||||
});
|
||||
|
||||
const $form = document.querySelector(".search__form");
|
||||
const $fallback = document.querySelector(".search__form--fallback");
|
||||
const $input = document.querySelector(".search__form--input");
|
||||
const $results = document.querySelector(".search__results");
|
||||
const $loadMoreButton = document.querySelector(".search__load-more");
|
||||
const $typeCheckboxes = document.querySelectorAll(
|
||||
'.search__form--type input[type="checkbox"]'
|
||||
);
|
||||
|
||||
$form.removeAttribute("action");
|
||||
$form.removeAttribute("method");
|
||||
if ($fallback) $fallback.remove();
|
||||
|
||||
const PAGE_SIZE = 10;
|
||||
let currentPage = 1;
|
||||
let currentResults = [];
|
||||
let total = 0;
|
||||
let debounceTimeout;
|
||||
|
||||
const parseMarkdown = (markdown) =>
|
||||
markdown
|
||||
? markdown
|
||||
.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>")
|
||||
.replace(/\*(.*?)\*/g, "<em>$1</em>")
|
||||
.replace(/\[(.*?)\]\((.*?)\)/g, '<a href="$2">$1</a>')
|
||||
.replace(/\n/g, "<br>")
|
||||
.replace(/[#*_~`]/g, "")
|
||||
: "";
|
||||
|
||||
const truncateDescription = (markdown, maxLength = 150) => {
|
||||
const plainText =
|
||||
new DOMParser().parseFromString(parseMarkdown(markdown), "text/html")
|
||||
.body.textContent || "";
|
||||
return plainText.length > maxLength
|
||||
? `${plainText.substring(0, maxLength)}...`
|
||||
: plainText;
|
||||
};
|
||||
|
||||
const formatArtistTitle = (title, totalPlays) =>
|
||||
totalPlays > 0
|
||||
? `${title} <strong class="highlight-text">${totalPlays} plays</strong>`
|
||||
: title;
|
||||
|
||||
const renderSearchResults = (results) => {
|
||||
const resultHTML = results
|
||||
.map(
|
||||
({ title, url, description, type, total_plays }) => `
|
||||
<li class="search__results--result">
|
||||
<a href="${url}">
|
||||
<h3>${
|
||||
type === "artist" && total_plays
|
||||
? formatArtistTitle(title, total_plays)
|
||||
: title
|
||||
}</h3>
|
||||
</a>
|
||||
<p>${truncateDescription(description)}</p>
|
||||
</li>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
|
||||
$results.innerHTML =
|
||||
resultHTML ||
|
||||
'<li class="search__results--no-results">No results found.</li>';
|
||||
$results.style.display = "block";
|
||||
};
|
||||
|
||||
const loadSearchIndex = async (query, types, page) => {
|
||||
try {
|
||||
const typeQuery = types.join(",");
|
||||
const response = await fetch(
|
||||
`https://coryd.dev/api/search?q=${query}&type=${typeQuery}&page=${page}&pageSize=${PAGE_SIZE}`
|
||||
);
|
||||
const { results, total: newTotal } = await response.json();
|
||||
total = newTotal;
|
||||
|
||||
const formattedResults = results.map((item) => ({
|
||||
...item,
|
||||
id: item.result_id,
|
||||
}));
|
||||
miniSearch.removeAll();
|
||||
miniSearch.addAll(formattedResults);
|
||||
return formattedResults;
|
||||
} catch (error) {
|
||||
console.error("Error fetching search data:", error);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
const getSelectedTypes = () =>
|
||||
Array.from($typeCheckboxes)
|
||||
.filter((cb) => cb.checked)
|
||||
.map((cb) => cb.value);
|
||||
|
||||
const updateSearchResults = (results) => {
|
||||
if (currentPage === 1) {
|
||||
renderSearchResults(results);
|
||||
} else {
|
||||
appendSearchResults(results);
|
||||
}
|
||||
$loadMoreButton.style.display =
|
||||
currentPage * PAGE_SIZE < total ? "block" : "none";
|
||||
};
|
||||
|
||||
const appendSearchResults = (results) => {
|
||||
const newResultsHTML = results
|
||||
.map(
|
||||
({ title, url, description, type, total_plays }) => `
|
||||
<li class="search__results--result">
|
||||
<a href="${url}">
|
||||
<h3>${
|
||||
type === "artist" && total_plays
|
||||
? formatArtistTitle(title, total_plays)
|
||||
: title
|
||||
}</h3>
|
||||
</a>
|
||||
<p>${truncateDescription(description)}</p>
|
||||
</li>
|
||||
`
|
||||
)
|
||||
.join("");
|
||||
$results.insertAdjacentHTML("beforeend", newResultsHTML);
|
||||
};
|
||||
|
||||
const handleSearch = async () => {
|
||||
const query = $input.value.trim();
|
||||
if (!query) {
|
||||
renderSearchResults([]);
|
||||
$loadMoreButton.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
const results = await loadSearchIndex(query, getSelectedTypes(), 1);
|
||||
currentResults = results;
|
||||
currentPage = 1;
|
||||
updateSearchResults(results);
|
||||
};
|
||||
|
||||
$input.addEventListener("input", () => {
|
||||
clearTimeout(debounceTimeout);
|
||||
debounceTimeout = setTimeout(handleSearch, 300);
|
||||
});
|
||||
|
||||
$typeCheckboxes.forEach((cb) =>
|
||||
cb.addEventListener("change", handleSearch)
|
||||
);
|
||||
|
||||
$loadMoreButton.addEventListener("click", async () => {
|
||||
currentPage++;
|
||||
const nextResults = await loadSearchIndex(
|
||||
$input.value.trim(),
|
||||
getSelectedTypes(),
|
||||
currentPage
|
||||
);
|
||||
currentResults = [...currentResults, ...nextResults];
|
||||
updateSearchResults(nextResults);
|
||||
});
|
||||
})();
|
||||
});
|
||||
</script>
|
60
src/pages/watching/favorites/movies/[...page].astro
Normal file
60
src/pages/watching/favorites/movies/[...page].astro
Normal file
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Grid from "@components/media/Grid.astro";
|
||||
import { fetchMovies } from "@utils/data/movies.js";
|
||||
import { shuffleArray } from "@utils/helpers/general.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const getStaticPaths = async ({ paginate }) => {
|
||||
const movies = await fetchMovies();
|
||||
const favoriteMovies = movies.favorites;
|
||||
return paginate(favoriteMovies, {
|
||||
pageSize: 15,
|
||||
});
|
||||
};
|
||||
|
||||
const { IconArrowLeft } = icons;
|
||||
const { page } = Astro.props;
|
||||
const paginatedMovies = page.data;
|
||||
const pagination = {
|
||||
currentPage: page.currentPage,
|
||||
totalPages: page.lastPage,
|
||||
hasPrevious: page.currentPage > 1,
|
||||
hasNext: page.currentPage < page.lastPage,
|
||||
previousPage: page.url.prev || null,
|
||||
nextPage: page.url.next || null,
|
||||
pages: Array.from({ length: page.lastPage }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href: i === 0 ? `/watching/favorites/movies` : `/watching/favorites/movies/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
const pageTitle = pagination.currentPage === 1 ? "Favorite movies" : `Favorite movies / page ${pagination.currentPage}`;
|
||||
const description = "These are my favorite movies. There are many like them, but these are mine.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={shuffleArray(page.data)[0].backdrop}
|
||||
>
|
||||
<a href="/watching" class="back-link">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to watching
|
||||
</a>
|
||||
{pagination.currentPage === 1 && (
|
||||
<>
|
||||
<h2 class="page-title">Favorite Movies</h2>
|
||||
<p>{description} <a href="/watching/favorites/shows">You can check out my favorite shows here.</a></p>
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
<Grid
|
||||
data={paginatedMovies}
|
||||
pagination={pagination}
|
||||
shape="poster"
|
||||
count={15}
|
||||
loading="eager"
|
||||
/>
|
||||
</Layout>
|
60
src/pages/watching/favorites/shows/[...page].astro
Normal file
60
src/pages/watching/favorites/shows/[...page].astro
Normal file
|
@ -0,0 +1,60 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Grid from "@components/media/Grid.astro";
|
||||
import { fetchShows } from "@utils/data/tv.js";
|
||||
import { shuffleArray } from "@utils/helpers/general.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const getStaticPaths = async ({ paginate }) => {
|
||||
const shows = await fetchShows();
|
||||
const favoriteShows = shows.favorites;
|
||||
return paginate(favoriteShows, {
|
||||
pageSize: 15,
|
||||
});
|
||||
};
|
||||
|
||||
const { IconArrowLeft } = icons;
|
||||
const { page } = Astro.props;
|
||||
const paginatedShows = page.data;
|
||||
const pagination = {
|
||||
currentPage: page.currentPage,
|
||||
totalPages: page.lastPage,
|
||||
hasPrevious: page.currentPage > 1,
|
||||
hasNext: page.currentPage < page.lastPage,
|
||||
previousPage: page.url.prev || null,
|
||||
nextPage: page.url.next || null,
|
||||
pages: Array.from({ length: page.lastPage }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href: i === 0 ? `/watching/favorites/shows` : `/watching/favorites/shows/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
const pageTitle = pagination.currentPage === 1 ? "Favorite shows" : `Favorite movies / page ${pagination.currentPage}`;
|
||||
const description = "These are my favorite TV shows. There are many like them, but these are mine.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={shuffleArray(page.data)[0].backdrop}
|
||||
>
|
||||
<a href="/watching" class="back-link">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to watching
|
||||
</a>
|
||||
{pagination.currentPage === 1 && (
|
||||
<>
|
||||
<h2 class="page-title">{pageTitle}</h2>
|
||||
<p>{description} <a href="/watching/favorites/movies">You can check out my favorite movies here.</a></p>
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
<Grid
|
||||
data={paginatedShows}
|
||||
pagination={pagination}
|
||||
shape="poster"
|
||||
count={15}
|
||||
loading="eager"
|
||||
/>
|
||||
</Layout>
|
71
src/pages/watching/index.astro
Normal file
71
src/pages/watching/index.astro
Normal file
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Hero from "@components/media/watching/Hero.astro";
|
||||
import Grid from "@components/media/Grid.astro";
|
||||
import Rss from "@components/blocks/banners/Rss.astro";
|
||||
import { fetchMovies } from "@utils/data/movies.js";
|
||||
import { fetchShows } from "@utils/data/tv.js";
|
||||
import { shuffleArray } from "@utils/helpers/general.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
const { IconMovie, IconDeviceTvOld, IconStar } = icons;
|
||||
const movies = await fetchMovies();
|
||||
const tv = await fetchShows();
|
||||
const featuredMovie = shuffleArray(movies.recentlyWatched)[0];
|
||||
const favoriteMovies = shuffleArray(movies.favorites);
|
||||
const favoriteShows = shuffleArray(tv.favorites);
|
||||
|
||||
const title = "Currently watching";
|
||||
const description =
|
||||
"Here's all of the TV and movies I've been watching presented in what is (hopefully) an organized fashion.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={title}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={movies.recentlyWatched[0].backdrop}
|
||||
>
|
||||
<h2 class="page-title">{title}</h2>
|
||||
<Hero movie={featuredMovie} />
|
||||
<p>{description}</p>
|
||||
<Rss
|
||||
url="/feeds/movies.xml"
|
||||
text="Subscribe to my movies feed or follow along on this page"
|
||||
/>
|
||||
<hr />
|
||||
<h3 id="movies">
|
||||
<a href="/watching/recent/movies">
|
||||
<div set:html={IconMovie({ size: 18 })}/> Recent movies
|
||||
</a>
|
||||
</h3>
|
||||
<Grid
|
||||
data={movies.recentlyWatched}
|
||||
shape="vertical"
|
||||
count={6}
|
||||
/>
|
||||
<h3 id="tv">
|
||||
<a href="/watching/recent/shows">
|
||||
<div set:html={IconDeviceTvOld({ size: 18 })}/> Recent shows
|
||||
</a>
|
||||
</h3>
|
||||
<Grid
|
||||
data={tv.recentlyWatched}
|
||||
shape="vertical"
|
||||
count={6}
|
||||
/>
|
||||
<h3 id="favorite-movies">
|
||||
<a href="/watching/favorites/movies">
|
||||
<div set:html={IconStar({ size: 18 })}/> Favorite movies
|
||||
</a>
|
||||
</h3>
|
||||
<Grid data={favoriteMovies} shape="vertical" count={6} />
|
||||
<h3 id="favorite-shows">
|
||||
<a href="/watching/favorites/shows">
|
||||
<div set:html={IconStar({ size: 18 })}/> Favorite shows
|
||||
</a>
|
||||
</h3>
|
||||
<Grid data={favoriteShows} shape="vertical" count={6} />
|
||||
</Layout>
|
120
src/pages/watching/movies/[slug].astro
Normal file
120
src/pages/watching/movies/[slug].astro
Normal file
|
@ -0,0 +1,120 @@
|
|||
---
|
||||
import { parseISO, format } from "date-fns";;
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
|
||||
import Warning from "@components/blocks/banners/Warning.astro";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
import { md } from "@utils/helpers/general.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { globals, movie } = await fetchGlobalData(Astro, Astro.url.pathname);
|
||||
|
||||
if (!movie) return Astro.redirect("/404", 404);
|
||||
|
||||
const {
|
||||
IconArrowLeft,
|
||||
IconHeart,
|
||||
IconNeedle,
|
||||
IconCircleCheck,
|
||||
} = icons;
|
||||
const pageTitle = `${movie.title} / Movies`;
|
||||
const description = movie.description || `Details about ${movie.title}.`;
|
||||
const alt = `${movie.title} / ${movie.year}${movie.rating ? ` (${movie.rating})` : ""}`;
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={movie.backdrop}
|
||||
>
|
||||
<a class="back-link" href="/watching" title="Go back to the watching index page">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to watching
|
||||
</a>
|
||||
<article class="watching focus">
|
||||
<img
|
||||
srcset={`
|
||||
${globals.cdn_url}${movie.backdrop}?class=bannersm&type=webp 256w,
|
||||
${globals.cdn_url}${movie.backdrop}?class=bannermd&type=webp 512w,
|
||||
${globals.cdn_url}${movie.backdrop}?class=bannerbase&type=webp 1024w
|
||||
`}
|
||||
sizes="(max-width: 450px) 256px,
|
||||
(max-width: 850px) 512px,
|
||||
1024px"
|
||||
src={`${globals.cdn_url}${movie.backdrop}?class=bannersm&type=webp`}
|
||||
alt={alt}
|
||||
class="image-banner"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
width="256"
|
||||
height="180"
|
||||
/>
|
||||
<div class="media-meta">
|
||||
<span class="title">
|
||||
<strong>{movie.title}</strong>
|
||||
{movie.year && !movie.rating && ` (${movie.year})`}
|
||||
</span>
|
||||
{
|
||||
movie.rating && (
|
||||
<span>
|
||||
{movie.rating}
|
||||
{movie.year && ` (${movie.year})`}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
movie.favorite && (
|
||||
<span class="sub-meta favorite">
|
||||
<div set:html={IconHeart({ size: 18 })}/> This is one of my favorite movies!
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
movie.tattoo && (
|
||||
<span class="sub-meta tattoo">
|
||||
<div set:html={IconNeedle({ size: 18 })}/> I have a tattoo inspired by this movie!
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
movie.collected && (
|
||||
<span class="sub-meta collected">
|
||||
<div set:html={IconCircleCheck({ size: 18 })}/> This movie is in my collection!
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
movie.lastWatched && (
|
||||
<span class="sub-meta">
|
||||
Last watched on{" "}
|
||||
{format(parseISO(movie.lastWatched), "PPPP")}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{movie.review && (
|
||||
<>
|
||||
<h2>My thoughts</h2>
|
||||
<Warning text="There are probably spoilers after this banner — this is a warning about them." />
|
||||
<div set:html={md(movie.review)}/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<AssociatedMedia
|
||||
artists={movie.artists}
|
||||
books={movie.books}
|
||||
genres={movie.genres}
|
||||
movies={movie.related_movies}
|
||||
posts={movie.posts}
|
||||
shows={movie.shows}
|
||||
/>
|
||||
|
||||
{movie.description && (
|
||||
<>
|
||||
<h2>Overview</h2>
|
||||
<div set:html={md(movie.description)}/>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
</Layout>
|
66
src/pages/watching/recent/movies/[...page].astro
Normal file
66
src/pages/watching/recent/movies/[...page].astro
Normal file
|
@ -0,0 +1,66 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Grid from "@components/media/Grid.astro";
|
||||
import { fetchMovies } from "@utils/data/movies.js";
|
||||
import { shuffleArray } from "@utils/helpers/general.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const getStaticPaths = async ({ paginate }) => {
|
||||
const movies = await fetchMovies();
|
||||
const recentlyWatchedMovies = movies.recentlyWatched;
|
||||
return paginate(recentlyWatchedMovies, {
|
||||
pageSize: 15,
|
||||
});
|
||||
};
|
||||
|
||||
const { IconArrowLeft } = icons;
|
||||
const { page } = Astro.props;
|
||||
const paginatedMovies = page.data;
|
||||
const pagination = {
|
||||
currentPage: page.currentPage,
|
||||
totalPages: page.lastPage,
|
||||
hasPrevious: page.currentPage > 1,
|
||||
hasNext: page.currentPage < page.lastPage,
|
||||
previousPage: page.url.prev || null,
|
||||
nextPage: page.url.next || null,
|
||||
pages: Array.from({ length: page.lastPage }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href: i === 0 ? `/watching/recent/movies` : `/watching/recent/movies/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
const pageTitle =
|
||||
pagination.currentPage === 1
|
||||
? "Recently watched movies"
|
||||
: `Recently watched movies / page ${pagination.currentPage}`;
|
||||
const description =
|
||||
"All of the movies I've watched recently. Some are good, some are terrible. A few are great.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={shuffleArray(page.data)[0].backdrop}
|
||||
>
|
||||
<a href="/watching" class="back-link">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to watching
|
||||
</a>
|
||||
{
|
||||
pagination.currentPage === 1 && (
|
||||
<>
|
||||
<h2 class="page-title">{pageTitle}</h2>
|
||||
<p>{description} Listed in descending order from most recent to least. <a href="/watching/recent/shows">You can check out my recently watched shows here.</a></p>
|
||||
<hr />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Grid
|
||||
data={paginatedMovies}
|
||||
pagination={pagination}
|
||||
shape="poster"
|
||||
count={15}
|
||||
loading="eager"
|
||||
/>
|
||||
</Layout>
|
67
src/pages/watching/recent/shows/[...page].astro
Normal file
67
src/pages/watching/recent/shows/[...page].astro
Normal file
|
@ -0,0 +1,67 @@
|
|||
---
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import Grid from "@components/media/Grid.astro";
|
||||
import { fetchShows } from "@utils/data/tv.js";
|
||||
import { shuffleArray } from "@utils/helpers/general.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
export const prerender = true;
|
||||
|
||||
export const getStaticPaths = async ({ paginate }) => {
|
||||
const shows = await fetchShows();
|
||||
const recentlyWatchedShows = shows.recentlyWatched || [];
|
||||
|
||||
return paginate(recentlyWatchedShows, {
|
||||
pageSize: 15,
|
||||
});
|
||||
};
|
||||
|
||||
const { IconArrowLeft } = icons;
|
||||
const { page } = Astro.props;
|
||||
const paginatedShows = page.data;
|
||||
const pagination = {
|
||||
currentPage: page.currentPage,
|
||||
totalPages: page.lastPage,
|
||||
hasPrevious: page.currentPage > 1,
|
||||
hasNext: page.currentPage < page.lastPage,
|
||||
previousPage: page.url.prev || null,
|
||||
nextPage: page.url.next || null,
|
||||
pages: Array.from({ length: page.lastPage }, (_, i) => ({
|
||||
number: i + 1,
|
||||
href:
|
||||
i === 0 ? `/watching/recent/shows` : `/watching/recent/shows/${i + 1}`,
|
||||
})),
|
||||
};
|
||||
const pageTitle =
|
||||
pagination.currentPage === 1
|
||||
? "Recently watched shows"
|
||||
: `Recently watched shows / page ${pagination.currentPage}`;
|
||||
const description = "All of the shows I've watched recently.";
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={shuffleArray(page.data)[0].backdrop}
|
||||
>
|
||||
<a href="/watching" class="back-link">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to watching
|
||||
</a>
|
||||
{
|
||||
pagination.currentPage === 1 && (
|
||||
<>
|
||||
<h2 class="page-title">{pageTitle}</h2>
|
||||
<p>{description} Listed in descending order from most recent to least. <a href="/watching/recent/movies">You can check out my recently watched movies here.</a></p>
|
||||
<hr />
|
||||
</>
|
||||
)
|
||||
}
|
||||
<Grid
|
||||
data={paginatedShows}
|
||||
pagination={pagination}
|
||||
shape="poster"
|
||||
count={15}
|
||||
loading="eager"
|
||||
/>
|
||||
</Layout>
|
113
src/pages/watching/shows/[slug].astro
Normal file
113
src/pages/watching/shows/[slug].astro
Normal file
|
@ -0,0 +1,113 @@
|
|||
---
|
||||
import { parseISO, format } from "date-fns";
|
||||
import Layout from "@layouts/Layout.astro";
|
||||
import AssociatedMedia from "@components/blocks/AssociatedMedia.astro";
|
||||
import Warning from "@components/blocks/banners/Warning.astro";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
import { md } from "@utils/helpers/general.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { globals, show } = await fetchGlobalData(Astro, Astro.url.pathname);
|
||||
|
||||
if (!show) return Astro.redirect("/404", 404);
|
||||
|
||||
const {
|
||||
IconArrowLeft,
|
||||
IconHeart,
|
||||
IconNeedle,
|
||||
IconCircleCheck,
|
||||
} = icons;
|
||||
const pageTitle = `${show.title} / TV`;
|
||||
const description = show.description || `Details about ${show.title}.`;
|
||||
const alt = `${show.title} / ${show.year}`;
|
||||
---
|
||||
|
||||
<Layout
|
||||
pageTitle={pageTitle}
|
||||
description={description}
|
||||
fullUrl={Astro.url.pathname}
|
||||
ogImage={show.backdrop}
|
||||
>
|
||||
<a class="back-link" href="/watching" title="Go back to the watching index page">
|
||||
<div set:html={IconArrowLeft({ size: 18 })}/> Back to watching
|
||||
</a>
|
||||
<article class="watching focus">
|
||||
<img
|
||||
srcset={`
|
||||
${globals.cdn_url}${show.backdrop}?class=bannersm&type=webp 256w,
|
||||
${globals.cdn_url}${show.backdrop}?class=bannermd&type=webp 512w,
|
||||
${globals.cdn_url}${show.backdrop}?class=bannerbase&type=webp 1024w
|
||||
`}
|
||||
sizes="(max-width: 450px) 256px,
|
||||
(max-width: 850px) 512px,
|
||||
1024px"
|
||||
src={`${globals.cdn_url}${show.backdrop}?class=bannersm&type=webp`}
|
||||
alt={alt}
|
||||
class="image-banner"
|
||||
loading="eager"
|
||||
decoding="async"
|
||||
width="256"
|
||||
height="180"
|
||||
/>
|
||||
<div class="media-meta">
|
||||
<span class="title">
|
||||
<strong>{show.title}</strong>
|
||||
{show.year && ` (${show.year})`}
|
||||
</span>
|
||||
{
|
||||
show.favorite && (
|
||||
<span class="sub-meta favorite">
|
||||
<div set:html={IconHeart({ size: 18 })}/> This is one of my favorite shows!
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
show.tattoo && (
|
||||
<span class="sub-meta tattoo">
|
||||
<div set:html={IconNeedle({ size: 18 })}/> I have a tattoo inspired by this show!
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
show.collected && (
|
||||
<span class="sub-meta collected">
|
||||
<div set:html={IconCircleCheck({ size: 18 })}/> This show is in my collection!
|
||||
</span>
|
||||
)
|
||||
}
|
||||
{
|
||||
show.episode?.formatted_episode && (
|
||||
<span class="sub-meta">
|
||||
I last watched{" "}
|
||||
<strong class="highlight-text">{show.episode.formatted_episode}</strong>{" "}
|
||||
on {format(parseISO(show.episode.last_watched_at), "PPPP")}.
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
|
||||
{show.review && (
|
||||
<>
|
||||
<h2>My thoughts</h2>
|
||||
<Warning text="There are probably spoilers after this banner — this is a warning about them." />
|
||||
<div set:html={md(show.review)}/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<AssociatedMedia
|
||||
artists={show.artists}
|
||||
books={show.books}
|
||||
genres={show.genres}
|
||||
movies={show.movies}
|
||||
posts={show.posts}
|
||||
shows={show.related_shows}
|
||||
/>
|
||||
|
||||
{show.description && (
|
||||
<>
|
||||
<h2>Overview</h2>
|
||||
<div set:html={md(show.description)}/>
|
||||
</>
|
||||
)}
|
||||
</article>
|
||||
</Layout>
|
31
src/styles/base/fonts.css
Normal file
31
src/styles/base/fonts.css
Normal file
|
@ -0,0 +1,31 @@
|
|||
@font-face {
|
||||
font-family: MonoLisa;
|
||||
src: url("/fonts/ml.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: MonoLisa;
|
||||
src: url("/fonts/mlb.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: MonoLisa;
|
||||
src: url("/fonts/mli.woff2") format("woff2");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: MonoLisa;
|
||||
src: url("/fonts/mlbi.woff2") format("woff2");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
512
src/styles/base/index.css
Normal file
512
src/styles/base/index.css
Normal file
|
@ -0,0 +1,512 @@
|
|||
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);
|
||||
}
|
||||
|
||||
:not(pre) > code {
|
||||
padding: var(--sizing-xs) var(--sizing-sm);
|
||||
}
|
||||
|
||||
pre {
|
||||
padding: var(--sizing-lg) var(--sizing-base);
|
||||
}
|
||||
|
||||
:not(pre) > code,
|
||||
pre {
|
||||
color: #e1e4e8;
|
||||
background-color: #161b22;
|
||||
border: var(--border-gray);
|
||||
border-radius: var(--border-radius-slight);
|
||||
}
|
||||
|
||||
/* 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, span, th, td, .post-meta, .media-title):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-base);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footnotes {
|
||||
border-top: var(--border-gray);
|
||||
|
||||
& ol li p {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#footnote-label {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
main,
|
||||
footer {
|
||||
width: 80%;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: 768px;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
margin: var(--sizing-3xl) auto 0;
|
||||
|
||||
& 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pre code {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
131
src/styles/base/reset.css
Normal file
131
src/styles/base/reset.css
Normal file
|
@ -0,0 +1,131 @@
|
|||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
:where([hidden]:not([hidden='until-found'])) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:where(html) {
|
||||
font-size: 100%;
|
||||
-webkit-text-size-adjust: none;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-gutter: stable;
|
||||
tab-size: 2;
|
||||
}
|
||||
|
||||
:where(html:has(dialog:modal[open])) {
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
:where(html:focus-within) {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
}
|
||||
|
||||
:where(body) {
|
||||
font-size: var(--font-size-base);
|
||||
line-height: var(--line-height-base);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
:where(button) {
|
||||
all: unset;
|
||||
}
|
||||
|
||||
:where(input, button, textarea, select) {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
:where(textarea) {
|
||||
resize: vertical;
|
||||
resize: block;
|
||||
}
|
||||
|
||||
:where(button, label, select, summary, [role='button'], [role='option']) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
:where(:disabled) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:where(label:has(> input:disabled), label:has(+ input:disabled)) {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:where(a) {
|
||||
color: inherit;
|
||||
text-underline-offset: var(--spacing-xs);
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ol {
|
||||
list-style-type: number;
|
||||
}
|
||||
|
||||
:where(ul, ol) {
|
||||
list-style-position: inside;
|
||||
}
|
||||
|
||||
:where(img, svg, video, canvas, audio, iframe, embed, object) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:where(p, h1, h2, h3) {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
:where(hr) {
|
||||
border: none;
|
||||
border-block-start: 1px solid;
|
||||
border-block-start-color: currentColor;
|
||||
color: inherit;
|
||||
block-size: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
:where(dialog, [popover]) {
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
inset: unset;
|
||||
max-width: unset;
|
||||
max-height: unset;
|
||||
}
|
||||
|
||||
:where(dialog:not([open], [popover]), [popover]:not(:popover-open)) {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
:where(:focus-visible) {
|
||||
outline: var(--border-default);
|
||||
outline-offset: 1px;
|
||||
border-radius: var(--border-radius-slight);
|
||||
box-shadow: 0 0 0 1px var(--accent-color);
|
||||
}
|
||||
|
||||
:where(:focus-visible, :target) {
|
||||
scroll-margin-block: 8vh;
|
||||
}
|
||||
|
||||
:where(.visually-hidden:not(:focus-within, :active)) {
|
||||
clip-path: inset(50%) !important;
|
||||
height: 1px !important;
|
||||
width: 1px !important;
|
||||
overflow: hidden !important;
|
||||
position: absolute !important;
|
||||
white-space: nowrap !important;
|
||||
border: 0 !important;
|
||||
user-select: none !important;
|
||||
}
|
162
src/styles/base/vars.css
Normal file
162
src/styles/base/vars.css
Normal file
|
@ -0,0 +1,162 @@
|
|||
:root {
|
||||
/* colors */
|
||||
--blue-100: #a2c4ff;
|
||||
--blue-200: #6b9eff;
|
||||
--blue-300: #4a78ff;
|
||||
--blue-400: #3364ff;
|
||||
--blue-500: #2553e6;
|
||||
--blue-600: #1e42c7;
|
||||
|
||||
--gray-100: #f9fafb;
|
||||
--gray-200: #eceef1;
|
||||
--gray-300: #dfe3e8;
|
||||
--gray-400: #959eae;
|
||||
--gray-500: #7f899b;
|
||||
--gray-600: #626d7f;
|
||||
--gray-700: #545e71;
|
||||
--gray-800: #4a5365;
|
||||
--gray-900: #14161a;
|
||||
|
||||
--gray-lighter: light-dark(var(--gray-200), var(--gray-700));
|
||||
--gray-light: light-dark(var(--gray-300), var(--gray-600));
|
||||
--gray-medium: var(--gray-400);
|
||||
--gray-dark: light-dark(var(--gray-800), var(--gray-300));
|
||||
|
||||
/* base theme */
|
||||
--color-lightest: var(--gray-100);
|
||||
--color-darkest: var(--gray-900);
|
||||
--text-color: light-dark(var(--color-darkest), var(--color-lightest));
|
||||
--background-color: light-dark(var(--color-lightest), var(--color-darkest));
|
||||
--text-color-inverted: light-dark(
|
||||
var(--color-lightest),
|
||||
var(--color-darkest)
|
||||
);
|
||||
--background-color-inverted: light-dark(
|
||||
var(--color-darkest),
|
||||
var(--color-lightest)
|
||||
);
|
||||
--accent-color: light-dark(var(--blue-400), var(--blue-200));
|
||||
--accent-color-hover: light-dark(var(--blue-600), var(--blue-100));
|
||||
--accent-color-active: light-dark(var(--blue-600), var(--blue-100));
|
||||
|
||||
--brand-buy-me-a-coffee: light-dark(#0d0c22, #ffdd00);
|
||||
--brand-github: light-dark(#333, #f5f5f5);
|
||||
--brand-fastmail: light-dark(#0067b9, #ffc107);
|
||||
--brand-mastodon: light-dark(#563acc, #858afa);
|
||||
--brand-npm: #cb3837;
|
||||
--brand-rss: #f26522;
|
||||
|
||||
--article: light-dark(#007272, #00ffff);
|
||||
--about: light-dark(#e4513a, #ff967d);
|
||||
--books: light-dark(#1a7b1a, #6fff6f);
|
||||
--collected: light-dark(#9a501a, #ffae73);
|
||||
--concerts: light-dark(#cb426e, #ff82aa);
|
||||
--country: light-dark(#146a67, #80dcdc);
|
||||
--error: light-dark(#b81f1f, #ff8b8b);
|
||||
--favorite: light-dark(#b03c72, #ff9ccd);
|
||||
--link: light-dark(#7b5cba, #e2b8ff);
|
||||
--music: light-dark(#3d7099, #76b8cc);
|
||||
--newsletter: light-dark(#37b0b0, #91fffa);
|
||||
--now: light-dark(#cc1076, #ff82d5);
|
||||
--search: light-dark(#6b5e3a, #c0b594);
|
||||
--tattoo: light-dark(#951b1b, #ff7373);
|
||||
--tv: light-dark(#cc3600, #ff8f66);
|
||||
--warning: light-dark(#cc6f00, #ffbf66);
|
||||
--webrings: light-dark(#b054b0, #ffb3ff);
|
||||
|
||||
/* borders */
|
||||
--border-default: 1px solid var(--accent-color);
|
||||
--border-default-hover: 1px solid var(--accent-color-hover);
|
||||
--border-gray: 1px solid var(--gray-light);
|
||||
|
||||
/* fonts */
|
||||
--font-mono: MonoLisa, Menlo, Consolas, Monaco, Liberation Mono,
|
||||
Lucida Console, ui-monospace, monospace;
|
||||
|
||||
/* text */
|
||||
--font-size-xs: 0.7rem;
|
||||
--font-size-sm: 0.85rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.15rem;
|
||||
--font-size-xl: 1.3rem;
|
||||
--font-size-2xl: 1.45rem;
|
||||
--font-size-3xl: 1.6rem;
|
||||
|
||||
--font-weight-base: 400;
|
||||
--font-weight-bold: 700;
|
||||
|
||||
--line-height-sm: 1;
|
||||
--line-height-md: 1.5;
|
||||
--line-height-base: 2;
|
||||
|
||||
/* sizing */
|
||||
--sizing-xs: 0.25rem;
|
||||
--sizing-sm: 0.5rem;
|
||||
--sizing-md: 0.75rem;
|
||||
--sizing-lg: 1rem;
|
||||
--sizing-base: 1.5rem;
|
||||
--sizing-xl: 1.75rem;
|
||||
--sizing-2xl: 2rem;
|
||||
--sizing-3xl: 2.25rem;
|
||||
|
||||
--sizing-svg-sm: 18px;
|
||||
--sizing-svg-base: 24px;
|
||||
|
||||
/* spacing */
|
||||
--spacing-xs: var(--sizing-xs);
|
||||
--spacing-sm: var(--sizing-sm);
|
||||
--spacing-md: var(--sizing-md);
|
||||
--spacing-lg: var(--sizing-lg);
|
||||
--spacing-base: var(--sizing-base);
|
||||
--spacing-xl: var(--sizing-xl);
|
||||
--spacing-2xl: var(--sizing-2xl);
|
||||
--spacing-3xl: var(--sizing-3xl);
|
||||
|
||||
--margin-vertical-base-horizontal-zero: var(--spacing-base) 0;
|
||||
|
||||
/* radii */
|
||||
--border-radius-slight: var(--sizing-xs);
|
||||
--border-radius-full: 9999px;
|
||||
|
||||
/* aspect ratios */
|
||||
--aspect-ratio-square: 1/1;
|
||||
--aspect-ratio-vertical: 2/3;
|
||||
--aspect-ratio-banner: 3/2;
|
||||
|
||||
/* grid columns */
|
||||
--grid-columns-one: repeat(1, minmax(0, 1fr));
|
||||
--grid-columns-two: repeat(2, minmax(0, 1fr));
|
||||
--grid-columns-three: repeat(3, minmax(0, 1fr));
|
||||
--grid-columns-four: repeat(4, minmax(0, 1fr));
|
||||
--grid-columns-six: repeat(6, minmax(0, 1fr));
|
||||
|
||||
--grid-poster: var(--grid-columns-two);
|
||||
--grid-square: var(--grid-columns-two);
|
||||
--grid-vertical: var(--grid-columns-three);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
--grid-poster: var(--grid-columns-three);
|
||||
--grid-square: var(--grid-columns-four);
|
||||
--grid-vertical: var(--grid-columns-six);
|
||||
}
|
||||
|
||||
/* transitions */
|
||||
--transition-ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--transition-duration-default: 300ms;
|
||||
|
||||
/* svgs */
|
||||
--stroke-width-default: 1.4;
|
||||
--stroke-width-bold: 2;
|
||||
--inline-margin-bottom: -5px;
|
||||
|
||||
/* shadows */
|
||||
--box-shadow-media: inset 0 -85px 60px -40px var(--color-darkest);
|
||||
--box-shadow-text-toggle: inset 0 -120px 60px -60px var(--background-color);
|
||||
--text-shadow-default: rgba(0, 0, 0, 0.7) 0px 0px 10px;
|
||||
|
||||
/* modals */
|
||||
--modal-overlay-background: light-dark(#ffffffbf, #000000bf);
|
||||
|
||||
/* input accent color */
|
||||
accent-color: var(--accent-color);
|
||||
}
|
14
src/styles/components/addon-links.css
Normal file
14
src/styles/components/addon-links.css
Normal file
|
@ -0,0 +1,14 @@
|
|||
.addon-links {
|
||||
display: grid;
|
||||
gap: var(--sizing-base);
|
||||
grid-template-columns: var(--grid-columns-one);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-template-columns: var(--grid-columns-two);
|
||||
}
|
||||
|
||||
& article {
|
||||
border-bottom: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
80
src/styles/components/banners.css
Normal file
80
src/styles/components/banners.css
Normal file
|
@ -0,0 +1,80 @@
|
|||
.banner {
|
||||
padding: var(--spacing-md);
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
border: 1px solid;
|
||||
border-radius: var(--border-radius-slight);
|
||||
|
||||
& a {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
& span {
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
& span,
|
||||
& svg {
|
||||
height: var(--sizing-svg-sm);
|
||||
width: var(--sizing-svg-sm);
|
||||
}
|
||||
|
||||
& p {
|
||||
font-size: var(--font-size-sm);
|
||||
color: var(--text-color);
|
||||
margin: 0;
|
||||
|
||||
a {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
& svg {
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.coffee,
|
||||
&.error,
|
||||
&.github,
|
||||
&.mastodon,
|
||||
&.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);
|
||||
}
|
||||
&.mastodon {
|
||||
--banner-accent-color: var(--brand-mastodon);
|
||||
}
|
||||
&.npm {
|
||||
--banner-accent-color: var(--brand-npm);
|
||||
}
|
||||
&.old-post {
|
||||
--banner-accent-color: var(--gray-dark);
|
||||
}
|
||||
&.rss {
|
||||
--banner-accent-color: var(--brand-rss);
|
||||
}
|
||||
&.warning {
|
||||
--banner-accent-color: var(--warning);
|
||||
}
|
||||
|
||||
border-color: var(--banner-accent-color);
|
||||
|
||||
& p a:is(:hover, :active, :focus) {
|
||||
color: var(--banner-accent-color);
|
||||
}
|
||||
|
||||
& svg {
|
||||
stroke: var(--banner-accent-color);
|
||||
}
|
||||
}
|
||||
}
|
28
src/styles/components/buttons.css
Normal file
28
src/styles/components/buttons.css
Normal file
|
@ -0,0 +1,28 @@
|
|||
@import url("./tab-buttons.css");
|
||||
@import url("./text-toggle.css");
|
||||
|
||||
button,
|
||||
.button {
|
||||
appearance: none;
|
||||
border: none;
|
||||
border: 2px solid var(--accent-color);
|
||||
border-radius: var(--border-radius-full);
|
||||
padding: var(--spacing-xs) var(--spacing-md);
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
line-height: var(--line-height-base);
|
||||
white-space: nowrap;
|
||||
color: var(--text-color-inverted);
|
||||
background-color: var(--accent-color);
|
||||
transition: color var(--transition-duration-default)
|
||||
var(--transition-ease-in-out);
|
||||
|
||||
&:not(.active):is(:hover, :active, :focus, :focus-within) {
|
||||
background-color: var(--accent-color-hover);
|
||||
border: 2px solid var(--accent-color-hover);
|
||||
transition: background-color var(--transition-duration-default)
|
||||
var(--transition-ease-in-out),
|
||||
border var(--transition-duration-default) var(--transition-ease-in-out),
|
||||
color var(--transition-duration-default) var(--transition-ease-in-out);
|
||||
}
|
||||
}
|
69
src/styles/components/forms.css
Normal file
69
src/styles/components/forms.css
Normal file
|
@ -0,0 +1,69 @@
|
|||
::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(--spacing-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(--spacing-base);
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search__load-more {
|
||||
margin-bottom: var(--spacing-base);
|
||||
}
|
67
src/styles/components/media-grid.css
Normal file
67
src/styles/components/media-grid.css
Normal file
|
@ -0,0 +1,67 @@
|
|||
.media-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-sm);
|
||||
|
||||
& ~ .pagination {
|
||||
margin-top: var(--spacing-base);
|
||||
}
|
||||
|
||||
&.poster {
|
||||
grid-template-columns: var(--grid-poster);
|
||||
|
||||
& a {
|
||||
aspect-ratio: var(--aspect-ratio-banner);
|
||||
}
|
||||
}
|
||||
|
||||
&.square {
|
||||
grid-template-columns: var(--grid-square);
|
||||
|
||||
& a {
|
||||
aspect-ratio: var(--aspect-ratio-square);
|
||||
}
|
||||
}
|
||||
|
||||
&.vertical {
|
||||
grid-template-columns: var(--grid-vertical);
|
||||
|
||||
& a {
|
||||
aspect-ratio: var(--aspect-ratio-vertical);
|
||||
}
|
||||
}
|
||||
|
||||
&:is(.poster, .square, .vertical) img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
& .item {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
& .meta-text {
|
||||
color: var(--color-lightest);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
padding: 0 var(--spacing-sm);
|
||||
bottom: var(--spacing-sm);
|
||||
|
||||
& .header,
|
||||
& .subheader {
|
||||
color: var(--color-lightest);
|
||||
font-size: var(--font-size-sm);
|
||||
line-height: var(--line-height-md);
|
||||
text-shadow: var(--text-shadow-default);
|
||||
}
|
||||
|
||||
& .header {
|
||||
font-weight: var(--font-weight-bold);
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 6;
|
||||
line-clamp: 6;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
136
src/styles/components/menu.css
Normal file
136
src/styles/components/menu.css
Normal file
|
@ -0,0 +1,136 @@
|
|||
menu {
|
||||
& .menu-primary {
|
||||
position: absolute;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
top: calc(var(--spacing-3xl) * 1.75);
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 3;
|
||||
|
||||
& > li {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: var(--spacing-sm) 0;
|
||||
width: 100%;
|
||||
background: var(--background-color);
|
||||
|
||||
& a,
|
||||
& .active {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
& a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
& .active {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
:is(.icon, .active) svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:is(.icon, .active) span {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#menu-toggle {
|
||||
display: none;
|
||||
|
||||
&:checked + .menu-button-container {
|
||||
& .menu-closed {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& .menu-open {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(:checked) + .menu-button-container {
|
||||
& .menu-closed {
|
||||
display: block;
|
||||
}
|
||||
|
||||
& .menu-open {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& ~ .menu-primary li {
|
||||
height: 0;
|
||||
padding: 0;
|
||||
font-size: var(--font-size-lg);
|
||||
}
|
||||
|
||||
&:checked ~ .menu-primary li {
|
||||
height: calc(var(--sizing-3xl) * 1.5);
|
||||
|
||||
@media (max-width: 767px) {
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
@media (max-width: 767px) {
|
||||
border-top: var(--border-gray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.menu-button-container {
|
||||
display: unset;
|
||||
|
||||
& svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.menu-primary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin: 0;
|
||||
gap: var(--spacing-md);
|
||||
position: relative;
|
||||
top: unset;
|
||||
left: unset;
|
||||
width: auto;
|
||||
|
||||
& > li {
|
||||
background: none;
|
||||
|
||||
& a {
|
||||
width: var(--sizing-svg-base);
|
||||
height: var(--sizing-svg-base);
|
||||
}
|
||||
|
||||
:is(.icon, .active) svg {
|
||||
display: block;
|
||||
}
|
||||
|
||||
:is(.icon, .active) span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#menu-toggle ~ .menu-primary li,
|
||||
#menu-toggle:checked ~ .menu-primary li {
|
||||
height: unset;
|
||||
}
|
||||
|
||||
.menu-button-container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
63
src/styles/components/modal.css
Normal file
63
src/styles/components/modal.css
Normal file
|
@ -0,0 +1,63 @@
|
|||
.modal-wrapper,
|
||||
.modal-body {
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.modal-wrapper {
|
||||
background: var(--modal-overlay-background);
|
||||
z-index: 3;
|
||||
|
||||
.modal-body {
|
||||
background: var(--background-color);
|
||||
padding: var(--spacing-lg) var(--spacing-base);
|
||||
overflow-y: auto;
|
||||
border-radius: var(--border-radius-slight);
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
max-width: 75%;
|
||||
max-height: 75%;
|
||||
inset: 12.5%;
|
||||
border: var(--border-gray);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.modal-input {
|
||||
display: none;
|
||||
|
||||
&:checked ~ .modal-wrapper {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:not(:checked) ~ .modal-wrapper {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-toggle,
|
||||
.modal-close {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
vertical-align: middle;
|
||||
|
||||
svg {
|
||||
stroke: var(--accent-color);
|
||||
|
||||
&:is(:hover, :focus, :active) {
|
||||
stroke: var(--accent-color-hover);
|
||||
}
|
||||
}
|
||||
}
|
103
src/styles/components/music-chart.css
Normal file
103
src/styles/components/music-chart.css
Normal file
|
@ -0,0 +1,103 @@
|
|||
.music-chart {
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
|
||||
& ol {
|
||||
padding-left: 0;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
list-style-position: outside;
|
||||
}
|
||||
}
|
||||
|
||||
& .item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
align-items: start;
|
||||
|
||||
&:not(:last-of-type) {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
& .progress-bar-wrapper {
|
||||
max-width: 40%;
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
margin-top: var(--spacing-sm);
|
||||
}
|
||||
}
|
||||
|
||||
& img {
|
||||
width: calc(var(--sizing-3xl) * 1.5);
|
||||
height: calc(var(--sizing-3xl) * 1.5);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
width: calc(var(--sizing-3xl) * 2);
|
||||
height: calc(var(--sizing-3xl) * 2);
|
||||
}
|
||||
}
|
||||
|
||||
& .info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: calc(75% - var(--sizing-lg));
|
||||
}
|
||||
}
|
||||
|
||||
& .meta {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: var(--spacing-md);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
width: calc(75% - var(--sizing-lg));
|
||||
}
|
||||
}
|
||||
|
||||
& .meta-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: start;
|
||||
gap: var(--spacing-xs);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: 85%;
|
||||
}
|
||||
}
|
||||
|
||||
& .title {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
& .title,
|
||||
& .subtext,
|
||||
& time {
|
||||
line-height: var(--line-height-md);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
& .subtext,
|
||||
& time {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
& time {
|
||||
margin-top: var(--spacing-sm);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
text-align: right;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
20
src/styles/components/paginator.css
Normal file
20
src/styles/components/paginator.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
.pagination {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: var(--spacing-base);
|
||||
|
||||
& a {
|
||||
display: flex;
|
||||
|
||||
&.disabled svg {
|
||||
cursor: not-allowed;
|
||||
stroke: var(--gray-medium);
|
||||
stroke-width: var(--stroke-width-default);
|
||||
}
|
||||
}
|
||||
|
||||
& p {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
13
src/styles/components/progress-bar.css
Normal file
13
src/styles/components/progress-bar.css
Normal file
|
@ -0,0 +1,13 @@
|
|||
.progress-bar-wrapper {
|
||||
display: flex;
|
||||
height: var(--sizing-lg);
|
||||
width: 100%;
|
||||
background-color: var(--gray-light);
|
||||
border-radius: var(--border-radius-full);
|
||||
overflow: hidden;
|
||||
|
||||
& .progress-bar {
|
||||
background-color: var(--accent-color);
|
||||
border-radius: var(--border-radius-full);
|
||||
}
|
||||
}
|
38
src/styles/components/tab-buttons.css
Normal file
38
src/styles/components/tab-buttons.css
Normal file
|
@ -0,0 +1,38 @@
|
|||
#tracks-recent,
|
||||
#tracks-chart,
|
||||
.tracks-recent,
|
||||
.tracks-chart {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#tracks-recent:checked ~ .tracks-recent,
|
||||
#tracks-chart:checked ~ .tracks-chart {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input[id="tracks-recent"] ~ .tracks-recent,
|
||||
input[id="tracks-chart"] ~ .tracks-chart {
|
||||
margin-top: var(--spacing-base);
|
||||
}
|
||||
|
||||
[for="tracks-recent"] {
|
||||
margin-right: var(--spacing-lg);
|
||||
}
|
||||
|
||||
#tracks-recent:checked ~ [for="tracks-recent"],
|
||||
#tracks-chart:checked ~ [for="tracks-chart"] {
|
||||
cursor: not-allowed;
|
||||
border-color: var(--accent-color);
|
||||
background-color: var(--accent-color);
|
||||
}
|
||||
|
||||
#tracks-recent:not(:checked) ~ [for="tracks-recent"],
|
||||
#tracks-chart:not(:checked) ~ [for="tracks-chart"] {
|
||||
color: var(--accent-color);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
#tracks-recent:not(:checked) ~ [for="tracks-recent"]:is(:hover, :active),
|
||||
#tracks-chart:not(:checked) ~ [for="tracks-chart"]:is(:hover, :active) {
|
||||
color: var(--accent-color-hover);
|
||||
}
|
23
src/styles/components/text-toggle.css
Normal file
23
src/styles/components/text-toggle.css
Normal file
|
@ -0,0 +1,23 @@
|
|||
[data-toggle-content] {
|
||||
&.text-toggle-hidden {
|
||||
position: relative;
|
||||
height: 500px;
|
||||
overflow: hidden;
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
|
||||
& p:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
content: "";
|
||||
box-shadow: var(--box-shadow-text-toggle);
|
||||
width: 100%;
|
||||
height: 20%;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
}
|
15
src/styles/components/youtube-player.css
Normal file
15
src/styles/components/youtube-player.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
lite-youtube {
|
||||
aspect-ratio: 16/9;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
border: var(--border-default) !important;
|
||||
border-radius: var(--border-radius-slight);
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
max-width: unset !important;
|
||||
|
||||
&:hover {
|
||||
border: var(--border-default-hover);
|
||||
transition: border var(--transition-duration-default)
|
||||
var(--transition-ease-in-out);
|
||||
}
|
||||
}
|
34
src/styles/index.css
Normal file
34
src/styles/index.css
Normal file
|
@ -0,0 +1,34 @@
|
|||
@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);
|
||||
|
||||
/* 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/banners.css") layer(components);
|
||||
@import url("./components/buttons.css") layer(components);
|
||||
@import url("./components/forms.css") layer(components);
|
||||
@import url("./components/media-grid.css") layer(components);
|
||||
@import url("./components/menu.css") layer(components);
|
||||
@import url("./components/modal.css") layer(components);
|
||||
@import url("./components/music-chart.css") layer(components);
|
||||
@import url("./components/paginator.css") layer(components);
|
||||
@import url("./components/progress-bar.css") layer(components);
|
||||
@import url("./components/youtube-player.css") layer(components);
|
24
src/styles/pages/about.css
Normal file
24
src/styles/pages/about.css
Normal file
|
@ -0,0 +1,24 @@
|
|||
:root {
|
||||
--avatar-size: 16rem;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
--avatar-size: 24rem;
|
||||
}
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
& img {
|
||||
width: var(--avatar-size);
|
||||
height: var(--avatar-size);
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
}
|
||||
|
||||
.about-title {
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
text-align: center;
|
||||
}
|
87
src/styles/pages/books.css
Normal file
87
src/styles/pages/books.css
Normal file
|
@ -0,0 +1,87 @@
|
|||
:is(.book-entry, .book-focus) img {
|
||||
height: auto;
|
||||
aspect-ratio: var(--aspect-ratio-vertical);
|
||||
}
|
||||
|
||||
.book-entry {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-sm);
|
||||
|
||||
&:not(:last-of-type) {
|
||||
padding-bottom: var(--spacing-base);
|
||||
border-bottom: var(--border-gray);
|
||||
}
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-base);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
& img {
|
||||
max-width: calc(var(--sizing-3xl) * 4);
|
||||
}
|
||||
|
||||
& .media-meta {
|
||||
margin-top: var(--sizing-base);
|
||||
align-items: center;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
margin-top: 0;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
& .description p:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
& .progress-bar-wrapper {
|
||||
max-width: 75%;
|
||||
margin-bottom: 0;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
margin-top: 0;
|
||||
max-width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.book-focus {
|
||||
& .book-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--spacing-base);
|
||||
margin-bottom: var(--spacing-base);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
& img {
|
||||
border: var(--border-default);
|
||||
}
|
||||
|
||||
& .media-meta {
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
width: auto;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
& .progress-bar-wrapper {
|
||||
max-width: 50%;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
36
src/styles/pages/contact.css
Normal file
36
src/styles/pages/contact.css
Normal file
|
@ -0,0 +1,36 @@
|
|||
.contact-wrapper {
|
||||
display: grid;
|
||||
grid-template-columns: var(--grid-columns-one);
|
||||
gap: var(--spacing-base);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-template-columns: var(--grid-columns-two);
|
||||
}
|
||||
|
||||
& .hp,
|
||||
& label > span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& textarea {
|
||||
height: calc(var(--sizing-3xl) * 5);
|
||||
}
|
||||
|
||||
& .column.description {
|
||||
& p:first-of-type {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.contact-success-wrapper {
|
||||
text-align: center;
|
||||
|
||||
& h2 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
15
src/styles/pages/links.css
Normal file
15
src/styles/pages/links.css
Normal file
|
@ -0,0 +1,15 @@
|
|||
.link-grid {
|
||||
display: grid;
|
||||
gap: var(--spacing-sm);
|
||||
grid-template-columns: var(--grid-columns-one);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
grid-template-columns: var(--grid-columns-two);
|
||||
}
|
||||
|
||||
& .link-box {
|
||||
border: var(--border-gray);
|
||||
border-radius: var(--border-radius-slight);
|
||||
padding: var(--spacing-sm) var(--spacing-md);
|
||||
}
|
||||
}
|
46
src/styles/pages/media.css
Normal file
46
src/styles/pages/media.css
Normal file
|
@ -0,0 +1,46 @@
|
|||
.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);
|
||||
}
|
||||
|
||||
.concerts.media-title {
|
||||
margin-top: var(--spacing-base);
|
||||
}
|
37
src/styles/pages/music.css
Normal file
37
src/styles/pages/music.css
Normal file
|
@ -0,0 +1,37 @@
|
|||
.artist-focus {
|
||||
& img {
|
||||
border: var(--border-default);
|
||||
aspect-ratio: var(--aspect-ratio-square);
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
max-width: calc(var(--sizing-3xl) * 6.75);
|
||||
}
|
||||
}
|
||||
|
||||
& .artist-display {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--spacing-xs);
|
||||
margin-bottom: var(--spacing-base);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
flex-direction: row;
|
||||
gap: var(--spacing-md);
|
||||
}
|
||||
|
||||
& .media-meta {
|
||||
margin-top: var(--spacing-base);
|
||||
|
||||
@media screen and (min-width: 768px) {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& table + p {
|
||||
font-size: var(--font-size-sm);
|
||||
margin: var(--spacing-base) 0 0;
|
||||
}
|
||||
}
|
29
src/styles/pages/watching.css
Normal file
29
src/styles/pages/watching.css
Normal file
|
@ -0,0 +1,29 @@
|
|||
.watching.hero {
|
||||
position: relative;
|
||||
|
||||
& img {
|
||||
aspect-ratio: var(--aspect-ratio-banner);
|
||||
}
|
||||
|
||||
& .meta-text {
|
||||
color: var(--color-lightest);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
left: var(--spacing-sm);
|
||||
bottom: var(--spacing-sm);
|
||||
|
||||
& .header {
|
||||
font-weight: var(--font-weight-bold);
|
||||
}
|
||||
|
||||
& .subheader {
|
||||
font-size: var(--font-size-sm);
|
||||
}
|
||||
|
||||
& .header,
|
||||
& .subheader {
|
||||
line-height: var(--line-height-md);
|
||||
text-shadow: var(--text-shadow-default);
|
||||
}
|
||||
}
|
||||
}
|
20
src/styles/pages/webrings.css
Normal file
20
src/styles/pages/webrings.css
Normal file
|
@ -0,0 +1,20 @@
|
|||
.webring-wrapper,
|
||||
.webring-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.webring-wrapper {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
margin: var(--margin-vertical-base-horizontal-zero);
|
||||
|
||||
& p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
& .webring-navigation {
|
||||
justify-content: center;
|
||||
gap: var(--spacing-sm);
|
||||
}
|
||||
}
|
46
src/utils/albumReleasesCalendar.js
Normal file
46
src/utils/albumReleasesCalendar.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { parseISO, format, isValid } from "date-fns";
|
||||
import { toZonedTime } from "date-fns-tz";
|
||||
import { createEvents } from "ics";
|
||||
|
||||
const nowUTC = toZonedTime(new Date(), "UTC");
|
||||
const formattedDate = format(nowUTC, "yyyyMMdd'T'HHmmss'Z'");
|
||||
|
||||
export async function albumReleasesCalendar(albumReleases) {
|
||||
if (!albumReleases || albumReleases.length === 0) return "";
|
||||
|
||||
const events = albumReleases
|
||||
.map((album) => {
|
||||
const date = parseISO(album["release_date"]);
|
||||
if (!isValid(date)) return null;
|
||||
|
||||
return {
|
||||
start: [
|
||||
date.getFullYear(),
|
||||
date.getMonth() + 1,
|
||||
date.getDate(),
|
||||
],
|
||||
startInputType: "local",
|
||||
startOutputType: "local",
|
||||
title: `Release: ${album["artist"]["name"]} - ${album["title"]}`,
|
||||
description: `Check out this new album release: ${album["url"]}. Read more about ${album["artist"]["name"]} at https://coryd.dev${album["artist"]["url"]}`,
|
||||
url: album["url"],
|
||||
uid: `${format(date, "yyyyMMdd")}-${album["artist"]["name"]}-${album["title"]}@coryd.dev`,
|
||||
timestamp: formattedDate,
|
||||
};
|
||||
})
|
||||
.filter((event) => event !== null);
|
||||
|
||||
const { error, value } = createEvents(events, {
|
||||
calName: "Album releases calendar / coryd.dev",
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Error creating events: ", error);
|
||||
events.forEach((event, index) => {
|
||||
console.error(`Event ${index}:`, event);
|
||||
});
|
||||
return "";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
1
src/utils/constants/index.js
Normal file
1
src/utils/constants/index.js
Normal file
|
@ -0,0 +1 @@
|
|||
export const CACHE_DURATION = 120 * 1000;
|
Some files were not shown because too many files have changed in this diff Show more
Reference in a new issue