merge in new changes
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled

This commit is contained in:
2025-08-11 16:24:43 -05:00
parent a484feb7cd
commit 1dc4ccfbc6
125 changed files with 9304 additions and 20383 deletions

View File

@@ -3,7 +3,7 @@ name: release-image
on:
push:
tags:
- 1.*
- 2.*
workflow_dispatch:

3
.gitignore vendored
View File

@@ -12,10 +12,9 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.local
.env.development
.env.production
# macOS-specific files

1
.npmrc
View File

@@ -1,2 +1,3 @@
registry=https://registry.npmjs.org/
engine-strict=true
save-exact=true

View File

@@ -1,7 +1,7 @@
ARG REGISTRY=docker.io
FROM ${REGISTRY}/node:22.18.0-alpine3.22 AS base
LABEL version="1.1.1"
LABEL version="2.0.0"
LABEL description="Astro based personal website"
ENV PNPM_HOME="/pnpm"

View File

@@ -1,74 +1,31 @@
# Alex Lebens Personal Site
# This is an open-source and simple blog built with Astro.
Personal site used for information about myself and blog.
## Features
- 🚀 **Maximum Performance** - Built with Astro.js for lightning-fast static sites
- 🎨 **Minimalist Design** - Clean UI that focuses on content
- 🌓 **Light/Dark Mode** - Smooth theme switching
- 📱 **Responsive** - Perfect experience on all devices
- **SPA Transitions** - Smooth page navigation with transition effects
- 📝 **Markdown & MDX** - Write posts with Markdown and extend with MDX
- 🔍 **SEO Optimized** - Meta tags, Open Graph, and Twitter Cards
- 📊 **Analytics** - Reading time, views, and statistics
- 🔖 **Categorization** - Tags and categories system
- 🔄 **RSS Feed** - Automatically generated RSS feed
- 🌐 **Internationalization Ready** - Prepared for multiple languages
- 🔒 **Secure** - No unnecessary client-side JavaScript
- 🐈 Simple And Beautiful
- 🖥️️ Responsive And Light/Dark mode
- 🐛 SiteMap & RSS Feed
- 🐝 Category Support
- 🐜 SEO and Responsiveness
- 🪲 Markdown And MDX
- 🏂🏾 Page Compression & Image Optimization
## Getting Started
### Development Commands
### Requirements
With dependencies installed, you can utilize the following npm scripts to manage your project's development lifecycle:
- Node.js 22+ and pnpm
- `pnpm run dev`: Starts a local development server with hot reloading enabled.
- `pnpm run preview`: Serves your build output locally for preview before deployment.
- `pnpm run build`: Bundles your site into static files for production.
### Installation
For detailed help with Astro CLI commands, visit [Astro's documentation](https://docs.astro.build/en/reference/cli-reference/).
```bash
# Clone repository
git clone https://gitea.alexlebens.dev/alexlebens/site-profile
## Thanks
# Navigate to project directory
cd site-profile
Thanks https://github.com/mearashadowfax/ScrewFast, https://github.com/godruoyi/gblog/tree/gblog-template
# Install dependencies
pnpm install
## License
```
### Development
```bash
# Start development server
pnpm dev
# Open browser at http://localhost:4321
```
### Build
```bash
# Create production build
pnpm build
# Preview production build
pnpm preview
```
## Project Structure
```
/
├── public/ # Static assets
├── src/
│ ├── components/ # Reusable UI components
│ ├── content/ # Blog content (Markdown/MDX)
│ ├── layouts/ # Page layouts
│ ├── pages/ # Pages and routes
│ ├── styles/ # CSS and Tailwind
│ └── utils/ # Utilities and helpers
├── astro.config.mjs # Astro configuration
├── tailwind.config.js # Tailwind configuration
└── tsconfig.json # TypeScript configuration
```
This project is released under the MIT License. Please read the [LICENSE](https://gitea.alexlebens.dev/alexlebens/site-profile/src/LICENSE.md) file for more details.

View File

@@ -1,8 +1,16 @@
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import react from '@astrojs/react';
import { defineConfig, passthroughImageService, sharpImageService } from 'astro/config';
import mdx from '@astrojs/mdx';
import node from '@astrojs/node';
import partytown from '@astrojs/partytown';
import react from '@astrojs/react';
import sitemap from '@astrojs/sitemap';
import tailwindcss from '@tailwindcss/vite';
import icon from 'astro-icon';
import swup from '@swup/astro';
import rehypePrettyCode from 'rehype-pretty-code';
import { transformerCopyButton } from '@rehype-pretty/transformers';
const getSiteURL = () => {
if (process.env.SITE_URL) {
@@ -13,7 +21,71 @@ const getSiteURL = () => {
export default defineConfig({
site: getSiteURL(),
integrations: [tailwindcss(), react()],
image: {
service: {
entrypoint: 'astro/assets/services/sharp',
}
},
prefetch: true,
integrations: [
mdx(),
partytown(),
react(),
sitemap(),
icon({
include: {
mdi: ['*'],
},
}),
swup({
theme: 'fade',
native: true,
cache: true,
preload: true,
accessibility: true,
smoothScrolling: true,
morph: ['#nav'],
}),
(await import('@playform/compress')).default({
CSS: true,
JavaScript: true,
HTML: {
'html-minifier-terser': {
collapseWhitespace: true,
minifyCSS: false,
minifyJS: true,
},
},
Image: false,
SVG: true,
Logger: 2,
}),
],
markdown: {
syntaxHighlight: false,
rehypePlugins: [
[
rehypePrettyCode,
{
theme: {
light: 'github-light',
dark: 'github-dark-dimmed',
},
keepBackground: false,
transformers: [
transformerCopyButton({
visibility: 'always',
feedbackDuration: 2500,
}),
],
},
],
],
},
plugins: {
'@tailwindcss/postcss': {},
@@ -28,4 +100,13 @@ export default defineConfig({
adapter: node({
mode: 'standalone',
}),
build: {
// Specifies the directory in the build output where Astro-generated assets (bundled JS and CSS for example) should live.
// see https://docs.astro.build/en/reference/configuration-reference/#buildassets
assets: 'assets',
// see https://docs.astro.build/en/reference/configuration-reference/#buildassetsprefix
assetsPrefix:
!!import.meta.env.S3_ENABLE || !!process.env.S3_ENABLE ? 'https://digitalocean.com' : '',
},
});

View File

@@ -1,43 +1,84 @@
{
"name": "site-profile",
"type": "module",
"version": "1.1.1",
"private": true,
"version": "2.0.0",
"homepage": "https://www.alexlebens.dev",
"bugs": {
"url": "https://gitea.alexlebens.dev/alexlebens/site-profile/issues",
"email": "alexander.lebens@gmail.com"
},
"repository": {
"type": "git",
"url": "https://gitea.alexlebens.dev/alexlebens/site-profile"
},
"license": "MIT",
"author": {
"name": "Alex Lebens",
"email": "alexander.lebens@gmail.com",
"url": "https://www.alexlebens.dev"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,md,mdx,astro}\"",
"lint": "eslint \"src/**/*.{js,ts,jsx,tsx,astro}\"",
"lint:fix": "eslint --fix \"src/**/*.{js,ts,jsx,tsx,astro}\"",
"astro": "astro"
"lint:fix": "eslint --fix \"src/**/*.{js,ts,jsx,tsx,astro}\""
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^4.3.3",
"@astrojs/node": "^9.3.3",
"@astrojs/partytown": "^2.1.4",
"@astrojs/react": "^4.3.0",
"@astrojs/rss": "^4.0.12",
"@directus/sdk": "^20.0.0",
"@astrojs/sitemap": "^3.4.2",
"@giscus/react": "^3.1.0",
"@iconify-json/mdi": "^1.1.63",
"@iconify-json/pajamas": "^1.2.13",
"@iconify-json/simple-icons": "^1.2.47",
"@playform/compress": "^0.0.4",
"@rehype-pretty/transformers": "^0.13.2",
"@swup/astro": "1.7.0",
"@tailwindcss/postcss": "^4.1.8",
"@tailwindcss/vite": "^4.1.8",
"@directus/sdk": "^20.0.0",
"@types/react": "^19.0.0",
"@types/unist": "^3.0.2",
"astro": "^5.12.8",
"astro-compressor": "^0.4.1",
"astro-icon": "^1.1.5",
"framer-motion": "^12.16.0",
"mdast-util-to-string": "^4.0.0",
"preline": "^3.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hotkeys-hook": "^5.1.0",
"react-icons": "^5.5.0",
"sanitize-html": "^2.17.0",
"tailwindcss": "^4.1.8"
"reading-time": "^1.5.0",
"rehype-pretty-code": "^0.14.1",
"sharp": "^0.34.3",
"sharp-ico": "^0.1.5",
"shiki": "^3.2.2",
"tailwindcss": "^4.1.11",
"ultrahtml": "^1.5.3"
},
"devDependencies": {
"@eslint-react/eslint-plugin": "^1.52.3",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@typescript-eslint/parser": "8.38.0",
"eslint": "9.32.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-astro": "1.3.1",
"astro-icon": "^1.1.5",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-astro": "^1.3.1",
"eslint-plugin-format": "^1.0.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.12",
"prettier-plugin-tailwindcss": "^0.6.14",
"timeago.js": "^4.0.2",
"typescript": "5.8.3",
"typescript-eslint": "8.38.0"
}
}

8691
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +0,0 @@
.DS_Store
.astro
.vscode
node_modules
dist

View File

@@ -1,98 +0,0 @@
name: release-image
on:
push:
tags:
- 0.*
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REPOSITORY_HOST }}
username: ${{ gitea.actor }}
password: ${{ secrets.REPOSITORY_TOKEN }}
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY_HOST }}
username: ${{ vars.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_SECRET }}
- name: Create Kubeconfig
run: |
mkdir $HOME/.kube
echo "${{ secrets.KUBECONFIG_BUILDX }}" > $HOME/.kube/config
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
with:
driver: kubernetes
driver-opts: |
namespace=gitea
qemu.install=true
buildkitd-config-inline: |
[registry."docker.io"]
mirrors = ["harbor.alexlebens.net/proxy-hub.docker/"]
- name: Available Platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Extract Metadata
id: meta
uses: docker/metadata-action@v5
with:
tags: |
type=ref,event=branch
type=ref,event=tag
images: |
${{ vars.REPOSITORY_HOST }}/${{ gitea.repository }}
${{ vars.REGISTRY_HOST }}/images/site-profile-new
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./Dockerfile
- name: ntfy Success
uses: niniyas/ntfy-action@master
if: success()
with:
url: '${{ secrets.NTFY_URL }}'
topic: '${{ secrets.NTFY_TOPIC }}'
title: 'Gitea Action'
priority: 3
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
tags: action,successfully,completed
details: 'Site Profile New build workflow has successfully completed!'
icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png'
- name: ntfy Failed
uses: niniyas/ntfy-action@master
if: failure()
with:
url: '${{ secrets.NTFY_URL }}'
topic: '${{ secrets.NTFY_TOPIC }}'
title: 'Gitea Action'
priority: 4
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
tags: action,failed
details: 'Site Profile New build workflow has failed!'
icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png'
actions: '[{"action": "view", "label": "Open Gitea", "url": "https://gitea.alexlebens.dev/alexlebens/site-profile-new/actions?workflow=release-image.yml", "clear": true}]'
image: true

View File

@@ -1,32 +0,0 @@
name: renovate
on:
schedule:
- cron: '@daily'
push:
branches:
- main
workflow_dispatch:
jobs:
renovate:
runs-on: ubuntu-latest
container: ghcr.io/renovatebot/renovate:41
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Renovate
run: renovate
env:
RENOVATE_PLATFORM: gitea
RENOVATE_ENDPOINT: ${{ vars.INSTANCE_URL }}
RENOVATE_REPOSITORIES: alexlebens/site-profile-new
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net>
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_REDIS_URL: ${{ vars.RENOVATE_REDIS_URL }}

View File

@@ -1,37 +0,0 @@
name: test-build
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 10.x
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 22.18.0
cache: pnpm
- name: Install Dependencies
run: pnpm install
- name: Lint Code
run: pnpm lint
- name: Build Project
run: pnpm build

View File

@@ -1,25 +0,0 @@
# build output
dist/
# generated types
.astro/
# dependencies
node_modules/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.production
# macOS-specific files
.DS_Store
# ide
.vscode/
site-profile-new.code-workspace

View File

@@ -1,3 +0,0 @@
registry=https://registry.npmjs.org/
engine-strict=true
save-exact=true

View File

@@ -1,36 +0,0 @@
ARG REGISTRY=docker.io
FROM ${REGISTRY}/node:22.18.0-alpine3.22 AS base
LABEL version="0.0.1"
LABEL description="Astro based personal website"
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
RUN corepack enable
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
FROM prod-deps AS build-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
FROM build-deps AS build
COPY . .
RUN pnpm run build
RUN pnpm prune --prod
FROM base AS runtime
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist
ENV HOST=0.0.0.0
ENV SITE_URL=https://www.alexlebens.dev
ENV DIRECTUS_URL=https://directus.alexlebens.dev
ENV PORT=4321
EXPOSE $PORT
CMD ["node", "./dist/server/entry.mjs"]

View File

@@ -1,31 +0,0 @@
# This is an open-source and simple blog built with Astro.
Personal site used for information about myself and blog.
## Features
- 🐈 Simple And Beautiful
- 🖥️️ Responsive And Light/Dark mode
- 🐛 SiteMap & RSS Feed
- 🐝 Category Support
- 🐜 SEO and Responsiveness
- 🪲 Markdown And MDX
- 🏂🏾 Page Compression & Image Optimization
### Development Commands
With dependencies installed, you can utilize the following npm scripts to manage your project's development lifecycle:
- `pnpm run dev`: Starts a local development server with hot reloading enabled.
- `pnpm run preview`: Serves your build output locally for preview before deployment.
- `pnpm run build`: Bundles your site into static files for production.
For detailed help with Astro CLI commands, visit [Astro's documentation](https://docs.astro.build/en/reference/cli-reference/).
## Thanks
Thanks https://github.com/mearashadowfax/ScrewFast, https://github.com/godruoyi/gblog/tree/gblog-template
## License
This project is released under the MIT License. Please read the [LICENSE](https://gitea.alexlebens.dev/alexlebens/site-profile/src/LICENSE.md) file for more details.

View File

@@ -1,112 +0,0 @@
import { defineConfig, passthroughImageService, sharpImageService } from 'astro/config';
import mdx from '@astrojs/mdx';
import node from '@astrojs/node';
import partytown from '@astrojs/partytown';
import react from '@astrojs/react';
import sitemap from '@astrojs/sitemap';
import tailwindcss from '@tailwindcss/vite';
import icon from 'astro-icon';
import swup from '@swup/astro';
import rehypePrettyCode from 'rehype-pretty-code';
import { transformerCopyButton } from '@rehype-pretty/transformers';
const getSiteURL = () => {
if (process.env.SITE_URL) {
return `https://${process.env.SITE_URL}`;
}
return 'http://localhost:4321';
};
export default defineConfig({
site: getSiteURL(),
image: {
service: {
entrypoint: 'astro/assets/services/sharp',
}
},
prefetch: true,
integrations: [
mdx(),
partytown(),
react(),
sitemap(),
icon({
include: {
mdi: ['*'],
},
}),
swup({
theme: 'fade',
native: true,
cache: true,
preload: true,
accessibility: true,
smoothScrolling: true,
morph: ['#nav'],
}),
(await import('@playform/compress')).default({
CSS: true,
JavaScript: true,
HTML: {
'html-minifier-terser': {
collapseWhitespace: true,
minifyCSS: false,
minifyJS: true,
},
},
Image: false,
SVG: true,
Logger: 2,
}),
],
markdown: {
syntaxHighlight: false,
rehypePlugins: [
[
rehypePrettyCode,
{
theme: {
light: 'github-light',
dark: 'github-dark-dimmed',
},
keepBackground: false,
transformers: [
transformerCopyButton({
visibility: 'always',
feedbackDuration: 2500,
}),
],
},
],
],
},
plugins: {
'@tailwindcss/postcss': {},
},
vite: {
plugins: [tailwindcss()],
},
output: 'static',
adapter: node({
mode: 'standalone',
}),
build: {
// Specifies the directory in the build output where Astro-generated assets (bundled JS and CSS for example) should live.
// see https://docs.astro.build/en/reference/configuration-reference/#buildassets
assets: 'assets',
// see https://docs.astro.build/en/reference/configuration-reference/#buildassetsprefix
assetsPrefix:
!!import.meta.env.S3_ENABLE || !!process.env.S3_ENABLE ? 'https://digitalocean.com' : '',
},
});

View File

@@ -1,11 +0,0 @@
import eslintPluginAstro from 'eslint-plugin-astro';
import eslintConfigPrettier from "eslint-config-prettier/flat";
export default [
...eslintPluginAstro.configs.recommended,
eslintConfigPrettier,
{
rules: {
}
}
];

View File

@@ -1,84 +0,0 @@
{
"name": "site-profile-new",
"type": "module",
"version": "1.0.0",
"homepage": "https://www.alexlebens.dev",
"bugs": {
"url": "https://gitea.alexlebens.dev/alexlebens/site-profile-new/issues",
"email": "alexander.lebens@gmail.com"
},
"repository": {
"type": "git",
"url": "https://gitea.alexlebens.dev/alexlebens/site-profile-new"
},
"license": "MIT",
"author": {
"name": "Alex Lebens",
"email": "alexander.lebens@gmail.com",
"url": "https://www.alexlebens.dev"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,md,mdx,astro}\"",
"lint": "eslint \"src/**/*.{js,ts,jsx,tsx,astro}\"",
"lint:fix": "eslint --fix \"src/**/*.{js,ts,jsx,tsx,astro}\""
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^4.3.3",
"@astrojs/node": "^9.3.3",
"@astrojs/partytown": "^2.1.4",
"@astrojs/react": "^4.3.0",
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.4.2",
"@giscus/react": "^3.1.0",
"@iconify-json/mdi": "^1.1.63",
"@iconify-json/pajamas": "^1.2.13",
"@iconify-json/simple-icons": "^1.2.47",
"@playform/compress": "^0.0.4",
"@rehype-pretty/transformers": "^0.13.2",
"@swup/astro": "1.7.0",
"@tailwindcss/postcss": "^4.1.8",
"@tailwindcss/vite": "^4.1.8",
"@directus/sdk": "^20.0.0",
"@types/react": "^19.0.0",
"@types/unist": "^3.0.2",
"astro": "^5.12.8",
"astro-compressor": "^0.4.1",
"astro-icon": "^1.1.5",
"framer-motion": "^12.16.0",
"mdast-util-to-string": "^4.0.0",
"preline": "^3.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"reading-time": "^1.5.0",
"rehype-pretty-code": "^0.14.1",
"sharp": "^0.34.3",
"sharp-ico": "^0.1.5",
"shiki": "^3.2.2",
"tailwindcss": "^4.1.11",
"ultrahtml": "^1.5.3"
},
"devDependencies": {
"@eslint-react/eslint-plugin": "^1.52.3",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"astro-icon": "^1.1.5",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-astro": "^1.3.1",
"eslint-plugin-format": "^1.0.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.14",
"timeago.js": "^4.0.2",
"typescript": "5.8.3",
"typescript-eslint": "8.38.0"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
}
export default config;

View File

@@ -1,23 +0,0 @@
/** @type {import("prettier").Config} */
const config = {
printWidth: 100,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'es5',
useTabs: false,
plugins: [
'prettier-plugin-astro',
'prettier-plugin-tailwindcss',
],
overrides: [
{
files: '*.astro',
options: {
parser: 'astro',
},
},
],
};
export default config;

View File

@@ -1,40 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"mergeConfidence:all-badges",
":rebaseStalePrs"
],
"timezone": "US/Central",
"labels": [],
"prHourlyLimit": 0,
"prConcurrentLimit": 0,
"packageRules": [
{
"description": "Label dependency",
"matchDatasources": [
"npm"
],
"addLabels": [
"dependency"
],
"automerge": false,
"minimumReleaseAge": "1 days"
},
{
"description": "Automerge dependency patch",
"matchDatasources": [
"npm"
],
"matchUpdateTypes": [
"patch"
],
"addLabels": [
"dependency",
"automerge"
],
"automerge": true,
"minimumReleaseAge": "1 days"
}
]
}

View File

@@ -1,144 +0,0 @@
---
import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import BrandLogo from '@components/ui/logos/BrandLogo.astro';
import Image from '@components/ui/images/Image.astro';
import { 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="w-full overflow-hidden bg-stone-300/40 dark:bg-stone-800/20"
transition:animate="none"
>
<div class="relative px-4 pt-16 pb-12 sm:px-6">
<div class="mx-auto max-w-[85rem]">
<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 rounded-lg">
<BrandLogo class="max-h-[40px] max-w-[40px] rounded-full" />
</div>
<span class="ml-3 text-xl font-bold text-neutral-800 dark:text-neutral-200">
Blog
</span>
</div>
</a>
<p class="mt-4 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
A description of something.
</p>
</div>
<!-- Left links -->
<div class="col-span-1 md:col-span-2">
<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="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="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="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="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}
class="h-full w-full object-cover object-center"
draggable="false"
loading="eager"
format="webp"
quality="low"
widths={[440]}
disableBlur={true}
/>
</div>
</div>
</div>
<!-- Bottom section -->
<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">
&copy; {currentYear} All rights reserved.
</p>
<div class="flex items-center space-x-2">
<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-xs text-neutral-600 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
>
<svg class="mr-1 h-4 w-4 text-[#FF5D01]" viewBox="0 0 36 36" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.833 22.958c.622-1.185 1.832-1.918 3.18-1.918 2.292 0 4.145 1.86 4.145 4.153 0 1.34-.626 2.54-1.601 3.303 1.223-1.299 1.97-3.048 1.97-4.971 0-3.994-3.243-7.233-7.242-7.233-2.818 0-5.26 1.6-6.469 3.933.78-2.912 3.428-5.06 6.577-5.06 3.75 0 6.79 3.035 6.79 6.78 0 2.606-1.468 4.868-3.616 6.002a4.163 4.163 0 0 0 2.285-3.724c0-2.293-1.853-4.153-4.145-4.153-1.348 0-2.558.733-3.18 1.918l1.306-3.03Z"
fill="currentColor"></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M22.155 12.056c-.622 1.185-1.832 1.918-3.18 1.918-2.292 0-4.145-1.86-4.145-4.153 0-1.34.626-2.54 1.601-3.303-1.223 1.299-1.97 3.048-1.97 4.971 0 3.994 3.243 7.233 7.242 7.233 2.818 0 5.26-1.6 6.469-3.933-.78 2.912-3.428 5.06-6.577 5.06-3.75 0-6.79-3.035-6.79-6.78 0-2.606 1.468-4.868 3.616-6.002a4.163 4.163 0 0 0-2.285 3.724c0 2.293 1.853 4.153 4.145 4.153 1.348 0 2.558-.733 3.18-1.918l-1.306 3.03Z"
fill="currentColor"></path>
</svg>
<span class="relative">
Astro
<span
class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"
>
</span>
</span>
</a>
</div>
</div>
</div>
</div>
</div>
</footer>

View File

@@ -1,2 +0,0 @@
/// <reference types="astro/client" />
/// <reference path="../.astro/types.d.ts" />

View File

@@ -1,93 +0,0 @@
---
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 '@styles/global.css';
interface Props {
title?: string;
description?: string;
ogImage?: any;
lang?: string;
structuredData?: object;
}
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>
<BaseHead
title={normalizeTitle}
description={description}
ogImage={ogImage}
ogTitle={title === '' ? global.name : title}
ogDescription={description}
structuredData={structuredData}
/>
<ClientRouter fallback="swap" />
<script is:inline>
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
window.localStorage.setItem('theme', theme);
</script>
</head>
<body class="bg-stone-200 selection:bg-yellow-400 selection:text-neutral-700 dark:bg-stone-700">
<!-- <div class="fixed inset-0 -z-10">
<div
class="bg-grid-pattern absolute inset-0 [mask-image:radial-gradient(white,transparent_85%)] bg-[center_top_-1px]"
>
</div>
</div> -->
<div class="mx-auto w-full max-w-(--breakpoint-2xl) flex-grow px-4 sm:px-6 lg:px-8">
<Header />
<main class="min-h-screen">
<slot />
</main>
</div>
<Footer />
<style>
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
</body>
</html>
<style>
.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);
}
:global(.dark) .bg-grid-pattern {
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.25) 1px, transparent 1px);
}
</style>

