feat: initial commit
This commit is contained in:
commit
0ff7457679
192 changed files with 24379 additions and 0 deletions
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>
|
Reference in a new issue