Compare commits

..

10 Commits

Author SHA1 Message Date
1a8473b964 feat: release 2.12.0
All checks were successful
test-build / guarddog (push) Successful in 1m11s
renovate / renovate (push) Successful in 1m19s
release-image-gitea / build (push) Successful in 2m28s
release-image-harbor / build (push) Successful in 2m19s
test-build / build (push) Successful in 5m33s
release-image-gitea / release (push) Successful in 7m39s
release-image-harbor / release (push) Successful in 7m16s
2026-02-16 23:08:06 -06:00
18211ad485 feat: update BaseHead
All checks were successful
renovate / renovate (push) Successful in 1m33s
test-build / build (push) Successful in 1m40s
test-build / guarddog (push) Successful in 1m59s
2026-02-16 23:04:42 -06:00
429cf94023 feat: organize to consistency pass on sections 2026-02-16 22:57:39 -06:00
0497731c45 feat: organize to consistency 2026-02-16 22:38:45 -06:00
6c2c6da91d feat: organize to consistency 2026-02-16 22:36:24 -06:00
19e17ea947 feat: remove option 2026-02-16 22:34:57 -06:00
3d9120c570 fix: remove unused property 2026-02-16 22:34:14 -06:00
875b8a7f47 fix: remove border from blog cards 2026-02-16 22:32:12 -06:00
1ddc76ae69 fix: remove errant semicolon 2026-02-16 22:30:04 -06:00
6423ffba63 feat: refactor blog components 2026-02-16 22:26:53 -06:00
37 changed files with 520 additions and 509 deletions

View File

@@ -27,7 +27,7 @@ ENV SITE_URL=https://www.alexlebens.dev
ENV DIRECTUS_URL=https://directus.alexlebens.net ENV DIRECTUS_URL=https://directus.alexlebens.net
ENV PORT=4321 ENV PORT=4321
LABEL version="2.11.0" LABEL version="2.12.0"
LABEL description="Astro based personal website" LABEL description="Astro based personal website"
EXPOSE $PORT EXPOSE $PORT

View File

