Compare commits

..

1 Commits

Author SHA1 Message Date
e463b8d956 chore(deps): update dependency eslint to v10
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 1m16s
2026-02-08 20:41:14 +00:00
81 changed files with 2327 additions and 3079 deletions

View File

@@ -22,7 +22,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 24.13.1 node-version: 24.13.0
cache: pnpm cache: pnpm
- name: Install Dependencies - name: Install Dependencies
@@ -48,13 +48,6 @@ jobs:
username: ${{ gitea.actor }} username: ${{ gitea.actor }}
password: ${{ secrets.REPOSITORY_TOKEN }} password: ${{ secrets.REPOSITORY_TOKEN }}
- name: Login to Docker
uses: docker/login-action@v3
with:
registry: ${{ vars.DH_REGISTRY }}
username: ${{ secrets.DH_USERNAME }}
password: ${{ secrets.DH_TOKEN }}
- name: Create Kubeconfig - name: Create Kubeconfig
run: | run: |
mkdir $HOME/.kube mkdir $HOME/.kube

View File

@@ -22,7 +22,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 24.13.1 node-version: 24.13.0
cache: pnpm cache: pnpm
- name: Install Dependencies - name: Install Dependencies
@@ -48,13 +48,6 @@ jobs:
username: ${{ vars.REGISTRY_USER }} username: ${{ vars.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_SECRET }} password: ${{ secrets.REGISTRY_SECRET }}
- name: Login to Docker
uses: docker/login-action@v3
with:
registry: ${{ vars.DH_REGISTRY }}
username: ${{ secrets.DH_USERNAME }}
password: ${{ secrets.DH_TOKEN }}
- name: Create Kubeconfig - name: Create Kubeconfig
run: | run: |
mkdir $HOME/.kube mkdir $HOME/.kube

View File

@@ -25,10 +25,8 @@ jobs:
RENOVATE_ENDPOINT: ${{ vars.INSTANCE_URL }} RENOVATE_ENDPOINT: ${{ vars.INSTANCE_URL }}
RENOVATE_REPOSITORIES: alexlebens/site-profile RENOVATE_REPOSITORIES: alexlebens/site-profile
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net> RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net>
RENOVATE_REDIS_URL: ${{ vars.RENOVATE_REDIS_URL }}
LOG_LEVEL: info LOG_LEVEL: info
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
RENOVATE_GIT_PRIVATE_KEY: ${{ secrets.RENOVATE_GIT_PRIVATE_KEY }} RENOVATE_GIT_PRIVATE_KEY: ${{ secrets.RENOVATE_GIT_PRIVATE_KEY }}
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }} RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
RENOVATE_REGISTRY_ALIASES: '{"dhi.io": "dhi.io"}' RENOVATE_REDIS_URL: ${{ vars.RENOVATE_REDIS_URL }}
RENOVATE_HOST_RULES: '[{"matchHost":"dhi.io","hostType":"docker","username":"${{ secrets.RENOVATE_DHI_USER }}","password":"${{ secrets.RENOVATE_DHI_TOKEN }}"}]'

View File

@@ -24,7 +24,7 @@ jobs:
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v6
with: with:
node-version: 24.13.1 node-version: 24.13.0
cache: pnpm cache: pnpm
- name: Install Dependencies - name: Install Dependencies
@@ -50,38 +50,3 @@ jobs:
icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png' icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png'
actions: '[{"action": "view", "label": "Open Gitea", "url": "https://gitea.alexlebens.dev/alexlebens/site-profile/actions?workflow=test-build.yaml", "clear": true}]' actions: '[{"action": "view", "label": "Open Gitea", "url": "https://gitea.alexlebens.dev/alexlebens/site-profile/actions?workflow=test-build.yaml", "clear": true}]'
image: true image: true
guarddog:
runs-on: ubuntu-js
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Install GuardDog
run: |
python3 -m pip install --upgrade pip
python3 -m pip install guarddog
- name: Run GuardDog
run: |
guarddog npm scan ./
- name: ntfy Failed
uses: niniyas/ntfy-action@master
if: failure()
with:
url: '${{ secrets.NTFY_URL }}'
topic: '${{ secrets.NTFY_TOPIC }}'
title: 'Security Failure - Site Profile'
priority: 4
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
tags: action,failed
details: 'Guarddog scan failed for Site Profile'
icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png'
actions: '[{"action": "view", "label": "Open Gitea", "url": "https://gitea.alexlebens.dev/alexlebens/site-profile/actions?workflow=test-build.yaml", "clear": true}]'
image: true

View File

@@ -1,13 +1,15 @@
FROM docker.io/node:24.13.1-alpine AS builder ARG REGISTRY=docker.io
FROM ${REGISTRY}/node:24.13.0-alpine AS base
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH" ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable RUN corepack enable
WORKDIR /app WORKDIR /app
COPY package.json pnpm-lock.yaml ./ COPY package.json pnpm-lock.yaml ./
FROM builder AS prod-deps FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
FROM prod-deps AS build-deps FROM prod-deps AS build-deps
@@ -16,18 +18,18 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
FROM build-deps AS build FROM build-deps AS build
COPY . . COPY . .
RUN pnpm run build RUN pnpm run build
RUN pnpm prune --prod
FROM dhi.io/node:24.13.1 AS runtime FROM base AS runtime
WORKDIR /app
COPY --from=prod-deps /app/node_modules /app/node_modules COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist COPY --from=build /app/dist /app/dist
ENV HOST=0.0.0.0 ENV HOST=0.0.0.0
ENV SITE_URL=https://www.alexlebens.dev ENV SITE_URL=https://www.alexlebens.dev
ENV DIRECTUS_URL=https://directus.alexlebens.net ENV DIRECTUS_URL=https://directus.alexlebens.dev
ENV PORT=4321 ENV PORT=4321
LABEL version="2.13.0" LABEL version="2.3.3"
LABEL description="Astro based personal website" LABEL description="Astro based personal website"
EXPOSE $PORT EXPOSE $PORT

View File

@@ -14,7 +14,7 @@ For detailed help with Astro CLI commands, visit [Astro's documentation](https:/
## Thanks ## Thanks
Thanks https://github.com/godruoyi/gblog/tree/gblog-template, https://github.com/mearashadowfax/ScrewFast, Thanks https://github.com/mearashadowfax/ScrewFast, https://github.com/godruoyi/gblog/tree/gblog-template
## License ## License

View File

@@ -20,10 +20,6 @@ export default defineConfig({
site: getSiteURL(), site: getSiteURL(),
image: { image: {
remotePatterns: [
{ protocol: 'https', hostname: '*.alexlebens.net' },
{ protocol: 'https', hostname: '*.jsdelivr.net' },
],
service: { service: {
entrypoint: 'astro/assets/services/sharp', entrypoint: 'astro/assets/services/sharp',
} }

View File

@@ -1,7 +1,7 @@
{ {
"name": "site-profile", "name": "site-profile",
"type": "module", "type": "module",
"version": "2.13.0", "version": "2.3.3",
"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",
@@ -28,27 +28,29 @@
}, },
"dependencies": { "dependencies": {
"@astrojs/check": "^0.9.6", "@astrojs/check": "^0.9.6",
"@astrojs/node": "^9.5.3", "@astrojs/node": "^9.5.2",
"@astrojs/partytown": "^2.1.4", "@astrojs/partytown": "^2.1.4",
"@astrojs/react": "^4.4.2", "@astrojs/react": "^4.4.2",
"@astrojs/rss": "^4.0.15", "@astrojs/rss": "^4.0.15",
"@astrojs/sitemap": "^3.7.0", "@astrojs/sitemap": "^3.7.0",
"@directus/sdk": "^21.1.0", "@directus/sdk": "^21.0.0",
"@giscus/react": "^3.1.0", "@giscus/react": "^3.1.0",
"@iconify-json/mdi": "^1.2.3", "@iconify-json/mdi": "^1.2.3",
"@iconify-json/pajamas": "^1.2.15", "@iconify-json/pajamas": "^1.2.15",
"@iconify-json/simple-icons": "^1.2.70", "@iconify-json/simple-icons": "^1.2.69",
"@playform/compress": "^0.2.1", "@playform/compress": "^0.2.1",
"@swup/astro": "^1.8.0", "@swup/astro": "^1.7.0",
"@tailwindcss/postcss": "^4.1.18", "@tailwindcss/postcss": "^4.1.18",
"@tailwindcss/vite": "^4.1.18", "@tailwindcss/vite": "^4.1.18",
"@types/react": "^19.2.14", "@types/react": "^19.2.10",
"@types/unist": "^3.0.3", "@types/unist": "^3.0.3",
"astro": "^5.17.2", "astro": "^5.17.1",
"astro-compressor": "^1.2.0",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"marked": "^17.0.2", "marked": "^17.0.1",
"marked-shiki": "^1.2.1", "marked-shiki": "^1.2.1",
"mdast-util-to-string": "^4.0.0", "mdast-util-to-string": "^4.0.0",
"motion": "^12.31.0",
"preline": "^4.0.1", "preline": "^4.0.1",
"react": "^19.2.4", "react": "^19.2.4",
"react-dom": "^19.2.4", "react-dom": "^19.2.4",
@@ -60,13 +62,14 @@
"ultrahtml": "^1.6.0" "ultrahtml": "^1.6.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint-react/eslint-plugin": "^2.13.0", "@eslint-react/eslint-plugin": "^2.9.4",
"@tailwindcss/forms": "^0.5.11", "@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"astro-icon": "^1.1.5",
"eslint": "^10.0.0", "eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8", "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-format": "^1.3.1",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.5.0", "eslint-plugin-react-refresh": "^0.5.0",
@@ -75,6 +78,6 @@
"prettier-plugin-tailwindcss": "^0.7.2", "prettier-plugin-tailwindcss": "^0.7.2",
"timeago.js": "^4.0.2", "timeago.js": "^4.0.2",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"typescript-eslint": "^8.55.0" "typescript-eslint": "^8.54.0"
} }
} }

1549
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,11 +3,10 @@ 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;
@@ -19,7 +18,6 @@ interface Props {
} }
const canonicalURL = Astro.url.href; const canonicalURL = Astro.url.href;
let { let {
title, title,
description, description,
@@ -29,14 +27,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 });
@@ -64,12 +62,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} />
@@ -78,10 +76,17 @@ 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,80 +1,92 @@
--- ---
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();
--- ---
<footer <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" transition:animate="none"
> >
<div class="relative px-4 sm:px-6 pt-16 pb-12"> <div class="relative px-4 pt-16 pb-12 sm:px-6">
<div class="max-w-340 mx-auto"> <div class="mx-auto max-w-[85rem]">
<div class="grid grid-cols-1 md:grid-cols-12 gap-10"> <div class="grid grid-cols-1 gap-10 md:grid-cols-12">
<!-- Brand section --> <!-- Brand section -->
<div class="col-span-1 md:col-span-3"> <div class="col-span-1 md:col-span-3">
<a href="/" class="group inline-block"> <a href="/" class="group inline-block">
<div class="flex items-center"> <div class="flex items-center">
<div class="mx-auto aspect-square overflow-hidden"> <div class="mx-auto aspect-square overflow-hidden rounded-lg">
<BrandLogo class="rounded-lg max-h-10 max-w-10"/> <BrandLogo class="max-h-[40px] max-w-[40px] rounded-full" />
</div> </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} {global.name}
</span> </span>
</div> </div>
</a> </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} {global.about}
</p> </p>
</div> </div>
<!-- Left links --> <!-- Left links -->
<div class="col-span-1 md:col-span-2"> <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-['']"> <h3
Site 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> </h3>
<ul class="mt-4 space-y-3"> <ul class="mt-4 space-y-3">
{NavigationLinks.map((link) => ( {
NavigationLinks.map((link) => (
<li> <li>
<a <a
href={link.url} 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> </a>
</li> </li>
))} ))
}
</ul> </ul>
</div> </div>
<!-- Right links --> <!-- Right links -->
<div class="col-span-1 md:col-span-3"> <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 Other
</h3> </h3>
<ul class="mt-4 space-y-3"> <ul class="mt-4 space-y-3">
{FooterLinks.map((link) => ( {
FooterLinks.map((link) => (
<li> <li>
<a <a
href={link.url} 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> </a>
</li> </li>
))} ))
}
</ul> </ul>
</div> </div>
<!-- Right image --> <!-- Right image -->
<div class="flex justify-center col-span-4 mt-10 md:mt-0"> <div class="col-span-3 mt-10 flex justify-center md:mt-0">
<div class="md:block max-h-115 max-w-55 -mt-10 scale-80 hidden"> <div class="-mt-10 hidden max-h-[460px] max-w-[220px] scale-80 md:block">
<Image <Image
src={footerImg} src={footerImg}
alt={global.footer_image_alt} alt={global.footer_image_alt}
@@ -90,37 +102,38 @@ const currentYear = new Date().getFullYear();
</div> </div>
</div> </div>
<!-- Bottom section --> <!-- Bottom section -->
<div class="border-t border-divider pt-8 mt-12"> <div class="mt-12 border-t border-neutral-400/30 pt-8 dark:border-neutral-600/50">
<div class="flex flex-col md:flex-row items-center justify-between gap-4"> <div class="flex flex-col items-center justify-between gap-4 md:flex-row">
<p class="text-secondary text-sm"> <p class="text-sm text-neutral-600 dark:text-neutral-400">
&copy; {currentYear} All rights reserved. &copy; {currentYear} All rights reserved.
</p> </p>
<div class="flex items-center">
<span class="text-secondary text-sm"> <div class="flex items-center space-x-2">
Weather provided by <span class="text-xs text-neutral-500 dark:text-neutral-400">Built with </span>
</span>
<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"
>
<span class="relative underline ml-1">
Open-Meteo.
</span>
</a>
<div class="ml-4"/>
<span class="text-secondary text-sm">
Built with
</span>
<a <a
href="https://astro.build" href="https://astro.build"
target="_blank" target="_blank"
rel="noopener noreferrer" 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"> <svg class="mr-1 h-4 w-4 text-[#FF5D01]" viewBox="0 0 36 36" fill="none">
Astro. <path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.833 22.958c.622-1.185 1.832-1.918 3.18-1.918 2.292 0 4.145 1.86 4.145 4.153 0 1.34-.626 2.54-1.601 3.303 1.223-1.299 1.97-3.048 1.97-4.971 0-3.994-3.243-7.233-7.242-7.233-2.818 0-5.26 1.6-6.469 3.933.78-2.912 3.428-5.06 6.577-5.06 3.75 0 6.79 3.035 6.79 6.78 0 2.606-1.468 4.868-3.616 6.002a4.163 4.163 0 0 0 2.285-3.724c0-2.293-1.853-4.153-4.145-4.153-1.348 0-2.558.733-3.18 1.918l1.306-3.03Z"
fill="currentColor"></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M22.155 12.056c-.622 1.185-1.832 1.918-3.18 1.918-2.292 0-4.145-1.86-4.145-4.153 0-1.34.626-2.54 1.601-3.303-1.223 1.299-1.97 3.048-1.97 4.971 0 3.994 3.243 7.233 7.242 7.233 2.818 0 5.26-1.6 6.469-3.933-.78 2.912-3.428 5.06-6.577 5.06-3.75 0-6.79-3.035-6.79-6.78 0-2.606 1.468-4.868 3.616-6.002a4.163 4.163 0 0 0-2.285 3.724c0 2.293 1.853 4.153 4.145 4.153 1.348 0 2.558-.733 3.18-1.918l-1.306 3.03Z"
fill="currentColor"></path>
</svg>
<span class="relative">
Astro
<span
class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"
>
</span>
</span> </span>
</a> </a>
</div> </div>

View File

@@ -1,6 +1,6 @@
--- ---
import BrandLogo from '@components/ui/logos/BrandLogo.astro'; import BrandLogo from '@components/ui/logos/BrandLogo.astro';
import ThemeToggleButton from '@components/buttons/ThemeToggleButton.astro'; import ThemeToggle from '@components/ui/buttons/ThemeToggle.astro';
import { NavigationLinks } from '@/config'; import { NavigationLinks } from '@/config';
const pathname = new URL(Astro.request.url).pathname; const pathname = new URL(Astro.request.url).pathname;
@@ -9,31 +9,31 @@ const currentPath = pathname.slice(1);
<header <header
id="nav" 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 <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" aria-label="Global"
> >
<div class="flex items-center justify-between ml-0"> <div class="flex items-center justify-between">
<a <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="/" href="/"
aria-label="Brand" aria-label="Brand"
> >
<BrandLogo class="h-full w-auto rounded-full object-cover" /> <BrandLogo class="h-full w-auto rounded-full object-cover" />
</a> </a>
<div class="md:hidden mr-auto ml-4">
<div class="ml-auto md:hidden">
<button <button
type="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" data-hs-collapse="#navbar-collapse-with-animation"
aria-controls="navbar-collapse-with-animation" aria-controls="navbar-collapse-with-animation"
aria-label="Toggle navigation" aria-label="Toggle navigation"
> >
<svg <svg
class="hs-collapse-open:hidden shrink-0 h-5 w-5" class="hs-collapse-open:hidden h-[1.25rem] w-[1.25rem] flex-shrink-0"
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 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> <line x1="3" x2="21" y1="18" y2="18"></line>
</svg> </svg>
<svg <svg
class="hs-collapse-open:block shrink-0 h-5 w-5 hidden" class="hs-collapse-open:block hidden h-[1.25rem] w-[1.25rem] flex-shrink-0"
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -63,34 +63,34 @@ const currentPath = pathname.slice(1);
</svg> </svg>
</button> </button>
</div> </div>
<div class="md:hidden ml-2 mr-2">
<span class="">
<ThemeToggleButton />
</span>
</div> </div>
</div>
<div class="flex md:flex-row items-center justify-between">
<div <div
id="navbar-collapse-with-animation" 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"
> >
<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"> <div
{NavigationLinks.map((item) => { 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)); const isActive = currentPath === (item.url === '/' ? '' : item.url.slice(1));
return ( return (
<a <a
href={item.url} 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} {item.name}
</a> </a>
); );
})} })
</div> }
</div> <span class="md:inline-block">
<div class="hidden md:flex ml-2"> <ThemeToggle />
<span class="">
<ThemeToggleButton />
</span> </span>
</div> </div>
</div> </div>