View File

@@ -1,27 +0,0 @@
import { createDirectus, rest } from '@directus/sdk';
import type {
Global,
Post,
Experience,
Education,
Certificate,
Project,
Skill,
} from '@lib/directusTypes';
import { getDirectusURL } from '@lib/directusFunctions';
type Schema = {
site_global: Global;
posts: Post[];
site_experience: Experience;
site_education: Education;
site_certificate: Certificate;
site_projects: Project;
site_skills: Skill;
};
const directus = createDirectus<Schema>(getDirectusURL()).with(rest());
export default directus;

View File

@@ -1,357 +0,0 @@
---
import { readSingleton } from '@directus/sdk';
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'));
---
<BaseLayout
title="Page Not Found"
description="Page Not Found"
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
name: `Page Not Found | ${global.name}`,
description: 'Page Not Found',
isPartOf: {
'@type': 'WebSite',
url: global.site_url,
name: global.name,
description: global.about,
},
}}
>
<section class="mt-20 grid place-content-center">
<div class="mx-auto max-w-screen-xl 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-9xl leading-none font-bold text-neutral-900 sm:text-[12rem] dark:text-neutral-100"
data-text="404"
>
Not Found
</h1>
</div>
<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>
<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 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 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>
const funFacts = [
"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.",
"In internet slang, '404' has become shorthand for something that's missing or someone who's clueless.",
"Some websites turn their 404 pages into games, like Google's Pac-Man 404 page that once existed.",
'The first web server was a NeXT computer used by Tim Berners-Lee at CERN, where the 404 error was born.',
'Many companies use creative 404 pages as a way to showcase their brand personality and humor.',
"The HTTP 1.0 specification from 1996 officially defined the 404 error as 'Not Found'.",
'Studies show that well-designed 404 pages can reduce bounce rates by up to 30%.',
'The most common cause of 404 errors is mistyped URLs.',
];
// Display a random fun fact
const funFactElement = document.getElementById('fun-fact');
if (funFactElement) {
const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)];
funFactElement.textContent = randomFact;
}
// Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => {
const animateContent = () => {
// Animate group 1
const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
};
animateContent();
});
</script>
<style>
/* Glitch effect for 404 text */
.glitch-wrapper {
position: relative;
display: inline-block;
}
.glitch {
position: relative;
display: inline-block;
animation: glitch-skew 1s infinite linear alternate-reverse;
}
.glitch::before,
.glitch::after {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.glitch::before {
left: 2px;
text-shadow: -2px 0 #ff00c1;
clip: rect(44px, 450px, 56px, 0);
animation: glitch-anim 5s infinite linear alternate-reverse;
}
.glitch::after {
left: -2px;
text-shadow:
-2px 0 #00fff9,
2px 2px #ff00c1;
animation: glitch-anim2 1s infinite linear alternate-reverse;
}
@keyframes glitch-anim {
0% {
clip: rect(31px, 9999px, 94px, 0);
transform: skew(0.85deg);
}
5% {
clip: rect(70px, 9999px, 71px, 0);
transform: skew(0.17deg);
}
10% {
clip: rect(9px, 9999px, 85px, 0);
transform: skew(0.4deg);
}
15% {
clip: rect(47px, 9999px, 18px, 0);
transform: skew(0.22deg);
}
20% {
clip: rect(7px, 9999px, 78px, 0);
transform: skew(0.96deg);
}
25% {
clip: rect(53px, 9999px, 54px, 0);
transform: skew(0.05deg);
}
30% {
clip: rect(84px, 9999px, 52px, 0);
transform: skew(0.94deg);
}
35% {
clip: rect(46px, 9999px, 7px, 0);
transform: skew(0.01deg);
}
40% {
clip: rect(2px, 9999px, 66px, 0);
transform: skew(0.66deg);
}
45% {
clip: rect(34px, 9999px, 33px, 0);
transform: skew(0.52deg);
}
50% {
clip: rect(80px, 9999px, 73px, 0);
transform: skew(0.9deg);
}
55% {
clip: rect(8px, 9999px, 81px, 0);
transform: skew(0.3deg);
}
60% {
clip: rect(10px, 9999px, 86px, 0);
transform: skew(0.85deg);
}
65% {
clip: rect(36px, 9999px, 25px, 0);
transform: skew(0.28deg);
}
70% {
clip: rect(75px, 9999px, 31px, 0);
transform: skew(0.46deg);
}
75% {
clip: rect(46px, 9999px, 87px, 0);
transform: skew(0.44deg);
}
80% {
clip: rect(19px, 9999px, 40px, 0);
transform: skew(0.07deg);
}
85% {
clip: rect(85px, 9999px, 88px, 0);
transform: skew(0.71deg);
}
90% {
clip: rect(1px, 9999px, 89px, 0);
transform: skew(0.76deg);
}
95% {
clip: rect(44px, 9999px, 25px, 0);
transform: skew(0.58deg);
}
100% {
clip: rect(31px, 9999px, 26px, 0);
transform: skew(0.6deg);
}
}
@keyframes glitch-anim2 {
0% {
clip: rect(65px, 9999px, 65px, 0);
transform: skew(0.16deg);
}
5% {
clip: rect(8px, 9999px, 42px, 0);
transform: skew(0.65deg);
}
10% {
clip: rect(64px, 9999px, 30px, 0);
transform: skew(0.42deg);
}
15% {
clip: rect(29px, 9999px, 49px, 0);
transform: skew(0.05deg);
}
20% {
clip: rect(25px, 9999px, 56px, 0);
transform: skew(0.09deg);
}
25% {
clip: rect(76px, 9999px, 98px, 0);
transform: skew(0.79deg);
}
30% {
clip: rect(72px, 9999px, 3px, 0);
transform: skew(0.12deg);
}
35% {
clip: rect(20px, 9999px, 60px, 0);
transform: skew(0.09deg);
}
40% {
clip: rect(61px, 9999px, 47px, 0);
transform: skew(0.45deg);
}
45% {
clip: rect(29px, 9999px, 69px, 0);
transform: skew(0.09deg);
}
50% {
clip: rect(82px, 9999px, 96px, 0);
transform: skew(0.05deg);
}
55% {
clip: rect(33px, 9999px, 91px, 0);
transform: skew(0.16deg);
}
60% {
clip: rect(56px, 9999px, 23px, 0);
transform: skew(0.01deg);
}
65% {
clip: rect(46px, 9999px, 21px, 0);
transform: skew(0.89deg);
}
70% {
clip: rect(50px, 9999px, 1px, 0);
transform: skew(0.85deg);
}
75% {
clip: rect(82px, 9999px, 33px, 0);
transform: skew(0.87deg);
}
80% {
clip: rect(94px, 9999px, 46px, 0);
transform: skew(0.64deg);
}
85% {
clip: rect(48px, 9999px, 95px, 0);
transform: skew(0.43deg);
}
90% {
clip: rect(60px, 9999px, 10px, 0);
transform: skew(0.29deg);
}
95% {
clip: rect(85px, 9999px, 62px, 0);
transform: skew(0.66deg);
}
100% {
clip: rect(61px, 9999px, 58px, 0);
transform: skew(0.74deg);
}
}
@keyframes glitch-skew {
0% {
transform: skew(-1deg);
}
10% {
transform: skew(0deg);
}
20% {
transform: skew(0.5deg);
}
30% {
transform: skew(-0.5deg);
}
40% {
transform: skew(0.2deg);
}
50% {
transform: skew(0deg);
}
60% {
transform: skew(-0.5deg);
}
70% {
transform: skew(0.8deg);
}
80% {
transform: skew(-0.2deg);
}
90% {
transform: skew(0.5deg);
}
100% {
transform: skew(0deg);
}
}
</style>

View File

@@ -1,107 +0,0 @@
---
import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro';
import Experience from '@components/ui/sections/Experience.astro';
import Projects from '@components/ui/sections/Projects.astro';
import Skills from '@components/ui/sections/Skills.astro';
import Education from '@components/ui/sections/Education.astro';
import portraitImg from '@images/portrait.avif';
const global = await directus.request(readSingleton('site_global'));
const description = 'About me.';
---
<BaseLayout
title="About Me"
description={description}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
name: `About | ${global.name}`,
description: description,
isPartOf: {
'@type': 'WebSite',
url: global.site_url,
name: global.name,
description: global.about,
},
}}
>
<HeroSection
title="About Me"
subTitle={global.about}
src={portraitImg}
alt={global.portrait_alt}
/>
<section class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<main class="relative grid max-w-7xl gap-12 p-8 max-sm:py-16 md:grid-cols-6 md:p-16 xl:gap-24">
<div class="space-y-12 md:col-span-8">
<Experience className="smooth-reveal-2" />
<Education className="smooth-reveal-2 mt-30" />
<Projects className="smooth-reveal-2 mt-30" />
<Skills className="smooth-reveal-2 mt-30" />
</div>
</main>
</section>
</BaseLayout>
<script>
// Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => {
const animateContent = () => {
// Animate group 1
const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
50 + index * 100
);
});
// Animate group 2
const smoothReveal2 = document.querySelectorAll('.smooth-reveal-2');
smoothReveal2.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
200 + index * 250
);
});
// Animate topic cards with staggered delay
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
smoothRevealCards.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
400 + index * 250
);
});
// Animate with just fade in with staggered delay
const smoothRevealFade = document.querySelectorAll('.smooth-reveal-fade');
smoothRevealFade.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal-fade');
},
100 + index * 250
);
});
};
animateContent();
});
</script>