@@ -1,7 +1,7 @@
{ {
"name": "site-profile", "name": "site-profile",
"type": "module", "type": "module",
"version": "2.11.0", "version": "2.12.0",
"homepage": "https://www.alexlebens.dev", "homepage": "https://www.alexlebens.dev",
"bugs": { "bugs": {
"url": "https://gitea.alexlebens.dev/alexlebens/site-profile/issues", "url": "https://gitea.alexlebens.dev/alexlebens/site-profile/issues",

View File

@@ -3,10 +3,11 @@ import { getImage } from 'astro:assets';
import { readSingleton } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus'; import directus from '@lib/directus';
import { SEO } from '@/config';
import brandSrc from '@images/brand_logo.png'; import brandSrc from '@images/brand_logo.png';
import faviconSvgSrc from '@images/favicon_icon.svg'; import faviconSvgSrc from '@images/favicon_icon.svg';
import faviconSrc from '@images/favicon_icon.png'; import faviconSrc from '@images/favicon_icon.png';
import { SEO } from '@/config';
interface Props { interface Props {
title: string; title: string;
@@ -18,6 +19,7 @@ interface Props {
} }
const canonicalURL = Astro.url.href; const canonicalURL = Astro.url.href;
let { let {
title, title,
description, description,
@@ -27,14 +29,14 @@ let {
structuredData = SEO.structuredData, structuredData = SEO.structuredData,
} = Astro.props; } = Astro.props;
const global = await directus.request(readSingleton('site_global'));
let card = 'summary_large_image'; let card = 'summary_large_image';
if (!ogImage) { if (!ogImage) {
ogImage = brandSrc; ogImage = brandSrc;
card = 'summary'; card = 'summary';
} }
const global = await directus.request(readSingleton('site_global'));
const faviconSvg = await getImage({ src: faviconSvgSrc, format: 'svg' }); const faviconSvg = await getImage({ src: faviconSvgSrc, format: 'svg' });
const appleTouchIcon = await getImage({ src: faviconSrc, width: 180, height: 180, format: 'png' }); const appleTouchIcon = await getImage({ src: faviconSrc, width: 180, height: 180, format: 'png' });
const socialImageRes = await getImage({ src: ogImage, width: 1200, height: 600 }); const socialImageRes = await getImage({ src: ogImage, width: 1200, height: 600 });
@@ -62,12 +64,12 @@ if (!socialImage.startsWith('http')) {
<meta http-equiv="X-UA-Compatible" content="ie=edge" /> <meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#facc15" /> <meta name="theme-color" content="#facc15" />
<meta name="robots" content="index, follow" />
<!-- Open Graph --> <!-- Open Graph -->
<meta property="og:type" content="website" /> <meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" /> <meta property="og:locale" content="en_US" />
<meta property="og:url" content={Astro.url} /> <meta property="og:url" content={Astro.url} />
<meta property="og:type" content="website" />
<meta property="og:title" content={ogTitle} /> <meta property="og:title" content={ogTitle} />
<meta property="og:site_name" content={global.name} /> <meta property="og:site_name" content={global.name} />
<meta property="og:description" content={ogDescription} /> <meta property="og:description" content={ogDescription} />
@@ -76,17 +78,10 @@ if (!socialImage.startsWith('http')) {
<meta content="600" property="og:image:height" /> <meta content="600" property="og:image:height" />
<meta content="image/png" property="og:image:type" /> <meta content="image/png" property="og:image:type" />
<!-- Twitter -->
<meta property="twitter:card" content={card} />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:domain" content={Astro.url} />
<meta property="twitter:title" content={ogTitle} />
<meta property="twitter:description" content={ogDescription} />
<meta property="twitter:image" content={socialImage} />
<!-- Links --> <!-- Links -->
<link href={canonicalURL} rel="canonical" /> <link href={canonicalURL} rel="canonical" />
<link rel="sitemap" href="/sitemap-index.xml" /> <link rel="sitemap" href="/sitemap-index.xml" />
<link rel="alternate" type="application/rss+xml" title={title} href="/rss.xml" />
<!--<link href="/manifest.json" rel="manifest" />--> <!--<link href="/manifest.json" rel="manifest" />-->
<link href="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" /> <link href="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" />
<link href={faviconSvg.src} rel="icon" type="image/svg+xml" sizes="any" /> <link href={faviconSvg.src} rel="icon" type="image/svg+xml" sizes="any" />

View File

@@ -1,13 +1,15 @@
--- ---
import { readSingleton } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import BrandLogo from '@components/ui/logos/BrandLogo.astro'; import BrandLogo from '@components/ui/logos/BrandLogo.astro';
import Image from '@components/ui/images/Image.astro'; import Image from '@components/ui/images/Image.astro';
import directus from '@lib/directus';
import { NavigationLinks, FooterLinks } from '@/config'; import { NavigationLinks, FooterLinks } from '@/config';
import footerImg from '@images/flowers.png'; import footerImg from '@images/flowers.png';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
--- ---

View File

@@ -1,61 +0,0 @@
---
import { Icon } from 'astro-icon/components';
import type { Post } from '@lib/directusTypes';
import { getDirectusImageURL } from '@lib/directusFunctions';
import Image from '@components/ui/images/Image.astro';
import { formatDate } from '@support/time';
interface Props {
post: Post;
}
const { post } = Astro.props;
const baseClasses = 'group group-hover smooth-reveal-cards rounded-xl flex flex-col';
const borderClasses = 'border border-stone-200/50 dark:border-stone-700/50';
const bgColorClasses =
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
---
<div class={`${baseClasses}`}>
<a
class={`rounded-xl duration-300 transition-all ${borderClasses} ${shadowClasses} ${bgColorClasses}`}
href={`/blog/${post.slug}/`}
data-astro-prefetch
>
<div
class="relative w-full flex-shrink-0 overflow-hidden rounded-t-xl before:absolute before:inset-x-0 before:z-[1] before:size-full"
>
<Image
class="h-auto w-full rounded-t-xl"
src={getDirectusImageURL(post.image)}
alt={post.image_alt}
draggable="false"
loading="eager"
format="webp"
width="800"
height="460"
/>
</div>
<div class="rounded-xl p-4 md:p-5">
<h3 class="text-xl font-bold text-neutral-600 dark:text-neutral-200">
{post.title}
</h3>
<div
class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-[44px] items-center font-medium text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-400"
>
<span class="relative inline-block overflow-hidden"> Read more </span>
<Icon
name="mdi:keyboard-arrow-right"
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
/>
<p class="ml-auto text-sm text-neutral-600 dark:text-neutral-400">
{formatDate(post.published_date)}
</p>
</div>
</div>
</a>
</div>

View File

@@ -1,44 +0,0 @@
---
import GoLinkPrimaryButton from '@components/buttons/GoLinkPrimaryButton.astro';
import Image from '@components/ui/images/Image.astro';
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
img: any;
imgAlt: any;
}
const { title, subTitle, btnExists, btnTitle, btnURL, img, imgAlt } = Astro.props;
---
<section
class="mx-auto max-w-[85rem] items-center gap-8 px-4 py-10 sm:px-6 sm:py-16 md:grid md:grid-cols-2 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 xl:gap-16 2xl:max-w-full"
>
<Image
class="h-full w-full rounded-xl object-cover sm:max-h-[320px] md:max-h-[360px]"
src={img}
alt={imgAlt}
draggable="false"
loading="lazy"
width="850"
height="420"
/>
<div class="mt-4 md:mt-0">
<h2
class="mb-4 text-4xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
>
{title}
</h2>
<p
class="mb-4 max-w-prose font-light text-pretty text-neutral-600 sm:text-lg dark:text-neutral-300"
>
{subTitle}
</p>
{btnExists ? <GoLinkPrimaryButton title={btnTitle} url={btnURL} /> : null}
</div>
</section>

View File

@@ -1,29 +0,0 @@
---
import type { Post } from '@lib/directusTypes';
import BlogCard from '@components/blog/BlogCard.astro';
interface Props {
posts: Post[];
}
const { posts } = Astro.props;
---
<section class="mx-auto mb-10 max-w-[85rem] px-4 py-8 sm:px-6 lg:px-8 2xl:max-w-full">
<div class="text-left">
<h2
id="recent-articles"
class="smooth-reveal-2 mb-10 text-5xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
>
Recent Posts
</h2>
</div>
<div class="flex flex-col md:flex-row md:space-x-12 lg:space-x-16">
<div class="w-full">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{posts.map((b) => <BlogCard post={b} />)}
</div>
</div>
</div>
</section>

View File

@@ -1,87 +0,0 @@
---
import GoLinkPrimaryButton from '@components/buttons/GoLinkPrimaryButton.astro';
import Image from '@components/ui/images/Image.astro';
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
single?: boolean;
imgOne?: any;
imgOneAlt?: any;
imgTwo?: any;
imgTwoAlt?: any;
}
const {
title,
subTitle,
btnExists,
btnTitle,
btnURL,
single,
imgOne,
imgOneAlt,
imgTwo,
imgTwoAlt,
} = Astro.props;
---
<section
class="mx-auto max-w-[85rem] items-center gap-16 px-4 py-10 sm:px-6 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div>
<h2
class="mb-4 text-4xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
>
{title}
</h2>
<p
class="mb-4 max-w-prose font-light text-pretty text-neutral-600 sm:text-lg dark:text-neutral-400"
>
{subTitle}
</p>
{btnExists ? <GoLinkPrimaryButton title={btnTitle} url={btnURL} /> : null}
</div>
{
single ? (
<div class="mt-8">
<Image
class="w-full rounded-lg"
src={imgOne}
alt={imgOneAlt}
format="webp"
loading="lazy"
width="850"
height="420"
/>
</div>
) : (
<div class="mt-8 grid grid-cols-2 gap-4">
<Image
class="w-full rounded-xl"
src={imgOne}
alt={imgOneAlt}
draggable="false"
format="webp"
loading="lazy"
width="400"
height="230"
/>
<Image
class="mt-4 w-full rounded-xl lg:mt-10"
src={imgTwo}
alt={imgTwoAlt}
draggable="false"
format="webp"
loading="lazy"
width="400"
height="230"
/>
</div>
)
}
</section>

View File

@@ -1,44 +0,0 @@
---
import type { Post } from '@lib/directusTypes';
import { getDirectusImageURL } from '@lib/directusFunctions';
import BlogLeftSection from '@components/blog/BlogLeftSection.astro';
import BlogRightSection from '@components/blog/BlogRightSection.astro';
interface Props {
posts: Post[];
}
const { posts } = Astro.props;
---
<section class="smooth-reveal">
{
posts.map((b, index) =>
index % 2 === 0 ? (
<BlogLeftSection
title={b.title}
subTitle={b.description}
btnExists={true}
btnTitle="Read More"
btnURL={`/blog/${b.slug}`}
img={getDirectusImageURL(b.image)}
imgAlt={b.image_alt}
/>
) : (
<BlogRightSection
title={b.title}
subTitle={b.description}
btnExists={true}
btnTitle="Read More"
btnURL={`/blog/${b.slug}`}
single={!b.image_second}
imgOne={getDirectusImageURL(b.image)}
imgOneAlt={b.image_alt}
imgTwo={getDirectusImageURL(b?.image_second)}
imgTwoAlt={b?.image_second_alt}
/>
)
)
}
</section>

View File

@@ -2,11 +2,10 @@
import { Icon } from 'astro-icon/components'; import { Icon } from 'astro-icon/components';
interface Props { interface Props {
title?: string;
url?: string; url?: string;
} }
const { title, url } = Astro.props; const { url } = Astro.props;
--- ---
<a <a

View File

@@ -1,11 +1,5 @@
--- ---
import Icon from '@components/ui/icons/icon.astro'; import Icon from '@components/ui/icons/icon.astro';
interface Props {
noArrow?: boolean;
}
const { noArrow } = Astro.props;
--- ---
<button <button
@@ -14,7 +8,7 @@ const { noArrow } = Astro.props;
data-astro-prefetch data-astro-prefetch
> >
<div class="button-text-title flex relative items-center text-center"> <div class="button-text-title flex relative items-center text-center">
{noArrow ? null : <Icon name="arrowLeft" />} <Icon name="arrowLeft" />
<span class="ml-2"> <span class="ml-2">
Go Back Go Back
</span> </span>

View File

@@ -33,8 +33,7 @@ const socialPlatforms: SocialPlatform[] = [
--- ---
<div class="inline-flex items-center gap-x-2"> <div class="inline-flex items-center gap-x-2">
{ {socialPlatforms.map((platform) => (
socialPlatforms.map((platform) => (
<a <a
class="button-base-hidden group inline-flex rounded-lg gap-x-2" class="button-base-hidden group inline-flex rounded-lg gap-x-2"
href={platform.url} href={platform.url}
@@ -43,9 +42,11 @@ const socialPlatforms: SocialPlatform[] = [
title={`Share on ${platform.name}`} title={`Share on ${platform.name}`}
> >
<div class="button-text-title-hidden flex relative items-center text-center"> <div class="button-text-title-hidden flex relative items-center text-center">
<Icon name={platform.svg} class="h-5 w-5" /> <Icon
name={platform.svg}
class="h-5 w-5"
/>
</div> </div>
</a> </a>
)) ))}
}
</div> </div>

View File

@@ -0,0 +1,58 @@
---
import { Icon } from 'astro-icon/components';
import type { Post } from '@lib/directusTypes';
import Image from '@components/ui/images/Image.astro';
import { getDirectusImageURL } from '@lib/directusFunctions';
interface Props {
post: Post;
}
const { post } = Astro.props;
---
<div class="smooth-reveal-cards group flex flex-col">
<a
class="card-base border-none!"
href={`/blog/${post.slug}/`}
data-astro-prefetch
>
<div class="relative shrink-0 rounded-t-xl w-full overflow-hidden before:absolute before:inset-x-0 before:z-1 before:size-full">
<Image
class="rounded-t-xl h-auto w-full"
src={getDirectusImageURL(post.image)}
alt={post.image_alt}
draggable="false"
loading="eager"
format="webp"
/>
</div>
<div class="rounded-xl p-4 md:p-5">
<h3 class="card-text-title text-xl">
{post.title}
</h3>
<div class="ml-6 flex">
<div class="relative inline-block w-full">
<div class="card-text-title card-hover-text-title flex relative items-center mx-auto min-h-11 sm:mx-0 sm:mt-4">
<span class="relative inline-block overflow-hidden ml-2">
Read more
</span>
<Icon
name="mdi:keyboard-arrow-right"
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
/>
<p class="card-text-description text-sm ml-auto">
{new Date(post.published_date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})}
</p>
</div>
</div>
</div>
</div>
</a>
</div>

View File

@@ -8,37 +8,30 @@ interface Props {
} }
const { slug, title, description, count, publishDate } = Astro.props; const { slug, title, description, count, publishDate } = Astro.props;
const baseClasses =
'group group-hover rounded-xl flex h-full min-h-[220px] cursor-pointer flex-col overflow-hidden';
const bgColorClasses =
'bg-neutral-100/60 dark:bg-neutral-800/60 hover:bg-neutral-100 dark:hover:bg-neutral-800/90 ';
--- ---
<a class={`rounded-xl`} href={`/categories/${slug}/`} data-astro-prefetch="false"> <div class="smooth-reveal-cards group h-full">
<div class={`${baseClasses}`}> <a
<div class="card-base flex flex-col h-full min-h-55"
class={`relative min-h-0 flex-grow overflow-hidden transition-all duration-300 ${bgColorClasses}`} href={`/categories/${slug}/`}
data-astro-prefetch
> >
<div class="relative grow overflow-hidden">
<div class="absolute inset-1 flex flex-col p-3 md:p-4 lg:p-5"> <div class="absolute inset-1 flex flex-col p-3 md:p-4 lg:p-5">
<div class="overflow-hidden"> <div class="overflow-hidden">
<h2 <h3 class="card-text-title-major card-hover-text-title whitespace-nowrap mb-4">
class="group-hover:text-steel dark:group-hover:text-bermuda transition-text mb-4 text-4xl font-extrabold tracking-tight text-balance whitespace-nowrap text-neutral-800 duration-300 dark:text-neutral-200"
>
{title} {title}
</h2> </h3>
<p class="mb-4 font-light text-neutral-600 sm:text-lg dark:text-neutral-400"> <p class="card-text-description mb-4">
{description} {description}
</p> </p>
</div> </div>
<div <div class="card-text-description flex items-center justify-between text-xs mt-auto pt-1 md:pt-2">
class="mt-auto flex items-center justify-between pt-1 text-xs text-neutral-600 md:pt-2 dark:text-neutral-300"
>
<span class="inline-flex items-center"> <span class="inline-flex items-center">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="12" width="16"
height="12" height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -51,8 +44,8 @@ const bgColorClasses =
<span class="inline-flex items-center"> <span class="inline-flex items-center">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="12" width="16"
height="12" height="16"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -66,5 +59,5 @@ const bgColorClasses =
</div> </div>
</div> </div>
</div> </div>
</div>
</a> </a>
</div>

View File

@@ -1,6 +1,8 @@
--- ---
import { Icon } from 'astro-icon/components'; import { Icon } from 'astro-icon/components';
import Logo from '@components/ui/logos/Logo.astro'; import Logo from '@components/ui/logos/Logo.astro';
import { getDirectusImageURL } from '@lib/directusFunctions';
interface Props { interface Props {
topic: string; topic: string;
@@ -12,7 +14,8 @@ interface Props {
logoIcon?: string; logoIcon?: string;
} }
const { topic, area, date, url, logoUrlLight, logoUrlDark, logoIcon } = Astro.props; const { topic, area, date, url, logoUrlLight, logoIcon } = Astro.props;
const logoUrlDark = Astro.props.logoUrlDark || logoUrlLight;
--- ---
<div class="smooth-reveal group flex flex-col"> <div class="smooth-reveal group flex flex-col">
@@ -25,8 +28,8 @@ const { topic, area, date, url, logoUrlLight, logoUrlDark, logoIcon } = Astro.pr
{logoUrlLight ? ( {logoUrlLight ? (
<div class="card-hover-icon-scale mr-5"> <div class="card-hover-icon-scale mr-5">
<Logo <Logo
srcLight={logoUrlLight} srcLight={getDirectusImageURL(logoUrlLight)}
srcDark={logoUrlDark} srcDark={getDirectusImageURL(logoUrlDark!)}
alt={`Logo of ${topic}`} alt={`Logo of ${topic}`}
/> />
</div> </div>

View File

@@ -9,13 +9,11 @@ interface Props {
} }
const { title, description, url, icon } = Astro.props; const { title, description, url, icon } = Astro.props;
const sizeClasses = 'h-30 w-100 md:w-[300px]';
--- ---
<div class="smooth-reveal-2 group flex flex-col"> <div class="smooth-reveal-2 group flex flex-col">
<a <a
class={`card-base flex items-center ${sizeClasses}`} class="card-base flex items-center h-30 w-100 md:w-75"
href={url} href={url}
data-astro-prefetch data-astro-prefetch
> >
@@ -29,9 +27,9 @@ const sizeClasses = 'h-30 w-100 md:w-[300px]';
<span class="card-text-title card-hover-text-title block text-lg"> <span class="card-text-title card-hover-text-title block text-lg">
{title} {title}
</span> </span>
<span class="card-text-description block mt-1"> <p class="card-text-description block mt-1">
{description} {description}
</span> </p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -39,9 +39,9 @@ const visitClass = visitSource ? 'card-hover-text-gitea' : 'card-hover-text-titl
<span class="card-text-title block text-lg"> <span class="card-text-title block text-lg">
{title} {title}
</span> </span>
<span class="card-text-description block mt-1"> <p class="card-text-description block mt-1">
{description} {description}
</span> </p>
</div> </div>
</div> </div>
{highlights && ( {highlights && (
@@ -55,7 +55,7 @@ const visitClass = visitSource ? 'card-hover-text-gitea' : 'card-hover-text-titl
)} )}
<div class="ml-6 flex"> <div class="ml-6 flex">
<div class="relative inline-block"> <div class="relative inline-block">
<div class={`card-text-title ${visitClass} flex relative items-center mx-auto min-h-11 font-semibold text-md sm:mx-0 sm:mt-4`}> <div class={`card-text-title ${visitClass} flex relative items-center font-semibold text-md min-h-11 mx-auto sm:mx-0 sm:mt-4`}>
{visitSource && <Icon name="pajamas:gitea" />} {visitSource && <Icon name="pajamas:gitea" />}
<span class="relative inline-block overflow-hidden ml-2"> <span class="relative inline-block overflow-hidden ml-2">
{visitText} {visitText}

View File

@@ -0,0 +1,55 @@
---
import { Icon } from 'astro-icon/components';
import Image from '@components/ui/images/Image.astro';
import { getDirectusImageURL } from '@lib/directusFunctions';
interface Props {
title: string;
subTitle: string;
url: string;
img: string;
imgAlt: string;
}
const { title, subTitle, url, img, imgAlt } = Astro.props;
---
<div class="smooth-reveal group">
<a
class="card-base-hidden md:grid md:grid-cols-2 lg:grid lg:grid-cols-2 items-center gap-8 xl:gap-16 max-w-340 2xl:max-w-full px-4 sm:px-6 lg:px-8 py-10 sm:py-16 lg:py-14 mx-auto"
href={url}
data-astro-prefetch
>
<div>
<Image
class="rounded-xl w-full h-full sm:max-h-80 md:max-h-90 object-cover"
src={getDirectusImageURL(img)}
alt={imgAlt}
draggable="false"
loading="lazy"
width="850"
height="420"
/>
</div>
<div>
<h2 class="card-text-header mb-4">
{title}
</h2>
<p class="card-text-title font-light text-pretty sm:text-lg max-w-prose mb-4">
{subTitle}
</p>
<div class="button-base button-bg-teal inline-flex rounded-lg gap-x-2">
<div class="button-text-title flex relative items-center text-center">
<span class="mr-2">
Read More
</span>
<Icon
name="mdi:keyboard-arrow-right"
class="button-hover-arrow"
/>
</div>
</div>
</div>
</a>
</div>

View File

@@ -0,0 +1,83 @@
---
import { Icon } from 'astro-icon/components';
import Image from '@components/ui/images/Image.astro';
import { getDirectusImageURL } from '@lib/directusFunctions';
interface Props {
title: string;
subTitle: string;
url: string;
single?: boolean;
imgOne: any;
imgOneAlt: any;
imgTwo?: any;
imgTwoAlt?: any;
}
const { title, subTitle, url, single, imgOne, imgOneAlt, imgTwo, imgTwoAlt } = Astro.props;
---
<div class="smooth-reveal group">
<a
class="card-base-hidden items-center lg:grid lg:grid-cols-2 gap-16 max-w-340 2xl:max-w-full px-4 py-10 sm:px-6 lg:px-8 lg:py-14 mx-auto"
href={url}
data-astro-prefetch
>
<div>
<h2 class="card-text-header mb-4">
{title}
</h2>
<p class="card-text-title font-light text-pretty sm:text-lg max-w-prose mb-4">
{subTitle}
</p>
<div class="button-base button-bg-teal inline-flex rounded-lg gap-x-2">
<div class="button-text-title flex relative items-center text-center">
<span class="mr-2">
Read More
</span>
<Icon
name="mdi:keyboard-arrow-right"
class="button-hover-arrow"
/>
</div>
</div>
</div>
{single ? (
<div>
<Image
class="rounded-xl w-full"
src={getDirectusImageURL(imgOne)}
alt={imgOneAlt}
format="webp"
loading="lazy"
width="850"
height="420"
/>
</div>
) : (
<div class="grid grid-cols-2 gap-4">
<Image
class="rounded-xl w-full"
src={getDirectusImageURL(imgOne)}
alt={imgOneAlt}
draggable="false"
format="webp"
loading="lazy"
width="400"
height="230"
/>
<Image
class="rounded-xl w-full mt-4 lg:mt-10"
src={getDirectusImageURL(imgTwo)}
alt={imgTwoAlt}
draggable="false"
format="webp"
loading="lazy"
width="400"
height="230"
/>
</div>
)}
</a>
</div>

View File

@@ -7,12 +7,10 @@ interface Props {
} }
const { dayName, label, icon, temp } = Astro.props; const { dayName, label, icon, temp } = Astro.props;
const sizeClasses = 'w-32 md:w-40';
--- ---
<div class="smooth-reveal-2 group flex flex-col"> <div class="smooth-reveal-2 group flex flex-col">
<div class={`card-base ${sizeClasses}`}> <div class="card-base w-32 md:w-40">
<div class="p-5 text-center"> <div class="p-5 text-center">
<span class="card-text-description block font-bold text-xs uppercase tracking-widest"> <span class="card-text-description block font-bold text-xs uppercase tracking-widest">
{dayName} {dayName}

View File

@@ -3,8 +3,8 @@ import { readItems } from '@directus/sdk';
import type { Application } from '@lib/directusTypes'; import type { Application } from '@lib/directusTypes';
import directus from '@lib/directus';
import HighlightsCard from '@components/cards/HighlightsCard.astro'; import HighlightsCard from '@components/cards/HighlightsCard.astro';
import directus from '@lib/directus';
const applications = ((await directus.request( const applications = ((await directus.request(
readItems('site_applications' as any, { readItems('site_applications' as any, {

View File

@@ -0,0 +1,93 @@
---
import { getCollection } from 'astro:content';
import { readItems } from '@directus/sdk';
import type { Post } from '@lib/directusTypes';
import CategoryCard from '@components/cards/CategoryCard.astro';
import directus from '@lib/directus';
import { timeago } from '@support/time';
const posts = await directus.request(
readItems('posts', {
filter: { published: { _eq: true } },
fields: ['*'],
sort: ['-published_date'],
})
);
const layoutPattern = [
{ col: 2, row: 2 },
{ col: 2, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 2 },
{ col: 2, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 1 },
];
const postMap: Map<string, Post[]> = posts
.sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf())
.reduce((acc, obj) => {
let posts = acc.get(obj.category);
if (!posts) {
posts = [];
}
posts.push(obj);
acc.set(obj.category, posts);
return acc;
}, new Map<string, Post[]>());
const categories = (await getCollection('categories'))
.sort((a, b) => {
const aCount = postMap.get(a.slug)?.length ?? 0;
const bCount = postMap.get(b.slug)?.length ?? 0;
return bCount - aCount;
})
.map((c, index) => {
const posts = postMap.get(c.slug);
const pattern = layoutPattern[index % layoutPattern.length];
const smColSpan = Math.min(pattern.col, 2);
const mdColSpan = Math.min(pattern.col, 4);
const rowSpan = pattern.row;
const rowSpanClass = rowSpan > 1 ? `row-span-${rowSpan}` : 'row-span-1';
const gridItemClass = `col-span-${smColSpan} md:col-span-${mdColSpan} ${rowSpanClass}`;
return {
...c,
posts,
gridItemClass,
layoutPattern: {
smCol: smColSpan,
mdCol: mdColSpan,
row: rowSpan,
index,
},
};
});
---
<section class="mx-auto px-4 py-10 sm:px-6 lg:px-8 lg:py-14 lg:pt-10 2xl:max-w-full">
<div class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 gap-4">
{categories.map((category) => {
return (
<div
class={category.gridItemClass}
style={category.layoutPattern.row > 1 ? 'grid-row: span 2 / span 2;' : ''}
>
<CategoryCard
slug={category.slug}
title={category.data.title}
description={category.data.description}
count={postMap.get(category.slug)?.length ?? 0}
publishDate={timeago(postMap.get(category.slug)?.[0]?.published_date)}
/>
</div>
);
})}
</div>
</section>

View File

@@ -3,9 +3,8 @@ import { readItems } from '@directus/sdk';
import type { Education, Certificate} from '@lib/directusTypes'; import type { Education, Certificate} from '@lib/directusTypes';
import directus from '@lib/directus';
import { getDirectusImageURL } from '@lib/directusFunctions';
import EducationCard from '@components/cards/EducationCard.astro'; import EducationCard from '@components/cards/EducationCard.astro';
import directus from '@lib/directus';
const educations = ((await directus.request( const educations = ((await directus.request(
readItems('site_education' as any, { readItems('site_education' as any, {
@@ -37,8 +36,8 @@ const certificates = ((await directus.request(
area={education.area} area={education.area}
date={education.graduationDate} date={education.graduationDate}
url={education.url} url={education.url}
logoUrlLight={getDirectusImageURL(education.logo)} logoUrlLight={education.logo}
logoUrlDark={getDirectusImageURL(education.logoDark)} logoUrlDark={education.logoDark}
/> />
))} ))}
</div> </div>

View File

@@ -78,7 +78,7 @@ const experiences = ((await directus.request(
</div> </div>
)} )}
{(experience.responsibilities || experience.achievements) && ( {(experience.responsibilities || experience.achievements) && (
<div class="relative flex flex-col gap-4 max-sm:h-auto! md:after:absolute md:after:bottom-0 md:after:h-12 md:after:w-full md:after:bg-gradient-to-t md:after:from-neutral-200 dark:md:after:from-stone-700 md:after:content-[''] " :class="expanded ? 'after:hidden' : ''" x-show="expanded" x-collapse.min.50px> <div class="relative flex flex-col gap-4 max-sm:h-auto! md:after:absolute md:after:bottom-0 md:after:h-12 md:after:w-full md:after:bg-linear-to-t md:after:from-neutral-200 dark:md:after:from-stone-700 md:after:content-[''] " :class="expanded ? 'after:hidden' : ''" x-show="expanded" x-collapse.min.50px>
{experience.responsibilities && ( {experience.responsibilities && (
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h4 class="text-header font-semibold"> <h4 class="text-header font-semibold">

View File

@@ -1,8 +1,8 @@
--- ---
import { readSingleton } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import FeaturesCard from '@components/cards/FeaturesCard.astro'; import FeaturesCard from '@components/cards/FeaturesCard.astro';
import directus from '@lib/directus';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
--- ---

View File

@@ -1,36 +0,0 @@
---
import { readItems } from '@directus/sdk';
import type { Post } from '@lib/directusTypes';
import directus from '@lib/directus';
import BlogCard from '@components/blog/BlogCard.astro';
const posts = await directus.request(
readItems('posts', {
filter: { published: { _eq: true } },
fields: ['*'],
sort: ['-published_date'],
})
);
const recentPosts = posts
.sort((a: Post, b: Post) => (new Date(b.published_date).getTime()) - (new Date(a.published_date).getTime()))
.slice(0, 3);
---
<section class="mx-auto mb-20 max-w-340 px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full">
<div class="mx-auto mb-10 max-w-2xl text-center lg:mb-14">
<h1 class="smooth-reveal card-text-header block">
Latest Posts
</h1>
<div class="smooth-reveal mx-auto mt-5 max-w-3xl text-center">
<span class="card-text-header-description">
Checkout my most recent thoughts here
</span>
</div>
</div>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{recentPosts.map((b) => <BlogCard post={b} />)}
</div>
</section>

View File

@@ -3,8 +3,8 @@ import { readItems } from '@directus/sdk';
import type { Project } from '@lib/directusTypes'; import type { Project } from '@lib/directusTypes';
import directus from '@lib/directus';
import HighlightsCard from '@components/cards/HighlightsCard.astro'; import HighlightsCard from '@components/cards/HighlightsCard.astro';
import directus from '@lib/directus';
const projects = ((await directus.request( const projects = ((await directus.request(
readItems('site_projects' as any, { readItems('site_projects' as any, {

View File

@@ -0,0 +1,29 @@
---
import type { Post } from '@lib/directusTypes';
import BlogCard from '@components/cards/BlogCard.astro';
interface Props {
posts: Post[];
title: string;
subTitle?: string;
}
const { posts, title, subTitle } = Astro.props;
---
<section class="mx-auto mb-20 max-w-340 px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full">
<div class="mx-auto mb-10 max-w-2xl text-center lg:mb-14">
<h1 class="smooth-reveal card-text-header block">
{title}
</h1>
<div class="smooth-reveal mx-auto mt-5 max-w-3xl text-center">
<span class="card-text-header-description">
{subTitle}
</span>
</div>
</div>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{posts.map((b) => <BlogCard post={b} />)}
</div>
</section>

View File

@@ -0,0 +1,35 @@
---
import type { Post } from '@lib/directusTypes';
import LargeBlogLeftCard from '@components/cards/LargeBlogLeftCard.astro';
import LargeBlogRightCard from '@components/cards/LargeBlogRightCard.astro';
interface Props {
posts: Post[];
}
const { posts } = Astro.props;
---
<section class="smooth-reveal">
{posts.map((post, index) => index % 2 === 0 ? (
<LargeBlogLeftCard
title={post.title}
subTitle={post.description}
url={`/blog/${post.slug}`}
img={post.image}
imgAlt={post.image_alt}
/>
) : (
<LargeBlogRightCard
title={post.title}
subTitle={post.description}
url={`/blog/${post.slug}`}
single={!post.image_second}
imgOne={post.image}
imgOneAlt={post.image_alt}
imgTwo={post?.image_second}
imgTwoAlt={post?.image_second_alt}
/>
))}
</section>

View File

@@ -3,6 +3,7 @@ import WeatherCard from '@components/cards/WeatherCard.astro';
import { getFiveDayForecast } from '@support/weather'; import { getFiveDayForecast } from '@support/weather';
const { latitude = "44.95", longitude = "-93.09", cityName = "St. Paul, Minnesota", timezone = "America/Chicago" } = Astro.props; const { latitude = "44.95", longitude = "-93.09", cityName = "St. Paul, Minnesota", timezone = "America/Chicago" } = Astro.props;
const { forecastDays, error } = await getFiveDayForecast(latitude, longitude, timezone); const { forecastDays, error } = await getFiveDayForecast(latitude, longitude, timezone);
--- ---

View File

@@ -28,6 +28,7 @@ const global = await directus.request(readSingleton('site_global'));
}, },
}} }}
> >
<section class="mt-20 grid place-content-center"> <section class="mt-20 grid place-content-center">
<div class="mx-auto max-w-7xl px-4 py-8 lg:px-6 lg:py-16"> <div class="mx-auto max-w-7xl px-4 py-8 lg:px-6 lg:py-16">
<div class="mx-auto max-w-screen-sm text-center"> <div class="mx-auto max-w-screen-sm text-center">
@@ -67,6 +68,7 @@ const global = await directus.request(readSingleton('site_global'));
</div> </div>
</div> </div>
</section> </section>
</BaseLayout> </BaseLayout>
<script> <script>

View File

@@ -1,18 +1,20 @@
--- ---
import { type CollectionEntry, getCollection } from 'astro:content'; import { type CollectionEntry, getCollection } from 'astro:content';
import getReadingTime from 'reading-time'; import getReadingTime from 'reading-time';
import { readItems, readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import { marked } from 'marked'; import { marked } from 'marked';
import markedShiki from 'marked-shiki'; import markedShiki from 'marked-shiki';
import { createHighlighter } from 'shiki'; import { createHighlighter } from 'shiki';
import { getDirectusImageURL } from '@lib/directusFunctions'; import { readItems, readSingleton } from '@directus/sdk';
import BaseLayout from '@layouts/BaseLayout.astro';
import Image from '@components/ui/images/Image.astro'; import Image from '@components/ui/images/Image.astro';
import SocialShareButton from '@components/buttons/SocialShareButton.astro'; import SocialShareButton from '@components/buttons/SocialShareButton.astro';
import BaseLayout from '@layouts/BaseLayout.astro';
import directus from '@lib/directus';
import { getDirectusImageURL } from '@lib/directusFunctions';
import { formatDateTime } from '@support/time'; import { formatDateTime } from '@support/time';
const post = Astro.props;
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await directus.request(readItems('posts')); const posts = await directus.request(readItems('posts'));
return posts.map((post) => ({ return posts.map((post) => ({
@@ -20,18 +22,19 @@ export async function getStaticPaths() {
props: post, props: post,
})); }));
} }
const post = Astro.props;
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
const category: CollectionEntry<'categories'> = (await getCollection('categories')) const category: CollectionEntry<'categories'> = (await getCollection('categories'))
.filter((c) => c.slug === post.category) .filter((c) => c.slug === post.category)
.pop() as CollectionEntry<'categories'>; .pop() as CollectionEntry<'categories'>;
const readingTime = getReadingTime(post.content); const readingTime = getReadingTime(post.content);
const highlighter = await createHighlighter({ const highlighter = await createHighlighter({
themes: ['github-light', 'github-dark', 'monokai'], themes: ['github-light', 'github-dark', 'monokai'],
langs: ['typescript', 'python', 'css', 'html', 'yaml', 'bash', 'json'], langs: ['typescript', 'python', 'css', 'html', 'yaml', 'bash', 'json'],
}); });
marked.use(markedShiki({ marked.use(markedShiki({
highlight(code, lang) { highlight(code, lang) {
return highlighter.codeToHtml(code, { return highlighter.codeToHtml(code, {
@@ -44,6 +47,7 @@ marked.use(markedShiki({
}); });
} }
})); }));
const content = marked.parse(post.content); const content = marked.parse(post.content);
--- ---
@@ -79,6 +83,7 @@ const content = marked.parse(post.content);
], ],
}} }}
> >
<section class="mx-auto max-w-6xl px-4 pt-8 pb-12 sm:px-6 lg:px-8 lg:pt-12"> <section class="mx-auto max-w-6xl px-4 pt-8 pb-12 sm:px-6 lg:px-8 lg:pt-12">
<div class="smooth-reveal relative w-full"> <div class="smooth-reveal relative w-full">
<div class="mt-4 rounded-2xl shadow-none sm:mt-0 sm:shadow-sm"> <div class="mt-4 rounded-2xl shadow-none sm:mt-0 sm:shadow-sm">
@@ -171,6 +176,7 @@ const content = marked.parse(post.content);
</div> </div>
</div> </div>
</section> </section>
<style is:inline> <style is:inline>
code[data-theme*=' '], code[data-theme*=' '],
code[data-theme*=' '] span { code[data-theme*=' '] span {
@@ -184,6 +190,7 @@ const content = marked.parse(post.content);
} }
} }
</style> </style>
</BaseLayout> </BaseLayout>
<script> <script>

View File

@@ -3,11 +3,12 @@ import { readItems, readSingleton } from '@directus/sdk';
import type { Post } from '@lib/directusTypes'; import type { Post } from '@lib/directusTypes';
import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import BlogSelectedArticles from '@components/blog/BlogSelectedArticles.astro';
import BlogRecentArticles from '@components/blog/BlogRecentArticles.astro';
import HeroSection from '@components/sections/HeroSection.astro'; import HeroSection from '@components/sections/HeroSection.astro';
import SelectedPostsSection from '@components/sections/SelectedPostsSection.astro';
import RecentPostsSection from '@components/sections/RecentPostsSection.astro';
import BaseLayout from '@layouts/BaseLayout.astro';
import directus from '@lib/directus';
import blogImg from '@images/autumn_tree.png'; import blogImg from '@images/autumn_tree.png';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
@@ -18,10 +19,11 @@ const posts = await directus.request(
sort: ['-published_date'], sort: ['-published_date'],
}) })
); );
const selectedPosts: Post[] = posts.filter((p) => p.selected).slice(0, 4);
const selectedPosts: Post[] = posts.filter((p) => p.selected).slice(0, 3);
const recentPosts: Post[] = posts.filter( const recentPosts: Post[] = posts.filter(
(p) => !selectedPosts.some((selected) => selected.slug === p.slug) (p) => !selectedPosts.some((selected) => selected.slug === p.slug)
).slice(0, 6); ).slice(0, 9);
--- ---
<BaseLayout <BaseLayout
@@ -43,10 +45,21 @@ const recentPosts: Post[] = posts.filter(
}, },
}} }}
> >
<HeroSection title="Blog" subTitle={global.about_blog} src={blogImg} alt={global.blog_image_alt} />
<BlogSelectedArticles posts={selectedPosts} /> <HeroSection
<BlogRecentArticles posts={recentPosts} /> title="Blog"
subTitle={global.about_blog}
src={blogImg}
alt={global.blog_image_alt}
/>
<SelectedPostsSection posts={selectedPosts} />
<RecentPostsSection
posts={recentPosts}
title="Recent Posts"
/>
</BaseLayout> </BaseLayout>
<script> <script>

View File

@@ -2,11 +2,12 @@
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
import { readItems, readSingleton } from '@directus/sdk'; import { readItems, readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import type { Post } from '@lib/directusTypes'; import type { Post } from '@lib/directusTypes';
import BaseLayout from '@layouts/BaseLayout.astro';
import BlogCard from '@components/blog/BlogCard.astro'; import BlogCard from '@components/cards/BlogCard.astro';
import HeaderSection from '@components/sections/HeaderSection.astro'; import HeaderSection from '@components/sections/HeaderSection.astro';
import BaseLayout from '@layouts/BaseLayout.astro';
import directus from '@lib/directus';
export async function getStaticPaths() { export async function getStaticPaths() {
const categories = await getCollection('categories'); const categories = await getCollection('categories');
@@ -26,6 +27,7 @@ const posts = await directus.request(
sort: ['-published_date'], sort: ['-published_date'],
}) })
); );
const categoriesPosts = posts const categoriesPosts = posts
.sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf()) .sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf())
.filter((b) => { .filter((b) => {
@@ -51,6 +53,7 @@ const categoriesPosts = posts
}, },
}} }}
> >
<HeaderSection <HeaderSection
title={category.data.title} title={category.data.title}
subTitle={category.data.description} subTitle={category.data.description}
@@ -59,9 +62,12 @@ const categoriesPosts = posts
btnURL="/categories" btnURL="/categories"
/> />
<section class="mx-auto mt-10 mb-10 max-w-[85rem] px-4 py-8 sm:px-6 lg:px-8 2xl:max-w-full"> <section class="max-w-340 2xl:max-w-full mx-auto mt-10 mb-10 px-4 py-8 sm:px-6 lg:px-8">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{categoriesPosts.map((b) => <BlogCard post={b} />)} {categoriesPosts.map((b) =>
<BlogCard post={b} />
)}
</div> </div>
</section> </section>
</BaseLayout> </BaseLayout>

View File

@@ -1,78 +1,14 @@
--- ---
import { getCollection } from 'astro:content'; import { readSingleton } from '@directus/sdk';
import { readItems, readSingleton } from '@directus/sdk';
import type { Post } from '@lib/directusTypes';
import directus from '@lib/directus'; import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro'; import BaseLayout from '@layouts/BaseLayout.astro';
import BlogCategoryCard from '@components/blog/BlogCategoryCard.astro';
import HeroSection from '@components/sections/HeroSection.astro'; import HeroSection from '@components/sections/HeroSection.astro';
import { timeago } from '@support/time'; import CategorySection from '@components/sections/CategorySection.astro';
import categoryImg from '@images/autumn_bench.png'; import categoryImg from '@images/autumn_bench.png';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
const posts = await directus.request(
readItems('posts', {
filter: { published: { _eq: true } },
fields: ['*'],
sort: ['-published_date'],
})
);
const postMap: Map<string, Post[]> = posts
.sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf())
.reduce((acc, obj) => {
let posts = acc.get(obj.category);
if (!posts) {
posts = [];
}
posts.push(obj);
acc.set(obj.category, posts);
return acc;
}, new Map<string, Post[]>());
const layoutPattern = [
{ col: 2, row: 2 },
{ col: 2, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 2 },
{ col: 2, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 1 },
];
const categories = (await getCollection('categories'))
.sort((a, b) => {
const aCount = postMap.get(a.slug)?.length ?? 0;
const bCount = postMap.get(b.slug)?.length ?? 0;
return bCount - aCount;
})
.map((c, index) => {
const posts = postMap.get(c.slug);
const pattern = layoutPattern[index % layoutPattern.length];
const smColSpan = Math.min(pattern.col, 2);
const mdColSpan = Math.min(pattern.col, 4);
const rowSpan = pattern.row;
const rowSpanClass = rowSpan > 1 ? `row-span-${rowSpan}` : 'row-span-1';
const gridItemClass = `col-span-${smColSpan} md:col-span-${mdColSpan} ${rowSpanClass} smooth-reveal-cards rounded-xl transition-all duration-300 shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg border border-stone-200/50 dark:border-stone-700/50`;
return {
...c,
posts,
gridItemClass,
layoutPattern: {
smCol: smColSpan,
mdCol: mdColSpan,
row: rowSpan,
index,
},
};
});
--- ---
<BaseLayout <BaseLayout
@@ -94,6 +30,7 @@ const categories = (await getCollection('categories'))
}, },
}} }}
> >
<HeroSection <HeroSection
title="Categories" title="Categories"
subTitle={global.about_categories} subTitle={global.about_categories}
@@ -101,28 +38,8 @@ const categories = (await getCollection('categories'))
alt={global.categories_image_alt} alt={global.categories_image_alt}
/> />
<section class="mx-auto px-4 py-10 sm:px-6 lg:px-8 lg:py-14 lg:pt-10 2xl:max-w-full"> <CategorySection />
<div class="grid grid-flow-row-dense grid-cols-2 gap-4 md:grid-cols-4">
{
categories.map((category) => {
return (
<div
class={category.gridItemClass}
style={category.layoutPattern.row > 1 ? 'grid-row: span 2 / span 2;' : ''}
>
<BlogCategoryCard
slug={category.slug}
title={category.data.title}
description={category.data.description}
count={postMap.get(category.slug)?.length ?? 0}
publishDate={timeago(postMap.get(category.slug)?.[0]?.published_date)}
/>
</div>
);
})
}
</div>
</section>
</BaseLayout> </BaseLayout>
<script> <script>

View File

@@ -1,18 +1,31 @@
--- ---
import { readSingleton } from '@directus/sdk'; import { readSingleton, readItems } from '@directus/sdk';
import type { Post } from '@lib/directusTypes';
import directus from '@lib/directus'; import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro'; import BaseLayout from '@layouts/BaseLayout.astro';
import HeroSection from '@components/sections/HeroSection.astro'; import HeroSection from '@components/sections/HeroSection.astro';
import FeaturesSection from '@components/sections/FeaturesSection.astro'; import FeatureSection from '@components/sections/FeatureSection.astro';
import WeatherSection from '@components/sections/WeatherSection.astro'; import WeatherSection from '@components/sections/WeatherSection.astro';
import LatestPostsSection from '@components/sections/LatestPostsSection.astro'; import RecentPostsSection from '@components/sections/RecentPostsSection.astro';
import GiteaSection from '@components/sections/GiteaSection.astro'; import GiteaSection from '@components/sections/GiteaSection.astro';
import homeImg from '@images/autumn_mountain.png'; import homeImg from '@images/autumn_mountain.png';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
const weather = await directus.request(readSingleton('site_weather')); const weather = await directus.request(readSingleton('site_weather'));
const posts = await directus.request(
readItems('posts', {
filter: { published: { _eq: true } },
fields: ['*'],
sort: ['-published_date'],
})
);
const recentPosts = posts
.sort((a: Post, b: Post) => (new Date(b.published_date).getTime()) - (new Date(a.published_date).getTime()))
.slice(0, 3);
--- ---
<BaseLayout <BaseLayout
@@ -34,6 +47,7 @@ const weather = await directus.request(readSingleton('site_weather'));
}, },
}} }}
> >
<HeroSection <HeroSection
title={`Hello, I'm <span class="text-steel dark:text-steel">Alex Lebens</span>`} title={`Hello, I'm <span class="text-steel dark:text-steel">Alex Lebens</span>`}
subTitle={global.about_description} subTitle={global.about_description}
@@ -43,7 +57,7 @@ const weather = await directus.request(readSingleton('site_weather'));
alt={global.home_image_alt} alt={global.home_image_alt}
/> />
<FeaturesSection /> <FeatureSection />
<WeatherSection <WeatherSection
server:defer server:defer
@@ -51,16 +65,20 @@ const weather = await directus.request(readSingleton('site_weather'));
longitude={weather.longitude} longitude={weather.longitude}
cityName={weather.location} cityName={weather.location}
timezone={weather.timezone} timezone={weather.timezone}
> />
</WeatherSection>
<LatestPostsSection /> <RecentPostsSection
posts={recentPosts}
title="Latest Posts"
subTitle="Checkout my most recent thoughts here"
/>
<GiteaSection <GiteaSection
title="Follow me on Gitea" title="Follow me on Gitea"
subTitle="I love open source and have my code availabile on my Gitea server." subTitle="I love open source and have my code availabile on my Gitea server."
url="https://gitea.alexlebens.dev" url="https://gitea.alexlebens.dev"
/> />
</BaseLayout> </BaseLayout>
<script> <script>

View File

@@ -37,7 +37,7 @@
@utility button-bg-teal { @utility button-bg-teal {
@apply transition-all duration-300 @apply transition-all duration-300
bg-bermuda hover:bg-turquoise dark:bg-turquoise dark:hover:bg-bermuda bg-bermuda hover:bg-turquoise group-hover:bg-turquoise dark:bg-turquoise dark:hover:bg-bermuda dark:group-hover:bg-bermuda
} }
@utility button-bg-neutral { @utility button-bg-neutral {
@@ -53,12 +53,20 @@
/* Card classes */ /* Card classes */
@utility card-base { @utility card-base {
@apply rounded-xl @apply transition-all duration-300
rounded-xl
border border-neutral-100 dark:border-stone-500/20 border border-neutral-100 dark:border-stone-500/20
bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90 bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90
shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg
} }
@utility card-base-hidden {
@apply transition-all duration-300
rounded-xl
border border-transparent
hover:bg-neutral-400/20 dark:hover:bg-neutral-800/40
}
@utility card-hover-icon-color { @utility card-hover-icon-color {
@apply transition-all duration-300 @apply transition-all duration-300
text-primary text-primary
@@ -79,9 +87,8 @@
@utility card-text-header-minor { @utility card-text-header-minor {
@apply text-header @apply text-header
md:text-3xl text-2xl md:text-3xl
text-2xl font-semibold leading-tight tracking-tight text-balance
font-semibold
} }
@utility card-text-header-description { @utility card-text-header-description {
@@ -95,6 +102,12 @@
font-bold font-bold
} }
@utility card-text-title-major {
@apply text-header
text-4xl md:text-3xl
font-bold leading-tight tracking-tight text-balance
}
@utility card-hover-text-title { @utility card-hover-text-title {
@apply transition-all duration-300 @apply transition-all duration-300
group-hover:text-main group-hover:text-main