View 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>

View File

@@ -8,30 +8,37 @@ 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 ';
--- ---
<div class="smooth-reveal-cards group h-full"> <a class={`rounded-xl`} href={`/categories/${slug}/`} data-astro-prefetch="false">
<a <div class={`${baseClasses}`}>
class="card-base flex flex-col h-full min-h-55" <div
href={`/categories/${slug}/`} class={`relative min-h-0 flex-grow overflow-hidden transition-all duration-300 ${bgColorClasses}`}
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">
<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} {title}
</h3> </h2>
<p class="card-text-description mb-4"> <p class="mb-4 font-light text-neutral-600 sm:text-lg dark:text-neutral-400">
{description} {description}
</p> </p>
</div> </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"> <span class="inline-flex items-center">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="16" width="12"
height="16" height="12"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -44,8 +51,8 @@ const { slug, title, description, count, publishDate } = Astro.props;
<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="16" width="12"
height="16" height="12"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -59,5 +66,5 @@ const { slug, title, description, count, publishDate } = Astro.props;
</div> </div>
</div> </div>
</div> </div>
</a>
</div> </div>
</a>

View File

@@ -0,0 +1,44 @@
---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.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 ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
</div>
</section>

View 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>

View File

@@ -0,0 +1,87 @@
---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.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 ? <PrimaryCTA 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

@@ -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>

View File

@@ -1,30 +0,0 @@
---
import { Icon } from 'astro-icon/components';
interface Props {
url?: string;
}
const { url } = Astro.props;
---
<a
class="button-base button-bg-gitea group inline-flex rounded-full gap-x-2"
href={url}
target="_blank"
rel="noopener noreferrer"
>
<div class="button-text-title flex relative items-center text-center">
<Icon
name="pajamas:gitea"
class="h-4 w-4 md:h-6 md:w-6"
/>
<span class="ml-2">
Continue to Gitea
</span>
<Icon
name="mdi:keyboard-arrow-right"
class="button-hover-arrow"
/>
</div>
</a>

View File

@@ -1,22 +0,0 @@
---
import Icon from '@components/ui/icons/icon.astro';
---
<button
class="button-base button-bg-blue group inline-flex rounded-lg gap-x-2"
id="back-button"
data-astro-prefetch
>
<div class="button-text-title flex relative items-center text-center">
<Icon name="arrowLeft" />
<span class="ml-2">
Go Back
</span>
</div>
</button>
<script>
document.getElementById('back-button')?.addEventListener('click', () => {
window.history.back();
});
</script>

View File

@@ -1,25 +0,0 @@
---
import { Icon } from 'astro-icon/components';
interface Props {
url?: string;
}
const { url } = Astro.props;
---
<a
class="button-base button-bg-teal group inline-flex rounded-lg gap-x-2"
href={url}
data-astro-prefetch
>
<div class="button-text-title flex relative items-center text-center">
<Icon
name="mdi:home-variant-outline"
class="card-hover-icon-scale h-3 w-3 md:h-5 md:w-5"
/>
<span class="ml-2">
Return Home
</span>
</div>
</a>

View File

@@ -1,29 +0,0 @@
---
import { Icon } from 'astro-icon/components';
interface Props {
title?: string;
url?: string;
noArrow?: boolean;
}
const { title, url, noArrow } = Astro.props;
---
<a
class="button-base button-bg-teal group inline-flex rounded-lg gap-x-2"
href={url}
data-astro-prefetch
>
<div class="button-text-title flex relative items-center text-center">
<span class="mr-2">
{title}
</span>
{noArrow ? null : (
<Icon
name="mdi:keyboard-arrow-right"
class="button-hover-arrow"
/>
)}
</div>
</a>

View File

@@ -1,20 +0,0 @@
---
interface Props {
title?: string;
url?: string;
}
const { title, url } = Astro.props;
---
<a
class="button-base button-bg-neutral group inline-flex rounded-lg gap-x-2"
href={url}
data-astro-prefetch
>
<div class="button-text-title flex relative items-center text-center">
<span>
{title}
</span>
</div>
</a>

View File

@@ -1,52 +0,0 @@
---
import Icon from '@components/ui/icons/icon.astro';
type SocialPlatform = {
name: string;
url: string;
svg: string;
};
interface Props {
pageTitle: string;
}
const { pageTitle } = Astro.props;
const socialPlatforms: SocialPlatform[] = [
{
name: 'Facebook',
url: `https://www.facebook.com/sharer/sharer.php?u=${Astro.url}`,
svg: 'facebook',
},
{
name: 'X',
url: `https://x.com/intent/tweet?url=${Astro.url}&text=${pageTitle}`,
svg: 'x',
},
{
name: 'LinkedIn',
url: `https://www.linkedin.com/sharing/share-offsite/?url=${Astro.url}`,
svg: 'linkedIn',
},
];
---
<div class="inline-flex items-center gap-x-2">
{socialPlatforms.map((platform) => (
<a
class="button-base-hidden group inline-flex rounded-lg gap-x-2"
href={platform.url}
target="_blank"
rel="noopener noreferrer"
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"
/>
</div>
</a>
))}
</div>

View File

@@ -1,55 +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';
import { formatDate } from '@support/time';
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">
{formatDate(post.published_date)}
</p>
</div>
</div>
</div>
</div>
</a>
</div>

View File

@@ -1,65 +0,0 @@
---
import { Icon } from 'astro-icon/components';
import Logo from '@components/ui/logos/Logo.astro';
import { getDirectusImageURL } from '@lib/directusFunctions';
interface Props {
topic: string;
area: string;
date: string;
url: string;
logoUrlLight?: string;
logoUrlDark?: string;
logoIcon?: string;
}
const { topic, area, date, url, logoUrlLight, logoIcon } = Astro.props;
const logoUrlDark = Astro.props.logoUrlDark || logoUrlLight;
---
<div class="smooth-reveal group flex flex-col">
<a
class="card-base flex items-center"
href={url}
>
<div class="p-4 md:p-10">
<div class="flex items-center">
{logoUrlLight ? (
<div class="card-hover-icon-scale mr-5">
<Logo
srcLight={getDirectusImageURL(logoUrlLight)}
srcDark={getDirectusImageURL(logoUrlDark!)}
alt={`Logo of ${topic}`}
/>
</div>
) : logoIcon ? (
<div class="mr-5 text-header">
<Icon name={logoIcon} class="card-hover-icon-scale h-12 w-12" />
</div>
) : null}
<div class="grow text-left">
<span class="card-text-title block text-lg">
{topic}
</span>
<span class="card-text-description block mt-1 font-medium text-xs uppercase">
{area} - {new Date(date).getFullYear()}
</span>
</div>
</div>
<div class="ml-6 flex">
<div class="relative inline-block">
<div class="card-text-title card-hover-text-title flex relative mx-auto min-h-11 items-center font-semibold text-md sm:mx-0 sm:mt-4">
<span class="relative inline-block overflow-hidden">
Visit Page
</span>
<Icon
name="mdi:keyboard-arrow-right"
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
/>
</div>
</div>
</div>
</div>
</a>
</div>

View File

@@ -1,37 +0,0 @@
---
import { Icon } from 'astro-icon/components';
interface Props {
title?: string;
description?: string;
url?: string;
icon?: string;
}
const { title, description, url, icon } = Astro.props;
---
<div class="smooth-reveal-2 group flex flex-col">
<a
class="card-base flex items-center h-30 w-100 md:w-75"
href={url}
data-astro-prefetch
>
<div class="p-5 w-full">
<div class="flex items-center">
<Icon
name={icon}
class="card-hover-icon-scale shrink-0 h-6 w-6 md:h-8 md:w-8 "
/>
<div class="ms-5 grow text-left">
<span class="card-text-title card-hover-text-title block text-lg">
{title}
</span>
<p class="card-text-description block mt-1">
{description}
</p>
</div>
</div>
</div>
</a>
</div>

View File

@@ -1,72 +0,0 @@
---
import { Icon } from 'astro-icon/components';
import Logo from '@components/ui/logos/Logo.astro';
interface Props {
title?: string;
description?: string;
url?: string;
logoUrlLight?: string;
logoUrlDark?: string;
highlights?: string[];
visitSource?: boolean;
}
const { title, description, url, logoUrlLight, logoUrlDark, highlights, visitSource } = Astro.props;
const visitText = visitSource ? 'Visit Source' : 'Visit Page';
const visitClass = visitSource ? 'card-hover-text-gitea' : 'card-hover-text-title';
---
<div class="smooth-reveal group flex flex-col">
<a
class="card-base flex items-center"
href={url}
>
<div class="p-4 md:p-10">
<div class="flex items-center mb-4">
{logoUrlLight && (
<div class="card-hover-icon-scale mr-5">
<Logo
srcLight={logoUrlLight}
srcDark={logoUrlDark}
alt={`Logo of ${title}`}
/>
</div>
)}
<div class="grow text-left">
<span class="card-text-title block text-lg">
{title}
</span>
<p class="card-text-description block mt-1">
{description}
</p>
</div>
</div>
{highlights && (
<ul class="card-text-description text-sm mt-1 flex flex-col list-disc gap-2 [&>li]:ml-4">
{highlights.map((highlight) => (
<li class="marker:text-accent">
{highlight}
</li>
))}
</ul>
)}
<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`}>
{visitSource && <Icon name="pajamas:gitea" />}
<span class="relative inline-block overflow-hidden ml-2">
{visitText}
</span>
<Icon
name="mdi:keyboard-arrow-right"
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
/>
</div>
</div>
</div>
</div>
</a>
</div>

View File

@@ -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 flex flex-col px-4 py-10 mx-auto">
<a
class="md:card-base-hidden group items-center md:grid md:grid-cols-2 lg:grid lg:grid-cols-2 gap-8 xl:gap-16 max-w-340 2xl:max-w-full md:px-8 md:py-8"
href={url}
data-astro-prefetch
>
<div>
<Image
class="rounded-2xl rounded-b-none md:rounded-2xl 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 class="bg-background-card md:bg-transparent group-hover:bg-neutral-100 md:group-hover:bg-transparent dark:group-hover:bg-neutral-800/90 md:dark:group-hover:bg-transparent rounded-b-2xl transition-all duration-300 p-6">
<h2 class="card-text-header mb-2">
{title}
</h2>
<p class="card-text-title font-light text-pretty sm:text-lg max-w-prose mb-8">
{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

@@ -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 flex flex-col px-5 py-10 mx-auto">
<a
class="md:card-base-hidden group flex flex-col-reverse md:items-center md:grid md:grid-cols-2 lg:grid lg:grid-cols-2 md:gap-8 xl:gap-16 max-w-340 2xl:max-w-full md:px-8 md:py-8"
href={url}
data-astro-prefetch
>
<div class="bg-background-card md:bg-transparent group-hover:bg-neutral-100 md:group-hover:bg-transparent dark:group-hover:bg-neutral-800/90 md:dark:group-hover:bg-transparent rounded-b-2xl transition-all duration-300 p-6">
<h2 class="card-text-header mb-2">
{title}
</h2>
<p class="card-text-title font-light text-pretty sm:text-lg max-w-prose mb-8">
{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-2xl rounded-b-none md:rounded-2xl 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

@@ -1,35 +0,0 @@
---
interface Props {
dayName: string;
label: string;
icon: string;
temp: number;
}
const { dayName, label, icon, temp } = Astro.props;
---
<div class="smooth-reveal-2 group flex flex-col">
<div class="card-base w-32 md:w-40">
<div class="p-5 text-center">
<span class="card-text-description block font-bold text-xs uppercase tracking-widest">
{dayName}
</span>
<div class="flex justify-center my-2">
<img
src={`https://openweathermap.org/img/wn/${icon}@2x.png`}
alt={label}
class="card-hover-icon-scale h-12 w-12"
/>
</div>
<div class="mt-2">
<span class="card-text-title card-hover-text-title block text-2xl">
{temp}°F
</span>
<span class="card-text-description mt-1 block text-xs capitalize">
{label}
</span>
</div>
</div>
</div>
</div>

View File