View File

@@ -1,181 +0,0 @@
---
import { type CollectionEntry, getCollection } from 'astro:content';
import getReadingTime from 'reading-time';
import { readItems, readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import { getDirectusImageURL } from '@lib/directusFunctions';
import BaseLayout from '@layouts/BaseLayout.astro';
import Image from '@components/ui/images/Image.astro';
import { formatDateTime } from '@support/time';
export async function getStaticPaths() {
const posts = await directus.request(readItems('posts'));
return posts.map((post) => ({
params: { slug: post.slug },
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);
---
<BaseLayout
title={post.title}
description={post.description}
ogImage={getDirectusImageURL(post.image)}
structuredData={{
'@context': 'https://schema.org',
'@type': 'NewsArticle',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
description: post.description,
isPartOf: {
'@type': 'WebSite',
url: `${global.site_url}/blog`,
name: global.name,
description: global.about,
},
image: [
// post.data.banner,
],
headline: post.title,
datePublished: post.published_date,
dateModified: post.updated_date,
author: [
{
'@type': 'Person',
name: `${global.name}`,
url: `${global.site_url}`,
},
],
}}
>
<section class="mx-auto max-w-6xl px-4 pt-8 pb-12 sm:px-6 lg:px-8 lg:pt-12">
<div class="smooth-reveal relative w-full">
<div class="mt-4 rounded-2xl shadow-none sm:mt-0 sm:shadow-sm">
<Image
class="max-h-[600px] w-full rounded-t-2xl object-cover"
src={getDirectusImageURL(post.image)}
alt={post.image_alt}
draggable="false"
format="webp"
loading="lazy"
inferSize={true}
/>
<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="mt-8 flex items-center whitespace-nowrap">
<li class="inline-flex items-center">
<a
class="flex items-center text-sm text-neutral-500 transition-all duration-300 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200"
href=`/categories/${category.slug}`
>
{category?.data?.title}
</a>
<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 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 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>
<article
class="prose prose-blog sm:prose-lg dark:prose-invert max-w-none text-justify text-neutral-800 dark:text-neutral-200"
>
<div set:html={post.content} />
</article>
<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>
</div>
</div>
</div>
</div>
</section>
<style is:inline>
code[data-theme*=' '],
code[data-theme*=' '] span {
color: var(--shiki-light);
}
html.dark {
code[data-theme*=' '],
code[data-theme*=' '] span {
color: var(--shiki-dark);
}
}
</style>
</BaseLayout>
<script>
// Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => {
const animateContent = () => {
const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 100
);
});
};
animateContent();
});
</script>

