summaryrefslogtreecommitdiff
path: root/src/components/blog
diff options
context:
space:
mode:
Diffstat (limited to 'src/components/blog')
-rw-r--r--src/components/blog/Grid.astro14
-rw-r--r--src/components/blog/GridItem.astro71
-rw-r--r--src/components/blog/Headline.astro12
-rw-r--r--src/components/blog/List.astro20
-rw-r--r--src/components/blog/ListItem.astro120
-rw-r--r--src/components/blog/Pagination.astro36
-rw-r--r--src/components/blog/RelatedPosts.astro31
-rw-r--r--src/components/blog/SinglePost.astro103
-rw-r--r--src/components/blog/Tags.astro43
-rw-r--r--src/components/blog/ToBlogLink.astro20
10 files changed, 470 insertions, 0 deletions
diff --git a/src/components/blog/Grid.astro b/src/components/blog/Grid.astro
new file mode 100644
index 0000000..1b62be4
--- /dev/null
+++ b/src/components/blog/Grid.astro
@@ -0,0 +1,14 @@
+---
+import Item from '~/components/blog/GridItem.astro';
+import type { Post } from '~/types';
+
+export interface Props {
+ posts: Array<Post>;
+}
+
+const { posts } = Astro.props;
+---
+
+<div class="grid gap-6 row-gap-5 md:grid-cols-2 lg:grid-cols-4 -mb-6">
+ {posts.map((post) => <Item post={post} />)}
+</div>
diff --git a/src/components/blog/GridItem.astro b/src/components/blog/GridItem.astro
new file mode 100644
index 0000000..cd02fa8
--- /dev/null
+++ b/src/components/blog/GridItem.astro
@@ -0,0 +1,71 @@
+---
+import { APP_BLOG } from 'astrowind:config';
+import type { Post } from '~/types';
+
+import Image from '~/components/common/Image.astro';
+
+import { findImage } from '~/utils/images';
+import { getPermalink } from '~/utils/permalinks';
+
+export interface Props {
+ post: Post;
+}
+
+const { post } = Astro.props;
+const image = await findImage(post.image);
+
+const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
+---
+
+<article
+ class="mb-6 transition intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade"
+>
+ <div class="relative md:h-64 bg-gray-400 dark:bg-slate-700 rounded shadow-lg mb-6">
+ {
+ image &&
+ (link ? (
+ <a href={link}>
+ <Image
+ src={image}
+ class="w-full md:h-full rounded shadow-lg bg-gray-400 dark:bg-slate-700"
+ widths={[400, 900]}
+ width={400}
+ sizes="(max-width: 900px) 400px, 900px"
+ alt={post.title}
+ aspectRatio="16:9"
+ layout="cover"
+ loading="lazy"
+ decoding="async"
+ />
+ </a>
+ ) : (
+ <Image
+ src={image}
+ class="w-full md:h-full rounded shadow-lg bg-gray-400 dark:bg-slate-700"
+ widths={[400, 900]}
+ width={400}
+ sizes="(max-width: 900px) 400px, 900px"
+ alt={post.title}
+ aspectRatio="16:9"
+ layout="cover"
+ loading="lazy"
+ decoding="async"
+ />
+ ))
+ }
+ </div>
+
+ <h3 class="text-xl sm:text-2xl font-bold leading-tight mb-2 font-heading dark:text-slate-300">
+ {
+ link ? (
+ <a class="inline-block hover:text-primary dark:hover:text-blue-700 transition ease-in duration-200" href={link}>
+ {post.title}
+ </a>
+ ) : (
+ post.title
+ )
+ }
+ </h3>
+
+ <p class="text-muted dark:text-slate-400 text-lg">{post.excerpt}</p>
+</article>
diff --git a/src/components/blog/Headline.astro b/src/components/blog/Headline.astro
new file mode 100644
index 0000000..5d3ccc6
--- /dev/null
+++ b/src/components/blog/Headline.astro
@@ -0,0 +1,12 @@
+---
+const { title = await Astro.slots.render('default'), subtitle = await Astro.slots.render('subtitle') } = Astro.props;
+---
+
+<header class="mb-8 md:mb-16 text-center max-w-3xl mx-auto">
+ <h1 class="text-4xl md:text-5xl font-bold leading-tighter tracking-tighter font-heading" set:html={title} />
+ {
+ subtitle && (
+ <div class="mt-2 md:mt-3 mx-auto text-xl text-gray-500 dark:text-slate-400 font-medium" set:html={subtitle} />
+ )
+ }
+</header>
diff --git a/src/components/blog/List.astro b/src/components/blog/List.astro
new file mode 100644
index 0000000..6a80ae3
--- /dev/null
+++ b/src/components/blog/List.astro
@@ -0,0 +1,20 @@
+---
+import Item from '~/components/blog/ListItem.astro';
+import type { Post } from '~/types';
+
+export interface Props {
+ posts: Array<Post>;
+}
+
+const { posts } = Astro.props;
+---
+
+<ul>
+ {
+ posts.map((post) => (
+ <li class="mb-12 md:mb-20">
+ <Item post={post} />
+ </li>
+ ))
+ }
+</ul>
diff --git a/src/components/blog/ListItem.astro b/src/components/blog/ListItem.astro
new file mode 100644
index 0000000..6a416d6
--- /dev/null
+++ b/src/components/blog/ListItem.astro
@@ -0,0 +1,120 @@
+---
+import type { ImageMetadata } from 'astro';
+import { Icon } from 'astro-icon/components';
+import Image from '~/components/common/Image.astro';
+import PostTags from '~/components/blog/Tags.astro';
+
+import { APP_BLOG } from 'astrowind:config';
+import type { Post } from '~/types';
+
+import { getPermalink } from '~/utils/permalinks';
+import { findImage } from '~/utils/images';
+import { getFormattedDate } from '~/utils/utils';
+
+export interface Props {
+ post: Post;
+}
+
+const { post } = Astro.props;
+const image = (await findImage(post.image)) as ImageMetadata | undefined;
+
+const link = APP_BLOG?.post?.isEnabled ? getPermalink(post.permalink, 'post') : '';
+---
+
+<article
+ class={`max-w-md mx-auto md:max-w-none grid gap-6 md:gap-8 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade ${image ? 'md:grid-cols-2' : ''}`}
+>
+ {
+ image &&
+ (link ? (
+ <a class="relative block group" href={link ?? 'javascript:void(0)'}>
+ <div class="relative h-0 pb-[56.25%] md:pb-[75%] md:h-72 lg:pb-[56.25%] overflow-hidden bg-gray-400 dark:bg-slate-700 rounded shadow-lg">
+ {image && (
+ <Image
+ src={image}
+ class="absolute inset-0 object-cover w-full h-full mb-6 rounded shadow-lg bg-gray-400 dark:bg-slate-700"
+ widths={[400, 900]}
+ width={900}
+ sizes="(max-width: 900px) 400px, 900px"
+ alt={post.title}
+ aspectRatio="16:9"
+ loading="lazy"
+ decoding="async"
+ />
+ )}
+ </div>
+ </a>
+ ) : (
+ <div class="relative h-0 pb-[56.25%] md:pb-[75%] md:h-72 lg:pb-[56.25%] overflow-hidden bg-gray-400 dark:bg-slate-700 rounded shadow-lg">
+ {image && (
+ <Image
+ src={image}
+ class="absolute inset-0 object-cover w-full h-full mb-6 rounded shadow-lg bg-gray-400 dark:bg-slate-700"
+ widths={[400, 900]}
+ width={900}
+ sizes="(max-width: 900px) 400px, 900px"
+ alt={post.title}
+ aspectRatio="16:9"
+ loading="lazy"
+ decoding="async"
+ />
+ )}
+ </div>
+ ))
+ }
+ <div class="mt-2">
+ <header>
+ <div class="mb-1">
+ <span class="text-sm">
+ <Icon name="tabler:clock" class="w-3.5 h-3.5 inline-block -mt-0.5 dark:text-gray-400" />
+ <time datetime={String(post.publishDate)} class="inline-block">{getFormattedDate(post.publishDate)}</time>
+ {
+ post.author && (
+ <>
+ {' '}
+ · <Icon name="tabler:user" class="w-3.5 h-3.5 inline-block -mt-0.5 dark:text-gray-400" />
+ <span>{post.author.replaceAll('-', ' ')}</span>
+ </>
+ )
+ }
+ {
+ post.category && (
+ <>
+ {' '}
+ ·{' '}
+ <a class="hover:underline" href={getPermalink(post.category.slug, 'category')}>
+ {post.category.title}
+ </a>
+ </>
+ )
+ }
+ </span>
+ </div>
+ <h2 class="text-xl sm:text-2xl font-bold leading-tight mb-2 font-heading dark:text-slate-300">
+ {
+ link ? (
+ <a
+ class="inline-block hover:text-primary dark:hover:text-blue-700 transition ease-in duration-200"
+ href={link}
+ >
+ {post.title}
+ </a>
+ ) : (
+ post.title
+ )
+ }
+ </h2>
+ </header>
+
+ {post.excerpt && <p class="flex-grow text-muted dark:text-slate-400 text-lg">{post.excerpt}</p>}
+ {
+ post.tags && Array.isArray(post.tags) ? (
+ <footer class="mt-5">
+ <PostTags tags={post.tags} />
+ </footer>
+ ) : (
+ <Fragment />
+ )
+ }
+ </div>
+</article>
diff --git a/src/components/blog/Pagination.astro b/src/components/blog/Pagination.astro
new file mode 100644
index 0000000..051587c
--- /dev/null
+++ b/src/components/blog/Pagination.astro
@@ -0,0 +1,36 @@
+---
+import { Icon } from 'astro-icon/components';
+import { getPermalink } from '~/utils/permalinks';
+import Button from '~/components/ui/Button.astro';
+
+export interface Props {
+ prevUrl?: string;
+ nextUrl?: string;
+ prevText?: string;
+ nextText?: string;
+}
+
+const { prevUrl, nextUrl, prevText = 'Newer posts', nextText = 'Older posts' } = Astro.props;
+---
+
+{
+ (prevUrl || nextUrl) && (
+ <div class="container flex">
+ <div class="flex flex-row mx-auto container justify-between">
+ <Button
+ variant="tertiary"
+ class={`md:px-3 px-3 mr-2 ${!prevUrl ? 'invisible' : ''}`}
+ href={getPermalink(prevUrl)}
+ >
+ <Icon name="tabler:chevron-left" class="w-6 h-6" />
+ <p class="ml-2">{prevText}</p>
+ </Button>
+
+ <Button variant="tertiary" class={`md:px-3 px-3 ${!nextUrl ? 'invisible' : ''}`} href={getPermalink(nextUrl)}>
+ <span class="mr-2">{nextText}</span>
+ <Icon name="tabler:chevron-right" class="w-6 h-6" />
+ </Button>
+ </div>
+ </div>
+ )
+}
diff --git a/src/components/blog/RelatedPosts.astro b/src/components/blog/RelatedPosts.astro
new file mode 100644
index 0000000..f4036e9
--- /dev/null
+++ b/src/components/blog/RelatedPosts.astro
@@ -0,0 +1,31 @@
+---
+import { APP_BLOG } from 'astrowind:config';
+
+import { getRelatedPosts } from '~/utils/blog';
+import BlogHighlightedPosts from '../widgets/BlogHighlightedPosts.astro';
+import type { Post } from '~/types';
+import { getBlogPermalink } from '~/utils/permalinks';
+
+export interface Props {
+ post: Post;
+}
+
+const { post } = Astro.props;
+
+const relatedPosts = post.tags ? await getRelatedPosts(post, 4) : [];
+---
+
+{
+ APP_BLOG.isRelatedPostsEnabled ? (
+ <BlogHighlightedPosts
+ classes={{
+ container:
+ 'pt-0 lg:pt-0 md:pt-0 intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade',
+ }}
+ title="Related Posts"
+ linkText="View All Posts"
+ linkUrl={getBlogPermalink()}
+ postIds={relatedPosts.map((post) => post.id)}
+ />
+ ) : null
+}
diff --git a/src/components/blog/SinglePost.astro b/src/components/blog/SinglePost.astro
new file mode 100644
index 0000000..297cca9
--- /dev/null
+++ b/src/components/blog/SinglePost.astro
@@ -0,0 +1,103 @@
+---
+import { Icon } from 'astro-icon/components';
+
+import Image from '~/components/common/Image.astro';
+import PostTags from '~/components/blog/Tags.astro';
+import SocialShare from '~/components/common/SocialShare.astro';
+
+import { getPermalink } from '~/utils/permalinks';
+import { getFormattedDate } from '~/utils/utils';
+
+import type { Post } from '~/types';
+
+export interface Props {
+ post: Post;
+ url: string | URL;
+}
+
+const { post, url } = Astro.props;
+---
+
+<section class="py-8 sm:py-16 lg:py-20 mx-auto">
+ <article>
+ <header
+ class={post.image
+ ? 'intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade'
+ : 'intersect-once intersect-quarter motion-safe:md:opacity-0 motion-safe:md:intersect:animate-fade'}
+ >
+ <div class="flex justify-between flex-col sm:flex-row max-w-3xl mx-auto mt-0 mb-2 px-4 sm:px-6 sm:items-center">
+ <p>
+ <Icon name="tabler:clock" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />
+ <time datetime={String(post.publishDate)} class="inline-block">{getFormattedDate(post.publishDate)}</time>
+ {
+ post.author && (
+ <>
+ {' '}
+ · <Icon name="tabler:user" class="w-4 h-4 inline-block -mt-0.5 dark:text-gray-400" />
+ <span class="inline-block">{post.author}</span>
+ </>
+ )
+ }
+ {
+ post.category && (
+ <>
+ {' '}
+ ·{' '}
+ <a class="hover:underline inline-block" href={getPermalink(post.category.slug, 'category')}>
+ {post.category.title}
+ </a>
+ </>
+ )
+ }
+ {
+ post.readingTime && (
+ <>
+ &nbsp;· <span>{post.readingTime}</span> min read
+ </>
+ )
+ }
+ </p>
+ </div>
+
+ <h1
+ class="px-4 sm:px-6 max-w-3xl mx-auto text-4xl md:text-5xl font-bold leading-tighter tracking-tighter font-heading"
+ >
+ {post.title}
+ </h1>
+ <p
+ class="max-w-3xl mx-auto mt-4 mb-8 px-4 sm:px-6 text-xl md:text-2xl text-muted dark:text-slate-400 text-justify"
+ >
+ {post.excerpt}
+ </p>
+
+ {
+ post.image ? (
+ <Image
+ src={post.image}
+ class="max-w-full lg:max-w-[900px] mx-auto mb-6 sm:rounded-md bg-gray-400 dark:bg-slate-700"
+ widths={[400, 900]}
+ sizes="(max-width: 900px) 400px, 900px"
+ alt={post?.excerpt || ''}
+ width={900}
+ height={506}
+ loading="eager"
+ decoding="async"
+ />
+ ) : (
+ <div class="max-w-3xl mx-auto px-4 sm:px-6">
+ <div class="border-t dark:border-slate-700" />
+ </div>
+ )
+ }
+ </header>
+ <div
+ class="mx-auto px-6 sm:px-6 max-w-3xl prose prose-md lg:prose-xl dark:prose-invert dark:prose-headings:text-slate-300 prose-headings:font-heading prose-headings:leading-tighter prose-headings:tracking-tighter prose-headings:font-bold prose-a:text-primary dark:prose-a:text-blue-400 prose-img:rounded-md prose-img:shadow-lg mt-8 prose-headings:scroll-mt-[80px] prose-li:my-0"
+ >
+ <slot />
+ </div>
+ <div class="mx-auto px-6 sm:px-6 max-w-3xl mt-8 flex justify-between flex-col sm:flex-row">
+ <PostTags tags={post.tags} class="mr-5 rtl:mr-0 rtl:ml-5" />
+ <SocialShare url={url} text={post.title} class="mt-5 sm:mt-1 align-middle text-gray-500 dark:text-slate-600" />
+ </div>
+ </article>
+</section>
diff --git a/src/components/blog/Tags.astro b/src/components/blog/Tags.astro
new file mode 100644
index 0000000..ae46a24
--- /dev/null
+++ b/src/components/blog/Tags.astro
@@ -0,0 +1,43 @@
+---
+import { getPermalink } from '~/utils/permalinks';
+
+import { APP_BLOG } from 'astrowind:config';
+import type { Post } from '~/types';
+
+export interface Props {
+ tags: Post['tags'];
+ class?: string;
+ title?: string | undefined;
+ isCategory?: boolean;
+}
+
+const { tags, class: className = 'text-sm', title = undefined, isCategory = false } = Astro.props;
+---
+
+{
+ tags && Array.isArray(tags) && (
+ <>
+ {title !== undefined && (
+ <span class="align-super font-normal underline underline-offset-4 decoration-2 dark:text-slate-400">
+ {title}
+ </span>
+ )}
+ <ul class={className}>
+ {tags.map((tag) => (
+ <li class="bg-gray-100 dark:bg-slate-700 inline-block mr-2 rtl:mr-0 rtl:ml-2 mb-2 py-0.5 px-2 lowercase font-medium">
+ {!APP_BLOG?.tag?.isEnabled ? (
+ tag.title
+ ) : (
+ <a
+ href={getPermalink(tag.slug, isCategory ? 'category' : 'tag')}
+ class="text-muted dark:text-slate-300 hover:text-primary dark:hover:text-gray-200"
+ >
+ {tag.title}
+ </a>
+ )}
+ </li>
+ ))}
+ </ul>
+ </>
+ )
+}
diff --git a/src/components/blog/ToBlogLink.astro b/src/components/blog/ToBlogLink.astro
new file mode 100644
index 0000000..7fb7a49
--- /dev/null
+++ b/src/components/blog/ToBlogLink.astro
@@ -0,0 +1,20 @@
+---
+import { Icon } from 'astro-icon/components';
+import { getBlogPermalink } from '~/utils/permalinks';
+import { I18N } from 'astrowind:config';
+import Button from '~/components/ui/Button.astro';
+
+const { textDirection } = I18N;
+---
+
+<div class="mx-auto px-6 sm:px-6 max-w-3xl pt-8 md:pt-4 pb-12 md:pb-20">
+ <Button variant="tertiary" class="px-3 md:px-3" href={getBlogPermalink()}>
+ {
+ textDirection === 'rtl' ? (
+ <Icon name="tabler:chevron-right" class="w-5 h-5 mr-1 -ml-1.5 rtl:-mr-1.5 rtl:ml-1" />
+ ) : (
+ <Icon name="tabler:chevron-left" class="w-5 h-5 mr-1 -ml-1.5 rtl:-mr-1.5 rtl:ml-1" />
+ )
+ } Back to Blog
+ </Button>
+</div>