@@ -1,30 +0,0 @@
---
import { readItems } from '@directus/sdk';
import type { Application } from '@lib/directusTypes';
import HighlightsCard from '@components/cards/HighlightsCard.astro';
import directus from '@lib/directus';
const applications = ((await directus.request(
readItems('site_applications' as any, {
fields: ['*'],
sort: ['-isActive'],
})
)) as unknown) as Application[];
---
<section class:list={['mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8 lg:py-14', Astro.props.className]}>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:gap-8 print:flex print:flex-col">
{applications.map((application: Application) => (
<HighlightsCard
title={application.name}
description={application.description}
url={application.url}
logoUrlLight={application.logoUrl}
logoUrlDark={application.logoUrl}
highlights={application.highlights}
/>
))}
</div>
</section>

View File

@@ -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>

View File

@@ -1,64 +0,0 @@
---
import { readItems } from '@directus/sdk';
import type { Education, Certificate} from '@lib/directusTypes';
import EducationCard from '@components/cards/EducationCard.astro';
import directus from '@lib/directus';
const educations = ((await directus.request(
readItems('site_education' as any, {
fields: ['*'],
sort: ['-graduationDate'],
})
)) as unknown) as Education[];
const certificates = ((await directus.request(
readItems('site_certificate' as any, {
fields: ['*'],
sort: ['-issuerDate'],
})
)) as unknown) as Certificate[];
---
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5">
Education
</h3>
<div class="mx-8">
<h4 class="smooth-reveal card-text-header-minor pt-5">
College
</h4>
<div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4 py-3">
{educations.map((education: Education) => (
<EducationCard
topic={education.institution}
area={education.area}
date={education.graduationDate}
url={education.url}
logoUrlLight={education.logo}
logoUrlDark={education.logoDark}
/>
))}
</div>
</div>
{certificates.length > 0 && (
<div class="mx-8">
<h4 class="smooth-reveal card-text-header-minor pt-8">
Certificates
</h4>
<div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4 py-3">
{certificates.map((certificate: Certificate) => (
<EducationCard
topic={certificate.name}
area={certificate.issuer}
date={certificate.issuerDate}
url={certificate.url}
logoIcon={certificate.logoName}
/>
))}
</div>
</div>
)}
</section>

View File

@@ -1,159 +0,0 @@
---
import { Icon } from 'astro-icon/components';
import { readItems } from '@directus/sdk';
import type { Experience } from '@lib/directusTypes';
import directus from '@lib/directus';
const experiences = ((await directus.request(
readItems('site_experience'as any, {
fields: ['*'],
sort: ['-endDate'],
})
)) as unknown) as Experience[];
---
<section class:list={['flex flex-col gap-8', Astro.props.className]}>
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-10">
Experience
</h3>
<ul class="ml-8 w-full flex flex-col">
{experiences.map((experience: Experience) => {
const startYear = new Date(experience.startDate).getFullYear();
const endYear = experience.endDate != null ? new Date(experience.endDate).getFullYear() : 'Present';
return (
<li class="relative">
<div class="smooth-reveal group relative grid sm:grid-cols-18 sm:gap-8 md:gap-6 pb-16">
<header class="relative sm:col-span-3 text-header font-semibold text-lg mt-1">
<time datetime={experience.startDate} data-title={experience.startDate}>
{startYear}
</time>
{' '}-{' '}
<time datetime={experience.endDate} data-title={experience.endDate}>
{endYear}
</time>
</header>
<div class="relative flex flex-col sm:col-span-12 pb-6">
<div class="absolute bg-stone-400 -translate-x-[1.71rem] rounded-full h-2 w-2 mt-4"/>
<h3>
<div
class="inline-flex items-center text-2xl leading-tight font-semibold"
aria-label="{position} - {company}"
>
<span class="text-header">
{experience.position} <span>@</span>
{experience.url ? (
<a
class="hover:text-main"
href={experience.url}
title={`Ver ${experience.name}`}
target="_blank"
>
{experience.name}
</a>
) : (
<span>{experience.name}</span>
)}
</span>
</div>
</h3>
{(experience.location || experience.location_type) && (
<div class="text-secondary text-sm">
{experience.location} {experience.location && experience.location_type && '-'} {experience.location_type}
</div>
)}
<div class="text-md mt-4 flex flex-col gap-4" x-data="{ expanded: false }">
{experience.summary && (
<div class="flex flex-col gap-1">
<h4 class="text-header font-semibold">
Summary:
</h4>
<ul class="flex flex-col text-primary list-disc gap-2 [&>li]:ml-4">
<li class="marker:text-main">
{experience.summary}
</li>
</ul>
</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>
{experience.responsibilities && (
<div class="flex flex-col gap-1">
<h4 class="text-header font-semibold">
Responsibilities:
</h4>
<ul class="flex flex-col text-primary list-disc gap-2 [&>li]:ml-4">
{experience.responsibilities.map(responsibility => (
<li class="marker:text-main">
{responsibility}
</li>
))}
</ul>
</div>
)}
{experience.achievements && (
<div class="flex flex-col gap-1">
<h4 class="text-header font-semibold">
Achievements:
</h4>
<ul class="flex flex-col text-primary list-disc gap-2 [&>li]:ml-4">
{experience.achievements.map(achievement => (
<li class="marker:text-main">
{achievement}
</li>
))}
</ul>
</div>
)}
</div>
<button @click="expanded = ! expanded" class="group/more flex items-center justify-center text-primary hover:text-primary-hover text-xs underline transition-all gap-1.5 w-fit cursor-pointer">
<span x-text="expanded ? 'Show less' : 'Show more'">
Show more
</span>
<svg
class="group-hover/more:translate-y-0.5 ease-out duration-300 h-4 w-4"
:class="{ 'rotate-180': expanded }"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
<ul
class="flex print:hidden flex-wrap gap-2"
aria-label="Technologies used"
>
{experience.skills && experience.skills.map(skill => {
const iconName = skill.toLowerCase();
const skillName = skill.split(':')[1].replace(/^language-/, '').replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
return (
<li class="flex items-center bg-steel/20 dark:bg-bermuda/20 text-neutral-800 dark:text-neutral-200 text-xs rounded-md border border-solid border-steel/20 dark:border-bermuda/20 gap-1 px-2 py-0.5">
<Icon name={`${iconName}`} class="h-4 w-4" /> <span>{skillName}</span>
</li>
)
})}
</ul>
)}
</div>
</div>
</div>
</li>
);
})}
</ul>
</section>
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

View File

@@ -1,31 +0,0 @@
---
import GoLinkPrimaryButton from '@components/buttons/GoLinkPrimaryButton.astro';
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
}
const { title, subTitle, btnExists, btnTitle, btnURL } = Astro.props;
---
<section class="mx-auto mt-10 px-4 sm:px-6 lg:px-8 lg:pt-10 2xl:max-w-full">
<div class="flex-wrap md:flex md:items-center md:justify-between">
<div class="w-full md:w-auto">
<h1 class="smooth-reveal card-text-header block lg:text-6xl">
{title}
</h1>
<p class="smooth-reveal card-text-header-description mt-4">
{subTitle}
</p>
{btnExists ? (
<div class="smooth-reveal mt-4 md:mt-8">
<GoLinkPrimaryButton title={btnTitle} url={btnURL}/>
</div>
) : null}
</div>
</div>
</section>

View File

@@ -1,56 +0,0 @@
---
import GoLinkPrimaryButton from '@components/buttons/GoLinkPrimaryButton.astro';
import GoLinkSecondaryButton from '@components/buttons/GoLinkSecondaryButton.astro';
import Image from '@components/ui/images/Image.astro';
interface Props {
title: string;
subTitle?: string;
primaryBtn?: string;
primaryBtnURL?: string;
secondaryBtn?: string;
secondaryBtnURL?: string;
src?: any;
alt?: string;
rounded?: boolean;
}
const { title, subTitle, primaryBtn, primaryBtnURL, secondaryBtn, secondaryBtnURL, src, alt } = Astro.props;
const roundedClasses = Astro.props.rounded ? "rounded-2xl" : null;
---
<section class="mx-auto grid max-w-340 gap-4 px-4 py-14 sm:px-6 md:grid-cols-2 md:items-center md:gap-8 lg:px-8 2xl:max-w-full">
<div>
<h1 class="smooth-reveal card-text-header block lg:text-7xl">
<Fragment set:html={title} />
</h1>
{subTitle && (
<p class="smooth-reveal card-text-header-description lg:w-4/5 mt-6">
{subTitle}
</p>
)}
<div class="smooth-reveal grid sm:inline-flex mt-7 w-full gap-3">
{primaryBtn && <GoLinkPrimaryButton title={primaryBtn} url={primaryBtnURL} />}
{secondaryBtn && <GoLinkSecondaryButton title={secondaryBtn} url={secondaryBtnURL} />}
</div>
</div>
<div class="smooth-reveal-fade md:block w-full hidden">
<div class="flex justify-center w-full top-12 md:ml-4 overflow-hidden">
{src && alt && (
<Image
src={src}
alt={alt}
class={`h-full w-105 scale-100 object-cover object-center ${roundedClasses}`}
draggable="false"
loading="eager"
format="webp"
quality="low"
widths={[840]}
disableBlur={true}
/>
)}
</div>
</div>
</section>

View File

@@ -1,32 +0,0 @@
---
import { readItems } from '@directus/sdk';
import type { Project } from '@lib/directusTypes';
import HighlightsCard from '@components/cards/HighlightsCard.astro';
import directus from '@lib/directus';
const projects = ((await directus.request(
readItems('site_projects' as any, {
fields: ['*'],
sort: ['-isActive'],
})
)) as unknown) as Project[];
---
<section class:list={['flex flex-col gap-y-8', Astro.props.className]}>
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5">
Projects
</h3>
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:gap-8 print:flex print:flex-col">
{projects.map((project: Project) => (
<HighlightsCard
title={project.name}
description={project.description}
url={project.source}
highlights={project.highlights}
visitSource={true}
/>
))}
</div>
</section>

View File

@@ -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>

View File

@@ -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 flex flex-col gap-4">
{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

@@ -1,37 +0,0 @@
---
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);
---
<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">
Weather in my Area
</h1>
<div class="smooth-reveal mx-auto mt-5 max-w-3xl text-center">
<span class="card-text-header-description">
Five day forecast for {cityName}
</span>
</div>
</div>
{error ? (
<div class="card-base p-10 text-accent text-center">
{error}
</div>
) : (
<div class="flex flex-wrap justify-center gap-4 lg:gap-6">
{forecastDays.map((forecastDay) => (
<WeatherCard
dayName={forecastDay.dayName}
label={forecastDay.label}
icon={forecastDay.icon}
temp={forecastDay.temp}
/>
))}
</div>
)}
</section>

View File

@@ -4,7 +4,7 @@ import Icon from '@components/ui/icons/icon.astro';
<button <button
type="button" type="button"
class="button-base button-bg-blue group inline-flex items-center rounded-lg p-2.5" class="focus-visible:ring-secondary group inline-flex items-center rounded-lg p-2.5 text-neutral-600 ring-neutral-500 transition duration-300 outline-none hover:bg-neutral-100 focus:outline-none focus-visible:ring-1 focus-visible:outline-none dark:text-neutral-400 dark:ring-neutral-200 dark:hover:bg-neutral-700"
data-bookmark-button="bookmark-button" data-bookmark-button="bookmark-button"
> >
<Icon name="bookmark" /> <Icon name="bookmark" />

View File

@@ -0,0 +1,32 @@
---
import { Icon } from 'astro-icon/components';
const { title, url } = Astro.props;
interface Props {
title?: string;
url?: string;
}
const baseClasses =
'group group-hover inline-flex items-center justify-center gap-x-3 rounded-full px-4 py-3 text-center text-sm font-medium text-neutral-200';
const borderClasses = 'border border-transparent';
const bgColorClasses =
'bg-gitea-primary hover:bg-gitea-secondary dark:bg-gitea-secondary dark:hover:bg-gitea-primary';
const shadowClasses = 'shadow-sm';
const fontSizeClasses = '2xl:text-base';
---
<a
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${shadowClasses} ${fontSizeClasses} `}
href={url}
target="_blank"
rel="noopener noreferrer"
>
<Icon name="pajamas:gitea" class="h-4 w-4 md:h-6 md:w-6" />
{title}
<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"
/>
</a>

View File

@@ -0,0 +1,35 @@
---
import Icon from '@components/ui/icons/icon.astro';
const { title, noArrow } = Astro.props;
interface Props {
title?: string;
url?: string;
noArrow?: boolean;
addHome?: boolean;
}
const baseClasses =
'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-50 ring-neutral-500 transition duration-300 focus-visible:ring outline-none';
const borderClasses = 'border border-transparent';
const bgColorClasses = 'bg-steel hover:bg-sky-800 active:bg-orange-500 dark:focus:outline-none';
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
const fontSizeClasses = '2xl:text-base';
const ringClasses = 'dark:ring-neutral-200';
---
<button
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses}`}
id="back-button"
data-astro-prefetch
>
{noArrow ? null : <Icon name="arrowLeft" />}
{title}
</button>
<script>
document.getElementById('back-button')?.addEventListener('click', () => {
window.history.back();
});
</script>

View File

@@ -0,0 +1,45 @@
---
import { Icon } from 'astro-icon/components';
const { title, url, noArrow, addHome, addClass } = Astro.props;
interface Props {
title?: string;
url?: string;
noArrow?: boolean;
addHome?: boolean;
addClass?: string;
}
const baseClasses =
'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-100 transition duration-300 ';
const borderClasses = 'border border-transparent';
const bgColorClasses = 'bg-bermuda hover:bg-turquoise dark:bg-turquoise dark:hover:bg-bermuda';
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
const fontSizeClasses = '2xl:text-base';
const ringClasses = 'dark:ring-neutral-200';
---
<a
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${addClass}`}
href={url}
data-astro-prefetch
>
{
addHome ? (
<Icon
name="mdi:home-variant-outline"
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
/>
) : null
}
{title}
{
noArrow ? null : (
<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"
/>
)
}
</a>

View File

@@ -0,0 +1,26 @@
---
const { title, url } = Astro.props;
interface Props {
title?: string;
url?: string;
}
const baseClasses =
'inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-center text-sm font-medium text-neutral-600 shadow-sm outline-none ring-neutral-500 focus-visible:ring transition duration-300';
const borderClasses = 'border border-neutral-200';
const bgColorClasses = 'bg-neutral-300';
const hoverClasses = 'hover:bg-neutral-400/50 hover:text-neutral-600 active:text-neutral-700';
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
const fontSizeClasses = '2xl:text-base';
const ringClasses = 'ring-neutral-500';
const darkClasses =
'dark:border-neutral-700 dark:bg-neutral-700 dark:text-neutral-300 dark:ring-neutral-200 dark:hover:bg-neutral-600 dark:focus:outline-none';
---
<a
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${darkClasses}`}
href={url}
>
{title}
</a>

View File

