diff options
| author | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-22 15:08:37 +0300 |
|---|---|---|
| committer | Dawid Rycerz <dawid@rycerz.xyz> | 2025-07-22 15:08:37 +0300 |
| commit | fcc2f4704e39b0e69b377cc138f75027721dac22 (patch) | |
| tree | 732fc94b354a26c08fba9cc9059f9c6c900182be /src/components/blog | |
Initial template
Diffstat (limited to 'src/components/blog')
| -rw-r--r-- | src/components/blog/Grid.astro | 14 | ||||
| -rw-r--r-- | src/components/blog/GridItem.astro | 71 | ||||
| -rw-r--r-- | src/components/blog/Headline.astro | 12 | ||||
| -rw-r--r-- | src/components/blog/List.astro | 20 | ||||
| -rw-r--r-- | src/components/blog/ListItem.astro | 120 | ||||
| -rw-r--r-- | src/components/blog/Pagination.astro | 36 | ||||
| -rw-r--r-- | src/components/blog/RelatedPosts.astro | 31 | ||||
| -rw-r--r-- | src/components/blog/SinglePost.astro | 103 | ||||
| -rw-r--r-- | src/components/blog/Tags.astro | 43 | ||||
| -rw-r--r-- | src/components/blog/ToBlogLink.astro | 20 |
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..73353ca --- /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-secondary 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..36602f2 --- /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-secondary 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..ac92cd3 --- /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 && ( + <> + · <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-primary 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> |
