Compare commits
1 Commits
2.15.1
...
0d1748d907
| Author | SHA1 | Date | |
|---|---|---|---|
|
0d1748d907
|
@@ -25,10 +25,8 @@ jobs:
|
||||
RENOVATE_ENDPOINT: ${{ vars.INSTANCE_URL }}
|
||||
RENOVATE_REPOSITORIES: alexlebens/site-profile
|
||||
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net>
|
||||
RENOVATE_REDIS_URL: ${{ vars.RENOVATE_REDIS_URL }}
|
||||
LOG_LEVEL: info
|
||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||
RENOVATE_GIT_PRIVATE_KEY: ${{ secrets.RENOVATE_GIT_PRIVATE_KEY }}
|
||||
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
|
||||
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 }}"}]'
|
||||
RENOVATE_REDIS_URL: ${{ vars.RENOVATE_REDIS_URL }}
|
||||
|
||||
@@ -22,11 +22,13 @@ WORKDIR /app
|
||||
COPY --from=prod-deps /app/node_modules /app/node_modules
|
||||
COPY --from=build /app/dist /app/dist
|
||||
|
||||
LABEL version="2.15.1"
|
||||
LABEL description="Astro based personal website"
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
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"
|
||||
|
||||
EXPOSE $PORT
|
||||
CMD ["node", "./dist/server/entry.mjs"]
|
||||
|
||||
@@ -9,17 +9,17 @@ import tailwindcss from '@tailwindcss/vite';
|
||||
import icon from 'astro-icon';
|
||||
import swup from '@swup/astro';
|
||||
|
||||
import { getSiteURL } from './src/support/url';
|
||||
const getSiteURL = () => {
|
||||
if (process.env.SITE_URL) {
|
||||
return `https://${process.env.SITE_URL}`;
|
||||
}
|
||||
return 'http://localhost:4321';
|
||||
};
|
||||
|
||||
export default defineConfig({
|
||||
site: getSiteURL(),
|
||||
|
||||
image: {
|
||||
remotePatterns: [
|
||||
{ protocol: 'https', hostname: '*.alexlebens.net' },
|
||||
{ protocol: 'https', hostname: '*.jsdelivr.net' },
|
||||
{ protocol: 'https', hostname: '*.icons8.com' },
|
||||
],
|
||||
service: {
|
||||
entrypoint: 'astro/assets/services/sharp',
|
||||
}
|
||||
|
||||
11
package.json
11
package.json
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "site-profile",
|
||||
"type": "module",
|
||||
"version": "2.15.1",
|
||||
"version": "2.10.1",
|
||||
"homepage": "https://www.alexlebens.dev",
|
||||
"bugs": {
|
||||
"url": "https://gitea.alexlebens.dev/alexlebens/site-profile/issues",
|
||||
@@ -39,16 +39,18 @@
|
||||
"@iconify-json/pajamas": "^1.2.15",
|
||||
"@iconify-json/simple-icons": "^1.2.70",
|
||||
"@playform/compress": "^0.2.1",
|
||||
"@swup/astro": "^1.8.0",
|
||||
"@swup/astro": "^1.7.0",
|
||||
"@tailwindcss/postcss": "^4.1.18",
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/unist": "^3.0.3",
|
||||
"astro": "^5.17.2",
|
||||
"astro-compressor": "^1.2.0",
|
||||
"astro-icon": "^1.1.5",
|
||||
"marked": "^17.0.2",
|
||||
"marked-shiki": "^1.2.1",
|
||||
"mdast-util-to-string": "^4.0.0",
|
||||
"motion": "^12.34.0",
|
||||
"preline": "^4.0.1",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
@@ -60,12 +62,13 @@
|
||||
"ultrahtml": "^1.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint-react/eslint-plugin": "^2.13.0",
|
||||
"@eslint-react/eslint-plugin": "^2.12.4",
|
||||
"@tailwindcss/forms": "^0.5.11",
|
||||
"@tailwindcss/typography": "^0.5.19",
|
||||
"astro-icon": "^1.1.5",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-astro": "^1.6.0",
|
||||
"eslint-plugin-astro": "^1.5.0",
|
||||
"eslint-plugin-format": "^1.4.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
|
||||
897
pnpm-lock.yaml
generated
897
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,10 @@ import { getImage } from 'astro:assets';
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import { SEO } from '@/config';
|
||||
|
||||
import brandSrc from '@images/brand_logo.png';
|
||||
import faviconSvgSrc from '@images/favicon_icon.svg';
|
||||
import faviconSrc from '@images/favicon_icon.png';
|
||||
import { SEO } from '@/config';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
@@ -19,7 +18,6 @@ interface Props {
|
||||
}
|
||||
|
||||
const canonicalURL = Astro.url.href;
|
||||
|
||||
let {
|
||||
title,
|
||||
description,
|
||||
@@ -29,14 +27,14 @@ let {
|
||||
structuredData = SEO.structuredData,
|
||||
} = Astro.props;
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
|
||||
let card = 'summary_large_image';
|
||||
if (!ogImage) {
|
||||
ogImage = brandSrc;
|
||||
card = 'summary';
|
||||
}
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
|
||||
const faviconSvg = await getImage({ src: faviconSvgSrc, format: 'svg' });
|
||||
const appleTouchIcon = await getImage({ src: faviconSrc, width: 180, height: 180, format: 'png' });
|
||||
const socialImageRes = await getImage({ src: ogImage, width: 1200, height: 600 });
|
||||
@@ -64,12 +62,12 @@ if (!socialImage.startsWith('http')) {
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||
<meta name="mobile-web-app-capable" content="yes" />
|
||||
<meta name="theme-color" content="#facc15" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
|
||||
<!-- Open Graph -->
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:locale" content="en_US" />
|
||||
<meta property="og:url" content={Astro.url} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content={ogTitle} />
|
||||
<meta property="og:site_name" content={global.name} />
|
||||
<meta property="og:description" content={ogDescription} />
|
||||
@@ -78,10 +76,17 @@ if (!socialImage.startsWith('http')) {
|
||||
<meta content="600" property="og:image:height" />
|
||||
<meta content="image/png" property="og:image:type" />
|
||||
|
||||
<!-- Twitter -->
|
||||
<meta property="twitter:card" content={card} />
|
||||
<meta property="twitter:url" content={Astro.url} />
|
||||
<meta property="twitter:domain" content={Astro.url} />
|
||||
<meta property="twitter:title" content={ogTitle} />
|
||||
<meta property="twitter:description" content={ogDescription} />
|
||||
<meta property="twitter:image" content={socialImage} />
|
||||
|
||||
<!-- Links -->
|
||||
<link href={canonicalURL} rel="canonical" />
|
||||
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||
<link rel="alternate" type="application/rss+xml" title={title} href="/rss.xml" />
|
||||
<!--<link href="/manifest.json" rel="manifest" />-->
|
||||
<link href="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" />
|
||||
<link href={faviconSvg.src} rel="icon" type="image/svg+xml" sizes="any" />
|
||||
|
||||
@@ -1,80 +1,92 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import BrandLogo from '@components/images/BrandLogo.astro';
|
||||
import directus from '@lib/directus';
|
||||
import BrandLogo from '@components/ui/logos/BrandLogo.astro';
|
||||
import Image from '@components/ui/images/Image.astro';
|
||||
import { NavigationLinks, FooterLinks } from '@/config';
|
||||
|
||||
import footerImg from '@images/flowers.png';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
|
||||
const currentYear = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer
|
||||
class="bg-background-accent w-full overflow-hidden"
|
||||
class="w-full overflow-hidden bg-stone-300/40 dark:bg-stone-800/20"
|
||||
transition:animate="none"
|
||||
>
|
||||
<div class="relative px-4 sm:px-6 pt-16 pb-12">
|
||||
<div class="max-w-340 mx-auto">
|
||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-10">
|
||||
<div class="relative px-4 pt-16 pb-12 sm:px-6">
|
||||
<div class="mx-auto max-w-340">
|
||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-12">
|
||||
<!-- Brand section -->
|
||||
<div class="col-span-1 md:col-span-3">
|
||||
<a href="/" class="group inline-block">
|
||||
<div class="flex items-center">
|
||||
<div class="mx-auto aspect-square overflow-hidden">
|
||||
<BrandLogo class="rounded-lg max-h-10 max-w-10"/>
|
||||
<div class="mx-auto aspect-square overflow-hidden rounded-lg">
|
||||
<BrandLogo class="max-h-10 max-w-10 rounded-full" />
|
||||
</div>
|
||||
<span class="text-header text-lg lg:text-2xl font-semibold leading-tight tracking-tight text-balance ml-3">
|
||||
|
||||
<span class="ml-3 text-xl font-bold text-neutral-800 dark:text-neutral-200">
|
||||
{global.name}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<p class="text-primary text-sm lg:text-base text-pretty leading-relaxed mt-4">
|
||||
|
||||
<p class="mt-4 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
|
||||
{global.about}
|
||||
</p>
|
||||
</div>
|
||||
<!-- Left links -->
|
||||
<div class="col-span-1 md:col-span-2">
|
||||
<h3 class="relative inline-block text-header after:bg-main text-sm uppercase font-semibold tracking-wider pb-2 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-['']">
|
||||
Site
|
||||
<h3
|
||||
class="after:bg-steel dark:after:bg-bermuda relative inline-block pb-2 text-sm font-semibold tracking-wider text-neutral-800 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-[''] dark:text-neutral-100"
|
||||
>
|
||||
Blog
|
||||
</h3>
|
||||
<ul class="mt-4 space-y-3">
|
||||
{NavigationLinks.map((link) => (
|
||||
<li>
|
||||
<a
|
||||
href={link.url}
|
||||
class="inline-flex items-center text-secondary hover:text-secondary-hover text-base transition-all duration-300 overflow-hidden"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
NavigationLinks.map((link) => (
|
||||
<li>
|
||||
<a
|
||||
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"
|
||||
>
|
||||
<span class="relative inline-block overflow-hidden">
|
||||
<span class="relative z-10">{link.name}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Right links -->
|
||||
<div class="col-span-1 md:col-span-3">
|
||||
<h3 class="relative inline-block text-header after:bg-main text-sm uppercase font-semibold tracking-wider pb-2 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-['']">
|
||||
<h3
|
||||
class="after:bg-steel dark:after:bg-bermuda relative inline-block pb-2 text-sm font-semibold tracking-wider text-neutral-800 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-[''] dark:text-neutral-100"
|
||||
>
|
||||
Other
|
||||
</h3>
|
||||
<ul class="mt-4 space-y-3">
|
||||
{FooterLinks.map((link) => (
|
||||
<li>
|
||||
<a
|
||||
href={link.url}
|
||||
class="inline-flex items-center text-secondary hover:text-secondary-hover text-base transition-all duration-300 overflow-hidden"
|
||||
>
|
||||
{link.name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
{
|
||||
FooterLinks.map((link) => (
|
||||
<li>
|
||||
<a
|
||||
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"
|
||||
>
|
||||
<span class="relative inline-block overflow-hidden">
|
||||
<span class="relative z-10">{link.name}</span>
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
))
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
<!-- Right image -->
|
||||
<div class="flex justify-center col-span-4 mt-10 md:mt-0">
|
||||
<div class="md:block max-h-115 max-w-55 -mt-10 scale-80 hidden">
|
||||
<div class="col-span-3 mt-10 flex justify-center md:mt-0">
|
||||
<div class="-mt-10 hidden max-h-[460px] max-w-[220px] scale-80 md:block">
|
||||
<Image
|
||||
src={footerImg}
|
||||
alt={global.footer_image_alt}
|
||||
@@ -84,42 +96,43 @@ const currentYear = new Date().getFullYear();
|
||||
format="webp"
|
||||
quality="low"
|
||||
widths={[440]}
|
||||
inferSize={true}
|
||||
disableBlur={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Bottom section -->
|
||||
<div class="border-t border-divider pt-8 mt-12">
|
||||
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||
<p class="text-secondary text-sm">
|
||||
<div class="mt-12 border-t border-neutral-400/30 pt-8 dark:border-neutral-600/50">
|
||||
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
||||
<p class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
© {currentYear} All rights reserved.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center">
|
||||
<span class="text-secondary text-sm">
|
||||
<p class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
Weather provided by
|
||||
</span>
|
||||
</p>
|
||||
<a
|
||||
href="https://open-meteo.com/"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group inline-flex items-center text-secondary hover:text-secondary-hover text-sm transition-all duration-300"
|
||||
class="group inline-flex items-center text-xs text-neutral-600 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
>
|
||||
<span class="relative underline ml-1">
|
||||
<span class="relative ml-1">
|
||||
Open-Meteo.
|
||||
</span>
|
||||
</a>
|
||||
<div class="ml-4"/>
|
||||
<span class="text-secondary text-sm">
|
||||
Built with
|
||||
</span>
|
||||
|
||||
<div class="ml-4"></div>
|
||||
|
||||
<span class="text-xs text-neutral-500 dark:text-neutral-400">Built with </span>
|
||||
<a
|
||||
href="https://astro.build"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group inline-flex items-center text-secondary hover:text-secondary-hover text-sm transition-all duration-300"
|
||||
class="group inline-flex items-center text-xs text-neutral-600 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
|
||||
>
|
||||
<span class="relative underline ml-1">
|
||||
<span class="relative ml-1">
|
||||
Astro.
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import BrandLogo from '@components/images/BrandLogo.astro';
|
||||
import ThemeToggleButton from '@components/buttons/ThemeToggleButton.astro';
|
||||
import BrandLogo from '@components/ui/logos/BrandLogo.astro';
|
||||
import ThemeToggle from '@components/ui/buttons/ThemeToggle.astro';
|
||||
import { NavigationLinks } from '@/config';
|
||||
|
||||
const pathname = new URL(Astro.request.url).pathname;
|
||||
@@ -9,30 +9,31 @@ const currentPath = pathname.slice(1);
|
||||
|
||||
<header
|
||||
id="nav"
|
||||
class="fixed flex flex-wrap md:flex-nowrap md:justify-start inset-x-0 top-0 w-full z-50"
|
||||
class="sticky inset-x-0 top-4 z-50 flex w-full flex-wrap text-sm transition-none md:flex-nowrap md:justify-start"
|
||||
>
|
||||
<nav
|
||||
class="nav-base relative md:flex md:items-center md:justify-between rounded-[36px] w-full px-4 mx-2 py-3 mt-4"
|
||||
class="relative mx-2 w-full rounded-[36px] border border-neutral-100 bg-neutral-100 px-4 py-3 md:flex md:items-center md:justify-between md:px-6 lg:px-8 dark:border-neutral-700/40 dark:bg-neutral-800/80"
|
||||
aria-label="Global"
|
||||
>
|
||||
<div class="flex items-center justify-between ml-0">
|
||||
<div class="flex items-center justify-between">
|
||||
<a
|
||||
class="flex-none rounded-full h-10.5"
|
||||
class="h-[42px] flex-none rounded-lg text-xl font-bold ring-neutral-500 outline-none focus-visible:ring dark:ring-neutral-200 dark:focus:outline-none"
|
||||
href="/"
|
||||
aria-label="Brand"
|
||||
>
|
||||
<BrandLogo class="h-full w-auto rounded-full object-cover"/>
|
||||
<BrandLogo class="h-full w-auto rounded-full object-cover" />
|
||||
</a>
|
||||
<div class="md:hidden mr-auto ml-4">
|
||||
|
||||
<div class="ml-auto md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
class="hs-collapse-toggle flex items-center justify-center text-secondary text-sm font-bold hover:bg-neutral-200 dark:hover:bg-neutral-700 rounded-full transition duration-300 disabled:pointer-events-none disabled:opacity-50 h-8 w-8"
|
||||
class="hs-collapse-toggle flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold text-neutral-600 transition duration-300 hover:bg-neutral-200 disabled:pointer-events-none disabled:opacity-50 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:focus:outline-none"
|
||||
data-hs-collapse="#navbar-collapse-with-animation"
|
||||
aria-controls="navbar-collapse-with-animation"
|
||||
aria-label="Toggle navigation"
|
||||
>
|
||||
<svg
|
||||
class="hs-collapse-open:hidden shrink-0 h-5 w-5"
|
||||
class="hs-collapse-open:hidden h-5 w-5 shrink-0"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -47,7 +48,7 @@ const currentPath = pathname.slice(1);
|
||||
<line x1="3" x2="21" y1="18" y2="18"></line>
|
||||
</svg>
|
||||
<svg
|
||||
class="hs-collapse-open:block shrink-0 h-5 w-5 hidden"
|
||||
class="hs-collapse-open:block hidden h-5 w-5 shrink-0"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -62,34 +63,34 @@ const currentPath = pathname.slice(1);
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="md:hidden ml-2 mr-2">
|
||||
<span class="">
|
||||
<ThemeToggleButton />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex md:flex-row items-center justify-between">
|
||||
|
||||
<div
|
||||
id="navbar-collapse-with-animation"
|
||||
class="hs-collapse hidden grow basis-full overflow-hidden transition-all duration-300 md:block"
|
||||
>
|
||||
<div
|
||||
id="navbar-collapse-with-animation"
|
||||
class="hs-collapse grow basis-full md:block transition-all duration-300 ml-2 mb-2 md:mb-0 hidden overflow-hidden md:overflow-visible"
|
||||
class="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"
|
||||
>
|
||||
<div class="flex flex-col md:flex-row md:items-center md:justify-end gap-x-0 md:gap-x-4 lg:gap-x-7 gap-y-4 md:gap-y-0 md:ps-7 mr-2 mt-5 md:mt-0">
|
||||
{NavigationLinks.map((item) => {
|
||||
const isActive = currentPath === (item.url === '/' ? '' : item.url.slice(1));
|
||||
return (
|
||||
<a
|
||||
href={item.url}
|
||||
class={`text-sm font-medium ${isActive ? 'text-active' : 'text-secondary hover:text-secondary-hover'}`}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hidden md:flex ml-2">
|
||||
<span class="">
|
||||
<ThemeToggleButton />
|
||||
{
|
||||
NavigationLinks.map((item) => {
|
||||
const isActive = currentPath === (item.url === '/' ? '' : item.url.slice(1));
|
||||
return (
|
||||
<a
|
||||
href={item.url}
|
||||
class={`text-sm font-medium ${
|
||||
isActive
|
||||
? 'text-orange-500 dark:text-orange-300'
|
||||
: 'text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100'
|
||||
}`}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
);
|
||||
})
|
||||
}
|
||||
<span class="md:inline-block">
|
||||
<ThemeToggle />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
61
src/components/blog/BlogCard.astro
Normal file
61
src/components/blog/BlogCard.astro
Normal file
@@ -0,0 +1,61 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
import { getDirectusImageURL } from '@lib/directusFunctions';
|
||||
import Image from '@components/ui/images/Image.astro';
|
||||
import { formatDate } from '@support/time';
|
||||
|
||||
interface Props {
|
||||
post: Post;
|
||||
}
|
||||
|
||||
const { post } = Astro.props;
|
||||
|
||||
const baseClasses = 'group group-hover smooth-reveal-cards rounded-xl flex flex-col';
|
||||
const borderClasses = 'border border-stone-200/50 dark:border-stone-700/50';
|
||||
const bgColorClasses =
|
||||
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
|
||||
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
|
||||
---
|
||||
|
||||
<div class={`${baseClasses}`}>
|
||||
<a
|
||||
class={`rounded-xl duration-300 transition-all ${borderClasses} ${shadowClasses} ${bgColorClasses}`}
|
||||
href={`/blog/${post.slug}/`}
|
||||
data-astro-prefetch
|
||||
>
|
||||
<div
|
||||
class="relative w-full flex-shrink-0 overflow-hidden rounded-t-xl before:absolute before:inset-x-0 before:z-[1] before:size-full"
|
||||
>
|
||||
<Image
|
||||
class="h-auto w-full rounded-t-xl"
|
||||
src={getDirectusImageURL(post.image)}
|
||||
alt={post.image_alt}
|
||||
draggable="false"
|
||||
loading="eager"
|
||||
format="webp"
|
||||
width="800"
|
||||
height="460"
|
||||
/>
|
||||
</div>
|
||||
<div class="rounded-xl p-4 md:p-5">
|
||||
<h3 class="text-xl font-bold text-neutral-600 dark:text-neutral-200">
|
||||
{post.title}
|
||||
</h3>
|
||||
<div
|
||||
class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-[44px] items-center font-medium text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-400"
|
||||
>
|
||||
<span class="relative inline-block overflow-hidden"> Read more </span>
|
||||
<Icon
|
||||
name="mdi:keyboard-arrow-right"
|
||||
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
|
||||
/>
|
||||
<p class="ml-auto text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{formatDate(post.published_date)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -8,30 +8,37 @@ interface Props {
|
||||
}
|
||||
|
||||
const { slug, title, description, count, publishDate } = Astro.props;
|
||||
|
||||
const baseClasses =
|
||||
'group group-hover rounded-xl flex h-full min-h-[220px] cursor-pointer flex-col overflow-hidden';
|
||||
const bgColorClasses =
|
||||
'bg-neutral-100/60 dark:bg-neutral-800/60 hover:bg-neutral-100 dark:hover:bg-neutral-800/90 ';
|
||||
---
|
||||
|
||||
<div class="smooth-reveal-cards group h-full">
|
||||
<a
|
||||
class="card-base flex flex-col h-full min-h-55"
|
||||
href={`/categories/${slug}/`}
|
||||
data-astro-prefetch
|
||||
>
|
||||
<div class="relative grow overflow-hidden">
|
||||
<a class={`rounded-xl`} href={`/categories/${slug}/`} data-astro-prefetch="false">
|
||||
<div class={`${baseClasses}`}>
|
||||
<div
|
||||
class={`relative min-h-0 flex-grow overflow-hidden transition-all duration-300 ${bgColorClasses}`}
|
||||
>
|
||||
<div class="absolute inset-1 flex flex-col p-3 md:p-4 lg:p-5">
|
||||
<div class="overflow-hidden">
|
||||
<h3 class="card-text-title-major card-hover-text-title whitespace-nowrap mb-4">
|
||||
<h2
|
||||
class="group-hover:text-steel dark:group-hover:text-bermuda transition-text mb-4 text-4xl font-extrabold tracking-tight text-balance whitespace-nowrap text-neutral-800 duration-300 dark:text-neutral-200"
|
||||
>
|
||||
{title}
|
||||
</h3>
|
||||
<p class="card-text-description mb-4">
|
||||
</h2>
|
||||
<p class="mb-4 font-light text-neutral-600 sm:text-lg dark:text-neutral-400">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
<div class="card-text-description flex items-center justify-between text-xs mt-auto pt-1 md:pt-2">
|
||||
<div
|
||||
class="mt-auto flex items-center justify-between pt-1 text-xs text-neutral-600 md:pt-2 dark:text-neutral-300"
|
||||
>
|
||||
<span class="inline-flex items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -44,8 +51,8 @@ const { slug, title, description, count, publishDate } = Astro.props;
|
||||
<span class="inline-flex items-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
width="12"
|
||||
height="12"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -59,5 +66,5 @@ const { slug, title, description, count, publishDate } = Astro.props;
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
44
src/components/blog/BlogLeftSection.astro
Normal file
44
src/components/blog/BlogLeftSection.astro
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||
import Image from '@components/ui/images/Image.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
btnExists?: boolean;
|
||||
btnTitle?: string;
|
||||
btnURL?: string;
|
||||
img: any;
|
||||
imgAlt: any;
|
||||
}
|
||||
|
||||
const { title, subTitle, btnExists, btnTitle, btnURL, img, imgAlt } = Astro.props;
|
||||
---
|
||||
|
||||
<section
|
||||
class="mx-auto max-w-[85rem] items-center gap-8 px-4 py-10 sm:px-6 sm:py-16 md:grid md:grid-cols-2 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 xl:gap-16 2xl:max-w-full"
|
||||
>
|
||||
<Image
|
||||
class="h-full w-full rounded-xl object-cover sm:max-h-[320px] md:max-h-[360px]"
|
||||
src={img}
|
||||
alt={imgAlt}
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
width="850"
|
||||
height="420"
|
||||
/>
|
||||
|
||||
<div class="mt-4 md:mt-0">
|
||||
<h2
|
||||
class="mb-4 text-4xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p
|
||||
class="mb-4 max-w-prose font-light text-pretty text-neutral-600 sm:text-lg dark:text-neutral-300"
|
||||
>
|
||||
{subTitle}
|
||||
</p>
|
||||
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
|
||||
</div>
|
||||
</section>
|
||||
29
src/components/blog/BlogRecentArticles.astro
Normal file
29
src/components/blog/BlogRecentArticles.astro
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
import BlogCard from '@components/blog/BlogCard.astro';
|
||||
|
||||
interface Props {
|
||||
posts: Post[];
|
||||
}
|
||||
|
||||
const { posts } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="mx-auto mb-10 max-w-[85rem] px-4 py-8 sm:px-6 lg:px-8 2xl:max-w-full">
|
||||
<div class="text-left">
|
||||
<h2
|
||||
id="recent-articles"
|
||||
class="smooth-reveal-2 mb-10 text-5xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
Recent Posts
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col md:flex-row md:space-x-12 lg:space-x-16">
|
||||
<div class="w-full">
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((b) => <BlogCard post={b} />)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
87
src/components/blog/BlogRightSection.astro
Normal file
87
src/components/blog/BlogRightSection.astro
Normal file
@@ -0,0 +1,87 @@
|
||||
---
|
||||
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||
import Image from '@components/ui/images/Image.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
btnExists?: boolean;
|
||||
btnTitle?: string;
|
||||
btnURL?: string;
|
||||
single?: boolean;
|
||||
imgOne?: any;
|
||||
imgOneAlt?: any;
|
||||
imgTwo?: any;
|
||||
imgTwoAlt?: any;
|
||||
}
|
||||
|
||||
const {
|
||||
title,
|
||||
subTitle,
|
||||
btnExists,
|
||||
btnTitle,
|
||||
btnURL,
|
||||
single,
|
||||
imgOne,
|
||||
imgOneAlt,
|
||||
imgTwo,
|
||||
imgTwoAlt,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<section
|
||||
class="mx-auto max-w-[85rem] items-center gap-16 px-4 py-10 sm:px-6 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 2xl:max-w-full"
|
||||
>
|
||||
<div>
|
||||
<h2
|
||||
class="mb-4 text-4xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
<p
|
||||
class="mb-4 max-w-prose font-light text-pretty text-neutral-600 sm:text-lg dark:text-neutral-400"
|
||||
>
|
||||
{subTitle}
|
||||
</p>
|
||||
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
|
||||
</div>
|
||||
|
||||
{
|
||||
single ? (
|
||||
<div class="mt-8">
|
||||
<Image
|
||||
class="w-full rounded-lg"
|
||||
src={imgOne}
|
||||
alt={imgOneAlt}
|
||||
format="webp"
|
||||
loading="lazy"
|
||||
width="850"
|
||||
height="420"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div class="mt-8 grid grid-cols-2 gap-4">
|
||||
<Image
|
||||
class="w-full rounded-xl"
|
||||
src={imgOne}
|
||||
alt={imgOneAlt}
|
||||
draggable="false"
|
||||
format="webp"
|
||||
loading="lazy"
|
||||
width="400"
|
||||
height="230"
|
||||
/>
|
||||
<Image
|
||||
class="mt-4 w-full rounded-xl lg:mt-10"
|
||||
src={imgTwo}
|
||||
alt={imgTwoAlt}
|
||||
draggable="false"
|
||||
format="webp"
|
||||
loading="lazy"
|
||||
width="400"
|
||||
height="230"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
44
src/components/blog/BlogSelectedArticles.astro
Normal file
44
src/components/blog/BlogSelectedArticles.astro
Normal file
@@ -0,0 +1,44 @@
|
||||
---
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
import { getDirectusImageURL } from '@lib/directusFunctions';
|
||||
import BlogLeftSection from '@components/blog/BlogLeftSection.astro';
|
||||
import BlogRightSection from '@components/blog/BlogRightSection.astro';
|
||||
|
||||
interface Props {
|
||||
posts: Post[];
|
||||
}
|
||||
|
||||
const { posts } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="smooth-reveal">
|
||||
{
|
||||
posts.map((b, index) =>
|
||||
index % 2 === 0 ? (
|
||||
<BlogLeftSection
|
||||
title={b.title}
|
||||
subTitle={b.description}
|
||||
btnExists={true}
|
||||
btnTitle="Read More"
|
||||
btnURL={`/blog/${b.slug}`}
|
||||
img={getDirectusImageURL(b.image)}
|
||||
imgAlt={b.image_alt}
|
||||
/>
|
||||
) : (
|
||||
<BlogRightSection
|
||||
title={b.title}
|
||||
subTitle={b.description}
|
||||
btnExists={true}
|
||||
btnTitle="Read More"
|
||||
btnURL={`/blog/${b.slug}`}
|
||||
single={!b.image_second}
|
||||
imgOne={getDirectusImageURL(b.image)}
|
||||
imgOneAlt={b.image_alt}
|
||||
imgTwo={getDirectusImageURL(b?.image_second)}
|
||||
imgTwoAlt={b?.image_second_alt}
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
</section>
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
interface Props {
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const { url } = Astro.props;
|
||||
---
|
||||
|
||||
<a
|
||||
class="button-base button-bg-gitea group inline-flex rounded-full gap-x-2"
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<div class="button-text-title flex relative items-center text-center">
|
||||
<Icon
|
||||
name="pajamas:gitea"
|
||||
class="h-4 w-4 md:h-6 md:w-6"
|
||||
/>
|
||||
<span class="ml-2">
|
||||
Continue to Gitea
|
||||
</span>
|
||||
<Icon
|
||||
name="mdi:keyboard-arrow-right"
|
||||
class="button-hover-arrow"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
<button
|
||||
class="button-base button-bg-blue group inline-flex rounded-lg gap-x-2"
|
||||
id="back-button"
|
||||
data-astro-prefetch
|
||||
>
|
||||
<div class="button-text-title flex relative items-center text-center">
|
||||
<svg
|
||||
class=" shrink-0 group-hover:-translate-x-1 transition duration-300 h-4 w-4"
|
||||
height=24
|
||||
width=24
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m15 18-6-6 6-6"/>
|
||||
</svg>
|
||||
<span class="ml-2">
|
||||
Go Back
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<script>
|
||||
document.getElementById('back-button')?.addEventListener('click', () => {
|
||||
window.history.back();
|
||||
});
|
||||
</script>
|
||||
@@ -1,25 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
interface Props {
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const { url } = Astro.props;
|
||||
---
|
||||
|
||||
<a
|
||||
class="button-base button-bg-teal group inline-flex rounded-lg gap-x-2"
|
||||
href={url}
|
||||
data-astro-prefetch
|
||||
>
|
||||
<div class="button-text-title flex relative items-center text-center">
|
||||
<Icon
|
||||
name="mdi:home-variant-outline"
|
||||
class="card-hover-icon-scale h-3 w-3 md:h-5 md:w-5"
|
||||
/>
|
||||
<span class="ml-2">
|
||||
Return Home
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
url?: string;
|
||||
noArrow?: boolean;
|
||||
}
|
||||
|
||||
const { title, url, noArrow } = Astro.props;
|
||||
---
|
||||
|
||||
<a
|
||||
class="button-base button-bg-teal group inline-flex rounded-lg gap-x-2"
|
||||
href={url}
|
||||
data-astro-prefetch
|
||||
>
|
||||
<div class="button-text-title flex relative items-center text-center">
|
||||
<span class="mr-2">
|
||||
{title}
|
||||
</span>
|
||||
{noArrow ? null : (
|
||||
<Icon
|
||||
name="mdi:keyboard-arrow-right"
|
||||
class="button-hover-arrow"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</a>
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
interface Props {
|
||||
title?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const { title, url } = Astro.props;
|
||||
---
|
||||
|
||||
<a
|
||||
class="button-base button-bg-neutral group inline-flex rounded-lg gap-x-2"
|
||||
href={url}
|
||||
data-astro-prefetch
|
||||
>
|
||||
<div class="button-text-title flex relative items-center text-center">
|
||||
<span>
|
||||
{title}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -1,59 +0,0 @@
|
||||
---
|
||||
import Logo from "@components/images/Logo.astro"
|
||||
|
||||
type SocialPlatform = {
|
||||
name: string;
|
||||
url: string;
|
||||
iconLight: string;
|
||||
iconDark: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
pageTitle: string;
|
||||
}
|
||||
|
||||
const { pageTitle } = Astro.props;
|
||||
|
||||
const socialPlatforms: SocialPlatform[] = [
|
||||
{
|
||||
name: 'Facebook',
|
||||
url: `https://www.facebook.com/sharer/sharer.php?u=${Astro.url}`,
|
||||
iconLight: 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/facebook.webp',
|
||||
iconDark: 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/facebook.webp',
|
||||
},
|
||||
{
|
||||
name: 'Twitter',
|
||||
url: `https://x.com/intent/tweet?url=${Astro.url}&text=${pageTitle}`,
|
||||
iconLight: 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/twitter.webp',
|
||||
iconDark: 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/twitter.webp',
|
||||
},
|
||||
{
|
||||
name: 'LinkedIn',
|
||||
url: `https://www.linkedin.com/sharing/share-offsite/?url=${Astro.url}`,
|
||||
iconLight: 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/linkedin.webp',
|
||||
iconDark: 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/linkedin.webp',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<div class="inline-flex items-center gap-x-2">
|
||||
{socialPlatforms.map((platform) => (
|
||||
<a
|
||||
class="button-base-hidden group inline-flex rounded-lg gap-x-2"
|
||||
href={platform.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
title={`Share on ${platform.name}`}
|
||||
>
|
||||
<div class="button-text-title-hidden flex relative items-center text-center">
|
||||
<Logo
|
||||
srcLight={platform.iconLight}
|
||||
srcDark={platform.iconDark}
|
||||
alt={platform.name}
|
||||
width="24"
|
||||
height="24"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { Image } from 'astro:assets';
|
||||
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
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"
|
||||
inferSize={true}
|
||||
/>
|
||||
</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>
|
||||
@@ -1,44 +0,0 @@
|
||||
---
|
||||
import Logo from "@components/images/Logo.astro"
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
logoUrlLight?: string;
|
||||
logoUrlDark?: string;
|
||||
}
|
||||
|
||||
const { title, description, url, logoUrlLight }: Props = Astro.props;
|
||||
const logoUrlDark = Astro.props.logoUrlDark || logoUrlLight;
|
||||
---
|
||||
|
||||
<div class="smooth-reveal-2 group flex flex-col">
|
||||
<a
|
||||
class="card-base flex items-center h-30 w-100 md:w-75"
|
||||
href={url}
|
||||
data-astro-prefetch
|
||||
>
|
||||
<div class="p-5 w-full">
|
||||
<div class="flex items-center">
|
||||
{logoUrlLight && (
|
||||
<div class="card-hover-icon-scale">
|
||||
<Logo
|
||||
srcLight={logoUrlLight}
|
||||
srcDark={logoUrlDark}
|
||||
alt={`Logo of ${title}`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div class="ms-5 grow text-left">
|
||||
<span class="card-text-title card-hover-text-title block text-lg">
|
||||
{title}
|
||||
</span>
|
||||
<p class="card-text-description block mt-1">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { Image } from 'astro:assets';
|
||||
|
||||
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"
|
||||
inferSize={true}
|
||||
/>
|
||||
</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>
|
||||
@@ -1,86 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { Image } from 'astro:assets';
|
||||
|
||||
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"
|
||||
inferSize={true}
|
||||
/>
|
||||
</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"
|
||||
inferSize={true}
|
||||
/>
|
||||
<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"
|
||||
inferSize={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</div>
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
import ImageTheme from '@components/images/ImageTheme.astro';
|
||||
|
||||
const {
|
||||
srcLight,
|
||||
srcDark,
|
||||
alt,
|
||||
width = 48,
|
||||
height = 48,
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<ImageTheme
|
||||
srcLight={srcLight}
|
||||
srcDark={srcDark}
|
||||
alt={alt}
|
||||
style=`color: transparent; width: ${width}px; height: ${height}px; object-fit: contain; max-height: 100%; max-width: 100%;`
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
width={width}
|
||||
height={height}
|
||||
/>
|
||||
@@ -1,93 +0,0 @@
|
||||
---
|
||||
import { getCollection } from 'astro:content';
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
import CategoryCard from '@components/cards/CategoryCard.astro';
|
||||
import directus from '@lib/directus';
|
||||
import { timeago } from '@support/time';
|
||||
|
||||
const posts = await directus.request(
|
||||
readItems('posts', {
|
||||
filter: { published: { _eq: true } },
|
||||
fields: ['*'],
|
||||
sort: ['-published_date'],
|
||||
})
|
||||
);
|
||||
|
||||
const layoutPattern = [
|
||||
{ col: 2, row: 2 },
|
||||
{ col: 2, row: 1 },
|
||||
{ col: 1, row: 1 },
|
||||
{ col: 1, row: 1 },
|
||||
{ col: 1, row: 2 },
|
||||
{ col: 2, row: 1 },
|
||||
{ col: 1, row: 1 },
|
||||
{ col: 1, row: 1 },
|
||||
{ col: 1, row: 1 },
|
||||
{ col: 1, row: 1 },
|
||||
];
|
||||
|
||||
const postMap: Map<string, Post[]> = posts
|
||||
.sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf())
|
||||
.reduce((acc, obj) => {
|
||||
let posts = acc.get(obj.category);
|
||||
if (!posts) {
|
||||
posts = [];
|
||||
}
|
||||
posts.push(obj);
|
||||
|
||||
acc.set(obj.category, posts);
|
||||
|
||||
return acc;
|
||||
}, new Map<string, Post[]>());
|
||||
|
||||
const categories = (await getCollection('categories'))
|
||||
.sort((a, b) => {
|
||||
const aCount = postMap.get(a.slug)?.length ?? 0;
|
||||
const bCount = postMap.get(b.slug)?.length ?? 0;
|
||||
return bCount - aCount;
|
||||
})
|
||||
.map((c, index) => {
|
||||
const posts = postMap.get(c.slug);
|
||||
const pattern = layoutPattern[index % layoutPattern.length];
|
||||
const smColSpan = Math.min(pattern.col, 2);
|
||||
const mdColSpan = Math.min(pattern.col, 4);
|
||||
const rowSpan = pattern.row;
|
||||
const rowSpanClass = rowSpan > 1 ? `row-span-${rowSpan}` : 'row-span-1';
|
||||
const gridItemClass = `col-span-${smColSpan} md:col-span-${mdColSpan} ${rowSpanClass}`;
|
||||
return {
|
||||
...c,
|
||||
posts,
|
||||
gridItemClass,
|
||||
layoutPattern: {
|
||||
smCol: smColSpan,
|
||||
mdCol: mdColSpan,
|
||||
row: rowSpan,
|
||||
index,
|
||||
},
|
||||
};
|
||||
});
|
||||
---
|
||||
|
||||
<section class="mx-auto px-4 py-10 sm:px-6 lg:px-8 lg:py-14 lg:pt-10 2xl:max-w-full">
|
||||
<div class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{categories.map((category) => {
|
||||
return (
|
||||
<div
|
||||
class={category.gridItemClass}
|
||||
style={category.layoutPattern.row > 1 ? 'grid-row: span 2 / span 2;' : ''}
|
||||
>
|
||||
<CategoryCard
|
||||
slug={category.slug}
|
||||
title={category.data.title}
|
||||
description={category.data.description}
|
||||
count={postMap.get(category.slug)?.length ?? 0}
|
||||
publishDate={timeago(postMap.get(category.slug)?.[0]?.published_date)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,159 +0,0 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Experience } from '@lib/directusTypes';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
|
||||
const experiences = ((await directus.request(
|
||||
readItems('site_experience'as any, {
|
||||
fields: ['*'],
|
||||
sort: ['-endDate'],
|
||||
})
|
||||
)) as unknown) as Experience[];
|
||||
---
|
||||
|
||||
<section class:list={['flex flex-col gap-8', Astro.props.className]}>
|
||||
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-10">
|
||||
Experience
|
||||
</h3>
|
||||
<ul class="flex flex-col w-full ml-8 pr-8">
|
||||
{experiences.map((experience: Experience) => {
|
||||
const startYear = new Date(experience.startDate).getFullYear();
|
||||
const endYear = experience.endDate != null ? new Date(experience.endDate).getFullYear() : 'Present';
|
||||
|
||||
return (
|
||||
<li class="relative">
|
||||
<div class="smooth-reveal group relative grid sm:grid-cols-18 sm:gap-8 md:gap-6 pb-16">
|
||||
<header class="relative sm:col-span-3 text-header font-semibold text-lg mt-1">
|
||||
<time datetime={experience.startDate} data-title={experience.startDate}>
|
||||
{startYear}
|
||||
</time>
|
||||
{' '}-{' '}
|
||||
<time datetime={experience.endDate} data-title={experience.endDate}>
|
||||
{endYear}
|
||||
</time>
|
||||
</header>
|
||||
<div class="relative flex flex-col sm:col-span-12 pb-6">
|
||||
<div class="absolute bg-accent -translate-x-[1.71rem] rounded-full h-2 w-2 mt-3"/>
|
||||
<h3>
|
||||
<div
|
||||
class="inline-flex items-center text-2xl leading-tight font-semibold"
|
||||
aria-label="{position} - {company}"
|
||||
>
|
||||
<span class="text-header">
|
||||
{experience.position} <span>@</span>
|
||||
{experience.url ? (
|
||||
<a
|
||||
class="hover:text-main"
|
||||
href={experience.url}
|
||||
title={`Ver ${experience.name}`}
|
||||
target="_blank"
|
||||
>
|
||||
{experience.name}
|
||||
</a>
|
||||
) : (
|
||||
<span>{experience.name}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</h3>
|
||||
{(experience.location || experience.location_type) && (
|
||||
<div class="text-secondary text-sm">
|
||||
{experience.location} {experience.location && experience.location_type && '-'} {experience.location_type}
|
||||
</div>
|
||||
)}
|
||||
<div class="text-md mt-4 flex flex-col gap-4" x-data="{ expanded: false }">
|
||||
{experience.summary && (
|
||||
<div class="flex flex-col gap-1">
|
||||
<h4 class="text-header font-semibold">
|
||||
Summary:
|
||||
</h4>
|
||||
<ul class="flex flex-col text-primary list-disc gap-2 [&>li]:ml-4">
|
||||
<li class="marker:text-main">
|
||||
{experience.summary}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{(experience.responsibilities || experience.achievements) && (
|
||||
<div class="relative flex flex-col gap-4" :class="expanded ? '' : 'mask-[linear-gradient(to_bottom,black_50%,transparent)]'" x-show="expanded" x-collapse.min.50px>
|
||||
{experience.responsibilities && (
|
||||
<div class="flex flex-col gap-1">
|
||||
<h4 class="text-header font-semibold">
|
||||
Responsibilities:
|
||||
</h4>
|
||||
<ul class="flex flex-col text-primary list-disc gap-2 [&>li]:ml-4">
|
||||
{experience.responsibilities.map(responsibility => (
|
||||
<li class="marker:text-main">
|
||||
{responsibility}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
{experience.achievements && (
|
||||
<div class="flex flex-col gap-1">
|
||||
<h4 class="text-header font-semibold">
|
||||
Achievements:
|
||||
</h4>
|
||||
<ul class="flex flex-col text-primary list-disc gap-2 [&>li]:ml-4">
|
||||
{experience.achievements.map(achievement => (
|
||||
<li class="marker:text-main">
|
||||
{achievement}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button @click="expanded = ! expanded" class="group/more flex items-center justify-center text-primary hover:text-primary-hover text-xs underline transition-all gap-1.5 w-fit cursor-pointer">
|
||||
<span x-text="expanded ? 'Show less' : 'Show more'">
|
||||
Show more
|
||||
</span>
|
||||
<svg
|
||||
class="group-hover/more:translate-y-0.5 ease-out duration-300 h-4 w-4"
|
||||
:class="{ 'rotate-180': expanded }"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<ul
|
||||
class="flex print:hidden flex-wrap gap-2 mt-2"
|
||||
aria-label="Technologies used"
|
||||
>
|
||||
{experience.skills && experience.skills.map(skill => {
|
||||
const iconName = skill.toLowerCase();
|
||||
const skillName = skill.split(':')[1].replace(/^language-/, '').replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
|
||||
return (
|
||||
<li class="flex items-center bg-steel/20 dark:bg-bermuda/20 text-neutral-800 dark:text-neutral-200 text-xs rounded-md border border-solid border-steel/20 dark:border-bermuda/20 gap-1 px-2 py-0.5">
|
||||
<Icon name={`${iconName}`} class="h-4 w-4" /> <span>{skillName}</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Alpine Plugins -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- Alpine Core -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
import GoLinkPrimaryButton from '@components/buttons/GoLinkPrimaryButton.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
btnExists?: boolean;
|
||||
btnTitle?: string;
|
||||
btnURL?: string;
|
||||
}
|
||||
|
||||
const { title, subTitle, btnExists, btnTitle, btnURL } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="mx-auto mt-10 px-4 sm:px-6 lg:px-8 lg:pt-10 2xl:max-w-full">
|
||||
<div class="flex-wrap md:flex md:items-center md:justify-between">
|
||||
<div class="w-full md:w-auto">
|
||||
<h1 class="smooth-reveal card-text-header block lg:text-6xl">
|
||||
{title}
|
||||
</h1>
|
||||
<p class="smooth-reveal card-text-header-description mt-4">
|
||||
{subTitle}
|
||||
</p>
|
||||
{btnExists ? (
|
||||
<div class="smooth-reveal mt-4 md:mt-8">
|
||||
<GoLinkPrimaryButton title={btnTitle} url={btnURL}/>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,57 +0,0 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
|
||||
import GoLinkPrimaryButton from '@components/buttons/GoLinkPrimaryButton.astro';
|
||||
import GoLinkSecondaryButton from '@components/buttons/GoLinkSecondaryButton.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
primaryBtn?: string;
|
||||
primaryBtnURL?: string;
|
||||
secondaryBtn?: string;
|
||||
secondaryBtnURL?: string;
|
||||
src?: any;
|
||||
alt?: string;
|
||||
rounded?: boolean;
|
||||
}
|
||||
|
||||
const { title, subTitle, primaryBtn, primaryBtnURL, secondaryBtn, secondaryBtnURL, src, alt } = Astro.props;
|
||||
|
||||
const roundedClasses = Astro.props.rounded ? "rounded-2xl" : null;
|
||||
---
|
||||
|
||||
<section class="mx-auto grid max-w-340 gap-4 px-4 py-14 sm:px-6 md:grid-cols-2 md:items-center md:gap-8 lg:px-8 2xl:max-w-full">
|
||||
<div>
|
||||
<h1 class="smooth-reveal card-text-header block lg:text-7xl">
|
||||
<Fragment set:html={title} />
|
||||
</h1>
|
||||
{subTitle && (
|
||||
<p class="smooth-reveal card-text-header-description lg:w-4/5 mt-6">
|
||||
{subTitle}
|
||||
</p>
|
||||
)}
|
||||
<div class="smooth-reveal grid sm:inline-flex mt-7 w-full gap-3">
|
||||
{primaryBtn && <GoLinkPrimaryButton title={primaryBtn} url={primaryBtnURL} />}
|
||||
{secondaryBtn && <GoLinkSecondaryButton title={secondaryBtn} url={secondaryBtnURL} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="smooth-reveal-fade md:block w-full hidden">
|
||||
<div class="flex justify-center w-full top-12 md:ml-4 overflow-hidden">
|
||||
{src && alt && (
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
class={`h-full w-105 scale-100 object-cover object-center ${roundedClasses}`}
|
||||
draggable="false"
|
||||
loading="eager"
|
||||
format="webp"
|
||||
quality="low"
|
||||
widths={[840]}
|
||||
inferSize={true}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
import BlogCard from '@components/cards/BlogCard.astro';
|
||||
|
||||
interface Props {
|
||||
posts: Post[];
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
}
|
||||
|
||||
const { posts, title, subTitle } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="max-w-340 2xl:max-w-full px-4 sm:px-6 lg:px-8 py-10 lg:py-14 mx-auto mb-2 md:mb-8">
|
||||
<div class="text-center max-w-2xl mx-auto mb-10 lg:mb-14">
|
||||
<h1 class="smooth-reveal card-text-header block">
|
||||
{title}
|
||||
</h1>
|
||||
<div class="smooth-reveal mx-auto mt-5 max-w-3xl text-center">
|
||||
<span class="card-text-header-description">
|
||||
{subTitle}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{posts.map((b) => <BlogCard post={b} />)}
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
import LargeBlogLeftCard from '@components/cards/LargeBlogLeftCard.astro';
|
||||
import LargeBlogRightCard from '@components/cards/LargeBlogRightCard.astro';
|
||||
|
||||
interface Props {
|
||||
posts: Post[];
|
||||
}
|
||||
|
||||
const { posts } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="smooth-reveal 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>
|
||||
@@ -1,28 +1,13 @@
|
||||
---
|
||||
|
||||
import Icon from '@components/ui/icons/icon.astro';
|
||||
---
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="button-base button-bg-blue group inline-flex items-center rounded-lg p-2.5"
|
||||
class="focus-visible:ring-secondary group inline-flex items-center rounded-lg p-2.5 text-neutral-600 ring-neutral-500 transition duration-300 outline-none hover:bg-neutral-100 focus:outline-none focus-visible:ring-1 focus-visible:outline-none dark:text-neutral-400 dark:ring-neutral-200 dark:hover:bg-neutral-700"
|
||||
data-bookmark-button="bookmark-button"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6 fill-none transition duration-300"
|
||||
height=24
|
||||
width=24
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.5"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path
|
||||
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
|
||||
class="fill-current text-neutral-500 transition duration-300 group-hover:text-red-400 group-hover:dark:text-red-400"
|
||||
/>
|
||||
</svg>
|
||||
<Icon name="bookmark" />
|
||||
</button>
|
||||
|
||||
<script>
|
||||
32
src/components/ui/buttons/GiteaBtn.astro
Normal file
32
src/components/ui/buttons/GiteaBtn.astro
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
const { title, url } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const baseClasses =
|
||||
'group group-hover inline-flex items-center justify-center gap-x-3 rounded-full px-4 py-3 text-center text-sm font-medium text-neutral-200';
|
||||
const borderClasses = 'border border-transparent';
|
||||
const bgColorClasses =
|
||||
'bg-gitea-primary hover:bg-gitea-secondary dark:bg-gitea-secondary dark:hover:bg-gitea-primary';
|
||||
const shadowClasses = 'shadow-sm';
|
||||
const fontSizeClasses = '2xl:text-base';
|
||||
---
|
||||
|
||||
<a
|
||||
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${shadowClasses} ${fontSizeClasses} `}
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Icon name="pajamas:gitea" class="h-4 w-4 md:h-6 md:w-6" />
|
||||
{title}
|
||||
<Icon
|
||||
name="mdi:keyboard-arrow-right"
|
||||
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
|
||||
/>
|
||||
</a>
|
||||
35
src/components/ui/buttons/GoBack.astro
Normal file
35
src/components/ui/buttons/GoBack.astro
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
import Icon from '@components/ui/icons/icon.astro';
|
||||
|
||||
const { title, noArrow } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
url?: string;
|
||||
noArrow?: boolean;
|
||||
addHome?: boolean;
|
||||
}
|
||||
|
||||
const baseClasses =
|
||||
'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-50 ring-neutral-500 transition duration-300 focus-visible:ring outline-none';
|
||||
const borderClasses = 'border border-transparent';
|
||||
const bgColorClasses = 'bg-steel hover:bg-sky-800 active:bg-orange-500 dark:focus:outline-none';
|
||||
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
|
||||
const fontSizeClasses = '2xl:text-base';
|
||||
const ringClasses = 'dark:ring-neutral-200';
|
||||
---
|
||||
|
||||
<button
|
||||
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses}`}
|
||||
id="back-button"
|
||||
data-astro-prefetch
|
||||
>
|
||||
{noArrow ? null : <Icon name="arrowLeft" />}
|
||||
{title}
|
||||
</button>
|
||||
|
||||
<script>
|
||||
document.getElementById('back-button')?.addEventListener('click', () => {
|
||||
window.history.back();
|
||||
});
|
||||
</script>
|
||||
45
src/components/ui/buttons/PrimaryCTA.astro
Normal file
45
src/components/ui/buttons/PrimaryCTA.astro
Normal file
@@ -0,0 +1,45 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
const { title, url, noArrow, addHome, addClass } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
url?: string;
|
||||
noArrow?: boolean;
|
||||
addHome?: boolean;
|
||||
addClass?: string;
|
||||
}
|
||||
|
||||
const baseClasses =
|
||||
'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-100 transition duration-300 ';
|
||||
const borderClasses = 'border border-transparent';
|
||||
const bgColorClasses = 'bg-bermuda hover:bg-turquoise dark:bg-turquoise dark:hover:bg-bermuda';
|
||||
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
|
||||
const fontSizeClasses = '2xl:text-base';
|
||||
const ringClasses = 'dark:ring-neutral-200';
|
||||
---
|
||||
|
||||
<a
|
||||
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${addClass}`}
|
||||
href={url}
|
||||
data-astro-prefetch
|
||||
>
|
||||
{
|
||||
addHome ? (
|
||||
<Icon
|
||||
name="mdi:home-variant-outline"
|
||||
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
{title}
|
||||
{
|
||||
noArrow ? null : (
|
||||
<Icon
|
||||
name="mdi:keyboard-arrow-right"
|
||||
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</a>
|
||||
26
src/components/ui/buttons/SecondaryCTA.astro
Normal file
26
src/components/ui/buttons/SecondaryCTA.astro
Normal file
@@ -0,0 +1,26 @@
|
||||
---
|
||||
const { title, url } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const baseClasses =
|
||||
'inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-center text-sm font-medium text-neutral-600 shadow-sm outline-none ring-neutral-500 focus-visible:ring transition duration-300';
|
||||
const borderClasses = 'border border-neutral-200';
|
||||
const bgColorClasses = 'bg-neutral-300';
|
||||
const hoverClasses = 'hover:bg-neutral-400/50 hover:text-neutral-600 active:text-neutral-700';
|
||||
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
|
||||
const fontSizeClasses = '2xl:text-base';
|
||||
const ringClasses = 'ring-neutral-500';
|
||||
const darkClasses =
|
||||
'dark:border-neutral-700 dark:bg-neutral-700 dark:text-neutral-300 dark:ring-neutral-200 dark:hover:bg-neutral-600 dark:focus:outline-none';
|
||||
---
|
||||
|
||||
<a
|
||||
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${darkClasses}`}
|
||||
href={url}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
150
src/components/ui/buttons/SocialShare.astro
Normal file
150
src/components/ui/buttons/SocialShare.astro
Normal file
@@ -0,0 +1,150 @@
|
||||
---
|
||||
import Icon from '@components/ui/icons/icon.astro';
|
||||
|
||||
const { pageTitle, title = 'Share' } = Astro.props;
|
||||
|
||||
interface Props {
|
||||
pageTitle: string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
type SocialPlatform = {
|
||||
name: string;
|
||||
url: string;
|
||||
svg: string;
|
||||
};
|
||||
|
||||
const socialPlatforms: SocialPlatform[] = [
|
||||
{
|
||||
name: 'Facebook',
|
||||
url: `https://www.facebook.com/share.php?u=${Astro.url}&title=${pageTitle}`,
|
||||
svg: 'facebook',
|
||||
},
|
||||
{
|
||||
name: 'X',
|
||||
url: `https://twitter.com/home/?status=${pageTitle}${Astro.url}`,
|
||||
svg: 'x',
|
||||
},
|
||||
{
|
||||
name: 'LinkedIn',
|
||||
url: `https://www.linkedin.com/shareArticle?mini=true&url=${Astro.url}&title=${pageTitle}`,
|
||||
svg: 'linkedIn',
|
||||
},
|
||||
];
|
||||
---
|
||||
|
||||
<div class="hs-dropdown relative inline-flex [--auto-close:inside] [--placement:top-left]">
|
||||
<button
|
||||
id="hs-dropup"
|
||||
type="button"
|
||||
class="hs-dropdown-toggle inline-flex items-center gap-x-2 rounded-lg px-4 py-3 text-sm font-medium text-neutral-600 ring-neutral-500 transition duration-300 outline-none hover:bg-neutral-100 hover:text-neutral-700 focus-visible:ring dark:text-neutral-400 dark:ring-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:outline-none"
|
||||
>
|
||||
<Icon name="share" />
|
||||
|
||||
{title}
|
||||
</button>
|
||||
|
||||
<div
|
||||
class="hs-dropdown-menu duration hs-dropdown-open:opacity-100 z-10 hidden w-72 divide-y divide-neutral-200 rounded-lg bg-neutral-50 p-2 opacity-0 shadow-md transition-[opacity,margin] dark:divide-neutral-700 dark:border dark:border-neutral-700 dark:bg-neutral-800"
|
||||
aria-labelledby="hs-dropup"
|
||||
>
|
||||
<div class="py-2 first:pt-0 last:pb-0">
|
||||
{
|
||||
socialPlatforms.map((platform) => (
|
||||
<a
|
||||
class="flex items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700"
|
||||
href={platform.url}
|
||||
>
|
||||
<Icon name={platform.svg} />
|
||||
Share on {platform.name}
|
||||
</a>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<div class="py-2 first:pt-0 last:pb-0">
|
||||
<button
|
||||
type="button"
|
||||
class="js-clipboard hover:text-dark focus-visible:ring-secondary group inline-flex w-full items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none focus-visible:ring-1 focus-visible:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700"
|
||||
data-clipboard-success-text="Copied"
|
||||
>
|
||||
<svg
|
||||
class="js-clipboard-default h-4 w-4 transition group-hover:rotate-6"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect>
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
class="js-clipboard-success hidden h-4 w-4 text-neutral-700 dark:text-neutral-300"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12"></polyline>
|
||||
</svg>
|
||||
<span class="js-clipboard-success-text">Copy link</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!--Import the necessary Dropdown and Clipboard plugins-->
|
||||
<!--https://preline.co/plugins/html/dropdown.html-->
|
||||
<!--<script is:inline src="/scripts/vendor/preline/dropdown/index.js"></script>-->
|
||||
|
||||
<!-- https://clipboardjs.com/ -->
|
||||
<!--<script is:inline src="/scripts/vendor/clipboard.min.js"></script>-->
|
||||
|
||||
<script is:inline>
|
||||
(function () {
|
||||
window.addEventListener('load', () => {
|
||||
const $clipboards = document.querySelectorAll('.js-clipboard');
|
||||
$clipboards.forEach((el) => {
|
||||
const clipboard = new ClipboardJS(el, {
|
||||
text: () => {
|
||||
return window.location.href;
|
||||
},
|
||||
});
|
||||
clipboard.on('success', () => {
|
||||
const $default = el.querySelector('.js-clipboard-default');
|
||||
const $success = el.querySelector('.js-clipboard-success');
|
||||
const $successText = el.querySelector('.js-clipboard-success-text');
|
||||
const successText = el.dataset.clipboardSuccessText || '';
|
||||
let oldSuccessText;
|
||||
|
||||
if ($successText) {
|
||||
oldSuccessText = $successText.textContent;
|
||||
$successText.textContent = successText;
|
||||
}
|
||||
if ($default && $success) {
|
||||
$default.style.display = 'none';
|
||||
$success.style.display = 'block';
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if ($successText && oldSuccessText) {
|
||||
$successText.textContent = oldSuccessText;
|
||||
}
|
||||
if ($default && $success) {
|
||||
$success.style.display = '';
|
||||
$default.style.display = '';
|
||||
}
|
||||
}, 800);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
@@ -5,14 +5,14 @@
|
||||
<button
|
||||
id="theme-toggle"
|
||||
data-theme-toggle
|
||||
class="group dark:hover:bg-steel/30 hover:bg-yellow-300/20 transition-all duration-300 relative rounded-full p-1.5 sm:p-2 touch-manipulation"
|
||||
class="group dark:hover:bg-steel/30 relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-yellow-300/20 focus:outline-hidden sm:p-2"
|
||||
aria-label="Toggle dark mode"
|
||||
>
|
||||
<div class="relative flex h-5 w-5 items-center justify-center">
|
||||
<div class="relative z-10 flex h-5 w-5 items-center justify-center">
|
||||
<!-- Sun icon -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-light absolute h-5 w-5 text-neutral-600 dark:text-neutral-400 scale-100 dark:scale-0 rotate-0 dark:-rotate-90 transition-all duration-500"
|
||||
class="icon-light absolute h-5 w-5 scale-100 rotate-0 text-neutral-600 transition-all duration-500 dark:scale-0 dark:-rotate-90 dark:text-neutral-400"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -29,7 +29,7 @@
|
||||
<!-- Moon icon -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="icon-dark absolute h-5 w-5 text-neutral-600 dark:text-neutral-400 scale-0 dark:scale-100 rotate-90 dark:rotate-0 transition-all duration-500"
|
||||
class="icon-dark absolute h-5 w-5 scale-0 rotate-90 text-neutral-600 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-neutral-400"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -43,23 +43,25 @@
|
||||
</button>
|
||||
|
||||
<script is:inline>
|
||||
const applyTheme = () => {
|
||||
const isDark =
|
||||
localStorage.theme === 'dark' ||
|
||||
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||
document.documentElement.classList.toggle('dark', isDark);
|
||||
};
|
||||
|
||||
applyTheme();
|
||||
// Use a function to persist theme when using SPA transitions
|
||||
// https://docs.astro.build/en/guides/view-transitions/#script-re-execution
|
||||
function applyTheme() {
|
||||
localStorage.theme === 'dark'
|
||||
? document.documentElement.classList.add('dark')
|
||||
: document.documentElement.classList.remove('dark');
|
||||
}
|
||||
|
||||
document.addEventListener('astro:after-swap', applyTheme);
|
||||
|
||||
applyTheme();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// Use a function to handle theme toggle to ensure it can be called from anywhere
|
||||
function setupThemeToggle() {
|
||||
const themeToggles = document.querySelectorAll('[data-theme-toggle]');
|
||||
|
||||
// Create theme switch overlay element
|
||||
// Create theme switch overlay element if it doesn't exist
|
||||
if (!document.querySelector('.theme-switch-overlay')) {
|
||||
const overlay = document.createElement('div');
|
||||
overlay.className = 'theme-switch-overlay fixed inset-0 pointer-events-none z-50';
|
||||
@@ -68,7 +70,9 @@
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
// Toggle theme when any theme toggle button is clicked
|
||||
themeToggles.forEach((toggle) => {
|
||||
// Add event listeners for both click and touch events
|
||||
['click', 'touchend'].forEach((eventType) => {
|
||||
toggle.addEventListener(
|
||||
eventType,
|
||||
@@ -88,10 +92,14 @@
|
||||
y = e.clientY - rect.top;
|
||||
}
|
||||
|
||||
// Set the position variables for the radial gradient
|
||||
document.documentElement.style.setProperty('--x', `${x}px`);
|
||||
document.documentElement.style.setProperty('--y', `${y}px`);
|
||||
|
||||
// Get the overlay element
|
||||
const overlay = document.querySelector('.theme-switch-overlay');
|
||||
|
||||
// Determine the new theme
|
||||
const isDark = document.documentElement.classList.contains('dark');
|
||||
const newTheme = isDark ? 'light' : 'dark';
|
||||
|
||||
@@ -102,6 +110,7 @@
|
||||
overlay.style.opacity = '1';
|
||||
}
|
||||
|
||||
// Add transition class
|
||||
document.documentElement.classList.add('theme-switching');
|
||||
|
||||
// Force a reflow to ensure all elements update
|
||||
@@ -115,7 +124,10 @@
|
||||
document.documentElement.classList.add('dark');
|
||||
}
|
||||
|
||||
// Store the preference
|
||||
localStorage.setItem('theme', newTheme);
|
||||
|
||||
// Dispatch a custom event for other components to react to
|
||||
document.dispatchEvent(
|
||||
new CustomEvent('themeChanged', {
|
||||
detail: { isDark: newTheme === 'dark' },
|
||||
@@ -125,10 +137,13 @@
|
||||
// Force another reflow to ensure all elements update
|
||||
document.body.offsetHeight;
|
||||
|
||||
// Hide overlay after theme has changed
|
||||
setTimeout(() => {
|
||||
if (overlay) {
|
||||
overlay.style.opacity = '0';
|
||||
}
|
||||
|
||||
// Remove transition class after animation completes
|
||||
document.documentElement.classList.remove('theme-switching');
|
||||
}, 300);
|
||||
}, 50);
|
||||
@@ -136,6 +151,25 @@
|
||||
{ passive: false }
|
||||
);
|
||||
});
|
||||
|
||||
// Add touch feedback
|
||||
toggle.addEventListener(
|
||||
'touchstart',
|
||||
() => {
|
||||
toggle.classList.add('active-touch');
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
|
||||
toggle.addEventListener(
|
||||
'touchend',
|
||||
() => {
|
||||
setTimeout(() => {
|
||||
toggle.classList.remove('active-touch');
|
||||
}, 150);
|
||||
},
|
||||
{ passive: true }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -167,32 +201,61 @@
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Smooth transition for the entire page when theme changes */
|
||||
:global(body) {
|
||||
transition:
|
||||
background-color 0.5s ease,
|
||||
color 0.5s ease;
|
||||
}
|
||||
|
||||
/* Theme transition overlay */
|
||||
:global(.theme-switch-overlay) {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 9999;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.3s ease-out;
|
||||
}
|
||||
|
||||
/* Ensure theme transitions apply to all elements */
|
||||
:global(.theme-switching *) {
|
||||
transition-duration: 0.5s !important;
|
||||
transition-property: background-color, border-color, color, fill, stroke !important;
|
||||
}
|
||||
|
||||
/* Subtle hover animation */
|
||||
#theme-toggle {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
min-height: 32px;
|
||||
min-width: 32px;
|
||||
-webkit-tap-highlight-color: transparent; /* Remove default mobile tap highlight */
|
||||
min-height: 32px; /* Ensure minimum touch target size */
|
||||
min-width: 32px; /* Ensure minimum touch target size */
|
||||
}
|
||||
|
||||
/* Only apply hover effects on non-touch devices */
|
||||
@media (hover: hover) {
|
||||
#theme-toggle:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
:global(:root:not(.dark)) #theme-toggle:hover .icon-light {
|
||||
filter: drop-shadow(0 0 2px rgba(251, 191, 36, 0.6));
|
||||
#theme-toggle:hover .icon-light:not(.dark .icon-light) {
|
||||
filter: drop-shadow-sm(0 0 2px rgba(251, 191, 36, 0.6));
|
||||
transform: scale(1.1) rotate(15deg);
|
||||
}
|
||||
|
||||
:global(:root.dark) #theme-toggle:hover .icon-dark {
|
||||
filter: drop-shadow(0 0 2px rgba(129, 140, 248, 0.6));
|
||||
#theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) {
|
||||
filter: drop-shadow-sm(0 0 2px rgba(129, 140, 248, 0.6));
|
||||
transform: scale(1.1) rotate(-15deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch feedback */
|
||||
#theme-toggle.active-touch {
|
||||
transform: scale(0.95);
|
||||
transition: transform 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
/* Optimize animations for mobile */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.icon-light,
|
||||
@@ -1,8 +1,6 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
import Logo from '@components/images/Logo.astro';
|
||||
import { getDirectusImageURL } from '@/support/url';
|
||||
import Logo from '@components/ui/logos/Logo.astro';
|
||||
|
||||
interface Props {
|
||||
topic: string;
|
||||
@@ -14,8 +12,7 @@ interface Props {
|
||||
logoIcon?: string;
|
||||
}
|
||||
|
||||
const { topic, area, date, url, logoUrlLight, logoIcon } = Astro.props;
|
||||
const logoUrlDark = Astro.props.logoUrlDark || logoUrlLight;
|
||||
const { topic, area, date, url, logoUrlLight, logoUrlDark, logoIcon } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="smooth-reveal group flex flex-col">
|
||||
@@ -28,8 +25,8 @@ const logoUrlDark = Astro.props.logoUrlDark || logoUrlLight;
|
||||
{logoUrlLight ? (
|
||||
<div class="card-hover-icon-scale mr-5">
|
||||
<Logo
|
||||
srcLight={getDirectusImageURL(logoUrlLight)}
|
||||
srcDark={getDirectusImageURL(logoUrlDark!)}
|
||||
srcLight={logoUrlLight}
|
||||
srcDark={logoUrlDark}
|
||||
alt={`Logo of ${topic}`}
|
||||
/>
|
||||
</div>
|
||||
39
src/components/ui/cards/FeaturesCard.astro
Normal file
39
src/components/ui/cards/FeaturesCard.astro
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
url?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
const { title, description, url, icon } = Astro.props;
|
||||
|
||||
const sizeClasses = 'h-30 w-100 md:w-[300px]';
|
||||
---
|
||||
|
||||
<div class="smooth-reveal-2 group flex flex-col">
|
||||
<a
|
||||
class={`card-base flex items-center ${sizeClasses}`}
|
||||
href={url}
|
||||
data-astro-prefetch
|
||||
>
|
||||
<div class="p-5 w-full">
|
||||
<div class="flex items-center">
|
||||
<Icon
|
||||
name={icon}
|
||||
class="card-hover-icon-scale shrink-0 h-6 w-6 md:h-8 md:w-8 "
|
||||
/>
|
||||
<div class="ms-5 grow text-left">
|
||||
<span class="card-text-title card-hover-text-title block text-lg">
|
||||
{title}
|
||||
</span>
|
||||
<span class="card-text-description block mt-1">
|
||||
{description}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
|
||||
import Logo from '@components/images/Logo.astro';
|
||||
import Logo from '@components/ui/logos/Logo.astro';
|
||||
|
||||
interface Props {
|
||||
title?: string;
|
||||
@@ -39,9 +39,9 @@ const visitClass = visitSource ? 'card-hover-text-gitea' : 'card-hover-text-titl
|
||||
<span class="card-text-title block text-lg">
|
||||
{title}
|
||||
</span>
|
||||
<p class="card-text-description block mt-1">
|
||||
<span class="card-text-description block mt-1">
|
||||
{description}
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{highlights && (
|
||||
@@ -55,7 +55,7 @@ const visitClass = visitSource ? 'card-hover-text-gitea' : 'card-hover-text-titl
|
||||
)}
|
||||
<div class="ml-6 flex">
|
||||
<div class="relative inline-block">
|
||||
<div class={`card-text-title ${visitClass} flex relative items-center font-semibold text-md min-h-11 mx-auto sm:mx-0 sm:mt-4`}>
|
||||
<div class={`card-text-title ${visitClass} flex relative mx-auto min-h-11 items-center font-semibold text-md sm:mx-0 sm:mt-4`}>
|
||||
{visitSource && <Icon name="pajamas:gitea" />}
|
||||
<span class="relative inline-block overflow-hidden ml-2">
|
||||
{visitText}
|
||||
@@ -7,10 +7,12 @@ interface Props {
|
||||
}
|
||||
|
||||
const { dayName, label, icon, temp } = Astro.props;
|
||||
|
||||
const sizeClasses = 'w-32 md:w-40';
|
||||
---
|
||||
|
||||
<div class="smooth-reveal-2 group flex flex-col">
|
||||
<div class="card-base w-32 md:w-40">
|
||||
<div class={`card-base ${sizeClasses}`}>
|
||||
<div class="p-5 text-center">
|
||||
<span class="card-text-description block font-bold text-xs uppercase tracking-widest">
|
||||
{dayName}
|
||||
39
src/components/ui/icons/icon.astro
Normal file
39
src/components/ui/icons/icon.astro
Normal file
@@ -0,0 +1,39 @@
|
||||
---
|
||||
import { Icons } from './icons.ts';
|
||||
|
||||
interface Path {
|
||||
d: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
const { name } = Astro.props;
|
||||
|
||||
const icon = (Icons as any)[name] || {};
|
||||
const paths: Path[] = icon.paths || [];
|
||||
---
|
||||
|
||||
{
|
||||
icon ? (
|
||||
<svg
|
||||
class={icon.class}
|
||||
height={icon.height}
|
||||
viewBox={icon.viewBox}
|
||||
width={icon.width}
|
||||
fill={icon.fill}
|
||||
clip-rule={icon.clipRule}
|
||||
fill-rule={icon.fillRule}
|
||||
stroke={icon.stroke}
|
||||
stroke-width={icon.strokeWidth}
|
||||
stroke-linecap={icon.strokeLinecap}
|
||||
stroke-linejoin={icon.strokeLinejoin}
|
||||
>
|
||||
<title>{icon.title}</title>
|
||||
<circle cx={icon.circleCx} cy={icon.circleCy} r={icon.circleR} />
|
||||
{paths.map((path) => (
|
||||
<path d={path.d} class={path.class || ''} />
|
||||
))}
|
||||
</svg>
|
||||
) : (
|
||||
'Icon not found'
|
||||
)
|
||||
}
|
||||
573
src/components/ui/icons/icons.ts
Normal file
573
src/components/ui/icons/icons.ts
Normal file
@@ -0,0 +1,573 @@
|
||||
export const Icons = {
|
||||
groups: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm150-400 82-80-82-82-80 82 80 80Zm573-10 87-140 88 140H723Zm-243-70q-50 0-85-35t-35-85q0-51 35-85.5t85-34.5q51 0 85.5 34.5T600-600q0 50-34.5 85T480-480Zm.351-180Q455-660 437.5-642.851t-17.5 42.5Q420-575 437.351-557.5t43 17.5Q506-540 523-557.351t17-43Q540-626 522.851-643t-42.5-17ZM480-600ZM0-240v-53q0-39.464 42-63.232T150.398-380q12.158 0 23.38.5T196-377.273q-8 17.273-12 34.842-4 17.57-4 37.431v65H0Zm240 0v-65q0-65 66.5-105T480-450q108 0 174 40t66 105v65H240Zm570-140q67.5 0 108.75 23.768T960-293v53H780v-65q0-19.861-3.5-37.431Q773-360 765-377.273q11-1.727 22.171-2.227 11.172-.5 22.829-.5Zm-330.2-10Q400-390 350-366q-50 24-50 61v5h360v-6q0-36-49.5-60t-130.7-24Zm.2 90Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
|
||||
width: 48,
|
||||
height: 48,
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
books: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M343-420h225v-60H343v60Zm0-90h395v-60H343v60Zm0-90h395v-60H343v60Zm-83 400q-24 0-42-18t-18-42v-560q0-24 18-42t42-18h560q24 0 42 18t18 42v560q0 24-18 42t-42 18H260Zm0-60h560v-560H260v560ZM140-80q-24 0-42-18t-18-42v-620h60v620h620v60H140Zm120-740v560-560Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
|
||||
width: 48,
|
||||
height: 48,
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
verified: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm346-60-76-130-151-31 17-147-96-112 96-111-17-147 151-31 76-131 134 62 134-62 77 131 150 31-17 147 96 111-96 112 17 147-150 31-77 130-134-62-134 62Zm27-79 107-45 110 45 67-100 117-30-12-119 81-92-81-94 12-119-117-28-69-100-108 45-110-45-67 100-117 28 12 119-81 94 81 92-12 121 117 28 70 100Zm107-341Zm-43 133 227-225-45-41-182 180-95-99-46 45 141 140Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
|
||||
width: 48,
|
||||
height: 48,
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
frame: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M480-480q-51 0-85.5-34.5T360-600q0-50 34.5-85t85.5-35q50 0 85 35t35 85q0 51-35 85.5T480-480Zm-.351-60Q505-540 522.5-557.149t17.5-42.5Q540-625 522.649-642.5t-43-17.5Q454-660 437-642.649t-17 43Q420-574 437.149-557t42.5 17ZM240-240v-76q0-27 17.5-47.5T300-397q42-22 86.943-32.5 44.942-10.5 93-10.5Q528-440 573-429.5t87 32.5q25 13 42.5 33.5T720-316v76H240Zm240-140q-47.546 0-92.773 13T300-328v28h360v-28q-42-26-87.227-39-45.227-13-92.773-13Zm0-220Zm0 300h180-360 180ZM140-80q-24 0-42-18t-18-42v-172h60v172h172v60H140ZM80-648v-172q0-24 18-42t42-18h172v60H140v172H80ZM648-80v-60h172v-172h60v172q0 24-18 42t-42 18H648Zm172-568v-172H648v-60h172q24 0 42 18t18 42v172h-60Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
|
||||
width: 48,
|
||||
height: 48,
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
tools: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M764-80q-6 0-11-2t-10-7L501-331q-5-5-7-10t-2-11q0-6 2-11t7-10l85-85q5-5 10-7t11-2q6 0 11 2t10 7l242 242q5 5 7 10t2 11q0 6-2 11t-7 10l-85 85q-5 5-10 7t-11 2Zm0-72 43-43-200-200-43 43 200 200ZM195-80q-6 0-11.5-2T173-89l-84-84q-5-5-7-10.5T80-195q0-6 2-11t7-10l225-225h85l38-38-175-175h-57L80-779l99-99 125 125v57l175 175 130-130-67-67 56-56H485l-18-18 128-128 18 18v113l56-56 169 169q15 15 23.5 34.5T870-600q0 20-6.5 38.5T845-528l-85-85-56 56-52-52-211 211v84L216-89q-5 5-10 7t-11 2Zm0-72 200-200v-43h-43L152-195l43 43Zm0 0-43-43 22 21 21 22Zm569 0 43-43-43 43Z',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'mt-2 h-6 w-6 flex-shrink-0 fill-neutral-700 hs-tab-active:fill-orange-400 dark:fill-neutral-300 dark:hs-tab-active:fill-orange-300 md:h-7 md:w-7',
|
||||
width: 48,
|
||||
height: 48,
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
dashboard: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M510-570v-270h330v270H510ZM120-450v-390h330v390H120Zm390 330v-390h330v390H510Zm-390 0v-270h330v270H120Zm60-390h210v-270H180v270Zm390 330h210v-270H570v270Zm0-450h210v-150H570v150ZM180-180h210v-150H180v150Zm210-330Zm180-120Zm0 180ZM390-330Z',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'mt-2 h-6 w-6 flex-shrink-0 fill-neutral-700 hs-tab-active:fill-orange-400 dark:fill-neutral-300 dark:hs-tab-active:fill-orange-300 md:h-7 md:w-7',
|
||||
width: 48,
|
||||
height: 48,
|
||||
viewBox: '0 -960 960 960',
|
||||
},
|
||||
house: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 md:h-5 md:w-5',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
home: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205 3 1m1.5.5-1.5-.5M6.75 7.364V3h-3v18m3-13.636 10.5-3.819',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'h-6 w-6 flex-shrink-0 group-hover:text-steel dark:group-hover:text-steel transition-all duration-200 text-neutral-600 dark:text-neutral-300 md:h-7 md:w-7',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
arrowUp: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm5 12 7-7 7 7',
|
||||
},
|
||||
{
|
||||
d: 'M12 19V5',
|
||||
},
|
||||
],
|
||||
class: 'h-5 w-5 flex-shrink-0 text-orange-400 dark:text-orange-300',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
checkCircle: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M10 18a8 8 0 100-16 8 8 0 000 16zM13.707 8.293a1 1 0 00-1.414-1.414L9 10.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z',
|
||||
},
|
||||
],
|
||||
class: 'h-5 w-5 shrink-0',
|
||||
viewBox: '0 0 20 20',
|
||||
fill: 'currentColor',
|
||||
fillRule: 'evenodd',
|
||||
clipRule: 'evenodd',
|
||||
},
|
||||
bookmark: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z',
|
||||
class:
|
||||
'fill-current text-neutral-500 transition duration-300 group-hover:text-red-400 group-hover:dark:text-red-400',
|
||||
},
|
||||
],
|
||||
class: 'h-6 w-6 fill-none transition duration-300',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
arrowRight: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm9 18 6-6-6-6',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 transition duration-300 group-hover:translate-x-1',
|
||||
width: 20,
|
||||
height: 20,
|
||||
viewBox: '0 0 22 22',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
arrowLeft: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm15 18-6-6 6-6',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 transition duration-300 group-hover:-translate-x-1',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
facebook: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z',
|
||||
},
|
||||
],
|
||||
class: 'size-4 flex-shrink-0 fill-current',
|
||||
viewBox: '0 0 24 24',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
x: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z',
|
||||
},
|
||||
],
|
||||
class: 'size-4 flex-shrink-0 fill-current',
|
||||
viewBox: '0 0 24 24',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
linkedIn: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
|
||||
},
|
||||
],
|
||||
class: 'size-4 flex-shrink-0 fill-current',
|
||||
viewBox: '0 0 24 24',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
share: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 group-hover:text-neutral-700',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
github: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z',
|
||||
},
|
||||
],
|
||||
class: 'w-4.5 h-4.5 transition flex-shrink-0 text-neutral-700 duration-300',
|
||||
width: 16,
|
||||
height: 16,
|
||||
viewBox: '0 0 16 16',
|
||||
fill: 'currentColor',
|
||||
},
|
||||
gitea: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z',
|
||||
},
|
||||
],
|
||||
class: 'w-6 h-6 transition flex-shrink-0 duration-300',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor',
|
||||
},
|
||||
arrowRightStatic: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm9 18 6-6-6-6',
|
||||
},
|
||||
],
|
||||
class: 'size-4 flex-shrink-0',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
openInNew: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm4.5 19.5 15-15m0 0H8.25m11.25 0v11.25',
|
||||
},
|
||||
],
|
||||
class: 'ml-0.5 w-3 h-3 md:w-4 md:h-4 inline pb-0.5',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '3',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
accordionNotActive: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm6 9 6 6 6-6',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'block h-5 w-5 flex-shrink-0 text-neutral-600 group-hover:text-neutral-500 hs-accordion-active:hidden dark:text-neutral-400',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
accordionActive: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm18 15-6-6-6 6',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'hidden h-5 w-5 flex-shrink-0 text-neutral-600 group-hover:text-neutral-500 hs-accordion-active:block dark:text-neutral-400',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
xFooter: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor',
|
||||
title: 'Twitter',
|
||||
},
|
||||
facebookFooter: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor',
|
||||
title: 'Facebook',
|
||||
},
|
||||
githubFooter: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor',
|
||||
title: 'GitHub',
|
||||
},
|
||||
googleFooter: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor',
|
||||
title: 'Google',
|
||||
},
|
||||
slackFooter: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z',
|
||||
},
|
||||
],
|
||||
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'currentColor',
|
||||
title: 'Slack',
|
||||
},
|
||||
quotation: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M7.39762 10.3C7.39762 11.0733 7.14888 11.7 6.6514 12.18C6.15392 12.6333 5.52552 12.86 4.76621 12.86C3.84979 12.86 3.09047 12.5533 2.48825 11.94C1.91222 11.3266 1.62421 10.4467 1.62421 9.29999C1.62421 8.07332 1.96459 6.87332 2.64535 5.69999C3.35231 4.49999 4.33418 3.55332 5.59098 2.85999L6.4943 4.25999C5.81354 4.73999 5.26369 5.27332 4.84476 5.85999C4.45201 6.44666 4.19017 7.12666 4.05926 7.89999C4.29491 7.79332 4.56983 7.73999 4.88403 7.73999C5.61716 7.73999 6.21938 7.97999 6.69067 8.45999C7.16197 8.93999 7.39762 9.55333 7.39762 10.3ZM14.6242 10.3C14.6242 11.0733 14.3755 11.7 13.878 12.18C13.3805 12.6333 12.7521 12.86 11.9928 12.86C11.0764 12.86 10.3171 12.5533 9.71484 11.94C9.13881 11.3266 8.85079 10.4467 8.85079 9.29999C8.85079 8.07332 9.19117 6.87332 9.87194 5.69999C10.5789 4.49999 11.5608 3.55332 12.8176 2.85999L13.7209 4.25999C13.0401 4.73999 12.4903 5.27332 12.0713 5.85999C11.6786 6.44666 11.4168 7.12666 11.2858 7.89999C11.5215 7.79332 11.7964 7.73999 12.1106 7.73999C12.8437 7.73999 13.446 7.97999 13.9173 8.45999C14.3886 8.93999 14.6242 9.55333 14.6242 10.3Z',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'absolute start-0 top-0 h-16 w-16 -translate-x-6 -translate-y-8 transform text-neutral-300 dark:text-neutral-700',
|
||||
width: 16,
|
||||
height: 16,
|
||||
viewBox: '0 0 16 16',
|
||||
fill: 'currentColor',
|
||||
},
|
||||
question: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
chatBubble: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155',
|
||||
},
|
||||
],
|
||||
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
mapPin: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z',
|
||||
},
|
||||
{
|
||||
d: 'M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
envelopeOpen: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M21.75 9v.906a2.25 2.25 0 0 1-1.183 1.981l-6.478 3.488M2.25 9v.906a2.25 2.25 0 0 0 1.183 1.981l6.478 3.488m8.839 2.51-4.66-2.51m0 0-1.023-.55a2.25 2.25 0 0 0-2.134 0l-1.022.55m0 0-4.661 2.51m16.5 1.615a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V8.844a2.25 2.25 0 0 1 1.183-1.981l7.5-4.039a2.25 2.25 0 0 1 2.134 0l7.5 4.039a2.25 2.25 0 0 1 1.183 1.98V19.5Z',
|
||||
},
|
||||
],
|
||||
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
earth: {
|
||||
paths: [
|
||||
{
|
||||
d: 'm20.893 13.393-1.135-1.135a2.252 2.252 0 0 1-.421-.585l-1.08-2.16a.414.414 0 0 0-.663-.107.827.827 0 0 1-.812.21l-1.273-.363a.89.89 0 0 0-.738 1.595l.587.39c.59.395.674 1.23.172 1.732l-.2.2c-.212.212-.33.498-.33.796v.41c0 .409-.11.809-.32 1.158l-1.315 2.191a2.11 2.11 0 0 1-1.81 1.025 1.055 1.055 0 0 1-1.055-1.055v-1.172c0-.92-.56-1.747-1.414-2.089l-.655-.261a2.25 2.25 0 0 1-1.383-2.46l.007-.042a2.25 2.25 0 0 1 .29-.787l.09-.15a2.25 2.25 0 0 1 2.37-1.048l1.178.236a1.125 1.125 0 0 0 1.302-.795l.208-.73a1.125 1.125 0 0 0-.578-1.315l-.665-.332-.091.091a2.25 2.25 0 0 1-1.591.659h-.18c-.249 0-.487.1-.662.274a.931.931 0 0 1-1.458-1.137l1.411-2.353a2.25 2.25 0 0 0 .286-.76m11.928 9.869A9 9 0 0 0 8.965 3.525m11.928 9.868A9 9 0 1 1 8.965 3.525',
|
||||
},
|
||||
],
|
||||
class: 'w-4 h-4 flex-shrink-0',
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '1.5',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
party: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M5.8 11.3 2 22l10.7-3.79',
|
||||
},
|
||||
{
|
||||
d: 'M4 3h.01',
|
||||
},
|
||||
{
|
||||
d: 'M22 8h.01',
|
||||
},
|
||||
{
|
||||
d: 'M15 2h.01',
|
||||
},
|
||||
{
|
||||
d: 'M22 20h.01',
|
||||
},
|
||||
{
|
||||
d: 'm22 2-2.24.75a2.9 2.9 0 0 0-1.96 3.12v0c.1.86-.57 1.63-1.45 1.63h-.38c-.86 0-1.6.6-1.76 1.44L14 10',
|
||||
},
|
||||
{
|
||||
d: 'm22 13-.82-.33c-.86-.34-1.82.2-1.98 1.11v0c-.11.7-.72 1.22-1.43 1.22H17',
|
||||
},
|
||||
{
|
||||
d: 'm11 2 .33.82c.34.86-.2 1.82-1.11 1.98v0C9.52 4.9 9 5.52 9 6.23V7',
|
||||
},
|
||||
{
|
||||
d: 'M11 13c1.93 1.93 2.83 4.17 2 5-.83.83-3.07-.07-5-2-1.93-1.93-2.83-4.17-2-5 .83-.83 3.07.07 5 2Z',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'w-6 h-6 group-hover:text-steel dark:group-hover:text-steel transition-all duration-200 text-neutral-600 dark:text-neutral-300',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
email: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'w-8 h-8 group-hover:text-steel dark:group-hover:text-steel transition-all duration-200 text-neutral-600 dark:text-neutral-300',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
sun: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4',
|
||||
},
|
||||
],
|
||||
circleCx: '12',
|
||||
circleCy: '12',
|
||||
circleR: '5',
|
||||
class:
|
||||
'icon-light absolute h-5 w-5 scale-100 rotate-0 text-neutral-800 transition-all duration-500 dark:scale-0 dark:-rotate-90 dark:text-neutral-200',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
moon: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'icon-dark absolute h-5 w-5 scale-0 rotate-90 text-neutral-800 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-neutral-200',
|
||||
width: 24,
|
||||
height: 24,
|
||||
viewBox: '0 0 24 24',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
arrow: {
|
||||
paths: [
|
||||
{
|
||||
d: 'M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z',
|
||||
},
|
||||
],
|
||||
class:
|
||||
'icon-dark absolute h-5 w-5 scale-0 rotate-90 text-neutral-800 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-neutral-200',
|
||||
width: 16,
|
||||
height: 16,
|
||||
viewBox: '0 0 20 20',
|
||||
fill: 'none',
|
||||
strokeWidth: '2',
|
||||
strokeLinecap: 'round',
|
||||
strokeLinejoin: 'round',
|
||||
stroke: 'currentColor',
|
||||
},
|
||||
};
|
||||
17
src/components/ui/images/Image.astro
Normal file
17
src/components/ui/images/Image.astro
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import { ImageMetadata } from 'astro';
|
||||
import { blurStyle } from '@support/image';
|
||||
|
||||
interface FsPathImage extends ImageMetadata {
|
||||
fsPath?: string;
|
||||
}
|
||||
|
||||
const props = Astro.props;
|
||||
|
||||
const image = props.src as FsPathImage;
|
||||
const showBlur = !props.disableBlur;
|
||||
const blurCSS = image.fsPath && showBlur ? await blurStyle(image.fsPath) : {};
|
||||
---
|
||||
|
||||
<Image {...props} style={blurCSS} inferSize={true} />
|
||||
@@ -1,7 +1,13 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import { blurStyle } from '@support/image';
|
||||
|
||||
const { srcLight, srcDark, alt, style, width, height } = Astro.props;
|
||||
const { srcLight, srcDark, alt, style, disableBlur, width, height } = Astro.props;
|
||||
|
||||
const showBlur = !disableBlur;
|
||||
|
||||
const blurLight = (srcLight?.fsPath && showBlur) ? await blurStyle(srcLight.fsPath) : {};
|
||||
const blurDark = (srcDark?.fsPath && showBlur) ? await blurStyle(srcDark.fsPath) : {};
|
||||
---
|
||||
|
||||
<div class="themed-image-container">
|
||||
@@ -9,20 +15,20 @@ const { srcLight, srcDark, alt, style, width, height } = Astro.props;
|
||||
src={srcLight}
|
||||
alt={alt}
|
||||
class={`light-logo ${style}`}
|
||||
style={blurLight}
|
||||
inferSize={true}
|
||||
width={width}
|
||||
height={height}
|
||||
inferSize={true}
|
||||
/>
|
||||
|
||||
<Image
|
||||
src={srcDark}
|
||||
alt={alt}
|
||||
class={`dark-logo ${style}`}
|
||||
style={blurDark}
|
||||
inferSize={true}
|
||||
width={width}
|
||||
height={height}
|
||||
inferSize={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
---
|
||||
import { Image } from 'astro:assets';
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import Image from '@components/ui/images/Image.astro';
|
||||
import logo from '@images/brand_logo.png';
|
||||
import directus from '@lib/directus';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
---
|
||||
|
||||
<Image
|
||||
src={logo}
|
||||
alt={global.name}
|
||||
draggable="false"
|
||||
loading="eager"
|
||||
inferSize={true}
|
||||
{...Astro.props}
|
||||
/>
|
||||
<Image src={logo} alt={global.name} {...Astro.props} draggable="false" loading="eager" />
|
||||
16
src/components/ui/logos/Logo.astro
Normal file
16
src/components/ui/logos/Logo.astro
Normal file
@@ -0,0 +1,16 @@
|
||||
---
|
||||
import ImageTheme from '@components/ui/images/ImageTheme.astro';
|
||||
|
||||
const { srcLight, srcDark, alt } = Astro.props;
|
||||
---
|
||||
|
||||
<ImageTheme
|
||||
srcLight={srcLight}
|
||||
srcDark={srcDark}
|
||||
alt={alt}
|
||||
style='color: transparent; width: 48px; height: 48px; object-fit: contain; max-height: 100%; max-width: 100%;'
|
||||
draggable="false"
|
||||
loading="lazy"
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
@@ -3,8 +3,8 @@ import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Application } from '@lib/directusTypes';
|
||||
|
||||
import HighlightsCard from '@components/cards/HighlightsCard.astro';
|
||||
import directus from '@lib/directus';
|
||||
import HighlightsCard from '@components/ui/cards/HighlightsCard.astro';
|
||||
|
||||
const applications = ((await directus.request(
|
||||
readItems('site_applications' as any, {
|
||||
@@ -3,8 +3,9 @@ import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Education, Certificate} from '@lib/directusTypes';
|
||||
|
||||
import EducationCard from '@components/cards/EducationCard.astro';
|
||||
import directus from '@lib/directus';
|
||||
import { getDirectusImageURL } from '@lib/directusFunctions';
|
||||
import EducationCard from '@components/ui/cards/EducationCard.astro';
|
||||
|
||||
const educations = ((await directus.request(
|
||||
readItems('site_education' as any, {
|
||||
@@ -26,7 +27,7 @@ const certificates = ((await directus.request(
|
||||
Education
|
||||
</h3>
|
||||
<div class="mx-8">
|
||||
<h4 class="smooth-reveal card-text-header-minor pt-5">
|
||||
<h4 class="smooth-reveal card-text-header-minor pt-5 ">
|
||||
College
|
||||
</h4>
|
||||
<div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4 py-3">
|
||||
@@ -36,8 +37,8 @@ const certificates = ((await directus.request(
|
||||
area={education.area}
|
||||
date={education.graduationDate}
|
||||
url={education.url}
|
||||
logoUrlLight={education.logo}
|
||||
logoUrlDark={education.logoDark}
|
||||
logoUrlLight={getDirectusImageURL(education.logo)}
|
||||
logoUrlDark={getDirectusImageURL(education.logoDark)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -55,8 +56,7 @@ const certificates = ((await directus.request(
|
||||
area={certificate.issuer}
|
||||
date={certificate.issuerDate}
|
||||
url={certificate.url}
|
||||
logoUrlLight={certificate.logo}
|
||||
logoUrlDark={certificate.logoDark}
|
||||
logoIcon={certificate.logoName}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
152
src/components/ui/sections/Experience.astro
Normal file
152
src/components/ui/sections/Experience.astro
Normal file
@@ -0,0 +1,152 @@
|
||||
---
|
||||
import { Icon } from 'astro-icon/components';
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Experience } from '@lib/directusTypes';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
|
||||
const experiences = await directus.request(
|
||||
readItems('site_experience', {
|
||||
fields: ['*'],
|
||||
sort: ['-endDate'],
|
||||
})
|
||||
);
|
||||
---
|
||||
|
||||
<section
|
||||
class:list={['flex flex-col gap-8', Astro.props.className]}
|
||||
|
||||
>
|
||||
<h3 class="relative flex w-full items-center gap-3 pb-10 text-5xl text-neutral-800 dark:text-neutral-200">Experience</h3>
|
||||
<ul class="ml-8 w-full flex flex-col">
|
||||
{
|
||||
experiences.map(
|
||||
(experience: Experience) => {
|
||||
const startYear = new Date(experience.startDate).getFullYear();
|
||||
const endYear = experience.endDate != null ? new Date(experience.endDate).getFullYear() : 'Present';
|
||||
|
||||
return (
|
||||
<li class="relative">
|
||||
<div class="group smooth-reveal relative grid pb-1 transition-all sm:grid-cols-18 sm:gap-8 md:gap-6 lg:hover:!opacity-100">
|
||||
<header class="relative mt-1 text-lg font-semibold sm:col-span-3 text-neutral-800 dark:text-neutral-200">
|
||||
<time datetime={experience.startDate} data-title={experience.startDate}>
|
||||
{startYear}
|
||||
</time>{' '}
|
||||
-{' '}
|
||||
<time datetime={experience.endDate} data-title={experience.endDate}>
|
||||
{endYear}
|
||||
</time>
|
||||
</header>
|
||||
<div class="relative flex flex-col pb-6 before:absolute before:mt-8 before:-ml-6 before:h-full before:w-px before:bg-stone-400 sm:col-span-12">
|
||||
<div class="absolute mt-4 h-2 w-2 -translate-x-[1.71rem] rounded-full bg-stone-400" />
|
||||
<h3>
|
||||
<div
|
||||
class="inline-flex items-center text-2xl leading-tight font-semibold"
|
||||
aria-label="{position} - {company}"
|
||||
>
|
||||
<span class="text-neutral-800 dark:text-neutral-200">
|
||||
{experience.position} <span>@</span>
|
||||
{experience.url ? (
|
||||
<a
|
||||
class="hover:text-steel dark:hover:text-bermuda"
|
||||
href={experience.url}
|
||||
title={`Ver ${experience.name}`}
|
||||
target="_blank"
|
||||
>
|
||||
{experience.name}
|
||||
</a>
|
||||
) : (
|
||||
<span>{experience.name}</span>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</h3>
|
||||
{(experience.location || experience.location_type) && (
|
||||
<div class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||
{experience.location} {experience.location && experience.location_type && '-'} {experience.location_type}
|
||||
</div>
|
||||
)}
|
||||
<div class="text-md mt-4 flex flex-col gap-4" x-data="{ expanded: false }">
|
||||
{experience.summary && (
|
||||
<div class="flex flex-col gap-1">
|
||||
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Summary:</h4>
|
||||
<ul class="flex list-disc flex-col gap-2 text-neutral-700 dark:text-neutral-400 [&>li]:ml-4">
|
||||
{Array.isArray(experience.summary) ? (
|
||||
experience.summary.map((item) => ({ item }))
|
||||
) : (
|
||||
<li class="marker:text-steel dark:marker:text-bermuda">{experience.summary}</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(experience.responsibilities || experience.achievements) && (
|
||||
<div class="relative flex flex-col gap-4 max-sm:!h-auto md:after:absolute md:after:bottom-0 md:after:h-12 md:after:w-full md:after:bg-gradient-to-t md:after:from-neutral-200 dark:md:after:from-stone-700 md:after:content-[''] " :class="expanded ? 'after:hidden' : ''" x-show="expanded" x-collapse.min.50px>
|
||||
{experience.responsibilities && (
|
||||
<div class="flex flex-col gap-1">
|
||||
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Responsibilities:</h4>
|
||||
<ul class="text-neutral-700 dark:text-neutral-400 [&>li]:ml-4 flex list-disc flex-col gap-2">
|
||||
{experience.responsibilities.map(responsibility => (
|
||||
<li class="marker:text-steel dark:marker:text-bermuda">{responsibility}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{experience.achievements && (
|
||||
<div class="flex flex-col gap-1">
|
||||
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Achievements:</h4>
|
||||
<ul class="text-neutral-700 dark:text-neutral-400 [&>li]:ml-4 flex list-disc flex-col gap-2">
|
||||
{experience.achievements.map(achievement => (
|
||||
<li class="marker:text-steel dark:marker:text-bermuda">{achievement}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button @click="expanded = ! expanded" class="group/more w-fit cursor-pointer items-center justify-center gap-1.5 text-xs underline text-neutral-700 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-neutral-400 transition-all flex">
|
||||
<span x-text="expanded ? 'Show less' : 'Show more'">Show more</span>
|
||||
<svg
|
||||
class="h-4 w-4 duration-200 ease-out group-hover/more:translate-y-0.5"
|
||||
:class="{ 'rotate-180': expanded }"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polyline points="6 9 12 15 18 9" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<ul class="flex print:hidden flex-wrap gap-2" aria-label="Technologies used">
|
||||
{experience.skills && experience.skills.map(skill => {
|
||||
const iconName = skill.toLowerCase();
|
||||
return (
|
||||
<li class="bg-steel/20 border-steel/20 text-neutral-800 dark:bg-bermuda/20 dark:border-bermuda/20 dark:text-neutral-200 flex gap-1 items-center border-solid border rounded-md px-2 py-0.5 text-xs">
|
||||
<Icon name={`mdi:${iconName}`} /> <span>{skill}</span>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
)
|
||||
}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Alpine Plugins -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
||||
|
||||
<!-- Alpine Core -->
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
@@ -1,39 +1,41 @@
|
||||
---
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import FeaturesCard from '@components/cards/FeaturesCard.astro';
|
||||
import directus from '@lib/directus';
|
||||
import FeaturesCard from '@components/ui/cards/FeaturesCard.astro';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
---
|
||||
|
||||
<section class="max-w-340 2xl:max-w-full px-4 sm:px-6 lg:px-8 py-10 lg:py-14 mx-auto mb-2 md:mb-8">
|
||||
<div class="flex flex-col sm:flex-row items-center justify-center gap-y-2 sm:gap-x-12 sm:gap-y-0 lg:gap-x-24">
|
||||
<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="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:gap-x-12 sm:gap-y-0 lg:gap-x-24"
|
||||
>
|
||||
<div class="max-w-5xl sm:px-6 lg:px-8">
|
||||
<div class="flex flex-wrap gap-6 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3 justify-center">
|
||||
<FeaturesCard
|
||||
title="Cloud Engineer"
|
||||
description="Full stack and cloud engineer."
|
||||
url="/about"
|
||||
logoUrlLight="https://img.icons8.com/cotton/64/cloud-development--v2.png"
|
||||
icon="mdi:cloud-outline"
|
||||
/>
|
||||
<FeaturesCard
|
||||
title="Homelab"
|
||||
description="Tinkering, testing, deploying, etc, etc ..."
|
||||
url="/categories/homelab/"
|
||||
logoUrlLight="https://img.icons8.com/cotton/64/smart-home-connection.png"
|
||||
icon="mdi:home-variant-outline"
|
||||
/>
|
||||
<FeaturesCard
|
||||
title="Documentation"
|
||||
description="Reference and guides for my homelab."
|
||||
url="https://docs.alexlebens.dev"
|
||||
logoUrlLight="https://img.icons8.com/cotton/64/bookmarked-document--v1.png"
|
||||
icon="mdi:file-document-multiple"
|
||||
/>
|
||||
<FeaturesCard
|
||||
title="Email"
|
||||
description={`Send me a message.`}
|
||||
url=`mailto:${global.email}`
|
||||
logoUrlLight="https://img.icons8.com/cotton/64/secured-letter--v3.png"
|
||||
icon="mdi:email-fast"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
35
src/components/ui/sections/HeaderSection.astro
Normal file
35
src/components/ui/sections/HeaderSection.astro
Normal file
@@ -0,0 +1,35 @@
|
||||
---
|
||||
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subTitle: string;
|
||||
btnExists?: boolean;
|
||||
btnTitle?: string;
|
||||
btnURL?: string;
|
||||
}
|
||||
|
||||
const { title, subTitle, btnExists, btnTitle, btnURL } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="mx-auto mt-10 px-4 sm:px-6 lg:px-8 lg:pt-10 2xl:max-w-full">
|
||||
<div class="flex-wrap md:flex md:items-center md:justify-between">
|
||||
<div class="w-full md:w-auto">
|
||||
<h1
|
||||
class="smooth-reveal block text-4xl font-bold tracking-tight text-balance text-neutral-800 md:text-5xl lg:text-6xl dark:text-neutral-200"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
<p class="smooth-reveal mt-4 text-lg text-pretty text-neutral-600 dark:text-neutral-400">
|
||||
{subTitle}
|
||||
</p>
|
||||
{
|
||||
btnExists ? (
|
||||
<div class="smooth-reveal mt-4 md:mt-8">
|
||||
<PrimaryCTA title={btnTitle} url={btnURL} />
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
66
src/components/ui/sections/HeroSection.astro
Normal file
66
src/components/ui/sections/HeroSection.astro
Normal file
@@ -0,0 +1,66 @@
|
||||
---
|
||||
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
|
||||
import Image from '@components/ui/images/Image.astro';
|
||||
|
||||
const { title, subTitle, primaryBtn, primaryBtnURL, secondaryBtn, secondaryBtnURL, src, alt } =
|
||||
Astro.props;
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
primaryBtn?: string;
|
||||
primaryBtnURL?: string;
|
||||
secondaryBtn?: string;
|
||||
secondaryBtnURL?: string;
|
||||
src?: any;
|
||||
alt?: string;
|
||||
rounded?: boolean;
|
||||
}
|
||||
|
||||
const roundedClasses = Astro.props.rounded ? "rounded-xl" : null;
|
||||
---
|
||||
|
||||
<section
|
||||
class="mx-auto grid max-w-340 gap-4 px-4 py-14 sm:px-6 md:grid-cols-2 md:items-center md:gap-8 lg:px-8 2xl:max-w-full"
|
||||
>
|
||||
<div>
|
||||
<h1
|
||||
class="smooth-reveal block text-3xl font-bold tracking-tight text-balance text-neutral-800 sm:text-4xl lg:text-7xl lg:leading-tight dark:text-neutral-200"
|
||||
>
|
||||
<Fragment set:html={title} />
|
||||
</h1>
|
||||
{
|
||||
subTitle && (
|
||||
<p class="smooth-reveal mt-6 text-lg leading-relaxed text-pretty text-neutral-700 lg:w-4/5 dark:text-neutral-300">
|
||||
{subTitle}
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
<div class="smooth-reveal mt-7 grid w-full gap-3 sm:inline-flex">
|
||||
{primaryBtn && <PrimaryCTA title={primaryBtn} url={primaryBtnURL} />}
|
||||
{secondaryBtn && <SecondaryCTA title={secondaryBtn} url={secondaryBtnURL} />}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="smooth-reveal-fade hidden w-full md:block">
|
||||
<div class="top-12 flex w-full justify-center overflow-hidden md:ml-4">
|
||||
{
|
||||
src && alt && (
|
||||
<Image
|
||||
src={src}
|
||||
alt={alt}
|
||||
class={`h-full w-[420px] scale-100 object-cover object-center ${roundedClasses}`}
|
||||
draggable="false"
|
||||
loading="eager"
|
||||
format="webp"
|
||||
quality="low"
|
||||
widths={[840]}
|
||||
disableBlur={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -1,20 +1,22 @@
|
||||
---
|
||||
import GiteaButton from '@components/buttons/GiteaButton.astro';
|
||||
import GiteaBtn from '@components/ui/buttons/GiteaBtn.astro';
|
||||
|
||||
const { title, subTitle, url } = Astro.props;
|
||||
const btnTitle = 'Continue to Gitea';
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subTitle?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const { title, subTitle, url } = Astro.props;
|
||||
---
|
||||
|
||||
<section class="relative max-w-340 pt-30 pb-30 px-4 sm:px-6 lg:px- mx-auto mb-2 md:mb-10">
|
||||
<!-- Animated shapes -->
|
||||
<div class="smooth-reveal absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]">
|
||||
<section class="lg:px- relative mx-auto mb-20 max-w-340 px-4 pt-30 pb-30 sm:px-6">
|
||||
<div
|
||||
class="smooth-reveal absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]"
|
||||
>
|
||||
<svg
|
||||
class="gitea-animate-hover gitea-animate-hover-1"
|
||||
class="animate-hover animate-hover-1"
|
||||
width="64"
|
||||
height="64"
|
||||
fill="none"
|
||||
@@ -44,7 +46,7 @@ const { title, subTitle, url } = Astro.props;
|
||||
</div>
|
||||
<div class="smooth-reveal absolute top-0 left-[85%] scale-75">
|
||||
<svg
|
||||
class="gitea-animate-hover gitea-animate-hover-2"
|
||||
class="animate-hover animate-hover-2"
|
||||
width="64"
|
||||
height="64"
|
||||
fill="none"
|
||||
@@ -78,9 +80,11 @@ const { title, subTitle, url } = Astro.props;
|
||||
d="M10.5 19H9M15 19h-1.5"></path>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="smooth-reveal absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]">
|
||||
<div
|
||||
class="smooth-reveal absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]"
|
||||
>
|
||||
<svg
|
||||
class="gitea-animate-hover gitea-animate-hover-3"
|
||||
class="animate-hover animate-hover-3"
|
||||
width="64"
|
||||
height="64"
|
||||
fill="none"
|
||||
@@ -102,54 +106,59 @@ const { title, subTitle, url } = Astro.props;
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
<!-- Heading -->
|
||||
<!-- Hero Section Heading -->
|
||||
<div class="smooth-reveal-2 mx-auto mt-5 max-w-xl text-center">
|
||||
<h1 class="card-text-header block">
|
||||
<h2
|
||||
class="block text-4xl leading-tight font-bold tracking-tight text-balance text-neutral-800 md:text-5xl lg:text-5xl dark:text-neutral-200"
|
||||
>
|
||||
{title}
|
||||
</h1>
|
||||
</h2>
|
||||
</div>
|
||||
<!-- Sub-heading -->
|
||||
<!-- Hero Section Sub-heading -->
|
||||
<div class="smooth-reveal-2 mx-auto mt-5 max-w-3xl text-center">
|
||||
{subTitle && (
|
||||
<p class="card-text-header-description">
|
||||
{subTitle}
|
||||
</p>
|
||||
)}
|
||||
{
|
||||
subTitle && (
|
||||
<p class="text-lg text-pretty text-neutral-600 dark:text-neutral-400">{subTitle}</p>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<!-- Gitea Button -->
|
||||
{url && (
|
||||
<div class="smooth-reveal-2 flex justify-center mt-8 gap-3">
|
||||
<GiteaButton url={url}/>
|
||||
</div>
|
||||
)}
|
||||
<!-- Github Button -->
|
||||
{
|
||||
url && (
|
||||
<div class="smooth-reveal-2 mt-8 flex justify-center gap-3">
|
||||
<GiteaBtn url={url} title={btnTitle} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@keyframes gitea-animate-hover {
|
||||
@keyframes animate-hover {
|
||||
from {
|
||||
transform: translateY(15px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(-15px);
|
||||
}
|
||||
}
|
||||
|
||||
.gitea-animate-hover {
|
||||
animation: gitea-animate-hover ease-in-out;
|
||||
.animate-hover {
|
||||
animation: animate-hover ease-in-out;
|
||||
|
||||
animation-iteration-count: infinite;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
.gitea-animate-hover-1 {
|
||||
.animate-hover-1 {
|
||||
animation-duration: 5s;
|
||||
}
|
||||
|
||||
.gitea-animate-hover-2 {
|
||||
.animate-hover-2 {
|
||||
animation-duration: 5.5s;
|
||||
}
|
||||
|
||||
.gitea-animate-hover-3 {
|
||||
.animate-hover-3 {
|
||||
animation-duration: 6s;
|
||||
}
|
||||
</style>
|
||||
32
src/components/ui/sections/LatestPosts.astro
Normal file
32
src/components/ui/sections/LatestPosts.astro
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import { readItems } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
import BlogCard from '@components/blog/BlogCard.astro';
|
||||
|
||||
const posts = await directus.request(
|
||||
readItems('posts', {
|
||||
filter: { published: { _eq: true } },
|
||||
fields: ['*'],
|
||||
sort: ['-published_date'],
|
||||
})
|
||||
);
|
||||
|
||||
const recentPosts = posts
|
||||
.sort((a: Post, b: Post) => (new Date(b.published_date).getTime()) - (new Date(a.published_date).getTime()))
|
||||
.slice(0, 3);
|
||||
---
|
||||
|
||||
<section class="mx-auto mb-20 max-w-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 block text-4xl font-bold text-neutral-800 md:text-5xl md:leading-tight lg:text-5xl dark:text-neutral-200"
|
||||
>
|
||||
Latest Posts
|
||||
</h1>
|
||||
</div>
|
||||
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{recentPosts.map((b) => <BlogCard post={b} />)}
|
||||
</div>
|
||||
</section>
|
||||
@@ -3,8 +3,8 @@ import { readItems } from '@directus/sdk';
|
||||
|
||||
import type { Project } from '@lib/directusTypes';
|
||||
|
||||
import HighlightsCard from '@components/cards/HighlightsCard.astro';
|
||||
import directus from '@lib/directus';
|
||||
import HighlightsCard from '@components/ui/cards/HighlightsCard.astro';
|
||||
|
||||
const projects = ((await directus.request(
|
||||
readItems('site_projects' as any, {
|
||||
@@ -6,53 +6,78 @@ import type { Skill } from '@lib/directusTypes';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
|
||||
const skills = ((await directus.request(
|
||||
readItems('site_skills' as any, {
|
||||
const skills = await directus.request(
|
||||
readItems('site_skills', {
|
||||
fields: ['*'],
|
||||
sort: ['-date_created'],
|
||||
})
|
||||
)) as unknown) as Skill[];
|
||||
);
|
||||
|
||||
const baseClasses = 'mx-2 min-w-[220px] sm:mx-4 sm:min-w-[280px]';
|
||||
const borderClasses =
|
||||
'border border-neutral-100 hover:border-neutral-200 dark:border-stone-500/20 dark:hover:border-neutral-800';
|
||||
const bgColorClasses = 'bg-neutral-100/80 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
|
||||
const hoverClasses = 'hover:-translate-y-2 hover:scale-105 ';
|
||||
const shadowClasses = 'shadow-xs hover:shadow-lg';
|
||||
---
|
||||
|
||||
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
|
||||
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5">
|
||||
<h3
|
||||
class="relative flex w-full items-center gap-3 pb-4 text-5xl text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
Skills
|
||||
</h3>
|
||||
<div>
|
||||
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8 mask-[linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]">
|
||||
<div class="">
|
||||
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
|
||||
<!-- Main slider container -->
|
||||
<div class="slider-track animate-slide flex">
|
||||
{[...skills, ...skills, ...skills].map((skill: Skill) => {
|
||||
return (
|
||||
<div class="skill-card card-base transform hover:-translate-y-2 hover:scale-105 transition-all duration-300 mx-2 min-w-55 sm:mx-4 sm:min-w-70">
|
||||
<div class="p-4 sm:p-6">
|
||||
<div class="flex items-center justify-between mb-4 sm:mb-6">
|
||||
<div class="flex items-center gap-2 sm:gap-4">
|
||||
<div class="flex items-center justify-center rounded-lg text-primary">
|
||||
<Icon name={skill.icon} class="h-8 w-8 sm:h-12 sm:w-12" />
|
||||
</div>
|
||||
<h3 class="text-neutral-900 dark:text-neutral-100 text-base font-semibold sm:text-xl">
|
||||
{skill.title}
|
||||
</h3>
|
||||
</div>
|
||||
<span class=" bg-neutral-200 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 font-mono text-xs sm:text-sm rounded-full px-2 sm:px-2.5 py-0.5 sm:py-1">
|
||||
{skill.level}%
|
||||
</span>
|
||||
</div>
|
||||
<div class="relative bg-stone-500/20 dark:bg-stone-500/20 rounded-full h-1.5 sm:h-2 w-full overflow-hidden">
|
||||
{
|
||||
[...skills, ...skills, ...skills].map((skill: Skill) => {
|
||||
return (
|
||||
<div
|
||||
class="progress-bar-animate bg-linear-to-r from-steel via-bermuda to-steel absolute top-0 left-0 h-full rounded-full transition-all duration-1000"
|
||||
style={`width: ${skill.level}%`}
|
||||
/>
|
||||
</div>
|
||||
<div class="flex justify-between text-secondary font-mono text-[10px] mt-1 sm:mt-2 sm:text-xs">
|
||||
<span>Beginner</span>
|
||||
<span>Advanced</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
class={`skill-card transform rounded-xl transition-all duration-300 ${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${shadowClasses}`}
|
||||
>
|
||||
<div class="p-4 sm:p-6">
|
||||
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
||||
<div class="flex items-center gap-2 sm:gap-4">
|
||||
<div class="flex transform items-center justify-center rounded-lg text-neutral-800 transition-transform group-hover:rotate-12 dark:text-neutral-200">
|
||||
<Icon name={skill.icon} class="h-8 w-8 sm:h-12 sm:w-12" />
|
||||
</div>
|
||||
<h3 class="text-base font-semibold text-neutral-900 sm:text-xl dark:text-neutral-100">
|
||||
{skill.title}
|
||||
</h3>
|
||||
</div>
|
||||
<span class="rounded-full bg-neutral-200 px-2 py-0.5 font-mono text-xs text-neutral-700 sm:px-2.5 sm:py-1 sm:text-sm dark:bg-neutral-800 dark:text-neutral-300">
|
||||
{skill.level}%
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="relative h-1.5 w-full overflow-hidden rounded-full bg-stone-500/20 sm:h-2 dark:bg-stone-500/20">
|
||||
<div
|
||||
class="progress-bar-animate from-steel via-bermuda to-steel absolute top-0 left-0 h-full rounded-full bg-linear-to-r transition-all duration-1000"
|
||||
style={`width: ${skill.level}%`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-1 flex justify-between font-mono text-[10px] text-neutral-600 sm:mt-2 sm:text-xs dark:text-neutral-400">
|
||||
<span>Beginner</span>
|
||||
<span>Advanced</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Gradient overlays -->
|
||||
<div
|
||||
class="absolute top-0 bottom-0 left-0 z-10 w-12 bg-linear-to-r from-neutral-200 to-transparent sm:w-24 dark:from-stone-700"
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-0 right-0 bottom-0 z-10 w-12 bg-linear-to-l from-neutral-200 to-transparent sm:w-24 dark:from-stone-700"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -60,6 +85,7 @@ const skills = ((await directus.request(
|
||||
|
||||
<script>
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
// Create infinite scrolling effect
|
||||
function setupInfiniteScroll() {
|
||||
const cards = document.querySelectorAll('.skill-card');
|
||||
if (!cards.length) return;
|
||||
@@ -67,6 +93,7 @@ const skills = ((await directus.request(
|
||||
|
||||
setupInfiniteScroll();
|
||||
|
||||
// Add hover effects to cards - only on non-touch devices
|
||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||
const cards = document.querySelectorAll('.skill-card');
|
||||
|
||||
@@ -117,7 +144,7 @@ const skills = ((await directus.request(
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Specific css to enable sliding effect */
|
||||
/* Tech Stack Slider */
|
||||
.slider-track {
|
||||
width: fit-content;
|
||||
animation: scroll 40s linear infinite;
|
||||
@@ -128,7 +155,7 @@ const skills = ((await directus.request(
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(-220px * 6 - 16px * 6));
|
||||
transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,7 +169,7 @@ const skills = ((await directus.request(
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(calc(-280px * 6 - 32px * 6));
|
||||
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,14 @@
|
||||
---
|
||||
import WeatherCard from '@components/cards/WeatherCard.astro';
|
||||
import WeatherCard from '@components/ui/cards/WeatherCard.astro';
|
||||
import { getFiveDayForecast } from '@support/weather';
|
||||
|
||||
const { latitude = "44.95", longitude = "-93.09", cityName = "St. Paul, Minnesota", timezone = "America/Chicago" } = Astro.props;
|
||||
|
||||
const { forecastDays, error } = await getFiveDayForecast(latitude, longitude, timezone);
|
||||
---
|
||||
|
||||
<section class="max-w-340 2xl:max-w-fullpx-4 sm:px-6 lg:px-8 py-10 lg:py-14 mx-auto mb-2 md:mb-8">
|
||||
<div class="text-center max-w-2xl mx-auto mb-10 lg:mb-14">
|
||||
<h1 class="smooth-reveal card-text-header block">
|
||||
<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="card-text-header smooth-reveal block font-bold md:leading-tight">
|
||||
Weather in my Area
|
||||
</h1>
|
||||
<div class="smooth-reveal mx-auto mt-5 max-w-3xl text-center">
|
||||
@@ -18,6 +17,7 @@ const { forecastDays, error } = await getFiveDayForecast(latitude, longitude, ti
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div class="card-base p-10 text-accent text-center">
|
||||
{error}
|
||||
@@ -2,13 +2,13 @@ import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
|
||||
export interface NavigationLink {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
|
||||
export const NavigationLinks: NavigationLink[] = [
|
||||
{ name: 'Home', url: '/' },
|
||||
{ name: 'Blog', url: '/blog/' },
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import { ClientRouter } from 'astro:transitions';
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import BaseHead from '@components/BaseHead.astro';
|
||||
import Footer from '@components/Footer.astro';
|
||||
import Header from '@components/Header.astro';
|
||||
import directus from '@lib/directus';
|
||||
|
||||
import '@styles/global.css';
|
||||
|
||||
@@ -20,16 +20,12 @@ interface Props {
|
||||
const { title, description = 'Alex Lebens', ogImage, lang = 'en', structuredData } = Astro.props;
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
|
||||
const normalizeTitle = !title ? global.name : `${title} | ${global.name}`;
|
||||
---
|
||||
|
||||
<html lang={lang}>
|
||||
<head>
|
||||
<title>
|
||||
{normalizeTitle}
|
||||
</title>
|
||||
|
||||
<title>{normalizeTitle}</title>
|
||||
<BaseHead
|
||||
title={normalizeTitle}
|
||||
description={description}
|
||||
@@ -38,9 +34,7 @@ const normalizeTitle = !title ? global.name : `${title} | ${global.name}`;
|
||||
ogDescription={description}
|
||||
structuredData={structuredData}
|
||||
/>
|
||||
|
||||
<ClientRouter fallback="swap" />
|
||||
|
||||
<script is:inline>
|
||||
const theme = (() => {
|
||||
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
||||
@@ -59,116 +53,41 @@ const normalizeTitle = !title ? global.name : `${title} | ${global.name}`;
|
||||
}
|
||||
window.localStorage.setItem('theme', theme);
|
||||
</script>
|
||||
|
||||
<!-- Rybbit Tracking Snippet -->
|
||||
<script
|
||||
src="https://rybbit.alexlebens.dev/api/script.js"
|
||||
data-site-id={global.rybbit_site_id}
|
||||
defer
|
||||
/>
|
||||
></script>
|
||||
</head>
|
||||
|
||||
<body class="bg-background selection:bg-yellow-400 m-0 p-0 overflow-x-hidden">
|
||||
|
||||
<!-- Sliding backgrounds -->
|
||||
<div class="bg"/>
|
||||
<div class="bg bg2"/>
|
||||
<div class="bg bg3"/>
|
||||
|
||||
<!-- Layout -->
|
||||
<div class="grow w-full max-w-(--breakpoint-2xl) px-4 sm:px-6 lg:px-8 py-20 mx-auto">
|
||||
|
||||
<body class="bg-stone-200 selection:bg-yellow-400 selection:text-neutral-700 dark:bg-stone-700">
|
||||
<div class="mx-auto w-full max-w-(--breakpoint-2xl) grow px-4 sm:px-6 lg:px-8">
|
||||
<Header />
|
||||
|
||||
<main class="has-js scroll-fade-container min-h-screen">
|
||||
<main class="min-h-screen">
|
||||
<slot />
|
||||
</main>
|
||||
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
|
||||
<style>
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script>
|
||||
document.addEventListener('astro:page-load', () => {
|
||||
const onScroll = () => {
|
||||
document.documentElement.style.setProperty('--scroll-offset', `${window.scrollY}px`);
|
||||
document.documentElement.classList.add('has-js');
|
||||
};
|
||||
|
||||
window.removeEventListener('scroll', onScroll);
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
||||
onScroll();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Fade away content below header when scrolling */
|
||||
.has-js .scroll-fade-container {
|
||||
-webkit-mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0px,
|
||||
transparent 16px,
|
||||
black 80px,
|
||||
black 100%
|
||||
);
|
||||
|
||||
mask-image: linear-gradient(
|
||||
to bottom,
|
||||
transparent 0px,
|
||||
transparent 16px,
|
||||
black 80px,
|
||||
black 100%
|
||||
);
|
||||
|
||||
-webkit-mask-size: 100vw 100vh;
|
||||
-webkit-mask-repeat: no-repeat;
|
||||
|
||||
-webkit-mask-position-y: var(--scroll-offset);
|
||||
mask-position-y: var(--scroll-offset);
|
||||
.bg-grid-pattern {
|
||||
background-size: 24px 24px;
|
||||
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.2) 1px, transparent 1px);
|
||||
transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1);
|
||||
}
|
||||
|
||||
/* Background that creates the "glimmer" effect */
|
||||
.bg {
|
||||
animation: slide 20s ease-in-out infinite alternate;
|
||||
background-image: linear-gradient(-60deg, var(--bg-primary) 33.3%, var(--bg-secondary) 33.3%, var(--bg-secondary) 66.6%, var(--bg-tertiary) 66.6%);
|
||||
filter: blur(80px);
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -50%;
|
||||
right: -50%;
|
||||
opacity: .5;
|
||||
position: fixed;
|
||||
z-index: -1;
|
||||
--bg-primary: #e5e5e5;
|
||||
--bg-secondary: #d9d9d9;
|
||||
--bg-tertiary: #ededed;
|
||||
}
|
||||
|
||||
:global(.dark) .bg {
|
||||
--bg-primary: #292524;
|
||||
--bg-secondary: #44403c;
|
||||
--bg-tertiary: #57534e;
|
||||
}
|
||||
|
||||
.bg2 {
|
||||
animation-direction: alternate-reverse;
|
||||
animation-duration: 30s;
|
||||
}
|
||||
|
||||
.bg3 {
|
||||
animation-duration: 25s;
|
||||
}
|
||||
|
||||
@keyframes slide {
|
||||
0% {
|
||||
transform:translateX(-25%);
|
||||
}
|
||||
100% {
|
||||
transform:translateX(25%);
|
||||
}
|
||||
:global(.dark) .bg-grid-pattern {
|
||||
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.25) 1px, transparent 1px);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
Skill,
|
||||
} from '@lib/directusTypes';
|
||||
|
||||
import { getDirectusURL } from '@/support/url';
|
||||
import { getDirectusURL } from '@lib/directusFunctions';
|
||||
|
||||
type Schema = {
|
||||
site_global: Global;
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
const getDirectusURL = () => {
|
||||
if (process.env.DIRECTUS_URL) {
|
||||
return `https://${process.env.DIRECTUS_URL}`;
|
||||
}
|
||||
return 'https://directus.alexlebens.net';
|
||||
};
|
||||
|
||||
const getSiteURL = () => {
|
||||
return 'https://www.alexlebens.dev';
|
||||
};
|
||||
|
||||
async function getDirectusImageURL(image: string) {
|
||||
return `${getDirectusURL()}/assets/${image}`;
|
||||
}
|
||||
|
||||
export { getDirectusURL, getSiteURL, getDirectusImageURL };
|
||||
export { getDirectusURL, getDirectusImageURL };
|
||||
@@ -92,8 +92,7 @@ export type Certificate = {
|
||||
issuer: string;
|
||||
issuerDate: string;
|
||||
url: string;
|
||||
logo: string;
|
||||
logoDark: string;
|
||||
logoName: string;
|
||||
};
|
||||
|
||||
export type Project = {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import GoBackButton from '@/components/buttons/GoBackButton.astro';
|
||||
import GoHomeButton from '@/components/buttons/GoHomeButton.astro';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import directus from '@lib/directus';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||
import GoBack from '@/components/ui/buttons/GoBack.astro';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
---
|
||||
@@ -28,44 +28,45 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
<section class="grid place-content-center mt-20">
|
||||
<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">
|
||||
<section class="mt-20 grid place-content-center">
|
||||
<div class="mx-auto max-w-7xl px-4 py-8 lg:px-6 lg:py-16">
|
||||
<div class="mx-auto max-w-screen-sm text-center">
|
||||
<div class="glitch-wrapper smooth-reveal">
|
||||
<h1
|
||||
class="glitch text-header text-9xl font-bold leading-none sm:text-[12rem]"
|
||||
class="glitch text-9xl leading-none font-bold text-neutral-900 sm:text-[12rem] dark:text-neutral-100"
|
||||
data-text="404"
|
||||
>
|
||||
Not Found
|
||||
</h1>
|
||||
</div>
|
||||
<h1 class="smooth-reveal text-yellow-500 dark:text-yellow-400 text-4xl md:text-5xl font-bold leading-tight tracking-tight text-balance mt-30">
|
||||
Page Not Found:
|
||||
|
||||
<h1
|
||||
class="text-dark smooth-reveal mb-4 text-7xl font-extrabold text-yellow-500 lg:text-9xl dark:text-yellow-400"
|
||||
>
|
||||
{`Page Not Found - ${global.name}`}
|
||||
</h1>
|
||||
<h1 class="smooth-reveal card-text-header mt-8 mb-30">
|
||||
{Astro.url.pathname.replace('/', '')}
|
||||
</h1>
|
||||
<div class="smooth-reveal card-base max-w-md p-6 mx-auto mt-16">
|
||||
<h3 class="card-text-title text-sm tracking-wider uppercase">
|
||||
<div
|
||||
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"
|
||||
>
|
||||
<h3
|
||||
class="text-sm font-medium tracking-wider text-neutral-500 uppercase dark:text-neutral-400"
|
||||
>
|
||||
Did you know?
|
||||
</h3>
|
||||
<p
|
||||
id="fun-fact"
|
||||
class="text-secondary text-sm mt-4 mb-2"
|
||||
>
|
||||
<p class="mt-2 text-sm text-neutral-600 dark:text-neutral-300" id="fun-fact">
|
||||
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.
|
||||
</p>
|
||||
</div>
|
||||
<div class="smooth-reveal flex flex-col sm:flex-row items-center justify-center gap-4 mt-10">
|
||||
<GoBackButton/>
|
||||
<GoHomeButton url={global.site_url} />
|
||||
<div
|
||||
class="smooth-reveal mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row"
|
||||
>
|
||||
<GoBack title="Go Back" />
|
||||
<PrimaryCTA title="Return Home" url={global.site_url} noArrow addHome />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import HeroSection from '@components/sections/HeroSection.astro';
|
||||
import ExperienceSection from '@components/sections/ExperienceSection.astro';
|
||||
import EducationSection from '@components/sections/EducationSection.astro';
|
||||
import ProjectSection from '@components/sections/ProjectSection.astro';
|
||||
import SkillsSliderSection from '@components/sections/SkillsSliderSection.astro';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import directus from '@lib/directus';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import HeroSection from '@components/ui/sections/HeroSection.astro';
|
||||
import Experience from '@components/ui/sections/Experience.astro';
|
||||
import EducationSection from '@components/ui/sections/EducationSection.astro';
|
||||
import ProjectSection from '@components/ui/sections/ProjectSection.astro';
|
||||
import SkillsSlider from '@components/ui/sections/SkillsSlider.astro';
|
||||
|
||||
import portraitImg from '@images/portrait.avif';
|
||||
|
||||
@@ -42,12 +42,12 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
rounded={true}
|
||||
/>
|
||||
|
||||
<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-12 md:gap-y-20">
|
||||
<ExperienceSection className="smooth-reveal" />
|
||||
<section class="mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
|
||||
<div class="flex flex-col gap-y-24 md:gap-y-32">
|
||||
<Experience className="smooth-reveal" />
|
||||
<EducationSection className="smooth-reveal" />
|
||||
<ProjectSection className="smooth-reveal" />
|
||||
<SkillsSliderSection className="smooth-reveal" />
|
||||
<SkillsSlider className="smooth-reveal" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
---
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import HeroSection from '@components/sections/HeroSection.astro';
|
||||
import ApplicationSection from '@components/sections/ApplicationSection.astro';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import directus from '@lib/directus';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import HeroSection from '@components/ui/sections/HeroSection.astro';
|
||||
import ApplicationSection from '@components/ui/sections/ApplicationSection.astro';
|
||||
|
||||
import applicationImg from '@images/cedar_tree.png';
|
||||
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
---
|
||||
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||
import { Image } from 'astro:assets';
|
||||
import getReadingTime from 'reading-time';
|
||||
import { readItems, readSingleton } from '@directus/sdk';
|
||||
|
||||
import directus from '@lib/directus';
|
||||
import { marked } from 'marked';
|
||||
import markedShiki from 'marked-shiki';
|
||||
import { createHighlighter } from 'shiki';
|
||||
import { readItems, readSingleton } from '@directus/sdk';
|
||||
|
||||
import SocialShareButton from '@components/buttons/SocialShareButton.astro';
|
||||
import { getDirectusImageURL } from '@lib/directusFunctions';
|
||||
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;
|
||||
import Image from '@components/ui/images/Image.astro';
|
||||
import { formatDateTime } from '@support/time';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const posts = await directus.request(readItems('posts'));
|
||||
@@ -22,19 +19,18 @@ export async function getStaticPaths() {
|
||||
props: post,
|
||||
}));
|
||||
}
|
||||
const post = Astro.props;
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
const category: CollectionEntry<'categories'> = (await getCollection('categories'))
|
||||
.filter((c) => c.slug === post.category)
|
||||
.pop() as CollectionEntry<'categories'>;
|
||||
|
||||
const readingTime = getReadingTime(post.content);
|
||||
|
||||
const highlighter = await createHighlighter({
|
||||
themes: ['github-light', 'github-dark'],
|
||||
themes: ['github-light', 'github-dark', 'monokai'],
|
||||
langs: ['typescript', 'python', 'css', 'html', 'yaml', 'bash', 'json'],
|
||||
});
|
||||
|
||||
marked.use(markedShiki({
|
||||
highlight(code, lang) {
|
||||
return highlighter.codeToHtml(code, {
|
||||
@@ -47,7 +43,6 @@ marked.use(markedShiki({
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
const content = marked.parse(post.content);
|
||||
---
|
||||
|
||||
@@ -68,7 +63,9 @@ const content = marked.parse(post.content);
|
||||
name: global.name,
|
||||
description: global.about,
|
||||
},
|
||||
image: [],
|
||||
image: [
|
||||
// post.data.banner,
|
||||
],
|
||||
headline: post.title,
|
||||
datePublished: post.published_date,
|
||||
dateModified: post.updated_date,
|
||||
@@ -81,12 +78,11 @@ const content = marked.parse(post.content);
|
||||
],
|
||||
}}
|
||||
>
|
||||
|
||||
<section class="max-w-6xl px-4 sm:px-6 lg:px-8 pt-8 lg:pt-12 pb-12 mx-auto">
|
||||
<section class="mx-auto max-w-6xl px-4 pt-8 pb-12 sm:px-6 lg:px-8 lg:pt-12">
|
||||
<div class="smooth-reveal relative w-full">
|
||||
<div class="sm:shadow-xs sm:dark:shadow-md rounded-2xl mt-4 sm:mt-0">
|
||||
<div class="mt-4 rounded-2xl shadow-none sm:mt-0 sm:shadow-sm">
|
||||
<Image
|
||||
class="rounded-2xl sm:rounded-b-none w-full max-h-150 object-cover"
|
||||
class="max-h-[600px] w-full rounded-t-2xl object-cover"
|
||||
src={getDirectusImageURL(post.image)}
|
||||
alt={post.image_alt}
|
||||
draggable="false"
|
||||
@@ -94,60 +90,83 @@ const content = marked.parse(post.content);
|
||||
loading="lazy"
|
||||
inferSize={true}
|
||||
/>
|
||||
<div class="sm:bg-background-card rounded-b-2xl px-0 sm:px-6 md:px-10 lg:px-14 py-6">
|
||||
<div class="text-center sm:text-left mt-4">
|
||||
<h2 class="card-text-header block">
|
||||
<div
|
||||
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="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}
|
||||
</h2>
|
||||
<ol class="flex items-center justify-center sm:justify-start whitespace-nowrap gap-2 sm:gap-0 mt-6 sm:mt-4">
|
||||
<ol class="mt-8 flex items-center whitespace-nowrap">
|
||||
<li class="inline-flex items-center">
|
||||
<a
|
||||
class="inline-flex items-center text-secondary hover:text-secondary-hover text-sm transition-all duration-300"
|
||||
class="flex items-center text-sm text-neutral-500 transition-all duration-300 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200"
|
||||
href=`/categories/${category.slug}`
|
||||
data-astro-prefetch
|
||||
>
|
||||
{category?.data?.title}
|
||||
</a>
|
||||
<span class="shrink-0 text-secondary text-sm mx-2 sm:mx-4">
|
||||
/
|
||||
</span>
|
||||
<svg
|
||||
class="mx-2 size-5 flex-shrink-0 text-neutral-500 dark:text-neutral-500"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M6 13L10 3" stroke="currentColor" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li class="inline-flex items-center">
|
||||
<span class="shrink-0 text-secondary text-sm">
|
||||
{formatDate(post.published_date)}
|
||||
</span>
|
||||
<span class="shrink-0 text-secondary text-sm mx-2 sm:mx-4">
|
||||
/
|
||||
</span>
|
||||
<li
|
||||
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"
|
||||
>
|
||||
{formatDateTime(post.published_date)}
|
||||
<svg
|
||||
class="mx-2 size-5 flex-shrink-0 text-neutral-500 dark:text-neutral-500"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 16 16"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M6 13L10 3" stroke="currentColor" stroke-linecap="round"></path>
|
||||
</svg>
|
||||
</li>
|
||||
<li class="inline-flex items-center">
|
||||
<span class="shrink-0 text-secondary text-sm">
|
||||
{readingTime.minutes.toPrecision(1)} minutes to read
|
||||
</span>
|
||||
<li
|
||||
class="inline-flex items-center text-sm text-neutral-500 transition-all duration-300 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200"
|
||||
aria-current="page"
|
||||
>
|
||||
{readingTime.minutes.toPrecision(1)} minutes to read
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="border-t border-divider mt-10 mb-10"/>
|
||||
|
||||
<article class="text-header prose prose-blog sm:prose-lg dark:prose-invert max-w-none">
|
||||
<article
|
||||
class="prose prose-blog sm:prose-lg dark:prose-invert max-w-none text-neutral-800 dark:text-neutral-200"
|
||||
>
|
||||
<div set:html={content} />
|
||||
</article>
|
||||
|
||||
<div class="grid sm:flex sm:items-center sm:justify-between gap-y-5 sm:gap-y-0 max-w-5xl mx-auto mt-10 md:mt-14">
|
||||
<div 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) => (
|
||||
<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">
|
||||
{tag}
|
||||
</span>
|
||||
))}
|
||||
<div
|
||||
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 gap-x-2 gap-y-1 sm:flex-nowrap sm:items-center sm:gap-y-0">
|
||||
{
|
||||
post.tags.map((tag: string) => (
|
||||
<span class="bg-steel/30 dark:bg-bermuda/60 inline-flex items-center gap-x-1.5 rounded-lg px-3 py-1.5 text-xs font-medium text-neutral-700 outline-none focus:outline-none focus-visible:ring focus-visible:outline-none dark:text-neutral-200">
|
||||
{tag}
|
||||
</span>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
<SocialShareButton pageTitle={post.title}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style is:inline>
|
||||
code[data-theme*=' '],
|
||||
code[data-theme*=' '] span {
|
||||
@@ -161,7 +180,6 @@ const content = marked.parse(post.content);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -3,12 +3,11 @@ import { readItems, readSingleton } from '@directus/sdk';
|
||||
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
import HeroSection from '@components/sections/HeroSection.astro';
|
||||
import SelectedPostsSection from '@components/sections/SelectedPostsSection.astro';
|
||||
import RecentPostsSection from '@components/sections/RecentPostsSection.astro';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import directus from '@lib/directus';
|
||||
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import BlogSelectedArticles from '@components/blog/BlogSelectedArticles.astro';
|
||||
import BlogRecentArticles from '@components/blog/BlogRecentArticles.astro';
|
||||
import HeroSection from '@components/ui/sections/HeroSection.astro';
|
||||
import blogImg from '@images/autumn_tree.png';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
@@ -19,11 +18,10 @@ const posts = await directus.request(
|
||||
sort: ['-published_date'],
|
||||
})
|
||||
);
|
||||
|
||||
const selectedPosts: Post[] = posts.filter((p) => p.selected).slice(0, 3);
|
||||
const selectedPosts: Post[] = posts.filter((p) => p.selected).slice(0, 4);
|
||||
const recentPosts: Post[] = posts.filter(
|
||||
(p) => !selectedPosts.some((selected) => selected.slug === p.slug)
|
||||
).slice(0, 9);
|
||||
).slice(0, 6);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -45,21 +43,10 @@ const recentPosts: Post[] = posts.filter(
|
||||
},
|
||||
}}
|
||||
>
|
||||
<HeroSection title="Blog" subTitle={global.about_blog} src={blogImg} alt={global.blog_image_alt} />
|
||||
|
||||
<HeroSection
|
||||
title="Blog"
|
||||
subTitle={global.about_blog}
|
||||
src={blogImg}
|
||||
alt={global.blog_image_alt}
|
||||
/>
|
||||
|
||||
<SelectedPostsSection posts={selectedPosts} />
|
||||
|
||||
<RecentPostsSection
|
||||
posts={recentPosts}
|
||||
title="Recent Posts"
|
||||
/>
|
||||
|
||||
<BlogSelectedArticles posts={selectedPosts} />
|
||||
<BlogRecentArticles posts={recentPosts} />
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -2,14 +2,11 @@
|
||||
import { getCollection } from 'astro:content';
|
||||
import { readItems, readSingleton } from '@directus/sdk';
|
||||
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
import HeaderSection from '@components/sections/HeaderSection.astro';
|
||||
import BlogCard from '@components/cards/BlogCard.astro';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import directus from '@lib/directus';
|
||||
|
||||
const { category } = Astro.props;
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import BlogCard from '@components/blog/BlogCard.astro';
|
||||
import HeaderSection from '@components/ui/sections/HeaderSection.astro';
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const categories = await getCollection('categories');
|
||||
@@ -19,6 +16,8 @@ export async function getStaticPaths() {
|
||||
}));
|
||||
}
|
||||
|
||||
const { category } = Astro.props;
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
const posts = await directus.request(
|
||||
readItems('posts', {
|
||||
@@ -27,7 +26,6 @@ const posts = await directus.request(
|
||||
sort: ['-published_date'],
|
||||
})
|
||||
);
|
||||
|
||||
const categoriesPosts = posts
|
||||
.sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf())
|
||||
.filter((b) => {
|
||||
@@ -53,7 +51,6 @@ const categoriesPosts = posts
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
<HeaderSection
|
||||
title={category.data.title}
|
||||
subTitle={category.data.description}
|
||||
@@ -62,12 +59,9 @@ const categoriesPosts = posts
|
||||
btnURL="/categories"
|
||||
/>
|
||||
|
||||
<section class="max-w-340 2xl:max-w-full mb-10 px-4 sm:px-6 lg:px-8 py-8 mx-auto mt-10">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{categoriesPosts.map((b) =>
|
||||
<BlogCard post={b} />
|
||||
)}
|
||||
<section class="mx-auto mt-10 mb-10 max-w-[85rem] px-4 py-8 sm:px-6 lg:px-8 2xl:max-w-full">
|
||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{categoriesPosts.map((b) => <BlogCard post={b} />)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
@@ -1,14 +1,78 @@
|
||||
---
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
import { getCollection } from 'astro:content';
|
||||
import { readItems, readSingleton } from '@directus/sdk';
|
||||
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
import HeroSection from '@components/sections/HeroSection.astro';
|
||||
import CategorySection from '@components/sections/CategorySection.astro';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import directus from '@lib/directus';
|
||||
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import BlogCategoryCard from '@components/blog/BlogCategoryCard.astro';
|
||||
import HeroSection from '@components/ui/sections/HeroSection.astro';
|
||||
import { timeago } from '@support/time';
|
||||
import categoryImg from '@images/autumn_bench.png';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
const posts = await directus.request(
|
||||
readItems('posts', {
|
||||
filter: { published: { _eq: true } },
|
||||
fields: ['*'],
|
||||
sort: ['-published_date'],
|
||||
})
|
||||
);
|
||||
|
||||
const postMap: Map<string, Post[]> = posts
|
||||
.sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf())
|
||||
.reduce((acc, obj) => {
|
||||
let posts = acc.get(obj.category);
|
||||
if (!posts) {
|
||||
posts = [];
|
||||
}
|
||||
posts.push(obj);
|
||||
|
||||
acc.set(obj.category, posts);
|
||||
|
||||
return acc;
|
||||
}, new Map<string, Post[]>());
|
||||
|
||||
const layoutPattern = [
|
||||
{ col: 2, row: 2 },
|
||||
{ col: 2, row: 1 },
|
||||
{ col: 1, row: 1 },
|
||||
{ col: 1, row: 1 },
|
||||
{ col: 1, row: 2 },
|
||||
{ col: 2, row: 1 },
|
||||
{ col: 1, row: 1 },
|
||||
{ col: 1, row: 1 },
|
||||
{ col: 1, row: 1 },
|
||||
{ col: 1, row: 1 },
|
||||
];
|
||||
|
||||
const categories = (await getCollection('categories'))
|
||||
.sort((a, b) => {
|
||||
const aCount = postMap.get(a.slug)?.length ?? 0;
|
||||
const bCount = postMap.get(b.slug)?.length ?? 0;
|
||||
return bCount - aCount;
|
||||
})
|
||||
.map((c, index) => {
|
||||
const posts = postMap.get(c.slug);
|
||||
const pattern = layoutPattern[index % layoutPattern.length];
|
||||
const smColSpan = Math.min(pattern.col, 2);
|
||||
const mdColSpan = Math.min(pattern.col, 4);
|
||||
const rowSpan = pattern.row;
|
||||
const rowSpanClass = rowSpan > 1 ? `row-span-${rowSpan}` : 'row-span-1';
|
||||
const gridItemClass = `col-span-${smColSpan} md:col-span-${mdColSpan} ${rowSpanClass} smooth-reveal-cards rounded-xl transition-all duration-300 shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg border border-stone-200/50 dark:border-stone-700/50`;
|
||||
return {
|
||||
...c,
|
||||
posts,
|
||||
gridItemClass,
|
||||
layoutPattern: {
|
||||
smCol: smColSpan,
|
||||
mdCol: mdColSpan,
|
||||
row: rowSpan,
|
||||
index,
|
||||
},
|
||||
};
|
||||
});
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -30,7 +94,6 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
<HeroSection
|
||||
title="Categories"
|
||||
subTitle={global.about_categories}
|
||||
@@ -38,8 +101,28 @@ const global = await directus.request(readSingleton('site_global'));
|
||||
alt={global.categories_image_alt}
|
||||
/>
|
||||
|
||||
<CategorySection />
|
||||
|
||||
<section class="mx-auto px-4 py-10 sm:px-6 lg:px-8 lg:py-14 lg:pt-10 2xl:max-w-full">
|
||||
<div class="grid grid-flow-row-dense grid-cols-2 gap-4 md:grid-cols-4">
|
||||
{
|
||||
categories.map((category) => {
|
||||
return (
|
||||
<div
|
||||
class={category.gridItemClass}
|
||||
style={category.layoutPattern.row > 1 ? 'grid-row: span 2 / span 2;' : ''}
|
||||
>
|
||||
<BlogCategoryCard
|
||||
slug={category.slug}
|
||||
title={category.data.title}
|
||||
description={category.data.description}
|
||||
count={postMap.get(category.slug)?.length ?? 0}
|
||||
publishDate={timeago(postMap.get(category.slug)?.[0]?.published_date)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,31 +1,17 @@
|
||||
---
|
||||
import { readSingleton, readItems } from '@directus/sdk';
|
||||
import { readSingleton } from '@directus/sdk';
|
||||
|
||||
import type { Post } from '@lib/directusTypes';
|
||||
|
||||
import HeroSection from '@components/sections/HeroSection.astro';
|
||||
import FeatureSection from '@components/sections/FeatureSection.astro';
|
||||
import WeatherSection from '@components/sections/WeatherSection.astro';
|
||||
import RecentPostsSection from '@components/sections/RecentPostsSection.astro';
|
||||
import GiteaSection from '@components/sections/GiteaSection.astro';
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import directus from '@lib/directus';
|
||||
|
||||
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||
import HeroSection from '@components/ui/sections/HeroSection.astro';
|
||||
import FeaturesSection from '@components/ui/sections/FeaturesSection.astro';
|
||||
import WeatherSection from '@components/ui/sections/WeatherSection.astro';
|
||||
import LatestPosts from '@components/ui/sections/LatestPosts.astro';
|
||||
import HeroSectionAlt from '@components/ui/sections/HeroSectionAlt.astro';
|
||||
import homeImg from '@images/autumn_mountain.png';
|
||||
|
||||
const global = await directus.request(readSingleton('site_global'));
|
||||
const weather = await directus.request(readSingleton('site_weather'));
|
||||
const posts = await directus.request(
|
||||
readItems('posts', {
|
||||
filter: { published: { _eq: true } },
|
||||
fields: ['*'],
|
||||
sort: ['-published_date'],
|
||||
})
|
||||
);
|
||||
|
||||
const recentPosts = posts
|
||||
.sort((a: Post, b: Post) => (new Date(b.published_date).getTime()) - (new Date(a.published_date).getTime()))
|
||||
.slice(0, 3);
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
@@ -47,7 +33,6 @@ const recentPosts = posts
|
||||
},
|
||||
}}
|
||||
>
|
||||
|
||||
<HeroSection
|
||||
title={`Hello, I'm <span class="text-steel dark:text-steel">Alex Lebens</span>`}
|
||||
subTitle={global.about_description}
|
||||
@@ -57,28 +42,24 @@ const recentPosts = posts
|
||||
alt={global.home_image_alt}
|
||||
/>
|
||||
|
||||
<FeatureSection />
|
||||
<FeaturesSection />
|
||||
|
||||
<WeatherSection
|
||||
server:defer
|
||||
latitude={weather.latitude}
|
||||
longitude={weather.longitude}
|
||||
latitude={weather.latitude}
|
||||
longitude={weather.longitude}
|
||||
cityName={weather.location}
|
||||
timezone={weather.timezone}
|
||||
/>
|
||||
>
|
||||
</WeatherSection>
|
||||
|
||||
<RecentPostsSection
|
||||
posts={recentPosts}
|
||||
title="Latest Posts"
|
||||
subTitle="Checkout my most recent thoughts here"
|
||||
/>
|
||||
<LatestPosts />
|
||||
|
||||
<GiteaSection
|
||||
<HeroSectionAlt
|
||||
title="Follow me on Gitea"
|
||||
subTitle="I love open source and have my code availabile on my Gitea server."
|
||||
url="https://gitea.alexlebens.dev"
|
||||
/>
|
||||
|
||||
</BaseLayout>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
// https://docs.astro.build/en/guides/integrations-guide/sitemap/#usage
|
||||
import type { APIRoute } from 'astro';
|
||||
|
||||
const getRobotsTxt = (sitemapURL: URL) => `\
|
||||
const robotsTxt = `
|
||||
User-agent: Googlebot
|
||||
Disallow:
|
||||
Allow: /
|
||||
Crawl-delay: 10
|
||||
|
||||
User-agent: Yandex
|
||||
Disallow:
|
||||
Allow: /
|
||||
Crawl-delay: 2
|
||||
|
||||
User-agent: archive.org_bot
|
||||
Disallow:
|
||||
Allow: /
|
||||
Crawl-delay: 2
|
||||
|
||||
User-agent: *
|
||||
Allow: /
|
||||
|
||||
Sitemap: ${sitemapURL.href}
|
||||
`;
|
||||
Sitemap: ${new URL('sitemap-index.xml', import.meta.env.SITE).href}`.trim();
|
||||
|
||||
export const GET: APIRoute = ({ site }) => {
|
||||
const sitemapURL = new URL('sitemap-index.xml', site);
|
||||
return new Response(getRobotsTxt(sitemapURL));
|
||||
export const GET: APIRoute = () => {
|
||||
return new Response(robotsTxt, {
|
||||
headers: {
|
||||
'Content-Type': 'text/plain; charset=utf-8',
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
@import 'tailwindcss';
|
||||
@import 'preline/variants.css';
|
||||
@import './utilities.css';
|
||||
|
||||
@plugin '@tailwindcss/typography';
|
||||
@plugin '@tailwindcss/forms';
|
||||
|
||||
@@ -9,50 +7,31 @@
|
||||
/* https://tailwindcss.com/docs/dark-mode */
|
||||
@custom-variant dark (&:where(.dark, .dark *));
|
||||
|
||||
/* Custom colors */
|
||||
@theme {
|
||||
/* Custom colors */
|
||||
--color-midnight: #0c354d;
|
||||
--color-ocean: #134e70;
|
||||
|
||||
--color-cobalt: #6c9cb0;
|
||||
--color-steel: #4682b4;
|
||||
|
||||
--color-turquoise: #0da797;
|
||||
--color-steel: #4682b4;
|
||||
--color-bermuda: #7fbab4;
|
||||
|
||||
--color-desert: #f9deb2;
|
||||
--color-bronze: #9e7f5e;
|
||||
|
||||
--color-gitea-primary: #609926;
|
||||
--color-gitea-secondary: #4c7a33;
|
||||
|
||||
/* Theme colors */
|
||||
--color-main: light-dark(var(--color-steel), var(--color-bermuda));
|
||||
--color-accent: light-dark(var(--color-bronze), var(--color-desert));
|
||||
--color-active: light-dark(var(--color-orange-500), var(--color-orange-300));
|
||||
|
||||
/* Text colors */
|
||||
--color-header: light-dark(var(--color-neutral-800), var(--color-neutral-200));
|
||||
--color-primary: light-dark(var(--color-neutral-600), var(--color-neutral-200));
|
||||
--color-primary-hover: light-dark(var(--color-neutral-800), var(--color-neutral-400));
|
||||
--color-secondary: light-dark(var(--color-neutral-500), var(--color-neutral-400));
|
||||
--color-secondary-hover: light-dark(var(--color-neutral-800), var(--color-neutral-200));
|
||||
|
||||
/* Object colors */
|
||||
--color-background: light-dark(var(--color-neutral-200), var(--color-stone-700));
|
||||
--color-background-accent: light-dark(color-mix(in srgb, var(--color-neutral-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 {
|
||||
:root {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
--theme-transition: 0.3s ease;
|
||||
--scroll-offset: 0px;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
:root:where(.dark, .dark *) {
|
||||
@@ -74,6 +53,9 @@
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow-x: hidden;
|
||||
--swup-fade-theme-duration: 0.2s;
|
||||
}
|
||||
|
||||
@@ -116,6 +98,63 @@
|
||||
}
|
||||
}
|
||||
|
||||
@utility card-base {
|
||||
@apply rounded-xl transition-all duration-300
|
||||
border border-neutral-100 dark:border-stone-500/20
|
||||
bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90
|
||||
shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg
|
||||
}
|
||||
|
||||
@utility card-hover-icon-color {
|
||||
@apply transition-all duration-300
|
||||
text-primary
|
||||
group-hover:text-main
|
||||
}
|
||||
|
||||
@utility card-hover-icon-scale {
|
||||
@apply transition-all duration-300
|
||||
drop-shadow-sm
|
||||
group-hover:scale-110
|
||||
}
|
||||
|
||||
@utility card-text-header {
|
||||
@apply text-header
|
||||
md:text-5xl
|
||||
text-4xl
|
||||
}
|
||||
|
||||
@utility card-text-header-minor {
|
||||
@apply text-header
|
||||
md:text-3xl
|
||||
text-2xl
|
||||
font-semibold
|
||||
}
|
||||
|
||||
@utility card-text-header-description {
|
||||
@apply text-primary
|
||||
text-lg
|
||||
text-pretty
|
||||
}
|
||||
|
||||
@utility card-text-title {
|
||||
@apply text-primary
|
||||
font-bold
|
||||
}
|
||||
|
||||
@utility card-text-description {
|
||||
@apply text-secondary
|
||||
}
|
||||
|
||||
@utility card-hover-text-title {
|
||||
@apply transition-all duration-300
|
||||
group-hover:text-main
|
||||
}
|
||||
|
||||
@utility card-hover-text-gitea {
|
||||
@apply transition-all duration-300
|
||||
group-hover:text-gitea-primary
|
||||
}
|
||||
|
||||
/* Content reveal animations */
|
||||
.smooth-reveal,
|
||||
.smooth-reveal-2,
|
||||
|
||||
@@ -1,135 +0,0 @@
|
||||
/* Button classes */
|
||||
@utility button-base {
|
||||
@apply transition-all duration-300
|
||||
border border-transparent
|
||||
shadow-sm hover:shadow-md dark:shadow-md dark:hover:shadow-lg
|
||||
px-4 py-3
|
||||
}
|
||||
|
||||
@utility button-base-hidden {
|
||||
@apply transition-all duration-300
|
||||
border border-transparent
|
||||
hover:bg-neutral-200 dark:hover:bg-neutral-700
|
||||
p-2
|
||||
}
|
||||
|
||||
@utility button-hover-arrow {
|
||||
@apply translate-y-px transition duration-300
|
||||
group-hover:translate-x-1
|
||||
h-3 w-3 md:h-5 md:w-5
|
||||
}
|
||||
|
||||
@utility button-text-title {
|
||||
@apply text-neutral-200 2xl:text-base
|
||||
text-sm font-bold
|
||||
}
|
||||
|
||||
@utility button-text-title-hidden {
|
||||
@apply transition-all duration-300
|
||||
text-neutral-600 group-hover:text-neutral-700 dark:text-neutral-400 dark:group-hover:text-neutral-300 2xl:text-base
|
||||
text-sm font-medium
|
||||
}
|
||||
|
||||
@utility button-bg-blue {
|
||||
@apply transition-all duration-300
|
||||
bg-cobalt hover:bg-steel dark:bg-steel dark:hover:bg-cobalt
|
||||
}
|
||||
|
||||
@utility button-bg-teal {
|
||||
@apply transition-all duration-300
|
||||
bg-bermuda hover:bg-turquoise group-hover:bg-turquoise dark:bg-turquoise dark:hover:bg-bermuda dark:group-hover:bg-bermuda
|
||||
}
|
||||
|
||||
@utility button-bg-neutral {
|
||||
@apply transition-all duration-300
|
||||
border border-neutral-100 dark:border-stone-500/20
|
||||
bg-background-card hover:bg-neutral-100 dark:hover:bg-neutral-800/90
|
||||
}
|
||||
|
||||
@utility button-bg-gitea {
|
||||
@apply transition-all duration-300
|
||||
bg-gitea-primary hover:bg-gitea-secondary dark:bg-gitea-secondary dark:hover:bg-gitea-primary
|
||||
}
|
||||
|
||||
/* Card classes */
|
||||
@utility card-base {
|
||||
@apply transition-all duration-300
|
||||
rounded-xl
|
||||
border border-neutral-100 dark:border-stone-500/20
|
||||
bg-background-card hover:bg-neutral-100 dark:hover:bg-neutral-800/90
|
||||
shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg
|
||||
}
|
||||
|
||||
@utility card-base-hidden {
|
||||
@apply transition-all duration-300
|
||||
rounded-2xl
|
||||
border border-transparent
|
||||
hover:bg-neutral-400/20 dark:hover:bg-neutral-800/40
|
||||
}
|
||||
|
||||
@utility card-hover-icon-color {
|
||||
@apply transition-all duration-300
|
||||
text-primary
|
||||
group-hover:text-main
|
||||
}
|
||||
|
||||
@utility card-hover-icon-scale {
|
||||
@apply transition-transform duration-300 will-change-transform
|
||||
drop-shadow-md dark:drop-shadow-xl dark:drop-shadow-neutral-500/60
|
||||
group-hover:scale-3d group-hover:scale-110
|
||||
}
|
||||
|
||||
@utility card-text-header {
|
||||
@apply text-header
|
||||
text-4xl md:text-5xl
|
||||
font-bold leading-tight tracking-tight text-balance
|
||||
}
|
||||
|
||||
@utility card-text-header-minor {
|
||||
@apply text-header
|
||||
text-2xl md:text-3xl
|
||||
font-semibold leading-tight tracking-tight text-balance
|
||||
}
|
||||
|
||||
@utility card-text-header-description {
|
||||
@apply text-primary
|
||||
text-lg
|
||||
text-pretty leading-relaxed
|
||||
}
|
||||
|
||||
@utility card-text-title {
|
||||
@apply text-primary
|
||||
font-bold
|
||||
}
|
||||
|
||||
@utility card-text-title-major {
|
||||
@apply text-header
|
||||
text-4xl md:text-3xl
|
||||
font-bold leading-tight tracking-tight text-balance
|
||||
}
|
||||
|
||||
@utility card-hover-text-title {
|
||||
@apply transition-all duration-300
|
||||
group-hover:text-main
|
||||
}
|
||||
|
||||
@utility card-hover-text-neutral {
|
||||
@apply transition-all duration-300
|
||||
group-hover:text-primary-hover
|
||||
}
|
||||
|
||||
@utility card-hover-text-gitea {
|
||||
@apply transition-all duration-300
|
||||
group-hover:text-gitea-primary
|
||||
}
|
||||
|
||||
@utility card-text-description {
|
||||
@apply text-secondary
|
||||
}
|
||||
|
||||
/* Misc */
|
||||
@utility nav-base {
|
||||
@apply border border-neutral-100 dark:border-stone-500/20
|
||||
bg-neutral-100 dark:bg-neutral-800
|
||||
shadow-xs dark:shadow-md
|
||||
}
|
||||
68
src/support/image.ts
Normal file
68
src/support/image.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
import fs from 'node:fs/promises';
|
||||
|
||||
interface BlurImageMetadata {
|
||||
/**
|
||||
* The width of the origin image
|
||||
*/
|
||||
width: number;
|
||||
/**
|
||||
* The height of the origin image
|
||||
*/
|
||||
height: number;
|
||||
/**
|
||||
* blurDataURL of the image
|
||||
*/
|
||||
blurDataURL: string;
|
||||
/**
|
||||
* blur image width
|
||||
*/
|
||||
blurWidth: number;
|
||||
/**
|
||||
* blur image height
|
||||
*/
|
||||
blurHeight: number;
|
||||
}
|
||||
|
||||
async function blurStyle(filePath: string) {
|
||||
const image = await blurImageMetadata(filePath);
|
||||
const svg = blurImageSVG(image);
|
||||
return {
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: '50% 50%',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundImage: `url("data:image/svg+xml;charset=utf-8,${svg}")`,
|
||||
};
|
||||
}
|
||||
|
||||
function blurImageSVG(image: BlurImageMetadata): string {
|
||||
const { blurDataURL, blurWidth, blurHeight, width, height } = image;
|
||||
|
||||
const std = 20;
|
||||
const svgWidth = blurWidth ? blurWidth * 40 : width;
|
||||
const svgHeight = blurHeight ? blurHeight * 40 : height;
|
||||
|
||||
const viewBox = svgWidth && svgHeight ? `viewBox='0 0 ${svgWidth} ${svgHeight}'` : '';
|
||||
|
||||
return `%3Csvg xmlns='http://www.w3.org/2000/svg' ${viewBox}%3E%3Cfilter id='b' color-interpolation-filters='sRGB'%3E%3CfeGaussianBlur stdDeviation='${std}'/%3E%3CfeColorMatrix values='1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 100 -1' result='s'/%3E%3CfeFlood x='0' y='0' width='100%25' height='100%25'/%3E%3CfeComposite operator='out' in='s'/%3E%3CfeComposite in2='SourceGraphic'/%3E%3CfeGaussianBlur stdDeviation='${std}'/%3E%3C/filter%3E%3Cimage width='100%25' height='100%25' x='0' y='0' preserveAspectRatio='xMidYMid slice' style='filter: url(%23b);' href='${blurDataURL}'/%3E%3C/svg%3E`;
|
||||
}
|
||||
|
||||
async function blurImageMetadata(filepath: string): Promise<BlurImageMetadata> {
|
||||
const { default: sharp } = await import('sharp');
|
||||
const buffer = await fs.readFile(filepath);
|
||||
|
||||
const img = sharp(buffer);
|
||||
const { width, height } = await img.metadata();
|
||||
if (width == null || height == null) {
|
||||
throw new Error(`Invalid image path: ${filepath}`);
|
||||
}
|
||||
|
||||
const aspectRatio = width / height;
|
||||
const blurWidth = 8;
|
||||
const blurHeight = Math.round(blurWidth / aspectRatio);
|
||||
const blurImage = await img.resize(blurWidth, blurHeight).webp({ quality: 10 }).toBuffer();
|
||||
const blurDataURL = `data:image/webp;base64,${blurImage.toString('base64')}`;
|
||||
|
||||
return { blurDataURL, blurHeight, blurWidth, width, height };
|
||||
}
|
||||
|
||||
export { blurStyle };
|
||||
@@ -17,6 +17,21 @@ const TimeAgoConfiguration: string[][] = [
|
||||
['%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 {
|
||||
if (!date) {
|
||||
return 'today';
|
||||
@@ -31,12 +46,4 @@ function timeago(date?: Date): string {
|
||||
return format(date, 'timeago');
|
||||
}
|
||||
|
||||
function formatDate(date: Date): string {
|
||||
return new Date(date).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
})
|
||||
}
|
||||
|
||||
export { formatDate, timeago };
|
||||
export { formatDate, timeago, formatDateTime };
|
||||
|
||||
Reference in New Issue
Block a user