Compare commits

..

52 Commits

Author SHA1 Message Date
d415dda661 feat: release 2.14.1
All checks were successful
release-image-gitea / build (push) Successful in 56s
renovate / renovate (push) Successful in 2m36s
release-image-gitea / release (push) Successful in 1m50s
test-build / build (push) Successful in 4m52s
release-image-harbor / build (push) Successful in 4m57s
test-build / guarddog (push) Successful in 5m25s
release-image-harbor / release (push) Successful in 3m17s
2026-02-18 22:42:56 -06:00
ea9ae016d7 fix: add env 2026-02-18 22:42:38 -06:00
0416ab7f9e feat: release 2.14.0
All checks were successful
test-build / guarddog (push) Successful in 30s
release-image-gitea / build (push) Successful in 48s
renovate / renovate (push) Successful in 2m21s
release-image-gitea / release (push) Successful in 1m57s
test-build / build (push) Successful in 4m7s
release-image-harbor / build (push) Successful in 6m46s
release-image-harbor / release (push) Successful in 4m20s
2026-02-18 21:48:17 -06:00
6f1728a909 feat: move url configuration to support file 2026-02-18 21:47:53 -06:00
db2711d878 feat: release 2.13.1
All checks were successful
test-build / guarddog (push) Successful in 30s
test-build / build (push) Successful in 59s
release-image-gitea / build (push) Successful in 50s
renovate / renovate (push) Successful in 2m26s
release-image-gitea / release (push) Successful in 1m52s
release-image-harbor / build (push) Successful in 6m14s
release-image-harbor / release (push) Successful in 11m50s
2026-02-18 21:26:50 -06:00
7f2a27248a feat: improve behavior of showmore, fix alignment 2026-02-18 21:26:23 -06:00
c927235a5a fix: info logs
All checks were successful
test-build / guarddog (push) Successful in 49s
test-build / build (push) Successful in 2m47s
release-image-harbor / build (push) Successful in 2m55s
release-image-harbor / release (push) Successful in 5m39s
release-image-gitea / build (push) Successful in 2m11s
release-image-gitea / release (push) Successful in 3m51s
renovate / renovate (push) Successful in 1m5s
2026-02-18 15:57:34 -06:00
8d5c02e2d1 fix: debug logs
All checks were successful
test-build / guarddog (push) Successful in 47s
renovate / renovate (push) Successful in 1m21s
test-build / build (push) Successful in 1m35s
2026-02-18 15:54:28 -06:00
1a34b932b0 fix: correct credentials
Some checks failed
test-build / guarddog (push) Successful in 44s
renovate / renovate (push) Successful in 59s
test-build / build (push) Has been cancelled
2026-02-18 15:52:25 -06:00
882063ea43 fix: correct matchhost
All checks were successful
test-build / guarddog (push) Successful in 52s
renovate / renovate (push) Successful in 59s
test-build / build (push) Successful in 1m18s
2026-02-18 15:41:20 -06:00
ba2477e7af fix: move host rules to workflow
All checks were successful
test-build / guarddog (push) Successful in 57s
test-build / build (push) Successful in 1m20s
renovate / renovate (push) Successful in 1m22s
2026-02-18 15:38:49 -06:00
879786484d feat: add creds for dhi
All checks were successful
test-build / guarddog (push) Successful in 38s
renovate / renovate (push) Successful in 57s
test-build / build (push) Successful in 2m11s
2026-02-18 15:33:03 -06:00
2c9486f687 feat: release 2.13.0 2026-02-18 15:23:41 -06:00
ba73c1b24f fix: add remote patterns for images
All checks were successful
test-build / guarddog (push) Successful in 1m20s
renovate / renovate (push) Successful in 1m43s
test-build / build (push) Successful in 2m6s
2026-02-18 15:22:35 -06:00
44bd1e4810 feat: change selected blogs to switch to card form on small screens 2026-02-18 15:07:34 -06:00
e52d85f931 feat: refactor pass along pages 2026-02-18 15:07:34 -06:00
21085a1620 feat: organize to consistency 2026-02-18 15:07:34 -06:00
744e72efc9 feat: update robots.txt 2026-02-18 15:07:34 -06:00
62dd636d4e feat: organize to consistency 2026-02-18 15:07:34 -06:00
b4d03a286c Merge pull request 'chore(deps): update tailwindcss monorepo to v4.2.0' (#344) from renovate/tailwindcss-monorepo into main
Some checks failed
renovate / renovate (push) Successful in 1m11s
test-build / build (push) Failing after 1m23s
test-build / guarddog (push) Successful in 1m37s
Reviewed-on: #344
2026-02-18 21:00:24 +00:00
442da55d5d Merge pull request 'chore(deps): update astro monorepo' (#345) from renovate/astro-monorepo into main
Some checks failed
renovate / renovate (push) Successful in 1m12s
test-build / guarddog (push) Successful in 55s
test-build / build (push) Failing after 1m20s
2026-02-18 20:57:05 +00:00
9b9c982f92 chore(deps): update astro monorepo
Some checks failed
renovate/stability-days Updates have not met minimum release age requirement
test-build / guarddog (pull_request) Successful in 49s
test-build / build (pull_request) Failing after 1m16s
2026-02-18 20:56:41 +00:00
1820650ada chore(deps): update tailwindcss monorepo to v4.2.0
Some checks are pending
renovate/stability-days Updates have not met minimum release age requirement
test-build / guarddog (pull_request) Successful in 37s
test-build / build (pull_request) Successful in 2m44s
2026-02-18 20:40:14 +00:00
fa2245e939 Merge pull request 'chore(deps): update dependency marked to v17.0.3' (#343) from renovate/marked-17.x-lockfile into main
All checks were successful
test-build / guarddog (push) Successful in 39s
renovate / renovate (push) Successful in 1m14s
test-build / build (push) Successful in 1m15s
2026-02-18 04:37:31 +00:00
12a8363dd2 chore(deps): update dependency marked to v17.0.3
Some checks are pending
renovate/stability-days Updates have not met minimum release age requirement
test-build / guarddog (pull_request) Successful in 52s
test-build / build (pull_request) Successful in 1m14s
2026-02-18 04:37:19 +00:00
4f365a4e60 Merge pull request 'chore(deps): update dependency @iconify-json/simple-icons to v1.2.71' (#342) from renovate/iconify-json-simple-icons-1.x-lockfile into main
Some checks failed
test-build / guarddog (push) Successful in 31s
test-build / build (push) Successful in 1m38s
renovate / renovate (push) Has been cancelled
2026-02-18 04:35:15 +00:00
12e74d29af chore(deps): update dependency @iconify-json/simple-icons to v1.2.71
Some checks are pending
renovate/stability-days Updates have not met minimum release age requirement
test-build / guarddog (pull_request) Successful in 37s
test-build / build (pull_request) Successful in 1m12s
2026-02-18 04:35:06 +00:00
7937090533 Merge pull request 'chore(deps): update dependency typescript-eslint to v8.56.0' (#341) from renovate/typescript-eslint-monorepo into main
Some checks failed
test-build / guarddog (push) Successful in 44s
test-build / build (push) Successful in 1m10s
renovate / renovate (push) Has been cancelled
Reviewed-on: #341
2026-02-18 04:33:53 +00:00
ebfd8cf4a7 chore(deps): update dependency typescript-eslint to v8.56.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / guarddog (pull_request) Successful in 30s
test-build / build (pull_request) Successful in 1m34s
2026-02-18 04:23:07 +00:00
8270728e8f feat: organize layout to consistency
All checks were successful
test-build / guarddog (push) Successful in 31s
test-build / build (push) Successful in 1m29s
renovate / renovate (push) Successful in 1m33s
2026-02-17 22:21:45 -06:00
20d8c7323f feat: tweak to gradient 2026-02-17 22:09:53 -06:00
5ac23f08a4 feat: improve navbar, add opacity fade beneath, layout, and refactor
All checks were successful
test-build / guarddog (push) Successful in 31s
test-build / build (push) Successful in 2m0s
renovate / renovate (push) Successful in 2m7s
2026-02-17 21:49:51 -06:00
c6f3179efb feat: organize footer to consistency 2026-02-17 17:44:40 -06:00
1a8473b964 feat: release 2.12.0
All checks were successful
test-build / guarddog (push) Successful in 1m11s
release-image-gitea / build (push) Successful in 2m28s
release-image-harbor / build (push) Successful in 2m19s
test-build / build (push) Successful in 5m33s
release-image-gitea / release (push) Successful in 7m39s
release-image-harbor / release (push) Successful in 7m16s
renovate / renovate (push) Successful in 2m5s
2026-02-16 23:08:06 -06:00
18211ad485 feat: update BaseHead
All checks were successful
renovate / renovate (push) Successful in 1m33s
test-build / build (push) Successful in 1m40s
test-build / guarddog (push) Successful in 1m59s
2026-02-16 23:04:42 -06:00
429cf94023 feat: organize to consistency pass on sections 2026-02-16 22:57:39 -06:00
0497731c45 feat: organize to consistency 2026-02-16 22:38:45 -06:00
6c2c6da91d feat: organize to consistency 2026-02-16 22:36:24 -06:00
19e17ea947 feat: remove option 2026-02-16 22:34:57 -06:00
3d9120c570 fix: remove unused property 2026-02-16 22:34:14 -06:00
875b8a7f47 fix: remove border from blog cards 2026-02-16 22:32:12 -06:00
1ddc76ae69 fix: remove errant semicolon 2026-02-16 22:30:04 -06:00
6423ffba63 feat: refactor blog components 2026-02-16 22:26:53 -06:00
505670dbf8 feat: remove unused packages
Some checks are pending
test-build / guarddog (push) Successful in 34s
test-build / build (push) Successful in 1m6s
release-image-harbor / build (push) Successful in 1m1s
release-image-harbor / release (push) Successful in 2m27s
release-image-gitea / build (push) Successful in 1m0s
release-image-gitea / release (push) Successful in 6m21s
renovate / renovate (push) Has started running
2026-02-16 00:28:58 -06:00
b3d7e7af2b chore(deps): update deps
All checks were successful
test-build / guarddog (push) Successful in 31s
renovate / renovate (push) Successful in 1m0s
test-build / build (push) Successful in 1m24s
2026-02-16 00:21:43 -06:00
440c95224d feat: release 2.11.0 2026-02-16 00:20:26 -06:00
b9ee82e9d8 Merge pull request 'chore(deps): update dependency eslint-plugin-astro to v1.6.0' (#340) from renovate/eslint-plugin-astro-1.x-lockfile into main
All checks were successful
test-build / guarddog (push) Successful in 33s
renovate / renovate (push) Successful in 49s
test-build / build (push) Successful in 1m26s
Reviewed-on: #340
2026-02-16 06:20:15 +00:00
3af9f08b7c chore(deps): update dependency eslint-plugin-astro to v1.6.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / guarddog (pull_request) Successful in 1m20s
test-build / build (pull_request) Successful in 2m18s
2026-02-16 06:09:50 +00:00
0bd56b172f Merge pull request 'chore(deps): update dependency @swup/astro to v1.8.0' (#339) from renovate/swup-astro-1.x-lockfile into main
All checks were successful
test-build / guarddog (push) Successful in 54s
renovate / renovate (push) Successful in 1m11s
test-build / build (push) Successful in 2m34s
Reviewed-on: #339
2026-02-16 06:08:50 +00:00
ebf70bd747 chore(deps): update dependency @swup/astro to v1.8.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / guarddog (pull_request) Successful in 38s
test-build / build (pull_request) Successful in 1m37s
2026-02-16 05:57:02 +00:00
9c5e9b6a5b Merge pull request 'chore(deps): update dependency @eslint-react/eslint-plugin to v2.13.0' (#338) from renovate/eslint-react-eslint-plugin-2.x-lockfile into main
All checks were successful
test-build / guarddog (push) Successful in 51s
test-build / build (push) Successful in 1m42s
renovate / renovate (push) Successful in 2m17s
Reviewed-on: #338
2026-02-16 05:55:20 +00:00
568f9e5164 chore(deps): update dependency @eslint-react/eslint-plugin to v2.13.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / guarddog (pull_request) Successful in 43s
test-build / build (pull_request) Successful in 1m50s
2026-02-16 05:40:13 +00:00
51 changed files with 1292 additions and 1153 deletions

View File

@@ -25,8 +25,10 @@ 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_REDIS_URL: ${{ vars.RENOVATE_REDIS_URL }} RENOVATE_REGISTRY_ALIASES: '{"dhi.io": "dhi.io"}'
RENOVATE_HOST_RULES: '[{"matchHost":"dhi.io","hostType":"docker","username":"${{ secrets.RENOVATE_DHI_USER }}","password":"${{ secrets.RENOVATE_DHI_TOKEN }}"}]'

View File

@@ -22,13 +22,11 @@ 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 LABEL version="2.14.1"
ENV SITE_URL=https://www.alexlebens.dev
ENV DIRECTUS_URL=https://directus.alexlebens.net
ENV PORT=4321
LABEL version="2.10.1"
LABEL description="Astro based personal website" LABEL description="Astro based personal website"
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE $PORT EXPOSE $PORT
CMD ["node", "./dist/server/entry.mjs"] CMD ["node", "./dist/server/entry.mjs"]

View File

@@ -9,17 +9,16 @@ import tailwindcss from '@tailwindcss/vite';
import icon from 'astro-icon'; import icon from 'astro-icon';
import swup from '@swup/astro'; import swup from '@swup/astro';
const getSiteURL = () => { import { getSiteURL } from './src/support/url';
if (process.env.SITE_URL) {
return `https://${process.env.SITE_URL}`;
}
return 'http://localhost:4321';
};
export default defineConfig({ 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.10.1", "version": "2.14.1",
"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",
@@ -39,18 +39,16 @@
"@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.70",
"@playform/compress": "^0.2.1", "@playform/compress": "^0.2.1",
"@swup/astro": "^1.7.0", "@swup/astro": "^1.8.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.14",
"@types/unist": "^3.0.3", "@types/unist": "^3.0.3",
"astro": "^5.17.2", "astro": "^5.17.2",
"astro-compressor": "^1.2.0",
"astro-icon": "^1.1.5", "astro-icon": "^1.1.5",
"marked": "^17.0.2", "marked": "^17.0.2",
"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.34.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",
@@ -62,13 +60,12 @@
"ultrahtml": "^1.6.0" "ultrahtml": "^1.6.0"
}, },
"devDependencies": { "devDependencies": {
"@eslint-react/eslint-plugin": "^2.12.4", "@eslint-react/eslint-plugin": "^2.13.0",
"@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.5.0", "eslint-plugin-astro": "^1.6.0",
"eslint-plugin-format": "^1.4.0", "eslint-plugin-format": "^1.4.0",
"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",

901
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,92 +1,80 @@
--- ---
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="w-full overflow-hidden bg-stone-300/40 dark:bg-stone-800/20" class="bg-background-accent w-full overflow-hidden"
transition:animate="none" transition:animate="none"
> >
<div class="relative px-4 pt-16 pb-12 sm:px-6"> <div class="relative px-4 sm:px-6 pt-16 pb-12">
<div class="mx-auto max-w-340"> <div class="max-w-340 mx-auto">
<div class="grid grid-cols-1 gap-10 md:grid-cols-12"> <div class="grid grid-cols-1 md:grid-cols-12 gap-10">
<!-- 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 rounded-lg"> <div class="mx-auto aspect-square overflow-hidden">
<BrandLogo class="max-h-10 max-w-10 rounded-full" /> <BrandLogo class="rounded-lg max-h-10 max-w-10"/>
</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 <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-['']">
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" Site
>
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="group flex items-center text-base text-neutral-600 transition-colors hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200" class="inline-flex items-center text-secondary hover:text-secondary-hover text-base transition-all duration-300 overflow-hidden"
> >
<span class="relative inline-block overflow-hidden"> {link.name}
<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 <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-['']">
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="group flex items-center text-base text-neutral-600 transition-colors hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200" class="inline-flex items-center text-secondary hover:text-secondary-hover text-base transition-all duration-300 overflow-hidden"
> >
<span class="relative inline-block overflow-hidden"> {link.name}
<span class="relative z-10">{link.name}</span>
</span>
</a> </a>
</li> </li>
)) ))}
}
</ul> </ul>
</div> </div>
<!-- Right image --> <!-- Right image -->
<div class="col-span-3 mt-10 flex justify-center md:mt-0"> <div class="flex justify-center col-span-4 mt-10 md:mt-0">
<div class="-mt-10 hidden max-h-[460px] max-w-[220px] scale-80 md:block"> <div class="md:block max-h-115 max-w-55 -mt-10 scale-80 hidden">
<Image <Image
src={footerImg} src={footerImg}
alt={global.footer_image_alt} alt={global.footer_image_alt}
@@ -102,37 +90,36 @@ const currentYear = new Date().getFullYear();
</div> </div>
</div> </div>
<!-- Bottom section --> <!-- Bottom section -->
<div class="mt-12 border-t border-neutral-400/30 pt-8 dark:border-neutral-600/50"> <div class="border-t border-divider pt-8 mt-12">
<div class="flex flex-col items-center justify-between gap-4 md:flex-row"> <div class="flex flex-col md:flex-row items-center justify-between gap-4">
<p class="text-sm text-neutral-600 dark:text-neutral-400"> <p class="text-secondary text-sm">
&copy; {currentYear} All rights reserved. &copy; {currentYear} All rights reserved.
</p> </p>
<div class="flex items-center"> <div class="flex items-center">
<p class="text-xs text-neutral-500 dark:text-neutral-400"> <span class="text-secondary text-sm">
Weather provided by Weather provided by
</p> </span>
<a <a
href="https://open-meteo.com/" href="https://open-meteo.com/"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
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" class="group inline-flex items-center text-secondary hover:text-secondary-hover text-sm transition-all duration-300"
> >
<span class="relative ml-1"> <span class="relative underline ml-1">
Open-Meteo. Open-Meteo.
</span> </span>
</a> </a>
<div class="ml-4"/>
<div class="ml-4"></div> <span class="text-secondary text-sm">
Built with
<span class="text-xs text-neutral-500 dark:text-neutral-400">Built with </span> </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-xs text-neutral-600 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100" class="group inline-flex items-center text-secondary hover:text-secondary-hover text-sm transition-all duration-300"
> >
<span class="relative ml-1"> <span class="relative underline ml-1">
Astro. Astro.
</span> </span>
</a> </a>

View File

@@ -9,31 +9,31 @@ const currentPath = pathname.slice(1);
<header <header
id="nav" id="nav"
class="sticky inset-x-0 top-4 z-50 flex w-full flex-wrap text-sm transition-none md:flex-nowrap md:justify-start" class="fixed flex flex-wrap md:flex-nowrap md:justify-start inset-x-0 top-0 w-full z-50"
> >
<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="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" class="nav-base relative md:flex md:items-center md:justify-between rounded-[36px] w-full px-4 mx-2 py-3 mt-4"
aria-label="Global" aria-label="Global"
> >
<div class="flex items-center justify-between"> <div class="flex items-center justify-between ml-0">
<a <a
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" class="flex-none rounded-full h-10.5"
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 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" 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"
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 h-5 w-5 shrink-0" class="hs-collapse-open:hidden shrink-0 h-5 w-5"
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 hidden h-5 w-5 shrink-0" class="hs-collapse-open:block shrink-0 h-5 w-5 hidden"
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@@ -63,33 +63,33 @@ 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 hidden grow basis-full overflow-hidden transition-all duration-300 md:block md:overflow-visible" 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"
> >
<div <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">
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) => {
>
{
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 ${ class={`text-sm font-medium ${isActive ? 'text-active' : 'text-secondary hover:text-secondary-hover'}`}
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>
<span class="md:inline-block"> </div>
<div class="hidden md:flex ml-2">
<span class="">
<ThemeToggleButton /> <ThemeToggleButton />
</span> </span>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
---
import { Icon } from 'astro-icon/components';
import type { Post } from '@lib/directusTypes';
import Image from '@components/ui/images/Image.astro';
import { formatDate } from '@support/time';
import { getDirectusImageURL } from '@/support/url';
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

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,55 @@
---
import { Icon } from 'astro-icon/components';
import Image from '@components/ui/images/Image.astro';
import { getDirectusImageURL } from '@/support/url';
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

@@ -0,0 +1,83 @@
---
import { Icon } from 'astro-icon/components';
import Image from '@components/ui/images/Image.astro';
import { getDirectusImageURL } from '@/support/url';
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

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

View File

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

View File

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

View File

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

View File

@@ -18,7 +18,7 @@ const experiences = ((await directus.request(
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-10"> <h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-10">
Experience Experience
</h3> </h3>
<ul class="ml-8 w-full flex flex-col"> <ul class="flex flex-col w-full ml-8 pr-8">
{experiences.map((experience: Experience) => { {experiences.map((experience: Experience) => {
const startYear = new Date(experience.startDate).getFullYear(); const startYear = new Date(experience.startDate).getFullYear();
const endYear = experience.endDate != null ? new Date(experience.endDate).getFullYear() : 'Present'; const endYear = experience.endDate != null ? new Date(experience.endDate).getFullYear() : 'Present';
@@ -36,7 +36,7 @@ const experiences = ((await directus.request(
</time> </time>
</header> </header>
<div class="relative flex flex-col sm:col-span-12 pb-6"> <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"/> <div class="absolute bg-accent -translate-x-[1.71rem] rounded-full h-2 w-2 mt-3"/>
<h3> <h3>
<div <div
class="inline-flex items-center text-2xl leading-tight font-semibold" class="inline-flex items-center text-2xl leading-tight font-semibold"
@@ -78,7 +78,7 @@ const experiences = ((await directus.request(
</div> </div>
)} )}
{(experience.responsibilities || experience.achievements) && ( {(experience.responsibilities || experience.achievements) && (
<div class="relative flex flex-col gap-4 max-sm:h-auto! md:after:absolute md:after:bottom-0 md:after:h-12 md:after:w-full md:after:bg-gradient-to-t md:after:from-neutral-200 dark:md:after:from-stone-700 md:after:content-[''] " :class="expanded ? 'after:hidden' : ''" x-show="expanded" x-collapse.min.50px> <div class="relative flex flex-col gap-4 after:absolute after:bottom-0 after:h-12 after:w-full after:bg-linear-to-t after:from-neutral-200 dark:after:from-stone-700 after:content-[''] " :class="expanded ? 'after:hidden' : ''" x-show="expanded" x-collapse.min.50px>
{experience.responsibilities && ( {experience.responsibilities && (
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<h4 class="text-header font-semibold"> <h4 class="text-header font-semibold">
@@ -129,7 +129,7 @@ const experiences = ((await directus.request(
</button> </button>
<ul <ul
class="flex print:hidden flex-wrap gap-2" class="flex print:hidden flex-wrap gap-2 mt-2"
aria-label="Technologies used" aria-label="Technologies used"
> >
{experience.skills && experience.skills.map(skill => { {experience.skills && experience.skills.map(skill => {

View File

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

View File

@@ -17,7 +17,7 @@ interface Props {
const { title, subTitle, primaryBtn, primaryBtnURL, secondaryBtn, secondaryBtnURL, src, alt } = Astro.props; const { title, subTitle, primaryBtn, primaryBtnURL, secondaryBtn, secondaryBtnURL, src, alt } = Astro.props;
const roundedClasses = Astro.props.rounded ? "rounded-xl" : null; 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"> <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">

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,35 @@
---
import type { Post } from '@lib/directusTypes';
import LargeBlogLeftCard from '@components/cards/LargeBlogLeftCard.astro';
import LargeBlogRightCard from '@components/cards/LargeBlogRightCard.astro';
interface Props {
posts: Post[];
}
const { posts } = Astro.props;
---
<section class="smooth-reveal 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

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

View File

@@ -2,13 +2,13 @@ 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/' },

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,12 +20,16 @@ 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>{normalizeTitle}</title> <title>
{normalizeTitle}
</title>
<BaseHead <BaseHead
title={normalizeTitle} title={normalizeTitle}
description={description} description={description}
@@ -34,7 +38,9 @@ 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')) {
@@ -53,30 +59,35 @@ const normalizeTitle = !title ? global.name : `${title} | ${global.name}`;
} }
window.localStorage.setItem('theme', theme); window.localStorage.setItem('theme', theme);
</script> </script>
<!-- Rybbit Tracking Snippet --> <!-- Rybbit Tracking Snippet -->
<script <script
src="https://rybbit.alexlebens.dev/api/script.js" src="https://rybbit.alexlebens.dev/api/script.js"
data-site-id={global.rybbit_site_id} data-site-id={global.rybbit_site_id}
defer defer
></script> />
</head> </head>
<body class="bg-stone-200 selection:bg-yellow-400 selection:text-neutral-700 dark:bg-stone-700">
<div class="mx-auto w-full max-w-(--breakpoint-2xl) grow px-4 sm:px-6 lg:px-8"> <body class="bg-background selection:bg-yellow-400">
<!-- Disabled texture background for now
<div class="fixed inset-0 -z-10">
<div class="bg-grid-pattern absolute inset-0 mask-[radial-gradient(white,transparent_85%)] bg-position-[center_top_-1px]"/>
</div>
-->
<div class="grow w-full max-w-(--breakpoint-2xl) px-4 sm:px-6 lg:px-8 py-20 mx-auto">
<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

@@ -12,7 +12,7 @@ import type {
Skill, Skill,
} from '@lib/directusTypes'; } from '@lib/directusTypes';
import { getDirectusURL } from '@lib/directusFunctions'; import { getDirectusURL } from '@/support/url';
type Schema = { type Schema = {
site_global: Global; site_global: Global;

View File

@@ -1,10 +1,10 @@
--- ---
import { readSingleton } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import GoBackButton from '@/components/buttons/GoBackButton.astro'; import GoBackButton from '@/components/buttons/GoBackButton.astro';
import GoHomeButton from '@/components/buttons/GoHomeButton.astro'; import GoHomeButton from '@/components/buttons/GoHomeButton.astro';
import BaseLayout from '@layouts/BaseLayout.astro';
import directus from '@lib/directus';
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
--- ---
@@ -28,45 +28,44 @@ const global = await directus.request(readSingleton('site_global'));
}, },
}} }}
> >
<section class="mt-20 grid place-content-center">
<div class="mx-auto max-w-7xl px-4 py-8 lg:px-6 lg:py-16"> <section class="grid place-content-center mt-20">
<div class="mx-auto max-w-screen-sm text-center"> <div class="max-w-7xl px-4 lg:px-6 py-8 lg:py-16 mx-auto">
<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-9xl leading-none font-bold text-neutral-900 sm:text-[12rem] dark:text-neutral-100" class="glitch text-header text-9xl font-bold leading-none sm:text-[12rem]"
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">
<h1 Page Not Found:
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>
<div <h1 class="smooth-reveal card-text-header mt-8 mb-30">
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" {Astro.url.pathname.replace('/', '')}
> </h1>
<h3 <div class="smooth-reveal card-base max-w-md p-6 mx-auto mt-16">
class="text-sm font-medium tracking-wider text-neutral-500 uppercase dark:text-neutral-400" <h3 class="card-text-title text-sm tracking-wider uppercase">
>
Did you know? Did you know?
</h3> </h3>
<p class="mt-2 text-sm text-neutral-600 dark:text-neutral-300" id="fun-fact"> <p
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 <div class="smooth-reveal flex flex-col sm:flex-row items-center justify-center gap-4 mt-10">
class="smooth-reveal mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row"
>
<GoBackButton/> <GoBackButton/>
<GoHomeButton url={global.site_url} /> <GoHomeButton url={global.site_url} />
</div> </div>
</div> </div>
</div> </div>
</section> </section>
</BaseLayout> </BaseLayout>
<script> <script>

View File

@@ -1,13 +1,13 @@
--- ---
import { readSingleton } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import HeroSection from '@components/sections/HeroSection.astro'; import HeroSection from '@components/sections/HeroSection.astro';
import ExperienceSection from '@components/sections/ExperienceSection.astro'; import ExperienceSection from '@components/sections/ExperienceSection.astro';
import EducationSection from '@components/sections/EducationSection.astro'; import EducationSection from '@components/sections/EducationSection.astro';
import ProjectSection from '@components/sections/ProjectSection.astro'; import ProjectSection from '@components/sections/ProjectSection.astro';
import SkillsSliderSection from '@components/sections/SkillsSliderSection.astro'; import SkillsSliderSection from '@components/sections/SkillsSliderSection.astro';
import BaseLayout from '@layouts/BaseLayout.astro';
import directus from '@lib/directus';
import portraitImg from '@images/portrait.avif'; import portraitImg from '@images/portrait.avif';
@@ -42,7 +42,7 @@ const global = await directus.request(readSingleton('site_global'));
rounded={true} rounded={true}
/> />
<section class="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8 lg:py-14"> <section class="max-w-7xl px-4 sm:px-6 lg:px-8 py-10 lg:py-14 mx-auto">
<div class="flex flex-col gap-y-24 md:gap-y-32"> <div class="flex flex-col gap-y-24 md:gap-y-32">
<ExperienceSection className="smooth-reveal" /> <ExperienceSection className="smooth-reveal" />
<EducationSection className="smooth-reveal" /> <EducationSection className="smooth-reveal" />

View File

@@ -1,10 +1,10 @@
--- ---
import { readSingleton } from '@directus/sdk'; import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import HeroSection from '@components/sections/HeroSection.astro'; import HeroSection from '@components/sections/HeroSection.astro';
import ApplicationSection from '@components/sections/ApplicationSection.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'; import applicationImg from '@images/cedar_tree.png';

View File

@@ -1,17 +1,19 @@
--- ---
import { type CollectionEntry, getCollection } from 'astro:content'; import { type CollectionEntry, getCollection } from 'astro:content';
import getReadingTime from 'reading-time'; import getReadingTime from 'reading-time';
import { readItems, readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import { marked } from 'marked'; import { marked } from 'marked';
import markedShiki from 'marked-shiki'; import markedShiki from 'marked-shiki';
import { createHighlighter } from 'shiki'; import { createHighlighter } from 'shiki';
import { getDirectusImageURL } from '@lib/directusFunctions'; import { readItems, readSingleton } from '@directus/sdk';
import BaseLayout from '@layouts/BaseLayout.astro';
import Image from '@components/ui/images/Image.astro'; import Image from '@components/ui/images/Image.astro';
import SocialShareButton from '@components/buttons/SocialShareButton.astro'; import SocialShareButton from '@components/buttons/SocialShareButton.astro';
import { formatDateTime } from '@support/time'; import BaseLayout from '@layouts/BaseLayout.astro';
import directus from '@lib/directus';
import { formatDate } from '@support/time';
import { getDirectusImageURL } from '@/support/url';
const post = Astro.props;
export async function getStaticPaths() { export async function getStaticPaths() {
const posts = await directus.request(readItems('posts')); const posts = await directus.request(readItems('posts'));
@@ -20,18 +22,19 @@ export async function getStaticPaths() {
props: post, props: post,
})); }));
} }
const post = Astro.props;
const global = await directus.request(readSingleton('site_global')); const global = await directus.request(readSingleton('site_global'));
const category: CollectionEntry<'categories'> = (await getCollection('categories')) const category: CollectionEntry<'categories'> = (await getCollection('categories'))
.filter((c) => c.slug === post.category) .filter((c) => c.slug === post.category)
.pop() as CollectionEntry<'categories'>; .pop() as CollectionEntry<'categories'>;
const readingTime = getReadingTime(post.content); const readingTime = getReadingTime(post.content);
const highlighter = await createHighlighter({ const highlighter = await createHighlighter({
themes: ['github-light', 'github-dark', 'monokai'], themes: ['github-light', 'github-dark'],
langs: ['typescript', 'python', 'css', 'html', 'yaml', 'bash', 'json'], langs: ['typescript', 'python', 'css', 'html', 'yaml', 'bash', 'json'],
}); });
marked.use(markedShiki({ marked.use(markedShiki({
highlight(code, lang) { highlight(code, lang) {
return highlighter.codeToHtml(code, { return highlighter.codeToHtml(code, {
@@ -44,6 +47,7 @@ marked.use(markedShiki({
}); });
} }
})); }));
const content = marked.parse(post.content); const content = marked.parse(post.content);
--- ---
@@ -64,9 +68,7 @@ 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,
@@ -79,11 +81,12 @@ 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="mt-4 rounded-2xl shadow-none sm:mt-0 sm:shadow-sm"> <div class="sm:shadow-xs sm:dark:shadow-md rounded-2xl mt-4 sm:mt-0">
<Image <Image
class="max-h-[600px] w-full rounded-t-2xl object-cover" class="rounded-2xl sm:rounded-b-none w-full max-h-150 object-cover"
src={getDirectusImageURL(post.image)} src={getDirectusImageURL(post.image)}
alt={post.image_alt} alt={post.image_alt}
draggable="false" draggable="false"
@@ -91,86 +94,60 @@ const content = marked.parse(post.content);
loading="lazy" loading="lazy"
inferSize={true} inferSize={true}
/> />
<div <div class="sm:bg-background-card rounded-b-2xl px-0 sm:px-6 md:px-10 lg:px-14 py-6">
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" <div class="text-center sm:text-left mt-4">
> <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="mt-8 flex items-center whitespace-nowrap"> <ol class="flex items-center justify-center sm:justify-start whitespace-nowrap gap-2 sm:gap-0 mt-6 sm:mt-4">
<li class="inline-flex items-center"> <li class="inline-flex items-center">
<a <a
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" class="inline-flex items-center text-secondary hover:text-secondary-hover text-sm transition-all duration-300"
href=`/categories/${category.slug}` href=`/categories/${category.slug}`
data-astro-prefetch
> >
{category?.data?.title} {category?.data?.title}
</a> </a>
<svg <span class="shrink-0 text-secondary text-sm mx-2 sm:mx-4">
class="mx-2 size-5 flex-shrink-0 text-neutral-500 dark:text-neutral-500" /
width="16" </span>
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 <li class="inline-flex items-center">
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" <span class="shrink-0 text-secondary text-sm">
> {formatDate(post.published_date)}
{formatDateTime(post.published_date)} </span>
<svg <span class="shrink-0 text-secondary text-sm mx-2 sm:mx-4">
class="mx-2 size-5 flex-shrink-0 text-neutral-500 dark:text-neutral-500" /
width="16" </span>
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 <li class="inline-flex items-center">
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" <span class="shrink-0 text-secondary text-sm">
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 <article class="text-header prose prose-blog sm:prose-lg dark:prose-invert max-w-none">
class="prose prose-blog sm:prose-lg dark:prose-invert max-w-none text-neutral-800 dark:text-neutral-200"
>
<div set:html={content} /> <div set:html={content} />
</article> </article>
<div <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">
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" <div class="flex flex-wrap sm:flex-nowrap sm:items-center gap-x-2 gap-y-1 sm:gap-y-0">
> {post.tags.map((tag: string) => (
<div class="flex flex-wrap gap-x-2 gap-y-1 sm:flex-nowrap sm:items-center sm:gap-y-0"> <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">
{
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 <SocialShareButton pageTitle={post.title}/>
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 {
@@ -184,6 +161,7 @@ const content = marked.parse(post.content);
} }
} }
</style> </style>
</BaseLayout> </BaseLayout>
<script> <script>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,31 +1,14 @@
// 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 robotsTxt = ` const getRobotsTxt = (sitemapURL: URL) => `\
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: ${new URL('sitemap-index.xml', import.meta.env.SITE).href}`.trim(); Sitemap: ${sitemapURL.href}
`;
export const GET: APIRoute = () => { export const GET: APIRoute = ({ site }) => {
return new Response(robotsTxt, { const sitemapURL = new URL('sitemap-index.xml', site);
headers: { return new Response(getRobotsTxt(sitemapURL));
'Content-Type': 'text/plain; charset=utf-8',
},
});
}; };

View File

@@ -9,8 +9,8 @@
/* https://tailwindcss.com/docs/dark-mode */ /* https://tailwindcss.com/docs/dark-mode */
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
/* Custom colors */
@theme { @theme {
/* Custom colors */
--color-midnight: #0c354d; --color-midnight: #0c354d;
--color-ocean: #134e70; --color-ocean: #134e70;
@@ -26,13 +26,24 @@
--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-main: light-dark(var(--color-steel), var(--color-bermuda));
--color-accent: light-dark(var(--color-bronze), var(--color-desert)); --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-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: light-dark(var(--color-neutral-600), var(--color-neutral-200));
--color-primary-hover: light-dark(var(--color-neutral-800), var(--color-neutral-400)); --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: 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 {

View File

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

@@ -17,21 +17,6 @@ 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';
@@ -46,4 +31,12 @@ function timeago(date?: Date): string {
return format(date, 'timeago'); return format(date, 'timeago');
} }
export { formatDate, timeago, formatDateTime }; function formatDate(date: Date): string {
return new Date(date).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
})
}
export { formatDate, timeago };

View File

@@ -1,12 +1,13 @@
const getDirectusURL = () => { const getDirectusURL = () => {
if (process.env.DIRECTUS_URL) {
return `https://${process.env.DIRECTUS_URL}`;
}
return 'https://directus.alexlebens.net'; return 'https://directus.alexlebens.net';
}; };
const getSiteURL = () => {
return 'https://www.alexlebens.dev';
};
async function getDirectusImageURL(image: string) { async function getDirectusImageURL(image: string) {
return `${getDirectusURL()}/assets/${image}`; return `${getDirectusURL()}/assets/${image}`;
} }
export { getDirectusURL, getDirectusImageURL }; export { getDirectusURL, getSiteURL, getDirectusImageURL };