View File

@@ -1,102 +0,0 @@
---
import { readItems, readSingleton } from '@directus/sdk';
import type { Post } from '@lib/directusTypes';
import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import BlogRecentCard from '@components/blog/BlogRecentCard.astro';
import BlogFeaturedArticle from '@components/blog/BlogFeaturedArticle.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro';
import blogImg from '@images/autumn_tree.png';
const global = await directus.request(readSingleton('site_global'));
const posts = await directus.request(
readItems('posts', {
fields: ['*'],
sort: ['-published_date'],
})
);
const selectedPosts: Post[] = posts.filter((p) => p.selected);
const description =
'Here are some articles that Alex Lebens believes are not bad, hope you enjoy them.';
---
<BaseLayout
title="Blog"
description={description}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
name: `Blog | ${global.name}`,
description: description,
isPartOf: {
'@type': 'WebSite',
url: global.site_url,
name: global.name,
description: global.about,
},
}}
>
<HeroSection title="Blog" subTitle={description} src={blogImg} alt={global.blog_image_alt} />
<BlogRecentCard posts={posts} />
<BlogFeaturedArticle posts={selectedPosts} />
</BaseLayout>
<script>
// Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => {
const animateContent = () => {
// Animate group 1
const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
200 + index * 300
);
});
// Animate group 2
const smoothReveal2 = document.querySelectorAll('.smooth-reveal-2');
smoothReveal2.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
500 + index * 100
);
});
// Animate topic cards with staggered delay
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
smoothRevealCards.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
1000 + index * 250
);
});
// Animate with just fade in with staggered delay
const smoothRevealFade = document.querySelectorAll('.smooth-reveal-fade');
smoothRevealFade.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal-fade');
},
100 + index * 250
);
});
};
animateContent();
});
</script>