@@ -0,0 +1,150 @@
---
import Icon from '@components/ui/icons/icon.astro';
const { pageTitle, title = 'Share' } = Astro.props;
interface Props {
pageTitle: string;
title?: string;
}
type SocialPlatform = {
name: string;
url: string;
svg: string;
};
const socialPlatforms: SocialPlatform[] = [
{
name: 'Facebook',
url: `https://www.facebook.com/share.php?u=${Astro.url}&title=${pageTitle}`,
svg: 'facebook',
},
{
name: 'X',
url: `https://twitter.com/home/?status=${pageTitle}${Astro.url}`,
svg: 'x',
},
{
name: 'LinkedIn',
url: `https://www.linkedin.com/shareArticle?mini=true&url=${Astro.url}&title=${pageTitle}`,
svg: 'linkedIn',
},
];
---
<div class="hs-dropdown relative inline-flex [--auto-close:inside] [--placement:top-left]">
<button
id="hs-dropup"
type="button"
class="hs-dropdown-toggle inline-flex items-center gap-x-2 rounded-lg px-4 py-3 text-sm font-medium text-neutral-600 ring-neutral-500 transition duration-300 outline-none hover:bg-neutral-100 hover:text-neutral-700 focus-visible:ring dark:text-neutral-400 dark:ring-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:outline-none"
>
<Icon name="share" />
{title}
</button>
<div
class="hs-dropdown-menu duration hs-dropdown-open:opacity-100 z-10 hidden w-72 divide-y divide-neutral-200 rounded-lg bg-neutral-50 p-2 opacity-0 shadow-md transition-[opacity,margin] dark:divide-neutral-700 dark:border dark:border-neutral-700 dark:bg-neutral-800"
aria-labelledby="hs-dropup"
>
<div class="py-2 first:pt-0 last:pb-0">
{
socialPlatforms.map((platform) => (
<a
class="flex items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700"
href={platform.url}
>
<Icon name={platform.svg} />
Share on {platform.name}
</a>
))
}
</div>
<div class="py-2 first:pt-0 last:pb-0">
<button
type="button"
class="js-clipboard hover:text-dark focus-visible:ring-secondary group inline-flex w-full items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none focus-visible:ring-1 focus-visible:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700"
data-clipboard-success-text="Copied"
>
<svg
class="js-clipboard-default h-4 w-4 transition group-hover:rotate-6"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect>
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
</svg>
<svg
class="js-clipboard-success hidden h-4 w-4 text-neutral-700 dark:text-neutral-300"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span class="js-clipboard-success-text">Copy link</span>
</button>
</div>
</div>
</div>
<!--Import the necessary Dropdown and Clipboard plugins-->
<!--https://preline.co/plugins/html/dropdown.html-->
<!--<script is:inline src="/scripts/vendor/preline/dropdown/index.js"></script>-->
<!-- https://clipboardjs.com/ -->
<!--<script is:inline src="/scripts/vendor/clipboard.min.js"></script>-->
<script is:inline>
(function () {
window.addEventListener('load', () => {
const $clipboards = document.querySelectorAll('.js-clipboard');
$clipboards.forEach((el) => {
const clipboard = new ClipboardJS(el, {
text: () => {
return window.location.href;
},
});
clipboard.on('success', () => {
const $default = el.querySelector('.js-clipboard-default');
const $success = el.querySelector('.js-clipboard-success');
const $successText = el.querySelector('.js-clipboard-success-text');
const successText = el.dataset.clipboardSuccessText || '';
let oldSuccessText;
if ($successText) {
oldSuccessText = $successText.textContent;
$successText.textContent = successText;
}
if ($default && $success) {
$default.style.display = 'none';
$success.style.display = 'block';
}
setTimeout(() => {
if ($successText && oldSuccessText) {
$successText.textContent = oldSuccessText;
}
if ($default && $success) {
$success.style.display = '';
$default.style.display = '';
}
}, 800);
});
});
});
})();
</script>

View File

@@ -5,14 +5,14 @@
<button <button
id="theme-toggle" id="theme-toggle"
data-theme-toggle data-theme-toggle
class="group dark:hover:bg-steel/30 hover:bg-yellow-300/20 transition-all duration-300 relative rounded-full p-1.5 sm:p-2 touch-manipulation" class="group dark:hover:bg-steel/30 relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-yellow-300/20 focus:outline-hidden sm:p-2"
aria-label="Toggle dark mode" aria-label="Toggle dark mode"
> >
<div class="relative flex h-5 w-5 items-center justify-center"> <div class="relative z-10 flex h-5 w-5 items-center justify-center">
<!-- Sun icon --> <!-- Sun icon -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="icon-light absolute h-5 w-5 text-neutral-600 dark:text-neutral-400 scale-100 dark:scale-0 rotate-0 dark:-rotate-90 transition-all duration-500" class="icon-light absolute h-5 w-5 scale-100 rotate-0 text-neutral-600 transition-all duration-500 dark:scale-0 dark:-rotate-90 dark:text-neutral-400"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -29,7 +29,7 @@
<!-- Moon icon --> <!-- Moon icon -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="icon-dark absolute h-5 w-5 text-neutral-600 dark:text-neutral-400 scale-0 dark:scale-100 rotate-90 dark:rotate-0 transition-all duration-500" class="icon-dark absolute h-5 w-5 scale-0 rotate-90 text-neutral-600 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-neutral-400"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
@@ -43,23 +43,25 @@
</button> </button>
<script is:inline> <script is:inline>
const applyTheme = () => { // Use a function to persist theme when using SPA transitions
const isDark = // https://docs.astro.build/en/guides/view-transitions/#script-re-execution
localStorage.theme === 'dark' || function applyTheme() {
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches); localStorage.theme === 'dark'
document.documentElement.classList.toggle('dark', isDark); ? document.documentElement.classList.add('dark')
}; : document.documentElement.classList.remove('dark');
}
applyTheme();
document.addEventListener('astro:after-swap', applyTheme); document.addEventListener('astro:after-swap', applyTheme);
applyTheme();
</script> </script>
<script> <script>
// Use a function to handle theme toggle to ensure it can be called from anywhere
function setupThemeToggle() { function setupThemeToggle() {
const themeToggles = document.querySelectorAll('[data-theme-toggle]'); const themeToggles = document.querySelectorAll('[data-theme-toggle]');
// Create theme switch overlay element // Create theme switch overlay element if it doesn't exist
if (!document.querySelector('.theme-switch-overlay')) { if (!document.querySelector('.theme-switch-overlay')) {
const overlay = document.createElement('div'); const overlay = document.createElement('div');
overlay.className = 'theme-switch-overlay fixed inset-0 pointer-events-none z-50'; overlay.className = 'theme-switch-overlay fixed inset-0 pointer-events-none z-50';
@@ -68,7 +70,9 @@
document.body.appendChild(overlay); document.body.appendChild(overlay);
} }
// Toggle theme when any theme toggle button is clicked
themeToggles.forEach((toggle) => { themeToggles.forEach((toggle) => {
// Add event listeners for both click and touch events
['click', 'touchend'].forEach((eventType) => { ['click', 'touchend'].forEach((eventType) => {
toggle.addEventListener( toggle.addEventListener(
eventType, eventType,
@@ -88,10 +92,14 @@
y = e.clientY - rect.top; y = e.clientY - rect.top;
} }
// Set the position variables for the radial gradient
document.documentElement.style.setProperty('--x', `${x}px`); document.documentElement.style.setProperty('--x', `${x}px`);
document.documentElement.style.setProperty('--y', `${y}px`); document.documentElement.style.setProperty('--y', `${y}px`);
// Get the overlay element
const overlay = document.querySelector('.theme-switch-overlay'); const overlay = document.querySelector('.theme-switch-overlay');
// Determine the new theme
const isDark = document.documentElement.classList.contains('dark'); const isDark = document.documentElement.classList.contains('dark');
const newTheme = isDark ? 'light' : 'dark'; const newTheme = isDark ? 'light' : 'dark';
@@ -102,6 +110,7 @@
overlay.style.opacity = '1'; overlay.style.opacity = '1';
} }
// Add transition class
document.documentElement.classList.add('theme-switching'); document.documentElement.classList.add('theme-switching');
// Force a reflow to ensure all elements update // Force a reflow to ensure all elements update
@@ -115,7 +124,10 @@
document.documentElement.classList.add('dark'); document.documentElement.classList.add('dark');
} }
// Store the preference
localStorage.setItem('theme', newTheme); localStorage.setItem('theme', newTheme);
// Dispatch a custom event for other components to react to
document.dispatchEvent( document.dispatchEvent(
new CustomEvent('themeChanged', { new CustomEvent('themeChanged', {
detail: { isDark: newTheme === 'dark' }, detail: { isDark: newTheme === 'dark' },
@@ -125,10 +137,13 @@
// Force another reflow to ensure all elements update // Force another reflow to ensure all elements update
document.body.offsetHeight; document.body.offsetHeight;
// Hide overlay after theme has changed
setTimeout(() => { setTimeout(() => {
if (overlay) { if (overlay) {
overlay.style.opacity = '0'; overlay.style.opacity = '0';
} }
// Remove transition class after animation completes
document.documentElement.classList.remove('theme-switching'); document.documentElement.classList.remove('theme-switching');
}, 300); }, 300);
}, 50); }, 50);
@@ -136,6 +151,25 @@
{ passive: false } { passive: false }
); );
}); });
// Add touch feedback
toggle.addEventListener(
'touchstart',
() => {
toggle.classList.add('active-touch');
},
{ passive: true }
);
toggle.addEventListener(
'touchend',
() => {
setTimeout(() => {
toggle.classList.remove('active-touch');
}, 150);
},
{ passive: true }
);
}); });
} }
@@ -167,32 +201,61 @@
</script> </script>
<style> <style>
/* Smooth transition for the entire page when theme changes */
:global(body) {
transition:
background-color 0.5s ease,
color 0.5s ease;
}
/* Theme transition overlay */
:global(.theme-switch-overlay) {
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
transition: opacity 0.3s ease-out;
}
/* Ensure theme transitions apply to all elements */
:global(.theme-switching *) {
transition-duration: 0.5s !important;
transition-property: background-color, border-color, color, fill, stroke !important;
}
/* Subtle hover animation */ /* Subtle hover animation */
#theme-toggle { #theme-toggle {
transform: translateY(0); transform: translateY(0);
box-shadow: 0 0 0 rgba(0, 0, 0, 0); box-shadow: 0 0 0 rgba(0, 0, 0, 0);
-webkit-tap-highlight-color: transparent; -webkit-tap-highlight-color: transparent; /* Remove default mobile tap highlight */
min-height: 32px; min-height: 32px; /* Ensure minimum touch target size */
min-width: 32px; min-width: 32px; /* Ensure minimum touch target size */
} }
/* Only apply hover effects on non-touch devices */
@media (hover: hover) { @media (hover: hover) {
#theme-toggle:hover { #theme-toggle:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
} }
:global(:root:not(.dark)) #theme-toggle:hover .icon-light { #theme-toggle:hover .icon-light:not(.dark .icon-light) {
filter: drop-shadow(0 0 2px rgba(251, 191, 36, 0.6)); filter: drop-shadow-sm(0 0 2px rgba(251, 191, 36, 0.6));
transform: scale(1.1) rotate(15deg); transform: scale(1.1) rotate(15deg);
} }
:global(:root.dark) #theme-toggle:hover .icon-dark { #theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) {
filter: drop-shadow(0 0 2px rgba(129, 140, 248, 0.6)); filter: drop-shadow-sm(0 0 2px rgba(129, 140, 248, 0.6));
transform: scale(1.1) rotate(-15deg); transform: scale(1.1) rotate(-15deg);
} }
} }
/* Touch feedback */
#theme-toggle.active-touch {
transform: scale(0.95);
transition: transform 0.15s ease-in-out;
}
/* Optimize animations for mobile */ /* Optimize animations for mobile */
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.icon-light, .icon-light,

View File

