feat: initial commit
This commit is contained in:
commit
0ff7457679
192 changed files with 24379 additions and 0 deletions
26
src/components/Footer.astro
Normal file
26
src/components/Footer.astro
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
import NavLink from "@components/nav/NavLink.astro";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
|
||||
const { nav } = await fetchGlobalData(Astro);
|
||||
---
|
||||
|
||||
<footer>
|
||||
<nav aria-label="Social icons" class="social">
|
||||
{
|
||||
nav.footer_icons.map((link) => (
|
||||
<NavLink url={link.permalink} title={link.title} icon={link.icon} />
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
<nav aria-label="Secondary site navigation" class="sub-pages">
|
||||
{
|
||||
nav.footer_text.map((link, index) => (
|
||||
<>
|
||||
<NavLink url={link.permalink} title={link.title} icon={link.icon} />
|
||||
{index < nav.footer_text.length - 1 && <span>/</span>}
|
||||
</>
|
||||
))
|
||||
}
|
||||
</nav>
|
||||
</footer>
|
21
src/components/Header.astro
Normal file
21
src/components/Header.astro
Normal file
|
@ -0,0 +1,21 @@
|
|||
---
|
||||
import Menu from "@components/nav/Menu.astro";
|
||||
|
||||
const { siteName, url, nav } = Astro.props;
|
||||
const isHomePage = url === "/";
|
||||
---
|
||||
|
||||
<section class="main-title">
|
||||
<h1>
|
||||
{
|
||||
isHomePage ? (
|
||||
siteName
|
||||
) : (
|
||||
<a href="/" tabindex="0">
|
||||
{siteName}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</h1>
|
||||
<Menu nav={nav} />
|
||||
</section>
|
42
src/components/IconMapper.astro
Normal file
42
src/components/IconMapper.astro
Normal file
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const {
|
||||
IconArticle,
|
||||
IconHeadphones,
|
||||
IconDeviceTvOld,
|
||||
IconBooks,
|
||||
IconLink,
|
||||
IconInfoCircle,
|
||||
IconSearch,
|
||||
IconRss,
|
||||
IconBrandMastodon,
|
||||
IconMail,
|
||||
IconBrandGithub,
|
||||
IconBrandNpm,
|
||||
IconCoffee,
|
||||
IconDeviceWatch,
|
||||
IconHeartHandshake,
|
||||
} = icons;
|
||||
const { icon, className } = Astro.props;
|
||||
const iconComponents = {
|
||||
article: IconArticle,
|
||||
headphones: IconHeadphones,
|
||||
"device-tv-old": IconDeviceTvOld,
|
||||
books: IconBooks,
|
||||
link: IconLink,
|
||||
"info-circle": IconInfoCircle,
|
||||
search: IconSearch,
|
||||
rss: IconRss,
|
||||
"brand-mastodon": IconBrandMastodon,
|
||||
mail: IconMail,
|
||||
"brand-github": IconBrandGithub,
|
||||
"brand-npm": IconBrandNpm,
|
||||
coffee: IconCoffee,
|
||||
"device-watch": IconDeviceWatch,
|
||||
"heart-handshake": IconHeartHandshake,
|
||||
};
|
||||
const SelectedIcon = iconComponents[icon?.toLowerCase()] || null;
|
||||
---
|
||||
|
||||
{SelectedIcon ? <div set:html={SelectedIcon({ size: 24, className })} /> : null}
|
105
src/components/blocks/AssociatedMedia.astro
Normal file
105
src/components/blocks/AssociatedMedia.astro
Normal file
|
@ -0,0 +1,105 @@
|
|||
---
|
||||
import { parseISO, format } from "date-fns";
|
||||
import IconMapper from "@components/IconMapper.astro";
|
||||
|
||||
const {
|
||||
artists = [],
|
||||
books = [],
|
||||
genres = [],
|
||||
movies = [],
|
||||
posts = [],
|
||||
shows = [],
|
||||
} = Astro.props;
|
||||
|
||||
const media = [
|
||||
...(artists || []),
|
||||
...(books || []),
|
||||
...(genres || []),
|
||||
...(movies || []),
|
||||
...(posts || []),
|
||||
...(shows || []),
|
||||
];
|
||||
|
||||
if (media.length === 0) return null;
|
||||
|
||||
const sections = [
|
||||
{
|
||||
key: "artists",
|
||||
icon: "headphones",
|
||||
cssClass: "music",
|
||||
label: "Related artist(s)",
|
||||
items: artists || [],
|
||||
},
|
||||
{
|
||||
key: "books",
|
||||
icon: "books",
|
||||
cssClass: "books",
|
||||
label: "Related book(s)",
|
||||
items: books || [],
|
||||
},
|
||||
{
|
||||
key: "genres",
|
||||
icon: "headphones",
|
||||
cssClass: "music",
|
||||
label: "Related genre(s)",
|
||||
items: genres || [],
|
||||
},
|
||||
{
|
||||
key: "movies",
|
||||
icon: "movie",
|
||||
cssClass: "movies",
|
||||
label: "Related movie(s)",
|
||||
items: movies || [],
|
||||
},
|
||||
{
|
||||
key: "posts",
|
||||
icon: "article",
|
||||
cssClass: "article",
|
||||
label: "Related post(s)",
|
||||
items: posts || [],
|
||||
},
|
||||
{
|
||||
key: "shows",
|
||||
icon: "device-tv-old",
|
||||
cssClass: "tv",
|
||||
label: "Related show(s)",
|
||||
items: shows || [],
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<div class="associated-media">
|
||||
{
|
||||
sections.map(({ key, icon, cssClass, label, items }) => {
|
||||
if (!items.length) return null;
|
||||
|
||||
return (
|
||||
<section id={key} class={cssClass}>
|
||||
<div class="media-title">
|
||||
<IconMapper icon={icon} /> {label}
|
||||
</div>
|
||||
<ul>
|
||||
{items.map((item) => (
|
||||
<li>
|
||||
<a href={item.url}>{item.title || item.name}</a>
|
||||
{key === "artists" && item.total_plays > 0 && (
|
||||
<strong class="highlight-text">
|
||||
{item.total_plays}{" "}
|
||||
{item.total_plays === 1 ? "play" : "plays"}
|
||||
</strong>
|
||||
)}
|
||||
{key === "books" && <span>by {item.author}</span>}
|
||||
{(key === "movies" || key === "shows") && (
|
||||
<span>({item.year})</span>
|
||||
)}
|
||||
{key === "posts" && (
|
||||
<span>({format(parseISO(item.date), "PPPP")})</span>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
63
src/components/blocks/BlockRenderer.astro
Normal file
63
src/components/blocks/BlockRenderer.astro
Normal file
|
@ -0,0 +1,63 @@
|
|||
---
|
||||
import { fetchAllPosts } from "@data/posts.js";
|
||||
import { fetchAnalyticsData } from "@data/analytics.js";
|
||||
import { fetchLinks } from "@data/links.js";
|
||||
|
||||
import AddonLinks from "@components/blocks/links/AddonLinks.astro";
|
||||
import AssociatedMedia from "@components/blocks//AssociatedMedia.astro";
|
||||
import GitHub from "@components/blocks/banners/GitHub.astro";
|
||||
import Hero from "@components/blocks//Hero.astro";
|
||||
import Modal from "@components/blocks//Modal.astro";
|
||||
import Npm from "@components/blocks/banners/Npm.astro";
|
||||
import Rss from "@components/blocks/banners/Rss.astro";
|
||||
import YouTubePlayer from "@components/blocks//YouTubePlayer.astro";
|
||||
|
||||
import { md } from "@utils/helpers/general.js";
|
||||
import { getPopularPosts } from "@utils/getPopularPosts.js";
|
||||
|
||||
const [analytics, links, posts] = await Promise.all([
|
||||
fetchAnalyticsData(),
|
||||
fetchLinks(),
|
||||
fetchAllPosts(),
|
||||
]);
|
||||
const popularPosts = getPopularPosts(posts, analytics);
|
||||
const { blocks } = Astro.props;
|
||||
---
|
||||
|
||||
<div>
|
||||
{
|
||||
blocks.map((block) => (
|
||||
<>
|
||||
{block.type === "addon_links" && (
|
||||
<AddonLinks popularPosts={popularPosts} links={links} />
|
||||
)}
|
||||
|
||||
{block.type === "associated_media" && (
|
||||
<AssociatedMedia media={block.media} />
|
||||
)}
|
||||
|
||||
{block.type === "divider" && <div set:html={md(block.markup)} />}
|
||||
|
||||
{block.type === "github_banner" && <GitHub url={block.url} />}
|
||||
|
||||
{block.type === "hero" && <Hero image={block.image} alt={block.alt} />}
|
||||
|
||||
{block.type === "markdown" && (
|
||||
<div set:html={md(block.text)} />
|
||||
)}
|
||||
|
||||
{block.type === "npm_banner" && (
|
||||
<Npm url={block.url} command={block.command} />
|
||||
)}
|
||||
|
||||
{block.type === "modal" && <Modal content={block.content} />}
|
||||
|
||||
{block.type === "rss_banner" && (
|
||||
<Rss url={block.url} text={block.text} />
|
||||
)}
|
||||
|
||||
{block.type === "youtube_player" && <YouTubePlayer url={block.url} />}
|
||||
</>
|
||||
))
|
||||
}
|
||||
</div>
|
26
src/components/blocks/Hero.astro
Normal file
26
src/components/blocks/Hero.astro
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
|
||||
const { image, alt } = Astro.props;
|
||||
const { globals } = await fetchGlobalData(Astro);
|
||||
---
|
||||
|
||||
<div class="hero">
|
||||
<img
|
||||
srcset={`
|
||||
${globals.cdn_url}${image}?class=bannersm&type=webp 256w,
|
||||
${globals.cdn_url}${image}?class=bannermd&type=webp 512w,
|
||||
${globals.cdn_url}${image}?class=bannerbase&type=webp 1024w
|
||||
`}
|
||||
sizes="(max-width: 450px) 256px,
|
||||
(max-width: 850px) 512px,
|
||||
1024px"
|
||||
src={`${globals.cdn_url}${image}?class=bannersm&type=webp`}
|
||||
alt={alt}
|
||||
class="image-banner"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
width="720"
|
||||
height="480"
|
||||
/>
|
||||
</div>
|
26
src/components/blocks/Modal.astro
Normal file
26
src/components/blocks/Modal.astro
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
import { md } from "@utils/helpers/general.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconCircleX, IconInfoCircle } = icons;
|
||||
const { content, id } = Astro.props;
|
||||
---
|
||||
<>
|
||||
<input
|
||||
class="modal-input"
|
||||
id={id}
|
||||
type="checkbox"
|
||||
tabindex="0"
|
||||
/>
|
||||
<label class="modal-toggle" for={id}>
|
||||
<div set:html={IconInfoCircle({ size: 24 })}/>
|
||||
</label>
|
||||
<div class="modal-wrapper">
|
||||
<div class="modal-body">
|
||||
<label class="modal-close" for={id}>
|
||||
<div set:html={IconCircleX({ size: 24 })}/>
|
||||
</label>
|
||||
<div set:html={md(content)}/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
7
src/components/blocks/YouTubePlayer.astro
Normal file
7
src/components/blocks/YouTubePlayer.astro
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
import { YouTube } from 'astro-embed';
|
||||
|
||||
const { url } = Astro.props;
|
||||
---
|
||||
|
||||
<YouTube id={url} />
|
14
src/components/blocks/banners/Coffee.astro
Normal file
14
src/components/blocks/banners/Coffee.astro
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconCoffee } = icons;
|
||||
---
|
||||
|
||||
<div class="banner coffee">
|
||||
<p>
|
||||
<span set:html={IconCoffee({ size: 24 })} />
|
||||
<a class="coffee" href="https://buymeacoffee.com/cory">
|
||||
If you found this post helpful, you can buy me a coffee.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
10
src/components/blocks/banners/Error.astro
Normal file
10
src/components/blocks/banners/Error.astro
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconAlertCircle } = icons;
|
||||
const { text } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner error">
|
||||
<p><span set:html={IconAlertCircle({ size: 24 })}/> {text}</p>
|
||||
</div>
|
14
src/components/blocks/banners/GitHub.astro
Normal file
14
src/components/blocks/banners/GitHub.astro
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconBrandGithub } = icons;
|
||||
const { url } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner github">
|
||||
<p>
|
||||
<span set:html={IconBrandGithub({ size: 24 })}/> Take a look at <a href={url}
|
||||
>the GitHub repository for this project</a
|
||||
>. (Give it a star if you feel like it.)
|
||||
</p>
|
||||
</div>
|
13
src/components/blocks/banners/Mastodon.astro
Normal file
13
src/components/blocks/banners/Mastodon.astro
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconBrandMastodon } = icons;
|
||||
const { url } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner mastodon">
|
||||
<p>
|
||||
<span set:html={IconBrandMastodon({ size: 24 })} />
|
||||
<a class="mastodon" href={url}> Discuss this post on Mastodon. </a>
|
||||
</p>
|
||||
</div>
|
14
src/components/blocks/banners/Npm.astro
Normal file
14
src/components/blocks/banners/Npm.astro
Normal file
|
@ -0,0 +1,14 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconBrandNpm } = icons;
|
||||
const { url, command } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner npm">
|
||||
<p>
|
||||
<span set:html={IconBrandNpm({ size: 24 })}/>
|
||||
<a href={url}>You can take a look at this package on NPM</a> or install it by
|
||||
running <code>{command}</code>.
|
||||
</p>
|
||||
</div>
|
18
src/components/blocks/banners/OldPost.astro
Normal file
18
src/components/blocks/banners/OldPost.astro
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconClockX } = icons;
|
||||
const { isOldPost } = Astro.props;
|
||||
---
|
||||
|
||||
{
|
||||
isOldPost && (
|
||||
<div class="banner old-post">
|
||||
<p>
|
||||
<span set:html={IconClockX({ size: 24 })}/>
|
||||
This post is over 3 years old. I've probably changed my mind since it
|
||||
was written and it <em>could</em> be out of date.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
13
src/components/blocks/banners/Rss.astro
Normal file
13
src/components/blocks/banners/Rss.astro
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconRss } = icons;
|
||||
const { url, text } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner rss">
|
||||
<p>
|
||||
<span set:html={IconRss({ size: 24 })}/>
|
||||
<a href={url}>{text}</a>.
|
||||
</p>
|
||||
</div>
|
13
src/components/blocks/banners/Warning.astro
Normal file
13
src/components/blocks/banners/Warning.astro
Normal file
|
@ -0,0 +1,13 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconAlertTriangle } = icons;
|
||||
const { text } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="banner warning">
|
||||
<p>
|
||||
<span set:html={IconAlertTriangle({ size: 24 })}/>
|
||||
{text}
|
||||
</p>
|
||||
</div>
|
10
src/components/blocks/links/AddonLinks.astro
Normal file
10
src/components/blocks/links/AddonLinks.astro
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
import PopularPosts from './PopularPosts.astro';
|
||||
import RecentLinks from './RecentLinks.astro';
|
||||
|
||||
const { popularPosts, links } = Astro.props;
|
||||
---
|
||||
<div class="addon-links">
|
||||
<PopularPosts popularPosts={popularPosts} />
|
||||
<RecentLinks links={links} />
|
||||
</div>
|
26
src/components/blocks/links/PopularPosts.astro
Normal file
26
src/components/blocks/links/PopularPosts.astro
Normal file
|
@ -0,0 +1,26 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconChartBarPopular } = icons;
|
||||
const { popularPosts } = Astro.props;
|
||||
---
|
||||
|
||||
{
|
||||
popularPosts && popularPosts.length > 0 && (
|
||||
<article>
|
||||
<h3>
|
||||
<a class="article" href="/posts">
|
||||
<div set:html={IconChartBarPopular({ size: 24 })}/>
|
||||
Popular posts
|
||||
</a>
|
||||
</h3>
|
||||
<ol type="1">
|
||||
{popularPosts.slice(0, 5).map((post) => (
|
||||
<li>
|
||||
<a href={post.url}>{post.title}</a>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
</article>
|
||||
)
|
||||
}
|
34
src/components/blocks/links/RecentLinks.astro
Normal file
34
src/components/blocks/links/RecentLinks.astro
Normal file
|
@ -0,0 +1,34 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconLink } = icons;
|
||||
const { links } = Astro.props;
|
||||
---
|
||||
|
||||
{
|
||||
links && links.length > 0 && (
|
||||
<article>
|
||||
<h3>
|
||||
<a class="link" href="/links">
|
||||
<div set:html={IconLink({ size: 24 })}/>
|
||||
Recent links
|
||||
</a>
|
||||
</h3>
|
||||
<ul>
|
||||
{links.slice(0, 5).map((link) => (
|
||||
<li>
|
||||
<a href={link.link} title={link.title}>
|
||||
{link.title}
|
||||
</a>
|
||||
{link.author && (
|
||||
<>
|
||||
{" "}
|
||||
via <a href={link.author.url}>{link.author.name}</a>
|
||||
</>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
)
|
||||
}
|
7
src/components/home/Intro.astro
Normal file
7
src/components/home/Intro.astro
Normal file
|
@ -0,0 +1,7 @@
|
|||
---
|
||||
const { intro } = Astro.props;
|
||||
---
|
||||
|
||||
<article class="intro">
|
||||
<div set:html={intro} />
|
||||
</article>
|
76
src/components/home/RecentActivity.astro
Normal file
76
src/components/home/RecentActivity.astro
Normal file
|
@ -0,0 +1,76 @@
|
|||
---
|
||||
import { fetchBooks } from "@utils/data/books.js";
|
||||
import { fetchLinks } from "@utils/data/links.js";
|
||||
import { fetchMovies } from "@utils/data/movies.js";
|
||||
import { fetchMusicWeek } from "@utils/data/music/week.js";
|
||||
import { fetchShows } from "@utils/data/tv.js";
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
import Rss from "@components/blocks/banners/Rss.astro";
|
||||
|
||||
const { IconActivity } = icons;
|
||||
const [music, tv, movies, books, links] = await Promise.all([
|
||||
fetchMusicWeek(),
|
||||
fetchShows(),
|
||||
fetchMovies(),
|
||||
fetchBooks(),
|
||||
fetchLinks(),
|
||||
]);
|
||||
|
||||
const artist = music.week?.artists[0];
|
||||
const track = music.week?.tracks[0];
|
||||
const show = tv.recentlyWatched[0];
|
||||
const movie = movies.recentlyWatched[0];
|
||||
const book = books.currentYear[0];
|
||||
const link = links[0];
|
||||
---
|
||||
|
||||
<article>
|
||||
<h2>
|
||||
<div set:html={IconActivity({ size: 24 })}/>
|
||||
Recent activity
|
||||
</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<span class="music">Top artist this week:</span>
|
||||
<a href={artist.artist_url}>{artist.artist_name}</a>
|
||||
</li>
|
||||
<li>
|
||||
<span class="music">Top track this week:</span>
|
||||
<a href={track.artist_url}>{track.track_name} by {track.artist_name}</a>
|
||||
</li>
|
||||
<li>
|
||||
<span class="tv">Last episode watched:</span>
|
||||
<strong class="highlight-text">{show.formatted_episode}</strong> of <a
|
||||
href={show.url}>{show.title}</a
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<span class="movies">Last movie watched:</span>
|
||||
<a href={movie.url}>{movie.title}</a>{
|
||||
movie.rating ? ` (${movie.rating})` : ""
|
||||
}
|
||||
</li>
|
||||
<li>
|
||||
<span class="books">Last book finished:</span>
|
||||
<a href={book.url}>{book.title}</a> by {book.author}{
|
||||
book.rating ? ` (${book.rating})` : ""
|
||||
}
|
||||
</li>
|
||||
<li>
|
||||
<span class="link">Last link shared:</span>
|
||||
<a href={link.link}>{link.title}</a>
|
||||
{
|
||||
link.author && (
|
||||
<span>
|
||||
{" "}
|
||||
via <a href={link.author.url}>{link.author.name}</a>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
</li>
|
||||
</ul>
|
||||
<Rss
|
||||
url="/feeds"
|
||||
text="Subscribe to my movies, books, links or activity feed(s)"
|
||||
/>
|
||||
</article>
|
36
src/components/home/RecentPosts.astro
Normal file
36
src/components/home/RecentPosts.astro
Normal file
|
@ -0,0 +1,36 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
import { fetchAllPosts } from "@utils/data/posts.js";
|
||||
import { md } from "@utils/helpers/general.js";
|
||||
|
||||
const { IconClock, IconStar, IconArrowRight } = icons;
|
||||
const posts = await fetchAllPosts();
|
||||
---
|
||||
|
||||
<h2>
|
||||
<div set:html={IconClock({ size: 24 })}/>
|
||||
Recent posts
|
||||
</h2>
|
||||
{
|
||||
posts.slice(0, 5).map((post) => (
|
||||
<article key={post.url}>
|
||||
<div class="post-meta">
|
||||
{post.featured && <div set:html={IconStar({ size: 16 })}/>}
|
||||
<time datetime={post.date}>
|
||||
{new Date(post.date).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
<h3>
|
||||
<a href={post.url}>{post.title}</a>
|
||||
</h3>
|
||||
<p set:html={md(post.description)} />
|
||||
</article>
|
||||
))
|
||||
}
|
||||
<a class="icon-link" href="/posts">
|
||||
View all posts <div set:html={IconArrowRight({ size: 16 })}/>
|
||||
</a>
|
75
src/components/media/Grid.astro
Normal file
75
src/components/media/Grid.astro
Normal file
|
@ -0,0 +1,75 @@
|
|||
---
|
||||
import Paginator from "@components/nav/Paginator.astro";
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
|
||||
const { data, count, shape, pagination, loading = "lazy" } = Astro.props;
|
||||
const { globals } = await fetchGlobalData(Astro);
|
||||
const pageCount = pagination?.pages?.length || 0;
|
||||
const hidePagination = pageCount <= 1;
|
||||
|
||||
function getImageAttributes(item, shape) {
|
||||
let imageUrl = item.grid.image;
|
||||
let imageClass = "";
|
||||
let width = 0;
|
||||
let height = 0;
|
||||
|
||||
switch (shape) {
|
||||
case "poster":
|
||||
imageUrl = item.grid.backdrop;
|
||||
imageClass = "banner";
|
||||
width = 256;
|
||||
height = 170;
|
||||
break;
|
||||
case "square":
|
||||
imageClass = "square";
|
||||
width = 200;
|
||||
height = 200;
|
||||
break;
|
||||
case "vertical":
|
||||
imageClass = "vertical";
|
||||
width = 200;
|
||||
height = 307;
|
||||
break;
|
||||
}
|
||||
|
||||
return { imageUrl, imageClass, width, height };
|
||||
}
|
||||
---
|
||||
|
||||
<div class={`media-grid ${shape}`}>
|
||||
{
|
||||
data.slice(0, count).map((item) => {
|
||||
const alt = item.grid.alt?.replace(/['"]/g, "");
|
||||
const { imageUrl, imageClass, width, height } = getImageAttributes(
|
||||
item,
|
||||
shape
|
||||
);
|
||||
|
||||
return (
|
||||
<a href={item.grid.url} title={alt}>
|
||||
<div class="item media-overlay">
|
||||
<div class="meta-text">
|
||||
<div class="header">{item.grid.title}</div>
|
||||
<div class="subheader">{item.grid.subtext}</div>
|
||||
</div>
|
||||
<img
|
||||
srcset={`
|
||||
${globals.cdn_url}${imageUrl}?class=${imageClass}sm&type=webp ${width}w,
|
||||
${globals.cdn_url}${imageUrl}?class=${imageClass}md&type=webp ${width * 2}w
|
||||
`}
|
||||
sizes={`(max-width: 450px) ${width}px, ${width * 2}px`}
|
||||
src={`${globals.cdn_url}${imageUrl}?class=${imageClass}sm&type=webp`}
|
||||
alt={alt}
|
||||
loading={loading}
|
||||
decoding="async"
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
{!hidePagination && <Paginator pagination={pagination} />}
|
9
src/components/media/ProgressBar.astro
Normal file
9
src/components/media/ProgressBar.astro
Normal file
|
@ -0,0 +1,9 @@
|
|||
---
|
||||
const { percentage } = Astro.props;
|
||||
---
|
||||
|
||||
{percentage && (
|
||||
<div class="progress-bar-wrapper" title={percentage}>
|
||||
<div style={`width: ${percentage}`} class="progress-bar"/>
|
||||
</div>
|
||||
)}
|
33
src/components/media/music/Chart.astro
Normal file
33
src/components/media/music/Chart.astro
Normal file
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import ProgressBar from "@components/media/ProgressBar.astro";
|
||||
|
||||
const { data, count } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="music-chart">
|
||||
<ol type="1">
|
||||
{
|
||||
data.slice(0, count).map((item) => {
|
||||
const percentage = `${item.chart.percentage}%`;
|
||||
const playsLabel = item.chart.plays === 1 ? "play" : "plays";
|
||||
|
||||
return (
|
||||
<li value={item.chart.rank}>
|
||||
<div class="item">
|
||||
<div class="info">
|
||||
<a class="title" href={item.chart.url}>
|
||||
{item.chart.title}
|
||||
</a>
|
||||
<span class="subtext">{item.chart.artist}</span>
|
||||
<span class="subtext">
|
||||
{item.chart.plays} {playsLabel}
|
||||
</span>
|
||||
</div>
|
||||
<ProgressBar percentage={percentage} />
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
}
|
||||
</ol>
|
||||
</div>
|
48
src/components/media/music/Recent.astro
Normal file
48
src/components/media/music/Recent.astro
Normal file
|
@ -0,0 +1,48 @@
|
|||
---
|
||||
import { fetchGlobalData } from "@utils/data/global/index.js";
|
||||
|
||||
const { data } = Astro.props;
|
||||
const { globals } = await fetchGlobalData(Astro);
|
||||
---
|
||||
|
||||
<div class="music-chart">
|
||||
{
|
||||
data.slice(0, 10).map((item) => (
|
||||
<div class="item">
|
||||
<div class="meta">
|
||||
<a href={item.chart.url}>
|
||||
<img
|
||||
srcset={`
|
||||
${globals.cdn_url}${item.chart.image}?class=w50&type=webp 50w,
|
||||
${globals.cdn_url}${item.chart.image}?class=w100&type=webp 100w
|
||||
`}
|
||||
sizes="(max-width: 450px) 50px, 100px"
|
||||
src={`${globals.cdn_url}${item.chart.image}?class=w50&type=webp`}
|
||||
alt={item.chart.alt.replace(/['"]/g, "")}
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
width="64"
|
||||
height="64"
|
||||
/>
|
||||
</a>
|
||||
<div class="meta-text">
|
||||
<a class="title" href={item.chart.url}>
|
||||
{item.chart.title}
|
||||
</a>
|
||||
<span class="subtext">{item.chart.subtext}</span>
|
||||
</div>
|
||||
</div>
|
||||
<time datetime={item.chart.played_at}>
|
||||
{new Date(item.chart.played_at).toLocaleString("en-US", {
|
||||
timeZone: "America/Los_Angeles",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
hour12: true,
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
18
src/components/media/watching/Hero.astro
Normal file
18
src/components/media/watching/Hero.astro
Normal file
|
@ -0,0 +1,18 @@
|
|||
---
|
||||
import Hero from "@components/blocks/Hero.astro";
|
||||
|
||||
const { movie } = Astro.props;
|
||||
---
|
||||
|
||||
<a href={movie.url}>
|
||||
<div class="watching media-overlay hero">
|
||||
<div class="meta-text">
|
||||
<div class="header">{movie.title}</div>
|
||||
<div class="subheader">
|
||||
{movie.rating && <span class="rating">{movie.rating} </span>}
|
||||
({movie.year})
|
||||
</div>
|
||||
</div>
|
||||
<Hero image={movie.backdrop} alt={movie.title} />
|
||||
</div>
|
||||
</a>
|
33
src/components/nav/Menu.astro
Normal file
33
src/components/nav/Menu.astro
Normal file
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
import NavLink from "@components/nav/NavLink.astro";
|
||||
|
||||
const { IconMenu2, IconX } = icons
|
||||
const { nav } = Astro.props;
|
||||
---
|
||||
|
||||
<menu>
|
||||
<input id="menu-toggle" type="checkbox" aria-hidden="true" />
|
||||
<label class="menu-button-container" for="menu-toggle" tabindex="0">
|
||||
<div class="menu-closed" aria-hidden="true">
|
||||
<div set:html={IconMenu2({ size: 24 })}/>
|
||||
</div>
|
||||
<div class="menu-open" aria-hidden="true">
|
||||
<div set:html={IconX({ size: 24 })}/>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
<ul
|
||||
class="menu-primary"
|
||||
aria-label="Primary site navigation"
|
||||
id="primary-navigation"
|
||||
>
|
||||
{
|
||||
nav.primary.map((link) => (
|
||||
<li>
|
||||
<NavLink url={link.permalink} title={link.title} icon={link.icon} />
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</menu>
|
27
src/components/nav/NavLink.astro
Normal file
27
src/components/nav/NavLink.astro
Normal file
|
@ -0,0 +1,27 @@
|
|||
---
|
||||
import IconMapper from "@components/IconMapper.astro";
|
||||
import { removeTrailingSlash } from "@utils/helpers/general.js";
|
||||
|
||||
const { url, title, icon } = Astro.props;
|
||||
const isHttp = url?.startsWith("http");
|
||||
const isActive = Astro.url.pathname === removeTrailingSlash(url);
|
||||
---
|
||||
|
||||
{
|
||||
isActive ? (
|
||||
<span class={`active icon ${icon?.toLowerCase()}`} aria-current="page">
|
||||
<IconMapper icon={icon} />
|
||||
<span>{title}</span>
|
||||
</span>
|
||||
) : (
|
||||
<a
|
||||
class={`icon ${icon}`}
|
||||
href={url}
|
||||
rel={isHttp ? "me" : undefined}
|
||||
aria-label={title}
|
||||
>
|
||||
<IconMapper icon={icon} />
|
||||
<span>{title}</span>
|
||||
</a>
|
||||
)
|
||||
}
|
50
src/components/nav/Paginator.astro
Normal file
50
src/components/nav/Paginator.astro
Normal file
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
import icons from "@cdransf/astro-tabler-icons";
|
||||
|
||||
const { IconArrowLeft, IconArrowRight } = icons;
|
||||
const { pagination } = Astro.props;
|
||||
const {
|
||||
currentPage,
|
||||
totalPages,
|
||||
hasPrevious,
|
||||
hasNext,
|
||||
previousPage,
|
||||
nextPage,
|
||||
pages,
|
||||
} = pagination;
|
||||
---
|
||||
|
||||
<nav aria-label="Pagination" class="pagination">
|
||||
<a
|
||||
href={hasPrevious ? previousPage : "#"}
|
||||
aria-label="Previous page"
|
||||
class={hasPrevious ? "" : "disabled"}
|
||||
>
|
||||
<div set:html={IconArrowLeft({ size: 24 })}/>
|
||||
</a>
|
||||
|
||||
<select class="client-side" aria-label="Page selection">
|
||||
{pages.map((page, index) => (
|
||||
<option
|
||||
value={index}
|
||||
data-href={page.href}
|
||||
selected={page.number === currentPage}
|
||||
>
|
||||
{page.number} of {totalPages}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<noscript>
|
||||
<p>
|
||||
<span aria-current="page">{currentPage}</span> of {totalPages}
|
||||
</p>
|
||||
</noscript>
|
||||
|
||||
<a
|
||||
href={hasNext ? nextPage : "#"}
|
||||
aria-label="Next page"
|
||||
class={hasNext ? "" : "disabled"}
|
||||
>
|
||||
<div set:html={IconArrowRight({ size: 24 })}/>
|
||||
</a>
|
||||
</nav>
|
8
src/components/utils/ToggleContent.astro
Normal file
8
src/components/utils/ToggleContent.astro
Normal file
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
const { content } = Astro.props;
|
||||
---
|
||||
|
||||
<div data-toggle-content class="text-toggle-hidden">
|
||||
<div set:html={content} />
|
||||
</div>
|
||||
<button data-toggle-button>Show more</button>
|
Reference in a new issue