Compare commits
1 Commits
renovate/p
...
539ef40813
| Author | SHA1 | Date | |
|---|---|---|---|
|
539ef40813
|
@@ -60,7 +60,7 @@ jobs:
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.14'
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install GuardDog
|
||||
run: |
|
||||
|
||||
@@ -27,7 +27,7 @@ ENV SITE_URL=https://www.alexlebens.dev
|
||||
ENV DIRECTUS_URL=https://directus.alexlebens.net
|
||||
ENV PORT=4321
|
||||
|
||||
LABEL version="2.12.0"
|
||||
LABEL version="2.10.1"
|
||||
LABEL description="Astro based personal website"
|
||||
|
||||
EXPOSE $PORT
|
||||
|
||||
11
package.json
11
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "site-profile",
|
||||
"type": "module",
|
||||
"version": "2.12.0",
|
||||
"version": "2.10.1",
|
||||
"homepage": "https://www.alexlebens.dev",
|
||||
"bugs": {
|
||||
"url": "https://gitea.alexlebens.dev/alexlebens/site-profile/issues",
|
||||
@@ -39,16 +39,18 @@
|
||||
"@iconify-json/pajamas": "^1.2.15",
|
||||
"@iconify-json/simple-icons": "^1.2.70",
|
||||
"@playform/compress": "^0.2.1",
|
||||
"@swup/astro": "^1.8.0",
|
||||
"@swup/astro": "^1.7.0",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/unist": "^3.0.3",
|
||||
"astro": "^5.17.2",
|
||||
"astro-compressor": "^1.2.0",
|
||||
"astro-icon": "^1.1.5",
|
||||
"marked": "^17.0.2",
|
||||
"marked-shiki": "^1.2.1",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"motion": "^12.34.0",
|
||||
"preline": "^4.0.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -60,12 +62,13 @@
|
||||
"ultrahtml": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-react/eslint-plugin": "^2.13.0",
|
||||
"@eslint-react/eslint-plugin": "^2.12.4",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"astro-icon": "^1.1.5",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-astro": "^1.6.0",
|
||||
"eslint-plugin-astro": "^1.5.0",
|
||||
"eslint-plugin-format": "^1.4.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
|
||||
556
pnpm-lock.yaml
generated
556
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,10 @@ import { getImage } from 'astro:assets';
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import { SEO } from '@/config';
|
||||
|
||||
import brandSrc from '@images/brand_logo.png';
|
||||
import faviconSvgSrc from '@images/favicon_icon.svg';
|
||||
import faviconSrc from '@images/favicon_icon.png';
|
||||
import { SEO } from '@/config';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -19,7 +18,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const canonicalURL = Astro.url.href;
|
||||
|
||||
let {
|
||||
title,
|
||||
description,
|
||||
@@ -29,14 +27,14 @@ let {
|
||||
structuredData = SEO.structuredData,
|
||||
} = Astro.props;
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
|
||||
let card = 'summary_large_image';
|
||||
if (!ogImage) {
|
||||
ogImage = brandSrc;
|
||||
card = 'summary';
|
||||
}
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
|
||||
const faviconSvg = await getImage({ src: faviconSvgSrc, format: 'svg' });
|
||||
const appleTouchIcon = await getImage({ src: faviconSrc, width: 180, height: 180, format: 'png' });
|
||||
const socialImageRes = await getImage({ src: ogImage, width: 1200, height: 600 });
|
||||
@@ -64,12 +62,12 @@ if (!socialImage.startsWith('http')) {
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#facc15" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={ogTitle} />
|
||||
<meta property="og:site_name" content={global.name} />
|
||||
<meta property="og:description" content={ogDescription} />
|
||||
@@ -78,10 +76,17 @@ if (!socialImage.startsWith('http')) {
|
||||
<meta content="600" property="og:image:height" />
|
||||
<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 -->
|
||||
<link href={canonicalURL} rel="canonical" />
|
||||
<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="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" />
|
||||
<link href={faviconSvg.src} rel="icon" type="image/svg+xml" sizes="any" />
|
||||
|
||||
@@ -1,80 +1,92 @@
|
||||
---
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import BrandLogo from '@components/ui/logos/BrandLogo.astro';
|
||||
import Image from '@components/ui/images/Image.astro';
|
||||
import directus from '@lib/directus';
|
||||
import { NavigationLinks, FooterLinks } from '@/config';
|
||||
|
||||
import footerImg from '@images/flowers.png';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer
|
||||
class="bg-background-accent w-full overflow-hidden"
|
||||
class="w-full overflow-hidden bg-stone-300/40 dark:bg-stone-800/20"
|
||||
transition:animate="none"
|
||||
>
|
||||
<div class="relative px-4 sm:px-6 pt-16 pb-12">
|
||||
<div class="max-w-340 mx-auto">
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-10">
|
||||
<div class="relative px-4 pt-16 pb-12 sm:px-6">
|
||||
<div class="mx-auto max-w-340">
|
||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-12">
|
||||
<!-- Brand section -->
|
||||
<div class="col-span-1 md:col-span-3">
|
||||
<a href="/" class="group inline-block">
|
||||
<div class="flex items-center">
|
||||
<div class="mx-auto aspect-square overflow-hidden">
|
||||
<BrandLogo class="rounded-lg max-h-10 max-w-10"/>
|
||||
<div class="mx-auto aspect-square overflow-hidden rounded-lg">
|
||||
<BrandLogo class="max-h-10 max-w-10 rounded-full" />
|
||||
</div>
|
||||
<span class="text-header text-lg lg:text-2xl font-semibold leading-tight tracking-tight text-balance ml-3">
|
||||
|
||||
<span class="ml-3 text-xl font-bold text-neutral-800 dark:text-neutral-200">
|
||||
{global.name}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<p class="text-primary text-sm lg:text-base text-pretty leading-relaxed mt-4">
|
||||
|
||||
<p class="mt-4 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
|
||||
{global.about}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Left links -->
|
||||
<div class="col-span-1 md:col-span-2">
|
||||
<h3 class="relative inline-block text-header after:bg-main text-sm uppercase font-semibold tracking-wider pb-2 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-['']">
|
||||
Site
|
||||
<h3
|
||||
class="after:bg-steel dark:after:bg-bermuda relative inline-block pb-2 text-sm font-semibold tracking-wider text-neutral-800 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-[''] dark:text-neutral-100"
|
||||
>
|
||||
Blog
|
||||
</h3>
|
||||
<ul class="mt-4 space-y-3">
|
||||
{NavigationLinks.map((link) => (
|
||||
{
|
||||
NavigationLinks.map((link) => (
|
||||
<li>
|
||||
<a
|
||||
href={link.url}
|
||||
class="inline-flex items-center text-secondary hover:text-secondary-hover text-base transition-all duration-300 overflow-hidden"
|
||||
class="group flex items-center text-base text-neutral-600 transition-colors hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200"
|
||||
>
|
||||
{link.name}
|
||||
<span class="relative inline-block overflow-hidden">
|
||||
<span class="relative z-10">{link.name}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Right links -->
|
||||
<div class="col-span-1 md:col-span-3">
|
||||
<h3 class="relative inline-block text-header after:bg-main text-sm uppercase font-semibold tracking-wider pb-2 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-['']">
|
||||
<h3
|
||||
class="after:bg-steel dark:after:bg-bermuda relative inline-block pb-2 text-sm font-semibold tracking-wider text-neutral-800 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-[''] dark:text-neutral-100"
|
||||
>
|
||||
Other
|
||||
</h3>
|
||||
<ul class="mt-4 space-y-3">
|
||||
{FooterLinks.map((link) => (
|
||||
{
|
||||
FooterLinks.map((link) => (
|
||||
<li>
|
||||
<a
|
||||
href={link.url}
|
||||
class="inline-flex items-center text-secondary hover:text-secondary-hover text-base transition-all duration-300 overflow-hidden"
|
||||
class="group flex items-center text-base text-neutral-600 transition-colors hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200"
|
||||
>
|
||||
{link.name}
|
||||
<span class="relative inline-block overflow-hidden">
|
||||
<span class="relative z-10">{link.name}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Right image -->
|
||||
<div class="flex justify-center col-span-4 mt-10 md:mt-0">
|
||||
<div class="md:block max-h-115 max-w-55 -mt-10 scale-80 hidden">
|
||||
<div class="col-span-3 mt-10 flex justify-center md:mt-0">
|
||||
<div class="-mt-10 hidden max-h-[460px] max-w-[220px] scale-80 md:block">
|
||||
<Image
|
||||
src={footerImg}
|
||||
alt={global.footer_image_alt}
|
||||
@@ -90,36 +102,37 @@ const currentYear = new Date().getFullYear();
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom section -->
|
||||
<div class="border-t border-neutral-400/30 dark:border-neutral-600/50 pt-8 mt-12">
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<p class="text-secondary text-sm">
|
||||
<div class="mt-12 border-t border-neutral-400/30 pt-8 dark:border-neutral-600/50">
|
||||
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
© {currentYear} All rights reserved.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center">
|
||||
<span class="text-secondary text-sm">
|
||||
<p class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Weather provided by
|
||||
</span>
|
||||
</p>
|
||||
<a
|
||||
href="https://open-meteo.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group inline-flex items-center text-secondary hover:text-secondary-hover text-sm transition-all duration-300"
|
||||
class="group inline-flex items-center text-xs text-neutral-600 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
>
|
||||
<span class="relative underline ml-1">
|
||||
<span class="relative ml-1">
|
||||
Open-Meteo.
|
||||
</span>
|
||||
</a>
|
||||
<div class="ml-4"/>
|
||||
<span class="text-secondary text-sm">
|
||||
Built with
|
||||
</span>
|
||||
|
||||
<div class="ml-4"></div>
|
||||
|
||||
<span class="text-xs text-neutral-500 dark:text-neutral-400">Built with </span>
|
||||
<a
|
||||
href="https://astro.build"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group inline-flex items-center text-secondary hover:text-secondary-hover text-sm transition-all duration-300"
|
||||
class="group inline-flex items-center text-xs text-neutral-600 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
>
|
||||
<span class="relative underline ml-1">
|
||||
<span class="relative ml-1">
|
||||
Astro.
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -9,31 +9,31 @@ const currentPath = pathname.slice(1);
|
||||
|
||||
<header
|
||||
id="nav"
|
||||
class="fixed flex flex-wrap md:flex-nowrap md:justify-start inset-x-0 top-0 w-full z-50"
|
||||
class="sticky inset-x-0 top-4 z-50 flex w-full flex-wrap text-sm transition-none md:flex-nowrap md:justify-start"
|
||||
>
|
||||
<div class="bg-linear-to-b from-background from-65% to-transparent to-90% absolute top-0 bottom-0 left-0 w-full h-36 z-0"/>
|
||||
<nav
|
||||
class="nav-base relative md:flex md:items-center md:justify-between rounded-[36px] w-full px-4 mx-2 py-3 mt-4"
|
||||
class="relative mx-2 w-full rounded-[36px] border border-neutral-100 bg-neutral-100 px-4 py-3 md:flex md:items-center md:justify-between md:px-6 lg:px-8 dark:border-neutral-700/40 dark:bg-neutral-800/80"
|
||||
aria-label="Global"
|
||||
>
|
||||
<div class="flex items-center justify-between ml-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<a
|
||||
class="flex-none rounded-full h-10.5"
|
||||
class="h-[42px] flex-none rounded-lg text-xl font-bold ring-neutral-500 outline-none focus-visible:ring dark:ring-neutral-200 dark:focus:outline-none"
|
||||
href="/"
|
||||
aria-label="Brand"
|
||||
>
|
||||
<BrandLogo class="h-full w-auto rounded-full object-cover"/>
|
||||
<BrandLogo class="h-full w-auto rounded-full object-cover" />
|
||||
</a>
|
||||
<div class="md:hidden mr-auto ml-4">
|
||||
|
||||
<div class="ml-auto md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
class="hs-collapse-toggle flex items-center justify-center text-secondary text-sm font-bold hover:bg-neutral-200 dark:hover:bg-neutral-700 rounded-full transition duration-300 disabled:pointer-events-none disabled:opacity-50 h-8 w-8"
|
||||
class="hs-collapse-toggle flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold text-neutral-600 transition duration-300 hover:bg-neutral-200 disabled:pointer-events-none disabled:opacity-50 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:focus:outline-none"
|
||||
data-hs-collapse="#navbar-collapse-with-animation"
|
||||
aria-controls="navbar-collapse-with-animation"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<svg
|
||||
class="hs-collapse-open:hidden shrink-0 h-5 w-5"
|
||||
class="hs-collapse-open:hidden h-5 w-5 shrink-0"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -48,7 +48,7 @@ const currentPath = pathname.slice(1);
|
||||
<line x1="3" x2="21" y1="18" y2="18"></line>
|
||||
</svg>
|
||||
<svg
|
||||
class="hs-collapse-open:block shrink-0 h-5 w-5 hidden"
|
||||
class="hs-collapse-open:block hidden h-5 w-5 shrink-0"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -63,33 +63,33 @@ const currentPath = pathname.slice(1);
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="md:hidden ml-2 mr-2">
|
||||
<span class="">
|
||||
<ThemeToggleButton />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex md:flex-row items-center justify-between">
|
||||
|
||||
<div
|
||||
id="navbar-collapse-with-animation"
|
||||
class="hs-collapse grow basis-full md:block transition-all duration-300 ml-2 mb-2 md:mb-0 hidden overflow-hidden md:overflow-visible"
|
||||
class="hs-collapse hidden grow basis-full overflow-hidden transition-all duration-300 md:block md:overflow-visible"
|
||||
>
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-end gap-x-0 md:gap-x-4 lg:gap-x-7 gap-y-4 md:gap-y-0 md:ps-7 mr-2 mt-5 md:mt-0">
|
||||
{NavigationLinks.map((item) => {
|
||||
<div
|
||||
class="mt-5 flex flex-col gap-x-0 gap-y-4 md:mt-0 md:flex-row md:items-center md:justify-end md:gap-x-4 md:gap-y-0 md:ps-7 lg:gap-x-7"
|
||||
>
|
||||
{
|
||||
NavigationLinks.map((item) => {
|
||||
const isActive = currentPath === (item.url === '/' ? '' : item.url.slice(1));
|
||||
return (
|
||||
<a
|
||||
href={item.url}
|
||||
class={`text-sm font-medium ${isActive ? 'text-active' : 'text-secondary hover:text-secondary-hover'}`}
|
||||
class={`text-sm font-medium ${
|
||||
isActive
|
||||
? 'text-orange-500 dark:text-orange-300'
|
||||
: 'text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100'
|
||||
}`}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden md:flex ml-2">
|
||||
<span class="">
|
||||
})
|
||||
}
|
||||
<span class="md:inline-block">
|
||||
<ThemeToggleButton />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
61
src/components/blog/BlogCard.astro
Normal file
61
src/components/blog/BlogCard.astro
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
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>
|
||||
@@ -8,30 +8,37 @@ interface 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 ';
|
||||
---
|
||||
|
||||
<div class="smooth-reveal-cards group h-full">
|
||||
<a
|
||||
class="card-base flex flex-col h-full min-h-55"
|
||||
href={`/categories/${slug}/`}
|
||||
data-astro-prefetch
|
||||
<a class={`rounded-xl`} href={`/categories/${slug}/`} data-astro-prefetch="false">
|
||||
<div class={`${baseClasses}`}>
|
||||
<div
|
||||
class={`relative min-h-0 flex-grow overflow-hidden transition-all duration-300 ${bgColorClasses}`}
|
||||
>
|
||||
<div class="relative grow overflow-hidden">
|
||||
<div class="absolute inset-1 flex flex-col p-3 md:p-4 lg:p-5">
|
||||
<div class="overflow-hidden">
|
||||
<h3 class="card-text-title-major card-hover-text-title whitespace-nowrap mb-4">
|
||||
<h2
|
||||
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}
|
||||
</h3>
|
||||
<p class="card-text-description mb-4">
|
||||
</h2>
|
||||
<p class="mb-4 font-light text-neutral-600 sm:text-lg dark:text-neutral-400">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-text-description flex items-center justify-between text-xs mt-auto pt-1 md:pt-2">
|
||||
<div
|
||||
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">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -44,8 +51,8 @@ const { slug, title, description, count, publishDate } = Astro.props;
|
||||
<span class="inline-flex items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -59,5 +66,5 @@ const { slug, title, description, count, publishDate } = Astro.props;
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
44
src/components/blog/BlogLeftSection.astro
Normal file
44
src/components/blog/BlogLeftSection.astro
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
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>
|
||||
29
src/components/blog/BlogRecentArticles.astro
Normal file
29
src/components/blog/BlogRecentArticles.astro
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
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>
|
||||
87
src/components/blog/BlogRightSection.astro
Normal file
87
src/components/blog/BlogRightSection.astro
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
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>
|
||||
44
src/components/blog/BlogSelectedArticles.astro
Normal file
44
src/components/blog/BlogSelectedArticles.astro
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
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>
|
||||
@@ -2,10 +2,11 @@
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const { url } = Astro.props;
|
||||
const { title, url } = Astro.props;
|
||||
---
|
||||
|
||||
<a
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
---
|
||||
import Icon from '@components/ui/icons/icon.astro';
|
||||
|
||||
interface Props {
|
||||
noArrow?: boolean;
|
||||
}
|
||||
|
||||
const { noArrow } = Astro.props;
|
||||
---
|
||||
|
||||
<button
|
||||
@@ -8,7 +14,7 @@ import Icon from '@components/ui/icons/icon.astro';
|
||||
data-astro-prefetch
|
||||
>
|
||||
<div class="button-text-title flex relative items-center text-center">
|
||||
<Icon name="arrowLeft" />
|
||||
{noArrow ? null : <Icon name="arrowLeft" />}
|
||||
<span class="ml-2">
|
||||
Go Back
|
||||
</span>
|
||||
|
||||
@@ -33,7 +33,8 @@ const socialPlatforms: SocialPlatform[] = [
|
||||
---
|
||||
|
||||
<div class="inline-flex items-center gap-x-2">
|
||||
{socialPlatforms.map((platform) => (
|
||||
{
|
||||
socialPlatforms.map((platform) => (
|
||||
<a
|
||||
class="button-base-hidden group inline-flex rounded-lg gap-x-2"
|
||||
href={platform.url}
|
||||
@@ -42,11 +43,9 @@ const socialPlatforms: SocialPlatform[] = [
|
||||
title={`Share on ${platform.name}`}
|
||||
>
|
||||
<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>
|
||||
</a>
|
||||
))}
|
||||
))
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
import Logo from '@components/ui/logos/Logo.astro';
|
||||
import { getDirectusImageURL } from '@lib/directusFunctions';
|
||||
|
||||
interface Props {
|
||||
topic: string;
|
||||
@@ -14,8 +12,7 @@ interface Props {
|
||||
logoIcon?: string;
|
||||
}
|
||||
|
||||
const { topic, area, date, url, logoUrlLight, logoIcon } = Astro.props;
|
||||
const logoUrlDark = Astro.props.logoUrlDark || logoUrlLight;
|
||||
const { topic, area, date, url, logoUrlLight, logoUrlDark, logoIcon } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="smooth-reveal group flex flex-col">
|
||||
@@ -28,8 +25,8 @@ const logoUrlDark = Astro.props.logoUrlDark || logoUrlLight;
|
||||
{logoUrlLight ? (
|
||||
<div class="card-hover-icon-scale mr-5">
|
||||
<Logo
|
||||
srcLight={getDirectusImageURL(logoUrlLight)}
|
||||
srcDark={getDirectusImageURL(logoUrlDark!)}
|
||||
srcLight={logoUrlLight}
|
||||
srcDark={logoUrlDark}
|
||||
alt={`Logo of ${topic}`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -9,11 +9,13 @@ interface 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">
|
||||
<a
|
||||
class="card-base flex items-center h-30 w-100 md:w-75"
|
||||
class={`card-base flex items-center ${sizeClasses}`}
|
||||
href={url}
|
||||
data-astro-prefetch
|
||||
>
|
||||
@@ -27,9 +29,9 @@ const { title, description, url, icon } = Astro.props;
|
||||
<span class="card-text-title card-hover-text-title block text-lg">
|
||||
{title}
|
||||
</span>
|
||||
<p class="card-text-description block mt-1">
|
||||
<span class="card-text-description block mt-1">
|
||||
{description}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -39,9 +39,9 @@ const visitClass = visitSource ? 'card-hover-text-gitea' : 'card-hover-text-titl
|
||||
<span class="card-text-title block text-lg">
|
||||
{title}
|
||||
</span>
|
||||
<p class="card-text-description block mt-1">
|
||||
<span class="card-text-description block mt-1">
|
||||
{description}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{highlights && (
|
||||
@@ -55,7 +55,7 @@ const visitClass = visitSource ? 'card-hover-text-gitea' : 'card-hover-text-titl
|
||||
)}
|
||||
<div class="ml-6 flex">
|
||||
<div class="relative inline-block">
|
||||
<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`}>
|
||||
<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`}>
|
||||
{visitSource && <Icon name="pajamas:gitea" />}
|
||||
<span class="relative inline-block overflow-hidden ml-2">
|
||||
{visitText}
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,83 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -7,10 +7,12 @@ interface 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="card-base w-32 md:w-40">
|
||||
<div class={`card-base ${sizeClasses}`}>
|
||||
<div class="p-5 text-center">
|
||||
<span class="card-text-description block font-bold text-xs uppercase tracking-widest">
|
||||
{dayName}
|
||||
|
||||
@@ -3,8 +3,8 @@ import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Application } from '@lib/directusTypes';
|
||||
|
||||
import HighlightsCard from '@components/cards/HighlightsCard.astro';
|
||||
import directus from '@lib/directus';
|
||||
import HighlightsCard from '@components/cards/HighlightsCard.astro';
|
||||
|
||||
const applications = ((await directus.request(
|
||||
readItems('site_applications' as any, {
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -3,8 +3,9 @@ import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Education, Certificate} from '@lib/directusTypes';
|
||||
|
||||
import EducationCard from '@components/cards/EducationCard.astro';
|
||||
import directus from '@lib/directus';
|
||||
import { getDirectusImageURL } from '@lib/directusFunctions';
|
||||
import EducationCard from '@components/cards/EducationCard.astro';
|
||||
|
||||
const educations = ((await directus.request(
|
||||
readItems('site_education' as any, {
|
||||
@@ -36,8 +37,8 @@ const certificates = ((await directus.request(
|
||||
area={education.area}
|
||||
date={education.graduationDate}
|
||||
url={education.url}
|
||||
logoUrlLight={education.logo}
|
||||
logoUrlDark={education.logoDark}
|
||||
logoUrlLight={getDirectusImageURL(education.logo)}
|
||||
logoUrlDark={getDirectusImageURL(education.logoDark)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ const experiences = ((await directus.request(
|
||||
</div>
|
||||
)}
|
||||
{(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-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>
|
||||
<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>
|
||||
{experience.responsibilities && (
|
||||
<div class="flex flex-col gap-1">
|
||||
<h4 class="text-header font-semibold">
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import FeaturesCard from '@components/cards/FeaturesCard.astro';
|
||||
import directus from '@lib/directus';
|
||||
import FeaturesCard from '@components/cards/FeaturesCard.astro';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
---
|
||||
36
src/components/sections/LatestPostsSection.astro
Normal file
36
src/components/sections/LatestPostsSection.astro
Normal file
@@ -0,0 +1,36 @@
|
||||
---
|
||||
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>
|
||||
@@ -3,8 +3,8 @@ import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Project } from '@lib/directusTypes';
|
||||
|
||||
import HighlightsCard from '@components/cards/HighlightsCard.astro';
|
||||
import directus from '@lib/directus';
|
||||
import HighlightsCard from '@components/cards/HighlightsCard.astro';
|
||||
|
||||
const projects = ((await directus.request(
|
||||
readItems('site_projects' as any, {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
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>
|
||||
@@ -3,7 +3,6 @@ import WeatherCard from '@components/cards/WeatherCard.astro';
|
||||
import { getFiveDayForecast } from '@support/weather';
|
||||
|
||||
const { latitude = "44.95", longitude = "-93.09", cityName = "St. Paul, Minnesota", timezone = "America/Chicago" } = Astro.props;
|
||||
|
||||
const { forecastDays, error } = await getFiveDayForecast(latitude, longitude, timezone);
|
||||
---
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import { ClientRouter } from 'astro:transitions';
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import BaseHead from '@components/BaseHead.astro';
|
||||
import Footer from '@components/Footer.astro';
|
||||
import Header from '@components/Header.astro';
|
||||
import directus from '@lib/directus';
|
||||
|
||||
import '@styles/global.css';
|
||||
|
||||
@@ -20,16 +20,12 @@ interface Props {
|
||||
const { title, description = 'Alex Lebens', ogImage, lang = 'en', structuredData } = Astro.props;
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
|
||||
const normalizeTitle = !title ? global.name : `${title} | ${global.name}`;
|
||||
---
|
||||
|
||||
<html lang={lang}>
|
||||
<head>
|
||||
<title>
|
||||
{normalizeTitle}
|
||||
</title>
|
||||
|
||||
<title>{normalizeTitle}</title>
|
||||
<BaseHead
|
||||
title={normalizeTitle}
|
||||
description={description}
|
||||
@@ -38,9 +34,7 @@ const normalizeTitle = !title ? global.name : `${title} | ${global.name}`;
|
||||
ogDescription={description}
|
||||
structuredData={structuredData}
|
||||
/>
|
||||
|
||||
<ClientRouter fallback="swap" />
|
||||
|
||||
<script is:inline>
|
||||
const theme = (() => {
|
||||
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
||||
@@ -59,35 +53,30 @@ const normalizeTitle = !title ? global.name : `${title} | ${global.name}`;
|
||||
}
|
||||
window.localStorage.setItem('theme', theme);
|
||||
</script>
|
||||
|
||||
<!-- Rybbit Tracking Snippet -->
|
||||
<script
|
||||
src="https://rybbit.alexlebens.dev/api/script.js"
|
||||
data-site-id={global.rybbit_site_id}
|
||||
defer
|
||||
/>
|
||||
|
||||
></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-background selection:bg-yellow-400">
|
||||
<!-- Disabled texture background for now
|
||||
<div class="fixed inset-0 -z-10">
|
||||
<div class="bg-grid-pattern absolute inset-0 mask-[radial-gradient(white,transparent_85%)] bg-position-[center_top_-1px]"/>
|
||||
</div>
|
||||
-->
|
||||
|
||||
<div class="grow w-full max-w-(--breakpoint-2xl) px-4 sm:px-6 lg:px-8 py-20 mx-auto">
|
||||
|
||||
<body class="bg-stone-200 selection:bg-yellow-400 selection:text-neutral-700 dark:bg-stone-700">
|
||||
<div class="mx-auto w-full max-w-(--breakpoint-2xl) grow px-4 sm:px-6 lg:px-8">
|
||||
<Header />
|
||||
|
||||
<main class="min-h-screen">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
|
||||
<style>
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
<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-screen-sm text-center">
|
||||
@@ -68,7 +67,6 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import getReadingTime from 'reading-time';
|
||||
import { readItems, readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import { marked } from 'marked';
|
||||
import markedShiki from 'marked-shiki';
|
||||
import { createHighlighter } from 'shiki';
|
||||
import { readItems, readSingleton } from '@directus/sdk';
|
||||
|
||||
import { getDirectusImageURL } from '@lib/directusFunctions';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import Image from '@components/ui/images/Image.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';
|
||||
|
||||
const post = Astro.props;
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await directus.request(readItems('posts'));
|
||||
return posts.map((post) => ({
|
||||
@@ -22,19 +20,18 @@ export async function getStaticPaths() {
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
const post = Astro.props;
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
const category: CollectionEntry<'categories'> = (await getCollection('categories'))
|
||||
.filter((c) => c.slug === post.category)
|
||||
.pop() as CollectionEntry<'categories'>;
|
||||
|
||||
const readingTime = getReadingTime(post.content);
|
||||
|
||||
const highlighter = await createHighlighter({
|
||||
themes: ['github-light', 'github-dark', 'monokai'],
|
||||
langs: ['typescript', 'python', 'css', 'html', 'yaml', 'bash', 'json'],
|
||||
});
|
||||
|
||||
marked.use(markedShiki({
|
||||
highlight(code, lang) {
|
||||
return highlighter.codeToHtml(code, {
|
||||
@@ -47,7 +44,6 @@ marked.use(markedShiki({
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
const content = marked.parse(post.content);
|
||||
---
|
||||
|
||||
@@ -83,7 +79,6 @@ 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">
|
||||
<div class="smooth-reveal relative w-full">
|
||||
<div class="mt-4 rounded-2xl shadow-none sm:mt-0 sm:shadow-sm">
|
||||
@@ -176,7 +171,6 @@ const content = marked.parse(post.content);
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style is:inline>
|
||||
code[data-theme*=' '],
|
||||
code[data-theme*=' '] span {
|
||||
@@ -190,7 +184,6 @@ const content = marked.parse(post.content);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -3,12 +3,11 @@ import { readItems, readSingleton } from '@directus/sdk';
|
||||
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
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 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 blogImg from '@images/autumn_tree.png';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
@@ -19,11 +18,10 @@ const posts = await directus.request(
|
||||
sort: ['-published_date'],
|
||||
})
|
||||
);
|
||||
|
||||
const selectedPosts: Post[] = posts.filter((p) => p.selected).slice(0, 3);
|
||||
const selectedPosts: Post[] = posts.filter((p) => p.selected).slice(0, 4);
|
||||
const recentPosts: Post[] = posts.filter(
|
||||
(p) => !selectedPosts.some((selected) => selected.slug === p.slug)
|
||||
).slice(0, 9);
|
||||
).slice(0, 6);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -45,21 +43,10 @@ const recentPosts: Post[] = posts.filter(
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HeroSection title="Blog" subTitle={global.about_blog} src={blogImg} alt={global.blog_image_alt} />
|
||||
|
||||
<HeroSection
|
||||
title="Blog"
|
||||
subTitle={global.about_blog}
|
||||
src={blogImg}
|
||||
alt={global.blog_image_alt}
|
||||
/>
|
||||
|
||||
<SelectedPostsSection posts={selectedPosts} />
|
||||
|
||||
<RecentPostsSection
|
||||
posts={recentPosts}
|
||||
title="Recent Posts"
|
||||
/>
|
||||
|
||||
<BlogSelectedArticles posts={selectedPosts} />
|
||||
<BlogRecentArticles posts={recentPosts} />
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -2,12 +2,11 @@
|
||||
import { getCollection } from 'astro:content';
|
||||
import { readItems, readSingleton } from '@directus/sdk';
|
||||
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
import BlogCard from '@components/cards/BlogCard.astro';
|
||||
import HeaderSection from '@components/sections/HeaderSection.astro';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import directus from '@lib/directus';
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import BlogCard from '@components/blog/BlogCard.astro';
|
||||
import HeaderSection from '@components/sections/HeaderSection.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const categories = await getCollection('categories');
|
||||
@@ -27,7 +26,6 @@ const posts = await directus.request(
|
||||
sort: ['-published_date'],
|
||||
})
|
||||
);
|
||||
|
||||
const categoriesPosts = posts
|
||||
.sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf())
|
||||
.filter((b) => {
|
||||
@@ -53,7 +51,6 @@ const categoriesPosts = posts
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
<HeaderSection
|
||||
title={category.data.title}
|
||||
subTitle={category.data.description}
|
||||
@@ -62,12 +59,9 @@ const categoriesPosts = posts
|
||||
btnURL="/categories"
|
||||
/>
|
||||
|
||||
<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">
|
||||
<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">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
@@ -1,14 +1,78 @@
|
||||
---
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { readItems, readSingleton } from '@directus/sdk';
|
||||
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import BlogCategoryCard from '@components/blog/BlogCategoryCard.astro';
|
||||
import HeroSection from '@components/sections/HeroSection.astro';
|
||||
import CategorySection from '@components/sections/CategorySection.astro';
|
||||
|
||||
import { timeago } from '@support/time';
|
||||
import categoryImg from '@images/autumn_bench.png';
|
||||
|
||||
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
|
||||
@@ -30,7 +94,6 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
<HeroSection
|
||||
title="Categories"
|
||||
subTitle={global.about_categories}
|
||||
@@ -38,8 +101,28 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
alt={global.categories_image_alt}
|
||||
/>
|
||||
|
||||
<CategorySection />
|
||||
|
||||
<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 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>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,31 +1,18 @@
|
||||
---
|
||||
import { readSingleton, readItems } from '@directus/sdk';
|
||||
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import HeroSection from '@components/sections/HeroSection.astro';
|
||||
import FeatureSection from '@components/sections/FeatureSection.astro';
|
||||
import FeaturesSection from '@components/sections/FeaturesSection.astro';
|
||||
import WeatherSection from '@components/sections/WeatherSection.astro';
|
||||
import RecentPostsSection from '@components/sections/RecentPostsSection.astro';
|
||||
import LatestPostsSection from '@components/sections/LatestPostsSection.astro';
|
||||
import GiteaSection from '@components/sections/GiteaSection.astro';
|
||||
|
||||
import homeImg from '@images/autumn_mountain.png';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
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
|
||||
@@ -47,7 +34,6 @@ const recentPosts = posts
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
<HeroSection
|
||||
title={`Hello, I'm <span class="text-steel dark:text-steel">Alex Lebens</span>`}
|
||||
subTitle={global.about_description}
|
||||
@@ -57,7 +43,7 @@ const recentPosts = posts
|
||||
alt={global.home_image_alt}
|
||||
/>
|
||||
|
||||
<FeatureSection />
|
||||
<FeaturesSection />
|
||||
|
||||
<WeatherSection
|
||||
server:defer
|
||||
@@ -65,20 +51,16 @@ const recentPosts = posts
|
||||
longitude={weather.longitude}
|
||||
cityName={weather.location}
|
||||
timezone={weather.timezone}
|
||||
/>
|
||||
>
|
||||
</WeatherSection>
|
||||
|
||||
<RecentPostsSection
|
||||
posts={recentPosts}
|
||||
title="Latest Posts"
|
||||
subTitle="Checkout my most recent thoughts here"
|
||||
/>
|
||||
<LatestPostsSection />
|
||||
|
||||
<GiteaSection
|
||||
title="Follow me on Gitea"
|
||||
subTitle="I love open source and have my code availabile on my Gitea server."
|
||||
url="https://gitea.alexlebens.dev"
|
||||
/>
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -28,16 +28,11 @@
|
||||
|
||||
--color-main: light-dark(var(--color-steel), var(--color-bermuda));
|
||||
--color-accent: light-dark(var(--color-bronze), var(--color-desert));
|
||||
--color-active: light-dark(var(--color-orange-500), var(--color-orange-300));
|
||||
|
||||
--color-header: light-dark(var(--color-neutral-800), var(--color-neutral-200));
|
||||
--color-primary: light-dark(var(--color-neutral-600), var(--color-neutral-200));
|
||||
--color-primary-hover: light-dark(var(--color-neutral-800), var(--color-neutral-400));
|
||||
--color-secondary: light-dark(var(--color-neutral-500), var(--color-neutral-400));
|
||||
--color-secondary-hover: light-dark(var(--color-neutral-800), var(--color-neutral-200));
|
||||
|
||||
--color-background: light-dark(var(--color-neutral-200), var(--color-stone-700));
|
||||
--color-background-accent: light-dark(color-mix(in srgb, var(--color-stone-300) 40%, transparent), color-mix(in srgb, var(--color-stone-800) 20%, transparent));
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
|
||||
@utility button-bg-teal {
|
||||
@apply transition-all duration-300
|
||||
bg-bermuda hover:bg-turquoise group-hover:bg-turquoise dark:bg-turquoise dark:hover:bg-bermuda dark:group-hover:bg-bermuda
|
||||
bg-bermuda hover:bg-turquoise dark:bg-turquoise dark:hover:bg-bermuda
|
||||
}
|
||||
|
||||
@utility button-bg-neutral {
|
||||
@@ -53,20 +53,12 @@
|
||||
|
||||
/* Card classes */
|
||||
@utility card-base {
|
||||
@apply transition-all duration-300
|
||||
rounded-xl
|
||||
@apply rounded-xl
|
||||
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
|
||||
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 {
|
||||
@apply transition-all duration-300
|
||||
text-primary
|
||||
@@ -87,8 +79,9 @@
|
||||
|
||||
@utility card-text-header-minor {
|
||||
@apply text-header
|
||||
text-2xl md:text-3xl
|
||||
font-semibold leading-tight tracking-tight text-balance
|
||||
md:text-3xl
|
||||
text-2xl
|
||||
font-semibold
|
||||
}
|
||||
|
||||
@utility card-text-header-description {
|
||||
@@ -102,22 +95,11 @@
|
||||
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 {
|
||||
@apply transition-all duration-300
|
||||
group-hover:text-main
|
||||
}
|
||||
|
||||
@utility card-hover-text-neutral {
|
||||
@apply transition-all duration-300
|
||||
group-hover:text-primary-hover
|
||||
}
|
||||
|
||||
@utility card-hover-text-gitea {
|
||||
@apply transition-all duration-300
|
||||
group-hover:text-gitea-primary
|
||||
@@ -126,10 +108,3 @@
|
||||
@utility card-text-description {
|
||||
@apply text-secondary
|
||||
}
|
||||
|
||||
/* Misc */
|
||||
@utility nav-base {
|
||||
@apply border border-neutral-100 dark:border-stone-500/20
|
||||
bg-neutral-100 dark:bg-neutral-800
|
||||
shadow-xs dark:shadow-md
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user