@@ -0,0 +1,45 @@
---
import { Icon } from 'astro-icon/components';
interface Props {
title?: string;
description?: string;
url?: string;
icon?: string;
}
const { title, description, url, icon } = Astro.props;
const baseClasses = 'smooth-reveal-2 group group-hover flex flex-col ';
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
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 h-30 ${borderClasses} ${bgColorClasses} ${shadowClasses}`}
href={url}
data-astro-prefetch
>
<div class="p-4 md:p-5">
<div class="flex">
<Icon
name={icon}
class="group-hover:text-steel dark:group-hover:text-bermuda h-6 w-6 text-neutral-600 transition-all duration-300 md:h-8 md:w-8 dark:text-neutral-200"
/>
<div class="ms-5 grow">
<span
class="group-hover:text-steel dark:group-hover:text-bermuda block text-lg font-bold text-neutral-600 transition-all duration-300 dark:text-neutral-300"
>
{title}
</span>
<span class="mt-1 block text-neutral-500 dark:text-neutral-400">
{description}
</span>
</div>
</div>
</div>
</a>
</div>

View File

@@ -1,52 +0,0 @@
---
import { Image } from 'astro:assets';
import { blurStyle } from '@support/image';
const { srcLight, srcDark, alt, style, disableBlur, width, height } = Astro.props;
const showBlur = !disableBlur;
const blurLight = (srcLight?.fsPath && showBlur) ? await blurStyle(srcLight.fsPath) : {};
const blurDark = (srcDark?.fsPath && showBlur) ? await blurStyle(srcDark.fsPath) : {};
---
<div class="themed-image-container">
<Image
src={srcLight}
alt={alt}
class={`light-logo ${style}`}
style={blurLight}
inferSize={true}
width={width}
height={height}
/>
<Image
src={srcDark}
alt={alt}
class={`dark-logo ${style}`}
style={blurDark}
inferSize={true}
width={width}
height={height}
/>
</div>
<style>
.themed-image-container {
display: grid;
grid-template-areas: "stack";
}
.themed-image-container :global(img) {
grid-area: stack;
}
:global(.dark) .light-logo {
display: none !important;
}
:global(.dark) .dark-logo {
display: block !important;
}
</style>

View File

@@ -1,16 +0,0 @@
---
import ImageTheme from '@components/ui/images/ImageTheme.astro';
const { srcLight, srcDark, alt } = Astro.props;
---
<ImageTheme
srcLight={srcLight}
srcDark={srcDark}
alt={alt}
style='color: transparent; width: 48px; height: 48px; object-fit: contain; max-height: 100%; max-width: 100%;'
draggable="false"
loading="lazy"
width="48"
height="48"
/>

View File

@@ -0,0 +1,129 @@
---
import { Icon } from 'astro-icon/components';
import { readItems } from '@directus/sdk';
import type { Education } from '@lib/directusTypes';
import directus from '@lib/directus';
const education = await directus.request(
readItems('site_education', {
fields: ['*'],
sort: ['-graduationDate'],
})
);
const certificate = await directus.request(
readItems('site_certificate', {
fields: ['*'],
sort: ['-issuerDate'],
})
);
const baseClasses = 'rounded-xl flex flex-col';
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
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';
---
<section class:list={['order-first flex flex-col gap-4', Astro.props.className]}>
<h3
class="smooth-reveal-1 relative flex w-full items-center gap-3 pb-5 text-5xl text-neutral-800 dark:text-neutral-200"
>
Education
</h3>
<div class="ml-8">
<h4 class="smooth-reveal-1 pt-5 text-2xl font-semibold text-neutral-800 dark:text-neutral-200">
College
</h4>
<ul class="space-y-4 py-3">
<div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4">
{
education.map(({ institution, area, url, graduationDate}) => {
return (
<div class="smooth-reveal-cards mt-4 rounded-xl">
<div
class={`p-4 md:p-6 transition-all duration-300 ${shadowClasses} ${bgColorClasses} ${baseClasses} ${borderClasses}`}
>
<h3 class="flex flex-row text-lg font-bold text-neutral-800 dark:text-neutral-200">
<Icon
name="mdi:university-outline"
class="mr-2 h-3 w-3 translate-y-1 md:h-5 md:w-5"
/>
{institution}
</h3>
<p class="mt-2 ml-7 text-xs font-medium text-neutral-600 uppercase dark:text-neutral-400">
{area} - {new Date(graduationDate).getFullYear()}
</p>
<div class="ml-6 flex">
<a
class="group group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
href={url}
>
<div class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-[44px] items-center text-sm font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
<span class="relative inline-block overflow-hidden"> Visit Page </span>
<Icon
name="mdi:keyboard-arrow-right"
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
/>
</div>
</a>
</div>
</div>
</div>
);
})
}
</div>
</ul>
</div>
{
certificate.length > 0 && (
<div class="ml-8">
<h4 class="smooth-reveal-1 pt-8 text-2xl font-semibold text-neutral-800 dark:text-neutral-200">
Certificates
</h4>
<ul class="space-y-4 py-3">
<div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4">
{certificate.map(({ name, issuer, url }) => {
return (
<div class="smooth-reveal-cards mt-4 rounded-xl">
<div
class={`p-4 md:p-6 transition-all duration-300 ${shadowClasses} ${bgColorClasses} ${baseClasses} ${borderClasses}`}
>
<h3 class="flex flex-row text-lg font-bold text-neutral-800 dark:text-neutral-200">
<Icon
name="mdi:script-text-outline"
class="mr-2 h-3 w-3 translate-y-1 md:h-5 md:w-5"
/>
{name}
</h3>
<p class="mt-2 ml-7 text-xs font-medium text-neutral-600 uppercase dark:text-neutral-400">
{issuer}
</p>
<div class="ml-6 flex">
<a
class="group group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
href={url}
>
<div class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-[44px] items-center text-sm font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
<span class="relative inline-block overflow-hidden"> Visit Page </span>
<Icon
name="mdi:keyboard-arrow-right"
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
/>
</div>
</a>
</div>
</div>
</div>
);
})}
</div>
</ul>
</div>
)
}
</section>

View File

@@ -0,0 +1,152 @@
---
import { Icon } from 'astro-icon/components';
import { readItems } from '@directus/sdk';
import type { Experience } from '@lib/directusTypes';
import directus from '@lib/directus';
const experiences = await directus.request(
readItems('site_experience', {
fields: ['*'],
sort: ['-endDate'],
})
);
---
<section
class:list={['flex flex-col gap-8', Astro.props.className]}
>
<h3 class="relative smooth-reveal-1 flex w-full items-center gap-3 pb-10 text-5xl text-neutral-800 dark:text-neutral-200">Experience</h3>
<ul class="ml-8 w-full flex flex-col">
{
experiences.map(
(experience: Experience) => {
const startYear = new Date(experience.startDate).getFullYear();
const endYear = experience.endDate != null ? new Date(experience.endDate).getFullYear() : 'Present';
return (
<li class="relative">
<div class="group smooth-reveal-2 relative grid pb-1 transition-all sm:grid-cols-18 sm:gap-8 md:gap-6 lg:hover:!opacity-100">
<header class="relative mt-1 text-lg font-semibold sm:col-span-3 text-neutral-800 dark:text-neutral-200">
<time datetime={experience.startDate} data-title={experience.startDate}>
{startYear}
</time>{' '}
-{' '}
<time datetime={experience.endDate} data-title={experience.endDate}>
{endYear}
</time>
</header>
<div class="relative flex flex-col pb-6 before:absolute before:mt-8 before:-ml-6 before:h-full before:w-px before:bg-stone-400 sm:col-span-12">
<div class="absolute mt-4 h-2 w-2 -translate-x-[1.71rem] rounded-full bg-stone-400" />
<h3>
<div
class="inline-flex items-center text-2xl leading-tight font-semibold"
aria-label="{position} - {company}"
>
<span class="text-neutral-800 dark:text-neutral-200">
{experience.position} <span>@</span>
{experience.url ? (
<a
class="hover:text-steel dark:hover:text-bermuda"
href={experience.url}
title={`Ver ${experience.name}`}
target="_blank"
>
{experience.name}
</a>
) : (
<span>{experience.name}</span>
)}
</span>
</div>
</h3>
{(experience.location || experience.location_type) && (
<div class="text-sm text-neutral-600 dark:text-neutral-400">
{experience.location} {experience.location && experience.location_type && '-'} {experience.location_type}
</div>
)}
<div class="text-md mt-4 flex flex-col gap-4" x-data="{ expanded: false }">
{experience.summary && (
<div class="flex flex-col gap-1">
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Summary:</h4>
<ul class="flex list-disc flex-col gap-2 text-neutral-700 dark:text-neutral-400 [&>li]:ml-4">
{Array.isArray(experience.summary) ? (
experience.summary.map((item) => ({ item }))
) : (
<li class="marker:text-steel dark:marker:text-bermuda">{experience.summary}</li>
)}
</ul>
</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-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="font-semibold text-neutral-800 dark:text-neutral-200">Responsibilities:</h4>
<ul class="text-neutral-700 dark:text-neutral-400 [&>li]:ml-4 flex list-disc flex-col gap-2">
{experience.responsibilities.map(responsibility => (
<li class="marker:text-steel dark:marker:text-bermuda">{responsibility}</li>
))}
</ul>
</div>
)}
{experience.achievements && (
<div class="flex flex-col gap-1">
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Achievements:</h4>
<ul class="text-neutral-700 dark:text-neutral-400 [&>li]:ml-4 flex list-disc flex-col gap-2">
{experience.achievements.map(achievement => (
<li class="marker:text-steel dark:marker:text-bermuda">{achievement}</li>
))}
</ul>
</div>
)}
</div>
<button @click="expanded = ! expanded" class="group/more w-fit cursor-pointer items-center justify-center gap-1.5 text-xs underline text-neutral-700 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-neutral-400 transition-all flex">
<span x-text="expanded ? 'Show less' : 'Show more'">Show more</span>
<svg
class="h-4 w-4 duration-200 ease-out group-hover/more:translate-y-0.5"
:class="{ 'rotate-180': expanded }"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
<ul class="flex print:hidden flex-wrap gap-2" aria-label="Technologies used">
{experience.skills && experience.skills.map(skill => {
const iconName = skill.toLowerCase();
return (
<li class="bg-steel/20 border-steel/20 text-neutral-800 dark:bg-bermuda/20 dark:border-bermuda/20 dark:text-neutral-200 flex gap-1 items-center border-solid border rounded-md px-2 py-0.5 text-xs">
<Icon name={`mdi:${iconName}`} /> <span>{skill}</span>
</li>
)
})}
</ul>
)}
</div>
</div>
</div>
</li>
);
}
)
}
</ul>
</section>
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

View File

@@ -1,16 +1,18 @@
--- ---
import { readSingleton } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
import FeaturesCard from '@components/cards/FeaturesCard.astro';
import directus from '@lib/directus'; import directus from '@lib/directus';
import FeaturesCard from '@components/ui/cards/FeaturesCard.astro';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
--- ---
<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"> <section class="mx-auto mb-20 max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full">
<div class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:gap-x-12 sm:gap-y-0 lg:gap-x-24"> <div
<div class="max-w-5xl sm:px-6 lg:px-8"> class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:gap-x-12 sm:gap-y-0 lg:gap-x-24"
<div class="flex flex-wrap gap-6 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3 justify-center"> >
<div class="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
<div class="grid gap-3 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3">
<FeaturesCard <FeaturesCard
title="Cloud Engineer" title="Cloud Engineer"
description="Full stack and cloud engineer." description="Full stack and cloud engineer."
@@ -23,12 +25,6 @@ const global = await directus.request(readSingleton('site_global'));
url="/categories/homelab/" url="/categories/homelab/"
icon="mdi:home-variant-outline" icon="mdi:home-variant-outline"
/> />
<FeaturesCard
title="Documentation"
description="Reference and guides for my homelab."
url="https://docs.alexlebens.dev"
icon="mdi:file-document-multiple"
/>
<FeaturesCard <FeaturesCard
title="Email" title="Email"
description={`Send me a message.`} description={`Send me a message.`}

View File

@@ -0,0 +1,35 @@
---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
}
const { title, subTitle, btnExists, btnTitle, btnURL } = Astro.props;
---
<section class="mx-auto mt-10 px-4 sm:px-6 lg:px-8 lg:pt-10 2xl:max-w-full">
<div class="flex-wrap md:flex md:items-center md:justify-between">
<div class="w-full md:w-auto">
<h1
class="smooth-reveal block text-4xl font-bold tracking-tight text-balance text-neutral-800 md:text-5xl lg:text-6xl dark:text-neutral-200"
>
{title}
</h1>
<p class="smooth-reveal mt-4 text-lg text-pretty text-neutral-600 dark:text-neutral-400">
{subTitle}
</p>
{
btnExists ? (
<div class="smooth-reveal mt-4 md:mt-8">
<PrimaryCTA title={btnTitle} url={btnURL} />
</div>
) : null
}
</div>
</div>
</section>

View File

@@ -0,0 +1,66 @@
---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import Image from '@components/ui/images/Image.astro';
const { title, subTitle, primaryBtn, primaryBtnURL, secondaryBtn, secondaryBtnURL, src, alt } =
Astro.props;
interface Props {
title: string;
subTitle?: string;
primaryBtn?: string;
primaryBtnURL?: string;
secondaryBtn?: string;
secondaryBtnURL?: string;
src?: any;
alt?: string;
rounded?: boolean;
}
const roundedClasses = Astro.props.rounded ? "rounded-xl" : null;
---
<section
class="mx-auto grid max-w-340 gap-4 px-4 py-14 sm:px-6 md:grid-cols-2 md:items-center md:gap-8 lg:px-8 2xl:max-w-full"
>
<div>
<h1
class="smooth-reveal block text-3xl font-bold tracking-tight text-balance text-neutral-800 sm:text-4xl lg:text-7xl lg:leading-tight dark:text-neutral-200"
>
<Fragment set:html={title} />
</h1>
{
subTitle && (
<p class="smooth-reveal mt-6 text-lg leading-relaxed text-pretty text-neutral-700 lg:w-4/5 dark:text-neutral-300">
{subTitle}
</p>
)
}
<div class="smooth-reveal mt-7 grid w-full gap-3 sm:inline-flex">
{primaryBtn && <PrimaryCTA title={primaryBtn} url={primaryBtnURL} />}
{secondaryBtn && <SecondaryCTA title={secondaryBtn} url={secondaryBtnURL} />}
</div>
</div>
<div class="smooth-reveal-fade hidden w-full md:block">
<div class="top-12 flex w-full justify-center overflow-hidden md:ml-4">
{
src && alt && (
<Image
src={src}
alt={alt}
class={`h-full w-[420px] scale-100 object-cover object-center ${roundedClasses}`}
draggable="false"
loading="eager"
format="webp"
quality="low"
widths={[840]}
disableBlur={true}
/>
)
}
</div>
</div>
</section>

View File

@@ -1,20 +1,22 @@
--- ---
import GiteaButton from '@components/buttons/GiteaButton.astro'; import GiteaBtn from '@components/ui/buttons/GiteaBtn.astro';
const { title, subTitle, url } = Astro.props;
const btnTitle = 'Continue to Gitea';
interface Props { interface Props {
title: string; title: string;
subTitle?: string; subTitle?: string;
url?: string; url?: string;
} }
const { title, subTitle, url } = Astro.props;
--- ---
<section class="lg:px- relative mx-auto mb-20 max-w-340 px-4 pt-30 pb-30 sm:px-6"> <section class="lg:px- relative mx-auto mb-20 max-w-[85rem] px-4 pt-30 pb-30 sm:px-6">
<!-- Animated shapes --> <div
<div class="smooth-reveal absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]"> class="smooth-reveal absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]"
>
<svg <svg
class="gitea-animate-hover gitea-animate-hover-1" class="animate-hover animate-hover-1"
width="64" width="64"
height="64" height="64"
fill="none" fill="none"
@@ -44,7 +46,7 @@ const { title, subTitle, url } = Astro.props;
</div> </div>
<div class="smooth-reveal absolute top-0 left-[85%] scale-75"> <div class="smooth-reveal absolute top-0 left-[85%] scale-75">
<svg <svg
class="gitea-animate-hover gitea-animate-hover-2" class="animate-hover animate-hover-2"
width="64" width="64"
height="64" height="64"
fill="none" fill="none"
@@ -78,9 +80,11 @@ const { title, subTitle, url } = Astro.props;
d="M10.5 19H9M15 19h-1.5"></path> d="M10.5 19H9M15 19h-1.5"></path>
</svg> </svg>
</div> </div>
<div class="smooth-reveal absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]"> <div
class="smooth-reveal absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]"
>
<svg <svg
class="gitea-animate-hover gitea-animate-hover-3" class="animate-hover animate-hover-3"
width="64" width="64"
height="64" height="64"
fill="none" fill="none"
@@ -102,54 +106,59 @@ const { title, subTitle, url } = Astro.props;
></path> ></path>
</svg> </svg>
</div> </div>
<!-- Heading --> <!-- Hero Section Heading -->
<div class="smooth-reveal-2 mx-auto mt-5 max-w-xl text-center"> <div class="smooth-reveal-2 mx-auto mt-5 max-w-xl text-center">
<h1 class="card-text-header block"> <h2
class="block text-4xl leading-tight font-bold tracking-tight text-balance text-neutral-800 md:text-5xl lg:text-5xl dark:text-neutral-200"
>
{title} {title}
</h1> </h2>
</div> </div>
<!-- Sub-heading --> <!-- Hero Section Sub-heading -->
<div class="smooth-reveal-2 mx-auto mt-5 max-w-3xl text-center"> <div class="smooth-reveal-2 mx-auto mt-5 max-w-3xl text-center">
{subTitle && ( {
<p class="card-text-header-description"> subTitle && (
{subTitle} <p class="text-lg text-pretty text-neutral-600 dark:text-neutral-400">{subTitle}</p>
</p> )
)} }
</div> </div>
<!-- Gitea Button --> <!-- Github Button -->
{url && ( {
<div class="smooth-reveal-2 flex justify-center mt-8 gap-3"> url && (
<GiteaButton url={url}/> <div class="smooth-reveal-2 mt-8 flex justify-center gap-3">
<GiteaBtn url={url} title={btnTitle} />
</div> </div>
)} )
}
</section> </section>
<style> <style>
@keyframes gitea-animate-hover { @keyframes animate-hover {
from { from {
transform: translateY(15px); transform: translateY(15px);
} }
to { to {
transform: translateY(-15px); transform: translateY(-15px);
} }
} }
.gitea-animate-hover { .animate-hover {
animation: gitea-animate-hover ease-in-out; animation: animate-hover ease-in-out;
animation-iteration-count: infinite; animation-iteration-count: infinite;
animation-direction: alternate; animation-direction: alternate;
} }
.gitea-animate-hover-1 { .animate-hover-1 {
animation-duration: 5s; animation-duration: 5s;
} }
.gitea-animate-hover-2 { .animate-hover-2 {
animation-duration: 5.5s; animation-duration: 5.5s;
} }
.gitea-animate-hover-3 { .animate-hover-3 {
animation-duration: 6s; animation-duration: 6s;
} }
</style> </style>

View File

@@ -0,0 +1,32 @@
---
import { readItems } from '@directus/sdk';
import directus from '@lib/directus';
import type { Post } from '@lib/directusTypes';
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-[85rem] 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 block text-4xl font-bold text-neutral-800 md:text-5xl md:leading-tight lg:text-5xl dark:text-neutral-200"
>
Latest Posts
</h1>
</div>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{recentPosts.map((b) => <BlogCard post={b} />)}
</div>
</section>

View File

@@ -0,0 +1,75 @@
---
import { Icon } from 'astro-icon/components';
import { readItems } from '@directus/sdk';
import type { Project } from '@lib/directusTypes';
import directus from '@lib/directus';
const projects = await directus.request(
readItems('site_projects', {
fields: ['*'],
sort: ['-isActive'],
})
);
const baseClasses = 'smooth-reveal-cards rounded-xl flex flex-col';
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
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';
---
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
<h3
class="relative flex w-full items-center gap-3 pb-10 text-5xl text-neutral-800 dark:text-neutral-200"
>
Projects
</h3>
<div class="ml-8 grid grid-cols-1 gap-3 md:grid-cols-2 print:flex print:flex-col">
{
projects.map((project: Project) => {
return (
<div class={`${baseClasses}`}>
<div
class={`rounded-xl transition-all duration-300 ${borderClasses} ${bgColorClasses} ${shadowClasses}`}
>
<div class="p-4 md:p-10">
<h3 class="text-lg font-bold text-gray-800 dark:text-white">{project.name}</h3>
<p class="mt-2 text-gray-500 dark:text-neutral-400">{project.description}</p>
<ul class="mt-1 flex list-disc flex-col gap-2 text-sm text-gray-500 dark:text-neutral-400 [&>li]:ml-4">
{project.highlights.map((highlight) => {
return <li class="marker:text-yellow-500">{highlight}</li>;
})}
</ul>
<div class="flex">
<a
class="group group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
href={project.url}
>
<div class="group-hover:text-steel dark:group-hover:text-bermuda transition-text text-md relative z-10 mx-auto flex min-h-[44px] items-center font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
<span class="relative inline-block overflow-hidden"> Visit Page </span>
<Icon
name="mdi:keyboard-arrow-right"
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
/>
</div>
</a>
<a
class="group group-hover relative ml-auto inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
href={project.source}
>
<div class="group-hover:text-gitea-primary dark:group-hover:text-gitea-primary transition-text text-md relative z-10 mx-auto flex min-h-[44px] items-center font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
<span class="relative inline-block overflow-hidden"> Source </span>
<Icon name="pajamas:gitea" class="ml-2 translate-y-0.5" />
</div>
</a>
</div>
</div>
</div>
</div>
);
})
}
</div>
</section>

View File

@@ -6,63 +6,86 @@ import type { Skill } from '@lib/directusTypes';
import directus from '@lib/directus'; import directus from '@lib/directus';
const skills = ((await directus.request( const skills = await directus.request(
readItems('site_skills' as any, { readItems('site_skills', {
fields: ['*'], fields: ['*'],
sort: ['-date_created'], sort: ['-date_created'],
}) })
)) as unknown) as Skill[]; );
const baseClasses = 'mx-2 min-w-[220px] sm:mx-4 sm:min-w-[280px]';
const borderClasses =
'border border-neutral-100 hover:border-neutral-200 dark:border-stone-500/20 dark:hover:border-neutral-800';
const bgColorClasses = 'bg-neutral-100/80 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
const hoverClasses = 'hover:-translate-y-2 hover:scale-105 ';
const shadowClasses = 'shadow-xs hover:shadow-lg';
--- ---
<section class:list={['flex flex-col gap-4', Astro.props.className]}> <section class:list={['flex flex-col gap-4', Astro.props.className]}>
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5"> <h3
class="relative flex w-full items-center gap-3 pb-4 text-5xl text-neutral-800 dark:text-neutral-200"
>
Skills Skills
</h3> </h3>
<div class=""> <div class="">
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8"> <div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
<!-- Main slider container --> <!-- Main slider container -->
<div class="slider-track animate-slide flex"> <div class="slider-track animate-slide flex">
{[...skills, ...skills, ...skills].map((skill: Skill) => { {
[...skills, ...skills, ...skills].map((skill: Skill) => {
return ( return (
<div class="skill-card card-base transform hover:-translate-y-2 hover:scale-105 transition-all duration-300 mx-2 min-w-55 sm:mx-4 sm:min-w-70"> <div
class={`skill-card transform rounded-xl transition-all duration-300 ${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${shadowClasses}`}
>
<div class="p-4 sm:p-6"> <div class="p-4 sm:p-6">
<div class="flex items-center justify-between mb-4 sm:mb-6"> <div class="mb-4 flex items-center justify-between sm:mb-6">
<div class="flex items-center gap-2 sm:gap-4"> <div class="flex items-center gap-2 sm:gap-4">
<div class="flex items-center justify-center rounded-lg text-primary"> <div class="flex transform items-center justify-center rounded-lg text-neutral-800 transition-transform group-hover:rotate-12 dark:text-neutral-200">
<Icon name={skill.icon} class="h-8 w-8 sm:h-12 sm:w-12" /> <Icon name={skill.icon} class="h-8 w-8 sm:h-12 sm:w-12" />
</div> </div>
<h3 class="text-neutral-900 dark:text-neutral-100 text-base font-semibold sm:text-xl"> <h3 class="text-base font-semibold text-neutral-900 sm:text-xl dark:text-neutral-100">
{skill.title} {skill.title}
</h3> </h3>
</div> </div>
<span class=" bg-neutral-200 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 font-mono text-xs sm:text-sm rounded-full px-2 sm:px-2.5 py-0.5 sm:py-1"> <span class="rounded-full bg-neutral-200 px-2 py-0.5 font-mono text-xs text-neutral-700 sm:px-2.5 sm:py-1 sm:text-sm dark:bg-neutral-800 dark:text-neutral-300">
{skill.level}% {skill.level}%
</span> </span>
</div> </div>
<div class="relative bg-stone-500/20 dark:bg-stone-500/20 rounded-full h-1.5 sm:h-2 w-full overflow-hidden">
<div class="relative h-1.5 w-full overflow-hidden rounded-full bg-stone-500/20 sm:h-2 dark:bg-stone-500/20">
<div <div
class="progress-bar-animate bg-linear-to-r from-steel via-bermuda to-steel absolute top-0 left-0 h-full rounded-full transition-all duration-1000" class="progress-bar-animate from-steel via-bermuda to-steel absolute top-0 left-0 h-full rounded-full bg-gradient-to-r transition-all duration-1000"
style={`width: ${skill.level}%`} style={`width: ${skill.level}%`}
/> />
</div> </div>
<div class="flex justify-between text-secondary font-mono text-[10px] mt-1 sm:mt-2 sm:text-xs">
<div class="mt-1 flex justify-between font-mono text-[10px] text-neutral-600 sm:mt-2 sm:text-xs dark:text-neutral-400">
<span>Beginner</span> <span>Beginner</span>
<span>Advanced</span> <span>Advanced</span>
</div> </div>
</div> </div>
</div> </div>
); );
})} })
}
</div>
<!-- Gradient overlays for smooth fade effect -->
<div
class="absolute top-0 bottom-0 left-0 z-10 w-12 bg-gradient-to-r from-neutral-200 to-transparent sm:w-24 dark:from-stone-700"
>
</div>
<div
class="absolute top-0 right-0 bottom-0 z-10 w-12 bg-gradient-to-l from-neutral-200 to-transparent sm:w-24 dark:from-stone-700"
>
</div> </div>
<!-- Gradient overlays -->
<div class="bg-linear-to-r from-neutral-200 to-transparent dark:from-stone-700 absolute top-0 bottom-0 left-0 z-10 w-12 sm:w-24"/>
<div class="bg-linear-to-l from-neutral-200 to-transparent dark:from-stone-700 absolute top-0 bottom-0 right-0 z-10 w-12 sm:w-24"/>
</div> </div>
</div> </div>
</section> </section>
<script> <script>
document.addEventListener('astro:page-load', () => { document.addEventListener('astro:page-load', () => {
// Create seamless infinite scrolling effect
function setupInfiniteScroll() { function setupInfiniteScroll() {
const cards = document.querySelectorAll('.skill-card'); const cards = document.querySelectorAll('.skill-card');
if (!cards.length) return; if (!cards.length) return;
@@ -70,6 +93,7 @@ const skills = ((await directus.request(
setupInfiniteScroll(); setupInfiniteScroll();
// Add hover effects to cards - only on non-touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const cards = document.querySelectorAll('.skill-card'); const cards = document.querySelectorAll('.skill-card');
@@ -120,7 +144,7 @@ const skills = ((await directus.request(
</script> </script>
<style> <style>
/* Specific css to enable sliding effect */ /* Tech Stack Slider */
.slider-track { .slider-track {
width: fit-content; width: fit-content;
animation: scroll 40s linear infinite; animation: scroll 40s linear infinite;
@@ -131,7 +155,7 @@ const skills = ((await directus.request(
transform: translateX(0); transform: translateX(0);
} }
100% { 100% {
transform: translateX(calc(-220px * 6 - 16px * 6)); transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
} }
} }
@@ -145,7 +169,7 @@ const skills = ((await directus.request(
transform: translateX(0); transform: translateX(0);
} }
100% { 100% {
transform: translateX(calc(-280px * 6 - 32px * 6)); transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
} }
} }
} }

View File

@@ -2,25 +2,24 @@ import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus'; import directus from '@lib/directus';
const global = await directus.request(readSingleton('site_global'));
export interface NavigationLink { export interface NavigationLink {
name: string; name: string;
url: string; url: string;
} }
const global = await directus.request(readSingleton('site_global'));
export const NavigationLinks: NavigationLink[] = [ export const NavigationLinks: NavigationLink[] = [
{ name: 'Home', url: '/' }, { name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog/' }, { name: 'Blog', url: '/blog/' },
{ name: 'Categories', url: '/categories/' }, { name: 'Categories', url: '/categories/' },
{ name: 'Apps', url: '/apps/' },
{ name: 'About Me', url: '/about/' }, { name: 'About Me', url: '/about/' },
]; ];
export const FooterLinks: NavigationLink[] = [ export const FooterLinks: NavigationLink[] = [
{ name: 'RSS', url: '/rss.xml' }, { name: 'RSS', url: '/rss.xml' },
{ name: 'Gitea', url: 'https://gitea.alexlebens.dev' }, { name: 'Gitea', url: '/https://gitea.alexlebens.dev' },
{ name: 'Docs', url: 'https://docs.alexlebens.dev' }, { name: 'Docs', url: '/https://docs.alexlebens.dev' },
]; ];
export const SEO = { export const SEO = {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 MiB

View File

@@ -2,10 +2,10 @@
import { ClientRouter } from 'astro:transitions'; import { ClientRouter } from 'astro:transitions';
import { readSingleton } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import BaseHead from '@components/BaseHead.astro'; import BaseHead from '@components/BaseHead.astro';
import Footer from '@components/Footer.astro'; import Footer from '@components/Footer.astro';
import Header from '@components/Header.astro'; import Header from '@components/Header.astro';
import directus from '@lib/directus';
import '@styles/global.css'; import '@styles/global.css';
@@ -20,16 +20,12 @@ interface Props {
const { title, description = 'Alex Lebens', ogImage, lang = 'en', structuredData } = Astro.props; const { title, description = 'Alex Lebens', ogImage, lang = 'en', structuredData } = Astro.props;
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
const normalizeTitle = !title ? global.name : `${title} | ${global.name}`; const normalizeTitle = !title ? global.name : `${title} | ${global.name}`;
--- ---
<html lang={lang}> <html lang={lang}>
<head> <head>
<title> <title>{normalizeTitle}</title>
{normalizeTitle}
</title>
<BaseHead <BaseHead
title={normalizeTitle} title={normalizeTitle}
description={description} description={description}
@@ -38,9 +34,7 @@ const normalizeTitle = !title ? global.name : `${title} | ${global.name}`;
ogDescription={description} ogDescription={description}
structuredData={structuredData} structuredData={structuredData}
/> />
<ClientRouter fallback="swap" /> <ClientRouter fallback="swap" />
<script is:inline> <script is:inline>
const theme = (() => { const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('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); window.localStorage.setItem('theme', theme);
</script> </script>
<!-- Rybbit Tracking Snippet -->
<script
src="https://rybbit.alexlebens.dev/api/script.js"
data-site-id={global.rybbit_site_id}
defer
/>
</head> </head>
<body class="bg-stone-200 selection:bg-yellow-400 selection:text-neutral-700 dark:bg-stone-700">
<body class="bg-background selection:bg-yellow-400"> <!-- <div class="fixed inset-0 -z-10">
<!-- Disabled texture background for now <div
<div class="fixed inset-0 -z-10"> class="bg-grid-pattern absolute inset-0 [mask-image:radial-gradient(white,transparent_85%)] bg-[center_top_-1px]"
<div class="bg-grid-pattern absolute inset-0 mask-[radial-gradient(white,transparent_85%)] bg-position-[center_top_-1px]"/> >
</div> </div>
--> </div> -->
<div class="mx-auto w-full max-w-(--breakpoint-2xl) flex-grow px-4 sm:px-6 lg:px-8">
<div class="grow w-full max-w-(--breakpoint-2xl) px-4 sm:px-6 lg:px-8 py-20 mx-auto">
<Header /> <Header />
<main class="min-h-screen"> <main class="min-h-screen">
<slot /> <slot />
</main> </main>
</div> </div>
<Footer /> <Footer />
<style>
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
</body> </body>
</html> </html>

View File

@@ -2,9 +2,7 @@ import { createDirectus, rest } from '@directus/sdk';
import type { import type {
Global, Global,
Weather,
Post, Post,
Application,
Experience, Experience,
Education, Education,
Certificate, Certificate,
@@ -16,9 +14,7 @@ import { getDirectusURL } from '@lib/directusFunctions';
type Schema = { type Schema = {
site_global: Global; site_global: Global;
site_weather: Weather;
posts: Post[]; posts: Post[];
site_applications: Application;
site_experience: Experience; site_experience: Experience;
site_education: Education; site_education: Education;
site_certificate: Certificate; site_certificate: Certificate;

View File

@@ -2,7 +2,7 @@ const getDirectusURL = () => {
if (process.env.DIRECTUS_URL) { if (process.env.DIRECTUS_URL) {
return `https://${process.env.DIRECTUS_URL}`; return `https://${process.env.DIRECTUS_URL}`;
} }
return 'https://directus.alexlebens.net'; return 'https://directus.alexlebens.dev';
}; };
async function getDirectusImageURL(image: string) { async function getDirectusImageURL(image: string) {

View File

@@ -3,35 +3,23 @@ export type Global = {
about: string; about: string;
about_description: string; about_description: string;
about_blog: string; about_blog: string;
about_applications: string;
about_categories: string; about_categories: string;
initials: string; initials: string;
email: string; email: string;
site_url: string; site_url: string;
rybbit_site_id: string;
logo: string; logo: string;
portrait: string; portrait: string;
portrait_alt: string; portrait_alt: string;
home_image: string; home_image: string;
home_image_alt: string; home_image_alt: string;
blog_image: string;
blog_image_alt: string;
categories_image: string; categories_image: string;
categories_image_alt: string; categories_image_alt: string;
applications_image: string; blog_image: string;
applications_image_alt: string; blog_image_alt: string;
footer_image: string; footer_image: string;
footer_image_alt: string; footer_image_alt: string;
}; };
export type Weather = {
id: string;
location: string;
latitude: string;
longitude: string;
timezone: string;
}
export type Post = { export type Post = {
slug: string; slug: string;
title: string; title: string;
@@ -49,16 +37,6 @@ export type Post = {
updated_date: Date; updated_date: Date;
}; };
export type Application = {
id: string;
name: string;
isActive: boolean;
description: string;
highlights: string[];
url: string;
logoUrl: string;
};
export type Experience = { export type Experience = {
id: string; id: string;
name: string; name: string;
@@ -82,8 +60,6 @@ export type Education = {
area: string; area: string;
studyType: string; studyType: string;
graduationDate: string; graduationDate: string;
logo: string;
logoDark: string;
}; };
export type Certificate = { export type Certificate = {
@@ -92,7 +68,6 @@ export type Certificate = {
issuer: string; issuer: string;
issuerDate: string; issuerDate: string;
url: string; url: string;
logoName: string;
}; };
export type Project = { export type Project = {

View File

@@ -1,10 +1,10 @@
--- ---
import { readSingleton } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
import GoBackButton from '@/components/buttons/GoBackButton.astro';
import GoHomeButton from '@/components/buttons/GoHomeButton.astro';
import BaseLayout from '@layouts/BaseLayout.astro';
import directus from '@lib/directus'; import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import GoBack from '@/components/ui/buttons/GoBack.astro';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
--- ---
@@ -28,44 +28,45 @@ const global = await directus.request(readSingleton('site_global'));
}, },
}} }}
> >
<section class="mt-20 grid place-content-center">
<section class="grid place-content-center mt-20"> <div class="mx-auto max-w-screen-xl px-4 py-8 lg:px-6 lg:py-16">
<div class="max-w-7xl px-4 lg:px-6 py-8 lg:py-16 mx-auto"> <div class="mx-auto max-w-screen-sm text-center">
<div class="text-center max-w-screen-sm mx-auto">
<div class="glitch-wrapper smooth-reveal"> <div class="glitch-wrapper smooth-reveal">
<h1 <h1
class="glitch text-header text-9xl font-bold leading-none sm:text-[12rem]" class="glitch text-9xl leading-none font-bold text-neutral-900 sm:text-[12rem] dark:text-neutral-100"
data-text="404" data-text="404"
> >
Not Found Not Found
</h1> </h1>
</div> </div>
<h1 class="smooth-reveal text-yellow-500 dark:text-yellow-400 text-4xl md:text-5xl font-bold leading-tight tracking-tight text-balance mt-30">
Page Not Found: <h1
class="text-dark smooth-reveal mb-4 text-7xl font-extrabold text-yellow-500 lg:text-9xl dark:text-yellow-400"
>
{`Page Not Found - ${global.name}`}
</h1> </h1>
<h1 class="smooth-reveal card-text-header mt-8 mb-30"> <div
{Astro.url.pathname.replace('/', '')} class="smooth-reveal mx-auto mt-16 max-w-md rounded-xl bg-neutral-100 p-6 shadow-xs dark:border-neutral-700/50 dark:bg-stone-800"
</h1> >
<div class="smooth-reveal card-base max-w-md p-6 mx-auto mt-16"> <h3
<h3 class="card-text-title text-sm tracking-wider uppercase"> class="text-sm font-medium tracking-wider text-neutral-500 uppercase dark:text-neutral-400"
>
Did you know? Did you know?
</h3> </h3>
<p <p class="mt-2 text-sm text-neutral-600 dark:text-neutral-300" id="fun-fact">
id="fun-fact"
class="text-secondary text-sm mt-4 mb-2"
>
The 404 error code originated when CERN's web server displayed room 404 (their server The 404 error code originated when CERN's web server displayed room 404 (their server
room) as the error message when a file wasn't found. room) as the error message when a file wasn't found.
</p> </p>
</div> </div>
<div class="smooth-reveal flex flex-col sm:flex-row items-center justify-center gap-4 mt-10"> <div
<GoBackButton/> class="smooth-reveal mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row"
<GoHomeButton url={global.site_url} /> >
<GoBack title="Go Back" />
<PrimaryCTA title="Return Home" url={global.site_url} noArrow addHome />
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</BaseLayout> </BaseLayout>
<script> <script>