View File

@@ -1,107 +0,0 @@
---
import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro';
import FeaturesSection from '@components/ui/sections/FeaturesSection.astro';
import LatestPosts from '@components/ui/sections/LatestPosts.astro';
import HeroSectionAlt from '@components/ui/sections/HeroSectionAlt.astro';
import homeImg from '@images/autumn_mountain.png';
const global = await directus.request(readSingleton('site_global'));
const description = 'Writing on technology, selfhosting, and me.';
---
<BaseLayout
title="Home"
description={description}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
name: `Home | ${global.name}`,
description: description,
isPartOf: {
'@type': 'WebSite',
url: global.site_url,
name: global.name,
description: global.about,
},
}}
>
<HeroSection
title={`Hello, I'm <span class="text-steel dark:text-steel">Alex Lebens</span>`}
subTitle={description}
primaryBtn="About Me"
primaryBtnURL="/about"
src={homeImg}
alt={global.home_image_alt}
/>
<FeaturesSection />
<LatestPosts />
<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>
// Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => {
const animateContent = () => {
// Animate group 1
const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
50 + index * 100
);
});
// Animate group 2
const smoothReveal2 = document.querySelectorAll('.smooth-reveal-2');
smoothReveal2.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
200 + index * 250
);
});
// Animate topic cards with staggered delay
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
smoothRevealCards.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
400 + index * 250
);
});
// Animate with just fade in with staggered delay
const smoothRevealFade = document.querySelectorAll('.smooth-reveal-fade');
smoothRevealFade.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal-fade');
},
100 + index * 250
);
});
};
animateContent();
});
</script>

View File

@@ -1,57 +0,0 @@
// copy from https://github.com/delucis/astro-blog-full-text-rss
// see https://github.com/delucis/astro-blog-full-text-rss/blob/latest/src/pages/rss.xml.ts
// get more context
import { getContainerRenderer as getMDXRenderer } from '@astrojs/mdx';
import rss, { type RSSFeedItem } from '@astrojs/rss';
import type { APIContext } from 'astro';
import { transform, walk } from 'ultrahtml';
import sanitize from 'ultrahtml/transformers/sanitize';
import { readItems, readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
const global = await directus.request(readSingleton('site_global'));
export async function GET(context: APIContext) {
// Get the URL to prepend to relative site links. Based on `site` in `astro.config.mjs`.
let baseUrl = context.site?.href || global.site_url;
if (baseUrl.at(-1) === '/') {
baseUrl = baseUrl.slice(0, -1);
}
// Load the content collection entries to add to our RSS feed.
const posts = await directus.request(
readItems('posts', {
fields: ['*'],
sort: ['-published_date'],
})
);
const feedItems: RSSFeedItem[] = [];
for (const post of posts) {
const content = await transform(post.content.replace(/^<!DOCTYPE html>/, ''), [
async (node) => {
await walk(node, (node) => {
if (node.name === 'a' && node.attributes.href?.startsWith('/')) {
node.attributes.href = baseUrl + node.attributes.href;
}
if (node.name === 'img' && node.attributes.src?.startsWith('/')) {
node.attributes.src = baseUrl + node.attributes.src;
}
});
return node;
},
sanitize({ dropElements: ['script', 'style'] }),
]);
feedItems.push({ ...post, link: `/blog/${post.slug}/`, content });
}
// Return our RSS feed XML response.
return rss({
title: global.name,
description: global.about,
site: baseUrl,
items: feedItems,
});
}

View File

@@ -1,92 +0,0 @@
@import 'tailwindcss';
@import 'preline/variants.css';
@plugin '@tailwindcss/typography';
@plugin '@tailwindcss/forms';
/* Dark mode support for Tailwind CSS v4 */
/* https://tailwindcss.com/docs/dark-mode */
@custom-variant dark (&:where(.dark, .dark *));
/* Add custom colors */
@theme {
--color-midnight: #0c354d;
--color-turquoise: #0da797;
--color-steel: #4682b4;
--color-bermuda: #7fbab4;
--color-desert: #f9deb2;
--color-bronze: #9e7f5e;
--color-gitea-primary: #609926;
--color-gitea-secondary: #4c7a33;
}
@layer base {
:root {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
--theme-transition: 0.3s ease;
}
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
html {
scroll-behavior: smooth;
scroll-padding-top: 5rem;
overflow-y: scroll;
}
body {
margin: 0;
padding: 0;
overflow-x: hidden;
--swup-fade-theme-duration: 0.2s;
}
button:not(:disabled),
[role='button']:not(:disabled) {
cursor: pointer;
}
body,
a,
button {
transition:
background-color var(--theme-transition),
color var(--theme-transition),
border-color var(--theme-transition);
}
}
/* Content reveal animations */
.smooth-reveal,
.smooth-reveal-2,
.smooth-reveal-cards {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
.smooth-reveal-fade {
opacity: 0;
transform: translateY(0px);
transition:
opacity 1.8s ease,
transform 0.8s ease;
}
.animate-reveal-fade {
opacity: 1 !important;
transform: translateY(0) !important;
}

View File

@@ -1,23 +0,0 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"],
"compilerOptions": {
"strictNullChecks": true,
"baseUrl": "src",
"jsx": "react-jsx",
"types": ["astro/client"],
"paths": {
"@/*": ["*"],
"@src/*": ["src/*"],
"@lib/*": ["lib/*"],
"@components/*": ["components/*"],
"@content/*": ["content/*"],
"@layouts/*": ["layouts/*"],
"@styles/*": ["styles/*"],
"@pages/*": ["pages/*"],
"@support/*": ["support/*"],
"@images/*": ["images/*"]
}
}
}

View File

@@ -0,0 +1,10 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"typescript.tsdk": "node_modules/typescript/lib"
}
}