View File

@@ -1,14 +1,13 @@
--- ---
import { readSingleton } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
import HeroSection from '@components/sections/HeroSection.astro';
import ExperienceSection from '@components/sections/ExperienceSection.astro';
import EducationSection from '@components/sections/EducationSection.astro';
import ProjectSection from '@components/sections/ProjectSection.astro';
import SkillsSliderSection from '@components/sections/SkillsSliderSection.astro';
import BaseLayout from '@layouts/BaseLayout.astro';
import directus from '@lib/directus'; import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro';
import Experience from '@components/ui/sections/Experience.astro';
import Projects from '@components/ui/sections/Projects.astro';
import Skills from '@components/ui/sections/Skills.astro';
import Education from '@components/ui/sections/Education.astro';
import portraitImg from '@images/portrait.avif'; import portraitImg from '@images/portrait.avif';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
@@ -33,7 +32,6 @@ const global = await directus.request(readSingleton('site_global'));
}, },
}} }}
> >
<HeroSection <HeroSection
title="About Me" title="About Me"
subTitle={global.about} subTitle={global.about}
@@ -42,21 +40,23 @@ const global = await directus.request(readSingleton('site_global'));
rounded={true} rounded={true}
/> />
<section class="max-w-7xl px-4 sm:px-6 lg:px-8 py-10 lg:py-14 mx-auto"> <section class="mx-auto max-w-340 px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<div class="flex flex-col gap-y-24 md:gap-y-32"> <main class="relative grid grid-cols-1 md:grid-cols-6 gap-12 p-8 md:p-16 xl:gap-24 max-w-7xl mx-auto">
<ExperienceSection className="smooth-reveal" /> <div class="space-y-12 col-span-1 md:col-span-6">
<EducationSection className="smooth-reveal" /> <Experience className="smooth-reveal-2" />
<ProjectSection className="smooth-reveal" /> <Education className="smooth-reveal-2 mt-30" />
<SkillsSliderSection className="smooth-reveal" /> <Projects className="smooth-reveal-2 mt-30" />
<Skills className="smooth-reveal-2 mt-30" />
</div> </div>
</main>
</section> </section>
</BaseLayout> </BaseLayout>
<script> <script>
// Add smooth reveal animations for content after loading // Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => { document.addEventListener('astro:page-load', () => {
const animateContent = () => { const animateContent = () => {
// Animate group 1
const smoothReveal = document.querySelectorAll('.smooth-reveal'); const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => { smoothReveal.forEach((el, index) => {
setTimeout( setTimeout(
@@ -67,6 +67,28 @@ const global = await directus.request(readSingleton('site_global'));
); );
}); });
// Animate group 2
const smoothReveal2 = document.querySelectorAll('.smooth-reveal-2');
smoothReveal2.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
200 + index * 250
);
});
// Animate topic cards with staggered delay
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
smoothRevealCards.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
400 + index * 250
);
});
// Animate with just fade in with staggered delay // Animate with just fade in with staggered delay
const smoothRevealFade = document.querySelectorAll('.smooth-reveal-fade'); const smoothRevealFade = document.querySelectorAll('.smooth-reveal-fade');
smoothRevealFade.forEach((el, index) => { smoothRevealFade.forEach((el, index) => {

View File

@@ -1,63 +0,0 @@
---
import { readSingleton } from '@directus/sdk';
import HeroSection from '@components/sections/HeroSection.astro';
import ApplicationSection from '@components/sections/ApplicationSection.astro';
import BaseLayout from '@layouts/BaseLayout.astro';
import directus from '@lib/directus';
import applicationImg from '@images/cedar_tree.png';
const global = await directus.request(readSingleton('site_global'));
---
<BaseLayout
title="Applications"
description={global.about_applications}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
name: `Applications | ${global.name}`,
description: global.about_applications,
isPartOf: {
'@type': 'WebSite',
url: global.site_url,
name: global.name,
description: global.about,
},
}}
>
<HeroSection
title="Applications"
subTitle={global.about_applications}
src={applicationImg}
alt={global.applications_image_alt}
/>
<ApplicationSection className="smooth-reveal-2" />
</BaseLayout>
<script>
// Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => {
const animateContent = () => {
// Animate group 1
const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
50 + index * 100
);
});
};
animateContent();
});
</script>

View File

@@ -1,19 +1,16 @@
--- ---
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 { readItems, readSingleton } from '@directus/sdk';
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 { getDirectusImageURL } from '@lib/directusFunctions';
import { formatDate } from '@support/time'; import BaseLayout from '@layouts/BaseLayout.astro';
import Image from '@components/ui/images/Image.astro';
const post = Astro.props; import { formatDateTime } from '@support/time';
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await directus.request(readItems('posts')); const posts = await directus.request(readItems('posts'));
@@ -22,19 +19,18 @@ 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'], 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, {
@@ -47,7 +43,6 @@ marked.use(markedShiki({
}); });
} }
})); }));
const content = marked.parse(post.content); const content = marked.parse(post.content);
--- ---
@@ -68,7 +63,9 @@ const content = marked.parse(post.content);
name: global.name, name: global.name,
description: global.about, description: global.about,
}, },
image: [], image: [
// post.data.banner,
],
headline: post.title, headline: post.title,
datePublished: post.published_date, datePublished: post.published_date,
dateModified: post.updated_date, dateModified: post.updated_date,
@@ -81,12 +78,11 @@ 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="max-w-6xl px-4 sm:px-6 lg:px-8 pt-8 lg:pt-12 pb-12 mx-auto">
<div class="smooth-reveal relative w-full"> <div class="smooth-reveal relative w-full">
<div class="sm:shadow-xs sm:dark:shadow-md rounded-2xl mt-4 sm:mt-0"> <div class="mt-4 rounded-2xl shadow-none sm:mt-0 sm:shadow-sm">
<Image <Image
class="rounded-2xl sm:rounded-b-none w-full max-h-150 object-cover" class="max-h-[600px] w-full rounded-t-2xl object-cover"
src={getDirectusImageURL(post.image)} src={getDirectusImageURL(post.image)}
alt={post.image_alt} alt={post.image_alt}
draggable="false" draggable="false"
@@ -94,60 +90,83 @@ const content = marked.parse(post.content);
loading="lazy" loading="lazy"
inferSize={true} inferSize={true}
/> />
<div class="sm:bg-background-card rounded-b-2xl px-0 sm:px-6 md:px-10 lg:px-14 py-6"> <div
<div class="text-center sm:text-left mt-4"> class="rounded-b-2xl px-0 py-6 sm:bg-neutral-100 sm:px-6 md:px-10 lg:px-14 sm:dark:bg-neutral-900/30"
<h2 class="card-text-header block"> >
<div class="mb-16">
<h2
class="mb-6 block text-3xl font-bold tracking-tight text-balance text-neutral-800 md:text-4xl lg:text-5xl dark:text-neutral-300"
>
{post.title} {post.title}
</h2> </h2>
<ol class="flex items-center justify-center sm:justify-start whitespace-nowrap gap-2 sm:gap-0 mt-6 sm:mt-4"> <ol class="mt-8 flex items-center whitespace-nowrap">
<li class="inline-flex items-center"> <li class="inline-flex items-center">
<a <a
class="inline-flex items-center text-secondary hover:text-secondary-hover text-sm transition-all duration-300" class="flex items-center text-sm text-neutral-500 transition-all duration-300 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200"
href=`/categories/${category.slug}` href=`/categories/${category.slug}`
data-astro-prefetch
> >
{category?.data?.title} {category?.data?.title}
</a> </a>
<span class="shrink-0 text-secondary text-sm mx-2 sm:mx-4"> <svg
/ class="mx-2 size-5 flex-shrink-0 text-neutral-500 dark:text-neutral-500"
</span> width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path d="M6 13L10 3" stroke="currentColor" stroke-linecap="round"></path>
</svg>
</li> </li>
<li class="inline-flex items-center"> <li
<span class="shrink-0 text-secondary text-sm"> class="inline-flex items-center text-sm text-neutral-500 transition-all duration-300 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200"
{formatDate(post.published_date)} >
</span> {formatDateTime(post.published_date)}
<span class="shrink-0 text-secondary text-sm mx-2 sm:mx-4"> <svg
/ class="mx-2 size-5 flex-shrink-0 text-neutral-500 dark:text-neutral-500"
</span> width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path d="M6 13L10 3" stroke="currentColor" stroke-linecap="round"></path>
</svg>
</li> </li>
<li class="inline-flex items-center"> <li
<span class="shrink-0 text-secondary text-sm"> class="inline-flex items-center text-sm text-neutral-500 transition-all duration-300 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200"
aria-current="page"
>
{readingTime.minutes.toPrecision(1)} minutes to read {readingTime.minutes.toPrecision(1)} minutes to read
</span>
</li> </li>
</ol> </ol>
</div> </div>
<div class="border-t border-divider mt-10 mb-10"/>
<article class="text-header prose prose-blog sm:prose-lg dark:prose-invert max-w-none"> <article
class="prose prose-blog sm:prose-lg dark:prose-invert max-w-none text-justify text-neutral-800 dark:text-neutral-200"
>
<div set:html={content} /> <div set:html={content} />
</article> </article>
<div class="grid sm:flex sm:items-center sm:justify-between gap-y-5 sm:gap-y-0 max-w-5xl mx-auto mt-10 md:mt-14"> <div
<div class="flex flex-wrap sm:flex-nowrap sm:items-center gap-x-2 gap-y-1 sm:gap-y-0"> class="mx-auto mt-10 grid max-w-screen-lg gap-y-5 sm:flex sm:items-center sm:justify-between sm:gap-y-0 md:mt-14"
{post.tags.map((tag: string) => ( >
<span class="inline-flex items-center button-base bg-cobalt dark:bg-turquoise text-neutral-100 text-xs font-bold rounded-lg gap-x-1.5 px-3 py-1.5"> <div class="flex flex-wrap gap-x-2 gap-y-1 sm:flex-nowrap sm:items-center sm:gap-y-0">
{
post.tags.map((tag: string) => (
<span class="bg-steel/30 dark:bg-bermuda/60 inline-flex items-center gap-x-1.5 rounded-lg px-3 py-1.5 text-xs font-medium text-neutral-700 outline-none focus:outline-none focus-visible:ring focus-visible:outline-none dark:text-neutral-200">
{tag} {tag}
</span> </span>
))} ))
}
</div> </div>
<SocialShareButton pageTitle={post.title}/>
</div> </div>
</div> </div>
</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 {
@@ -161,7 +180,6 @@ const content = marked.parse(post.content);
} }
} }
</style> </style>
</BaseLayout> </BaseLayout>
<script> <script>

View File