View File

@@ -1,104 +0,0 @@
---
---
<div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden">
<!-- Dot pattern background -->
<div
class="bg-grid-pattern theme-transition-bg absolute inset-0 [mask-image:radial-gradient(white,transparent_85%)] bg-[center_top_-1px]"
>
</div>
<!-- Ambient glow effects -->
<div
class="animate-glow theme-transition-bg absolute top-1/4 left-1/4 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-zinc-400/20 opacity-50 blur-3xl dark:bg-zinc-500/20"
>
</div>
<div
class="animate-glow animation-delay-1000 theme-transition-bg absolute right-1/4 bottom-1/3 h-64 w-64 translate-x-1/2 translate-y-1/2 rounded-full bg-zinc-300/20 opacity-40 blur-3xl dark:bg-zinc-600/20"
>
</div>
<!-- Theme transition overlay -->
<div
id="theme-transition-overlay"
class="pointer-events-none absolute inset-0 bg-white opacity-0 dark:bg-zinc-900"
>
</div>
</div>
<script>
// Theme transition script
document.addEventListener('astro:page-load', () => {
const themeToggle = document.querySelector('[data-theme-toggle]');
const overlay = document.getElementById('theme-transition-overlay');
if (themeToggle && overlay) {
themeToggle.addEventListener('click', () => {
document.documentElement.classList.add('theme-transitioning');
overlay.style.opacity = '0.15';
overlay.style.transition = 'opacity 0.3s ease';
setTimeout(() => {
overlay.style.opacity = '0';
setTimeout(() => {
document.documentElement.classList.remove('theme-transitioning');
}, 700);
}, 300);
});
}
});
</script>
<style>
/* Grid pattern for dots */
.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);
}
/* Dark mode version */
:global(.dark) .bg-grid-pattern {
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.15) 1px, transparent 1px);
}
/* Ambient glow animations */
.animate-glow {
animation: glow 12s cubic-bezier(0.4, 0, 0.6, 1) infinite;
transition:
background-color 0.7s cubic-bezier(0.65, 0, 0.35, 1),
opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1);
}
.animation-delay-1000 {
animation-delay: 1s;
}
@keyframes glow {
0%,
100% {
opacity: 0.4;
transform: translate(0, 0) scale(1);
}
25% {
opacity: 0.5;
transform: translate(5%, 5%) scale(1.1);
}
50% {
opacity: 0.3;
transform: translate(0, 10%) scale(0.95);
}
75% {
opacity: 0.5;
transform: translate(-5%, 5%) scale(1.05);
}
}
/* Theme transition overlay */
#theme-transition-overlay {
transition: opacity 0.3s ease;
z-index: 10;
}
</style>

View File