@@ -3,12 +3,11 @@ import { readItems, readSingleton } from '@directus/sdk';
import type { Post } from '@lib/directusTypes'; 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 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/ui/sections/HeroSection.astro';
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'));
@@ -19,11 +18,10 @@ 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, 9); ).slice(0, 6);
--- ---
<BaseLayout <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 <BlogSelectedArticles posts={selectedPosts} />
title="Blog" <BlogRecentArticles posts={recentPosts} />
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,14 +2,11 @@
import { getCollection } from 'astro:content'; import { getCollection } from 'astro:content';
import { readItems, readSingleton } from '@directus/sdk'; import { readItems, readSingleton } from '@directus/sdk';
import type { Post } from '@lib/directusTypes';
import HeaderSection from '@components/sections/HeaderSection.astro';
import BlogCard from '@components/cards/BlogCard.astro';
import BaseLayout from '@layouts/BaseLayout.astro';
import directus from '@lib/directus'; import directus from '@lib/directus';
import type { Post } from '@lib/directusTypes';
const { category } = Astro.props; import BaseLayout from '@layouts/BaseLayout.astro';
import BlogCard from '@components/blog/BlogCard.astro';
import HeaderSection from '@components/ui/sections/HeaderSection.astro';
export async function getStaticPaths() { export async function getStaticPaths() {
const categories = await getCollection('categories'); const categories = await getCollection('categories');
@@ -19,6 +16,8 @@ export async function getStaticPaths() {
})); }));
} }
const { category } = Astro.props;
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
const posts = await directus.request( const posts = await directus.request(
readItems('posts', { readItems('posts', {
@@ -27,7 +26,6 @@ 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) => {
@@ -53,7 +51,6 @@ const categoriesPosts = posts
}, },
}} }}
> >
<HeaderSection <HeaderSection
title={category.data.title} title={category.data.title}
subTitle={category.data.description} subTitle={category.data.description}
@@ -62,12 +59,9 @@ const categoriesPosts = posts
btnURL="/categories" btnURL="/categories"
/> />
<section class="max-w-340 2xl:max-w-full mb-10 px-4 sm:px-6 lg:px-8 py-8 mx-auto mt-10"> <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 sm:grid-cols-2 lg:grid-cols-3 gap-6"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{categoriesPosts.map((b) => {categoriesPosts.map((b) => <BlogCard post={b} />)}
<BlogCard post={b} />
)}
</div> </div>
</section> </section>
</BaseLayout> </BaseLayout>

View File

@@ -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 HeroSection from '@components/sections/HeroSection.astro';
import CategorySection from '@components/sections/CategorySection.astro';
import BaseLayout from '@layouts/BaseLayout.astro';
import directus from '@lib/directus'; import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import BlogCategoryCard from '@components/blog/BlogCategoryCard.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro';
import { timeago } from '@support/time';
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
@@ -30,7 +94,6 @@ const global = await directus.request(readSingleton('site_global'));
}, },
}} }}
> >
<HeroSection <HeroSection
title="Categories" title="Categories"
subTitle={global.about_categories} subTitle={global.about_categories}
@@ -38,8 +101,28 @@ const global = await directus.request(readSingleton('site_global'));
alt={global.categories_image_alt} 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> </BaseLayout>
<script> <script>

View File

@@ -1,31 +1,15 @@
--- ---
import { readSingleton, readItems } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
import type { Post } from '@lib/directusTypes';
import HeroSection from '@components/sections/HeroSection.astro';
import FeatureSection from '@components/sections/FeatureSection.astro';
import WeatherSection from '@components/sections/WeatherSection.astro';
import RecentPostsSection from '@components/sections/RecentPostsSection.astro';
import GiteaSection from '@components/sections/GiteaSection.astro';
import BaseLayout from '@layouts/BaseLayout.astro';
import directus from '@lib/directus'; import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro';
import FeaturesSection from '@components/ui/sections/FeaturesSection.astro';
import LatestPosts from '@components/ui/sections/LatestPosts.astro';
import HeroSectionAlt from '@components/ui/sections/HeroSectionAlt.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 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
@@ -47,7 +31,6 @@ const recentPosts = posts
}, },
}} }}
> >
<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}
@@ -57,28 +40,15 @@ const recentPosts = posts
alt={global.home_image_alt} alt={global.home_image_alt}
/> />
<FeatureSection /> <FeaturesSection />
<WeatherSection <LatestPosts />
server:defer
latitude={weather.latitude}
longitude={weather.longitude}
cityName={weather.location}
timezone={weather.timezone}
/>
<RecentPostsSection <HeroSectionAlt
posts={recentPosts}
title="Latest Posts"
subTitle="Checkout my most recent thoughts here"
/>
<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>
@@ -131,11 +101,5 @@ const recentPosts = posts
}; };
animateContent(); animateContent();
const observer = new MutationObserver(() => {
animateContent();
});
observer.observe(document.body, { childList: true, subtree: true });
}); });
</script> </script>

View File

@@ -1,14 +1,31 @@
// https://docs.astro.build/en/guides/integrations-guide/sitemap/#usage // https://docs.astro.build/en/guides/integrations-guide/sitemap/#usage
import type { APIRoute } from 'astro'; import type { APIRoute } from 'astro';
const getRobotsTxt = (sitemapURL: URL) => `\ const robotsTxt = `
User-agent: Googlebot
Disallow:
Allow: /
Crawl-delay: 10
User-agent: Yandex
Disallow:
Allow: /
Crawl-delay: 2
User-agent: archive.org_bot
Disallow:
Allow: /
Crawl-delay: 2
User-agent: * User-agent: *
Allow: / Allow: /
Sitemap: ${sitemapURL.href} Sitemap: ${new URL('sitemap-index.xml', import.meta.env.SITE).href}`.trim();
`;
export const GET: APIRoute = ({ site }) => { export const GET: APIRoute = () => {
const sitemapURL = new URL('sitemap-index.xml', site); return new Response(robotsTxt, {
return new Response(getRobotsTxt(sitemapURL)); headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
});
}; };

View File

@@ -1,7 +1,5 @@
@import 'tailwindcss'; @import 'tailwindcss';
@import 'preline/variants.css'; @import 'preline/variants.css';
@import './utilities.css';
@plugin '@tailwindcss/typography'; @plugin '@tailwindcss/typography';
@plugin '@tailwindcss/forms'; @plugin '@tailwindcss/forms';
@@ -9,41 +7,16 @@
/* https://tailwindcss.com/docs/dark-mode */ /* https://tailwindcss.com/docs/dark-mode */
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@theme {
/* Custom colors */ /* Custom colors */
@theme {
--color-midnight: #0c354d; --color-midnight: #0c354d;
--color-ocean: #134e70;
--color-cobalt: #6c9cb0;
--color-steel: #4682b4;
--color-turquoise: #0da797; --color-turquoise: #0da797;
--color-steel: #4682b4;
--color-bermuda: #7fbab4; --color-bermuda: #7fbab4;
--color-desert: #f9deb2; --color-desert: #f9deb2;
--color-bronze: #9e7f5e; --color-bronze: #9e7f5e;
--color-gitea-primary: #609926; --color-gitea-primary: #609926;
--color-gitea-secondary: #4c7a33; --color-gitea-secondary: #4c7a33;
/* Theme colors */
--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));
/* Text colors */
--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));
/* Object colors */
--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));
--color-background-card: light-dark(color-mix(in srgb, var(--color-neutral-100) 80%, transparent), color-mix(in srgb, var(--color-neutral-800) 60%, transparent));
--color-divider: light-dark(color-mix(in srgb, var(--color-neutral-400) 50%, transparent), color-mix(in srgb, var(--color-neutral-500) 50%, transparent));
} }
@layer base { @layer base {
@@ -51,11 +24,6 @@
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
--theme-transition: 0.3s ease; --theme-transition: 0.3s ease;
color-scheme: light;
}
:root:where(.dark, .dark *) {
color-scheme: dark;
} }
*, *,

View File

@@ -1,135 +0,0 @@
/* Button classes */
@utility button-base {
@apply transition-all duration-300
border border-transparent
shadow-sm hover:shadow-md dark:shadow-md dark:hover:shadow-lg
px-4 py-3
}
@utility button-base-hidden {
@apply transition-all duration-300
border border-transparent
hover:bg-neutral-100 dark:hover:bg-neutral-700
p-2
}
@utility button-hover-arrow {
@apply translate-y-px transition duration-300
group-hover:translate-x-1
h-3 w-3 md:h-5 md:w-5
}
@utility button-text-title {
@apply text-neutral-200 2xl:text-base
text-sm font-bold
}
@utility button-text-title-hidden {
@apply transition-all duration-300
text-neutral-600 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-300 2xl:text-base
text-sm font-medium
}
@utility button-bg-blue {
@apply transition-all duration-300
bg-cobalt hover:bg-steel dark:bg-steel dark:hover:bg-cobalt
}
@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
}
@utility button-bg-neutral {
@apply transition-all duration-300
border border-neutral-100 dark:border-stone-500/20
bg-background-card hover:bg-neutral-100 dark:hover:bg-neutral-800/90
}
@utility button-bg-gitea {
@apply transition-all duration-300
bg-gitea-primary hover:bg-gitea-secondary dark:bg-gitea-secondary dark:hover:bg-gitea-primary
}
/* Card classes */
@utility card-base {
@apply transition-all duration-300
rounded-xl
border border-neutral-100 dark:border-stone-500/20
bg-background-card hover:bg-neutral-100 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-2xl
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
group-hover:text-main
}
@utility card-hover-icon-scale {
@apply transition-all duration-300
drop-shadow-sm
group-hover:scale-110
}
@utility card-text-header {
@apply text-header
text-4xl md:text-5xl
font-bold leading-tight tracking-tight text-balance
}
@utility card-text-header-minor {
@apply text-header
text-2xl md:text-3xl
font-semibold leading-tight tracking-tight text-balance
}
@utility card-text-header-description {
@apply text-primary
text-lg
text-pretty leading-relaxed
}
@utility card-text-title {
@apply text-primary
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
}
@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
}

View File

@@ -1,6 +1,6 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
interface BlurImageMetadata { export interface BlurImageMetadata {
/** /**
* The width of the origin image * The width of the origin image
*/ */
@@ -23,7 +23,7 @@ interface BlurImageMetadata {
blurHeight: number; blurHeight: number;
} }
async function blurStyle(filePath: string) { export async function blurStyle(filePath: string) {
const image = await blurImageMetadata(filePath); const image = await blurImageMetadata(filePath);
const svg = blurImageSVG(image); const svg = blurImageSVG(image);
return { return {
@@ -64,5 +64,3 @@ async function blurImageMetadata(filepath: string): Promise<BlurImageMetadata> {
return { blurDataURL, blurHeight, blurWidth, width, height }; return { blurDataURL, blurHeight, blurWidth, width, height };
} }
export { blurStyle };

View File

@@ -1,6 +1,6 @@
import { join } from 'node:path'; import { join } from 'node:path';
function resolveFilePath(path: string) { export function resolveFilePath(path: string) {
if (path.startsWith('/')) { if (path.startsWith('/')) {
return resolveFilePathPublic(path); return resolveFilePathPublic(path);
} }
@@ -8,14 +8,12 @@ function resolveFilePath(path: string) {
return resolveFilePathInternal(path); return resolveFilePathInternal(path);
} }
function resolveFilePathPublic(path: string) { export function resolveFilePathPublic(path: string) {
return join(process.cwd(), path); return join(process.cwd(), path);
} }
function resolveFilePathInternal(path: string) { export function resolveFilePathInternal(path: string) {
const normalizePath = path.startsWith('@') ? path.replace('@', '') : path; const normalizePath = path.startsWith('@') ? path.replace('@', '') : path;
return join(process.cwd(), 'src/', normalizePath); return join(process.cwd(), 'src/', normalizePath);
} }
export { resolveFilePath, resolveFilePathPublic, resolveFilePathInternal };

View File

@@ -17,6 +17,21 @@ const TimeAgoConfiguration: string[][] = [
['%s years ago', 'in %s years'], ['%s years ago', 'in %s years'],
]; ];
function formatDate(date: Date): string {
const year = new Date(date).getFullYear();
const month = String(new Date(date).getMonth() + 1).padStart(2, '0');
const day = String(new Date(date).getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
function formatDateTime(date: Date): string {
const hours = String(new Date(date).getHours()).padStart(2, '0');
const minutes = String(new Date(date).getMinutes()).padStart(2, '0');
return `${formatDate(date)} ${hours}:${minutes}`;
}
function timeago(date?: Date): string { function timeago(date?: Date): string {
if (!date) { if (!date) {
return 'today'; return 'today';
@@ -31,12 +46,4 @@ function timeago(date?: Date): string {
return format(date, 'timeago'); return format(date, 'timeago');
} }
function formatDate(date: Date): string { export { formatDate, timeago, formatDateTime };
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
export { formatDate, timeago };

View File

@@ -1,65 +0,0 @@
interface DayForecast {
date: string;
temp: number;
code: number;
label: string;
icon: string;
dayName: string;
}
interface WeatherResult {
forecastDays: DayForecast[];
error: string | null;
}
const getWeatherInfo = (code: number) => {
if (code === 0) return { label: 'Clear', icon: '01d' };
if (code >= 1 && code <= 3) return { label: 'Partly Cloudy', icon: '02d' };
if (code === 45 || code === 48) return { label: 'Foggy', icon: '50d' };
if (code >= 51 && code <= 55) return { label: 'Drizzle', icon: '09d' };
if (code >= 61 && code <= 65) return { label: 'Rainy', icon: '10d' };
if (code === 66 || code === 67) return { label: 'Freezing Rain', icon: '13d' };
if (code >= 71 && code <= 75) return { label: 'Snowy', icon: '13d' };
if (code === 77) return { label: 'Snow Grains', icon: '13d' };
if (code >= 80 && code <= 82) return { label: 'Showers', icon: '09d' };
if (code === 85 || code === 86) return { label: 'Snow Showers', icon: '13d' };
if (code >= 95 && code <= 99) return { label: 'Stormy', icon: '11d' };
return { label: 'Unknown', icon: '03d' };
};
export const getDayName = (dateStr: string) => {
const date = new Date(`${dateStr}T00:00:00`);
return date.toLocaleDateString('en-US', { weekday: 'short' });
};
async function getFiveDayForecast(latitude: string, longitude: string, timezone: string): Promise<WeatherResult> {
const url = `https://api.open-meteo.com/v1/forecast?latitude=${latitude}&longitude=${longitude}&daily=weather_code,temperature_2m_max&timezone=${timezone}&temperature_unit=fahrenheit`;
let data: any;
try {
const response = await fetch(url);
if (!response.ok) throw new Error("Weather service unavailable");
data = await response.json();
} catch (e: unknown) {
return { forecastDays: [], error: "Failed to load weather" };
}
const forecastDays = data.daily.time
.slice(0, 5)
.map((date: string, index: number): DayForecast => {
return {
date,
temp: Math.round(data.daily.temperature_2m_max[index]),
code: data.daily.weather_code[index],
label: getWeatherInfo(data.daily.weather_code[index]).label,
icon: getWeatherInfo(data.daily.weather_code[index]).icon,
dayName: getDayName(date)
};
});
return { forecastDays, error: null };
}
export { getFiveDayForecast };