@@ -1,130 +1,58 @@
---
import directus from '../lib/directus';
import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton('global'));
const links = await directus.request(readSingleton('links'));
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();
const navLinks = [
{ text: 'Home', href: '/' },
{ text: 'Blog', href: '/blog' },
{ text: 'About', href: '/about' },
{ text: 'RSS', href: '/rss' },
];
const socialLinks = [
{
name: 'GitHub',
href: links.github,
icon: `<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path>`,
},
{
name: 'Gitea',
href: links.gitea,
icon: `<path d="M7 5C7 3.89543 7.89543 3 9 3C10.1046 3 11 3.89543 11 5C11 5.34168 10.9143 5.66336 10.7633 5.9447H11.3438C13.5529 5.9447 15.3438 7.73556 15.3438 9.9447V11.2244C15.9301 11.5731 16.323 12.213 16.323 12.9447C16.323 14.0493 15.4276 14.9447 14.323 14.9447C13.2184 14.9447 12.323 14.0493 12.323 12.9447C12.323 12.1959 12.7345 11.5432 13.3438 11.2004V9.9447C13.3438 8.84013 12.4483 7.9447 11.3438 7.9447H10V17.2676C10.5978 17.6134 11 18.2597 11 19C11 20.1046 10.1046 21 9 21C7.89543 21 7 20.1046 7 19C7 18.2597 7.4022 17.6134 8 17.2676V6.73244C7.4022 6.38663 7 5.74028 7 5Z" fill="currentColor"/>`,
},
{
name: 'LinkedIn',
href: links.linkedin,
icon: `<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path>`,
},
];
---
<footer
class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
class="w-full overflow-hidden bg-stone-300/40 dark:bg-stone-800/20"
transition:animate="none"
>
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<div
class="theme-transition-all animate-float-slow absolute -top-40 -right-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
>
</div>
<div
class="theme-transition-all animate-float-slow animation-delay-2000 absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
>
</div>
<div
class="theme-transition-all animate-float-slow animation-delay-1000 absolute top-20 left-1/4 h-40 w-40 rounded-full bg-zinc-200/50 opacity-30 blur-2xl dark:bg-zinc-700/20"
>
</div>
</div>
<div class="relative px-4 pt-16 pb-12 sm:px-6">
<div class="mx-auto max-w-4xl">
<div class="mx-auto max-w-[85rem]">
<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 rounded-lg">
<img
src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.logo}`
alt="logo"
class="max-h-[40px] max-w-[40px] object-cover"
loading="eager"
/>
<BrandLogo class="max-h-[40px] max-w-[40px] rounded-full" />
</div>
<span
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
>
<span class="ml-3 text-xl font-bold text-neutral-800 dark:text-neutral-200">
Blog
</span>
</div>
</a>
<p
class="theme-transition-color mt-4 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400"
>
{global.description}
<p class="mt-4 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
A description of something.
</p>
<!-- Social links -->
<div class="mt-6 flex items-center space-x-4">
{
socialLinks.map((social) => (
<a
href={social.href}
target="_blank"
rel="noopener noreferrer"
class="hover group relative flex h-10 w-10 transform items-center justify-center rounded-full bg-zinc-100 text-zinc-500 transition-all duration-300 hover:-translate-y-1 hover:text-zinc-900 hover:ring-2 hover:ring-zinc-300 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100 dark:hover:ring-zinc-700"
aria-label={social.name}
>
<span class="absolute inset-0 rounded-full bg-gradient-to-br from-zinc-200 to-zinc-300 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-700 dark:to-zinc-600" />
<svg
class="relative z-10 h-5 w-5 transition-transform duration-300 group-hover:scale-110"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<Fragment set:html={social.icon} />
</svg>
</a>
))
}
</div>
</div>
<!-- Quick links -->
<div class="col-span-1 md:col-span-3">
<!-- Left links -->
<div class="col-span-1 md:col-span-2">
<h3
class="theme-transition-color after:bg-turquoise dark:after:bg-turquoise relative inline-block pb-2 text-sm font-semibold tracking-wider text-zinc-900 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-[''] dark:text-zinc-100"
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"
>
Navigation
Blog
</h3>
<ul class="mt-4 space-y-3">
{
navLinks.map((link) => (
NavigationLinks.map((link) => (
<li>
<a
href={link.href}
class="group flex items-center text-base text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
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.text}</span>
<span class="relative z-10">{link.name}</span>
</span>
</a>
</li>
@@ -132,30 +60,63 @@ const socialLinks = [
}
</ul>
</div>
<!-- Right links -->
<div class="col-span-1 md:col-span-3">
<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="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="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}
class="h-full w-full object-cover object-center"
draggable="false"
loading="eager"
format="webp"
quality="low"
widths={[440]}
disableBlur={true}
/>
</div>
</div>
</div>
<!-- Bottom section -->
<div class="theme-transition-all mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
<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="theme-transition-color text-sm text-zinc-600 dark:text-zinc-400">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
&copy; {currentYear} All rights reserved.
</p>
<div class="flex items-center space-x-2">
<span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
>Built with
</span>
<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-xs text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
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"
>
<svg
class="mr-1 h-4 w-4 text-[#FF5D01] group-hover:animate-pulse"
viewBox="0 0 36 36"
fill="none"
>
<svg class="mr-1 h-4 w-4 text-[#FF5D01]" viewBox="0 0 36 36" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
@@ -181,67 +142,3 @@ const socialLinks = [
</div>
</div>
</footer>
<style>
.theme-transition-all {
transition-property: background-color, border-color, color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
.theme-transition-color {
transition-property: color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
.theme-transition-bg {
transition-property: background-color;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.2);
}
}
@keyframes float-slow {
0%,
100% {
transform: translateY(0) translateX(0);
}
25% {
transform: translateY(-10px) translateX(10px);
}
50% {
transform: translateY(-5px) translateX(-5px);
}
75% {
transform: translateY(10px) translateX(5px);
}
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.animate-float-slow {
animation: float-slow 20s ease-in-out infinite;
}
.animation-delay-1000 {
animation-delay: 1s;
}
.animation-delay-2000 {
animation-delay: 2s;
}
</style>

View File

@@ -1,36 +0,0 @@
---
export interface Props {
date?: Date | string;
}
const { date } = Astro.props;
const parsedDate = typeof date === 'string' ? new Date(date) : date;
---
{
parsedDate && (
<time datetime={parsedDate.toISOString()} class="z-10 flex items-center gap-1.5">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-3.5 w-3.5 sm:h-4 sm:w-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0
A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
{parsedDate.toLocaleDateString('en-us', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
)
}

View File

@@ -1,259 +0,0 @@
---
import ThemeToggle from './ThemeToggle.astro';
import directus from '../lib/directus';
import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton('global'));
const links = await directus.request(readSingleton('links'));
const navItems = [
{ text: 'Home', href: '/' },
{ text: 'Blog', href: '/blog' },
{ text: 'About', href: '/about' },
{ text: 'Gitea', href: links.gitea },
{ text: 'RSS', href: 'rss.xml' },
];
const pathname = new URL(Astro.request.url).pathname;
const currentPath = pathname.slice(1);
---
<header
class="fixed top-0 right-0 left-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
transition:animate="none"
>
<div class="mx-auto flex max-w-3xl items-center justify-between px-4">
<!-- Logo -->
<a
href="/"
class="from-midnight to-turquoise relative flex h-10 w-10 items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br text-xl shadow-lg transition-transform"
>
<div class="mx-auto aspect-square overflow-hidden rounded-lg">
<img
src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.logo}`
alt="logo"
class="max-h-[40px] max-w-[40px] object-cover"
loading="eager"
/>
</div>
</a>
<!-- Desktop navigation -->
<nav class="hidden items-center space-x-6 sm:flex">
{
navItems.map((item) => {
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
return (
<a
href={item.href}
class={`text-sm font-medium ${
isActive
? 'text-zinc-900 dark:text-zinc-100'
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100'
}`}
>
{item.text}
</a>
);
})
}
<ThemeToggle />
</nav>
<!-- Mobile menu button -->
<button id="mobile-menu-button" class="flex items-center sm:hidden" aria-label="Menu">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6 text-zinc-900 dark:text-white"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
</svg>
</button>
</div>
</header>
<!-- Mobile menu overlay -->
<div
id="mobile-menu"
class="pointer-events-none fixed inset-0 z-50 flex flex-col bg-white opacity-0 transition-all duration-300 ease-in-out dark:bg-zinc-900"
>
<div class="flex items-center justify-between border-b border-zinc-100 p-4 dark:border-zinc-800">
<a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">{global.initals}</a>
<button
id="close-menu-button"
class="rounded-md p-2 text-zinc-900 transition-colors hover:bg-zinc-100 dark:text-white dark:hover:bg-zinc-800"
aria-label="Close menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<nav class="flex flex-1 flex-col items-center justify-center space-y-6 text-center">
{
navItems.map((item, index) => {
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
return (
<a
href={item.href}
class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
isActive
? 'text-zinc-900 dark:text-zinc-100'
: 'text-zinc-600 group-hover:text-zinc-900 dark:text-zinc-400 dark:group-hover:text-zinc-100'
}`}
style={`transition-delay: ${index * 0.05}s;`}
>
{item.text}
</a>
);
})
}
<div class="mobile-nav-item translate-y-4 pt-4 opacity-0" style="transition-delay: 0.25s;">
<ThemeToggle />
</div>
</nav>
</div>
<!-- Spacer to prevent content from hiding behind fixed header -->
<div class="h-16"></div>
<script>
// Mobile menu toggle with animations
document.addEventListener('astro:page-load', () => {
const mobileMenuButton = document.getElementById('mobile-menu-button');
const closeMenuButton = document.getElementById('close-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
const navItems = document.querySelectorAll('.mobile-nav-item');
// Open menu with animations
mobileMenuButton?.addEventListener('click', () => {
if (!mobileMenu) return;
// Prevent body scrolling
document.body.style.overflow = 'hidden';
// Show menu with fade in
mobileMenu.classList.remove('pointer-events-none');
mobileMenu.classList.add('pointer-events-auto');
// Animate opacity
setTimeout(() => {
mobileMenu.style.opacity = '1';
// Animate each nav item with staggered delay
navItems.forEach((item) => {
setTimeout(() => {
item.classList.remove('opacity-0', 'translate-y-4');
}, 150);
});
}, 50);
});
// Close menu with animations
const closeMenu = () => {
if (!mobileMenu) return;
// Fade out nav items first
navItems.forEach((item) => {
item.classList.add('opacity-0', 'translate-y-4');
});
// Then fade out the menu
setTimeout(() => {
mobileMenu.style.opacity = '0';
// After animation completes, hide menu and restore scrolling
setTimeout(() => {
mobileMenu.classList.remove('pointer-events-auto');
mobileMenu.classList.add('pointer-events-none');
document.body.style.overflow = '';
}, 300);
}, 100);
};
// Close button event
closeMenuButton?.addEventListener('click', closeMenu);
// Close menu when clicking a link
const mobileLinks = mobileMenu?.querySelectorAll('a');
mobileLinks?.forEach((link) => {
link.addEventListener('click', closeMenu);
});
// Close menu on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && mobileMenu?.classList.contains('pointer-events-auto')) {
closeMenu();
}
});
// Add smooth animation to header on scroll
const header = document.querySelector('header');
let lastScrollY = window.scrollY;
window.addEventListener('scroll', () => {
if (!header) return;
const currentScrollY = window.scrollY;
// Add shadow on scroll
if (currentScrollY > 10) {
header.classList.add('shadow-xs');
} else {
header.classList.remove('shadow-xs');
}
// Update last scroll position
lastScrollY = currentScrollY;
});
});
</script>
<style>
/* Smooth animations for mobile navigation */
.mobile-nav-item {
transition:
opacity 0.5s ease,
transform 0.5s ease,
color 0.3s ease;
}
/* Header transition */
header {
transition:
box-shadow 0.3s ease,
transform 0.3s ease,
background-color 0.3s ease;
}
/* Mobile menu button hover effect */
#mobile-menu-button {
transition: transform 0.2s ease;
}
#mobile-menu-button:hover {
transform: scale(1.05);
}
/* Mobile menu transition */
#mobile-menu {
transition: opacity 0.3s ease;
backdrop-filter: blur-sm(4px);
}
</style>

View File

@@ -1,109 +0,0 @@
---
export interface Props {
title: string;
url: string;
class?: string;
}
const { title, url, class: className = '' } = Astro.props;
const encodedTitle = encodeURIComponent(title);
const encodedUrl = encodeURIComponent(url);
---
<div class={`flex items-center gap-4 mt-8 ${className}`}>
<span class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Share:</span>
<div class="flex gap-2">
<a
href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
target="_blank"
rel="noopener noreferrer"
class="hover rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on Twitter"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
>
<path
d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"
>
</path>
</svg>
</a>
<a
href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`}
target="_blank"
rel="noopener noreferrer"
class="hover rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on Facebook"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
>
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"> </path>
</svg>
</a>
<a
href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`}
target="_blank"
rel="noopener noreferrer"
class="hover rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on LinkedIn"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
>
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z">
</path>
<rect x="2" y="9" width="4" height="12"></rect>
<circle cx="4" cy="4" r="2"></circle>
</svg>
</a>
<button
id="copy-link-button"
class="relative rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Copy link"
title="Copy link to clipboard"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-4 w-4"
>
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"> </path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"> </path>
</svg>
<span
id="copy-tooltip"
class="absolute -top-8 left-1/2 -translate-x-1/2 transform rounded-sm bg-zinc-800 px-2 py-1 text-xs whitespace-nowrap text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
>
Copied!
</span>
</button>
</div>
</div>

View File

@@ -1,28 +0,0 @@
---
export interface Props {
tags: string[];
class?: string;
}
const { tags = [], class: className = '' } = Astro.props;
---
{
tags && (
<div class={`mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start ${className}`}>
{tags.slice(0, 2).map((postTag) => (
<a
href={`/tags/${postTag}`}
class={`inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-600 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700`}
>
#{postTag}
</a>
))}
{tags.length > 2 && (
<span class="inline-flex items-center rounded-full bg-zinc-50 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-900 dark:text-zinc-400">
+{tags.length - 2}
</span>
)}
</div>
)
}

View File

@@ -1,318 +0,0 @@
---
---
<button
id="theme-toggle"
data-theme-toggle
class="group hover:bg-desert/50 dark:hover:bg-midnight/50 relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 focus:ring-2 focus:ring-zinc-300 focus:outline-hidden sm:p-2 dark:focus:ring-zinc-700"
aria-label="Toggle dark mode"
>
<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 scale-100 rotate-0 text-zinc-800 transition-all duration-500 dark:scale-0 dark:-rotate-90 dark:text-zinc-200"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="5"></circle>
<path
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"
></path>
</svg>
<!-- Moon icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-dark absolute h-5 w-5 scale-0 rotate-90 text-zinc-800 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-zinc-200"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</div>
<!-- Ripple effect -->
<span
class="absolute inset-0 h-full w-full bg-zinc-200 opacity-0 transition-opacity duration-300 group-active:opacity-20 dark:bg-zinc-700"
></span>
</button>
<script is:inline>
// 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 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';
overlay.style.opacity = '0';
overlay.style.transition = 'opacity 0.3s ease-out';
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,
(e) => {
e.preventDefault();
e.stopPropagation();
// Get click/touch position for radial animation
let x, y;
if (e.type === 'touchend' && e.changedTouches && e.changedTouches[0]) {
const rect = toggle.getBoundingClientRect();
x = e.changedTouches[0].clientX - rect.left;
y = e.changedTouches[0].clientY - rect.top;
} else {
const rect = toggle.getBoundingClientRect();
x = e.clientX - rect.left;
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';
// Show overlay during transition
if (overlay) {
overlay.style.backgroundColor =
newTheme === 'dark' ? 'rgba(24, 24, 27, 0.3)' : 'rgba(255, 255, 255, 0.3)';
overlay.style.opacity = '1';
}
// Add transition class
document.documentElement.classList.add('theme-switching');
// Add ripple effect
const ripple = document.createElement('span');
ripple.className = 'theme-toggle-ripple';
toggle.appendChild(ripple);
// Force a reflow to ensure all elements update
document.body.offsetHeight;
// Toggle dark mode with a slight delay to allow overlay to appear
setTimeout(() => {
if (isDark) {
document.documentElement.classList.remove('dark');
} else {
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' },
})
);
// 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');
ripple.remove();
}, 300);
}, 50);
},
{ 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 }
);
});
}
// Run setup on load
document.addEventListener('astro:page-load', setupThemeToggle);
// Also run on page visibility change to ensure theme is consistent
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
const currentTheme = localStorage.getItem('theme');
if (currentTheme === 'dark') {
document.documentElement.classList.add('dark');
} else if (currentTheme === 'light') {
document.documentElement.classList.remove('dark');
}
}
});
// Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches }) => {
if (!localStorage.getItem('theme')) {
if (matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
});
</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;
}
/* Ripple animation */
.theme-toggle-ripple {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
width: 100%;
height: 100%;
border-radius: 50%;
background-color: rgba(161, 161, 170, 0.3);
animation: ripple 0.8s ease-out;
}
@keyframes ripple {
0% {
transform: translate(-50%, -50%) scale(0);
opacity: 0.5;
}
100% {
transform: translate(-50%, -50%) scale(2.5);
opacity: 0;
}
}
/* Subtle hover animation */
#theme-toggle {
transform: translateY(0);
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
-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);
}
#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);
}
#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,
.icon-dark {
transition: all 0.2s ease-out !important;
}
#theme-toggle,
#theme-toggle:hover {
transform: none;
transition: none;
}
.theme-toggle-ripple {
animation-duration: 0.4s;
}
}
/* Adjust size for very small screens */
@media (max-width: 320px) {
#theme-toggle {
padding: 0.25rem !important;
}
}
</style>

5
src/env.d.ts vendored
View File

@@ -1,3 +1,8 @@
<<<<<<< HEAD
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
/// <reference types="astro/content" />
=======
/// <reference types="astro/client" />
/// <reference path="../.astro/types.d.ts" />
>>>>>>> 184f0c7 (fix path)

View File

Before

Width:  |  Height:  |  Size: 11 MiB

After

Width:  |  Height:  |  Size: 11 MiB

View File

Before

Width:  |  Height:  |  Size: 13 MiB

After

Width:  |  Height:  |  Size: 13 MiB

View File

Before

Width:  |  Height:  |  Size: 7.8 MiB

After

Width:  |  Height:  |  Size: 7.8 MiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.8 MiB

After

Width:  |  Height:  |  Size: 6.8 MiB

View File

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -1,17 +1,93 @@
---
import Layout from './Layout.astro';
import directus from '../lib/directus';
import { ClientRouter } from 'astro:transitions';
import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton('global'));
import directus from '@lib/directus';
import BaseHead from '@components/BaseHead.astro';
import Footer from '@components/Footer.astro';
import Header from '@components/Header.astro';
export interface Props {
title: string;
import '@styles/global.css';
interface Props {
title?: string;
description?: string;
ogImage?: any;
lang?: string;
structuredData?: object;
}
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}`;
---
<Layout title={global.title} description={global.title}>
<slot />
</Layout>
<html lang={lang}>
<head>
<title>{normalizeTitle}</title>
<BaseHead
title={normalizeTitle}
description={description}
ogImage={ogImage}
ogTitle={title === '' ? global.name : title}
ogDescription={description}
structuredData={structuredData}
/>
<ClientRouter fallback="swap" />
<script is:inline>
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
window.localStorage.setItem('theme', theme);
</script>
</head>
<body class="bg-stone-200 selection:bg-yellow-400 selection:text-neutral-700 dark:bg-stone-700">
<!-- <div class="fixed inset-0 -z-10">
<div
class="bg-grid-pattern absolute inset-0 [mask-image:radial-gradient(white,transparent_85%)] bg-[center_top_-1px]"
>
</div>
</div> -->
<div class="mx-auto w-full max-w-(--breakpoint-2xl) flex-grow px-4 sm:px-6 lg:px-8">
<Header />
<main class="min-h-screen">
<slot />
</main>
</div>
<Footer />
<style>
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
</body>
</html>
<style>
.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);
}
:global(.dark) .bg-grid-pattern {
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.25) 1px, transparent 1px);
}
</style>

View File

@@ -1,165 +0,0 @@
---
import Layout from './Layout.astro';
import FormattedDate from '../components/FormattedDate.astro';
import ShareButtons from '../components/ShareButtons.astro';
import TagList from '../components/TagList.astro';
import './styles/markdown.css';
import directus from '../lib/directus';
import { readItems } from '@directus/sdk';
export async function getStaticPaths() {
const posts = await directus.request(
readItems('posts', {
fields: ['*'],
})
);
return posts.map((post) => ({ params: { slug: post.slug }, props: post }));
}
const post = Astro.props;
let canonicalURL;
try {
canonicalURL = new URL(Astro.url.pathname, Astro.site || process.env.SITE_URL);
} catch (error) {
console.error('Error creating canonical URL:', error);
canonicalURL = new URL('https://www.example.com');
}
---
<Layout title={post.title} description={post.description}>
<article class="prose prose-zinc dark:prose-invert lg:prose-lg mx-auto max-w-4xl">
<div class="hero-text mb-12">
<h1
class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 sm:text-5xl dark:text-zinc-100"
>
{post.title}
</h1>
<p
class="mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400"
>
<!-- {post.description} -->
</p>
<div
class="hero-text mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"
>
<FormattedDate date={post.published_date} />
</div>
<div
class="hero-text mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"
>
<TagList tags={post.tags} />
</div>
</div>
<!-- Hero image -->
{
post.image && (
<div class="relative mb-8 overflow-hidden rounded-xl shadow-lg sm:mb-12">
<div class="aspect-[16/9] w-full">
<img
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
alt={post.image_alt}
class="h-full w-full object-cover"
loading="eager"
/>
</div>
<div class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent" />
</div>
)
}
<div class="markdown-content">
<slot />
</div>
<!-- Add the like button after the content -->
<div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
<ShareButtons url={canonicalURL.toString()} title={post.title} />
</div>
</div>
{
post.updated_date && (
<div class="mt-8 text-sm text-zinc-500 italic dark:text-zinc-400">
Last updated on <FormattedDate date={post.updated_date} />
</div>
)
}
</article>
<slot name="after-article" />
</Layout>
<script>
document.addEventListener('astro:page-load', () => {
// Add smooth reveal animations for content after loading
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text div, .hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p, .hero-text + a'
);
heroElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
// Animate posts with staggered delay
const articles = document.querySelectorAll('article.group');
articles.forEach((article, index) => {
setTimeout(
() => {
article.classList.add('animate-reveal');
},
500 + index * 150
);
});
};
animateContent();
});
</script>
<style>
/* Content reveal animations */
.hero-text h1,
.hero-text div,
.hero-text ~ div,
.hero-text span,
.hero-text p,
.hero-text + a,
article.group {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
/* Hero image styling */
article img:first-of-type {
border-radius: 1rem;
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.1),
0 8px 10px -6px rgba(0, 0, 0, 0.1);
transition: transform 0.3s ease;
}
article img:first-of-type:hover {
transform: scale(1.01);
}
</style>

Some files were not shown because too many files have changed in this diff Show More