merge in new changes
@@ -3,7 +3,7 @@ name: release-image
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 1.*
|
- 2.*
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
3
.gitignore
vendored
@@ -12,10 +12,9 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
# environment variables
|
# environment variables
|
||||||
.env
|
.env
|
||||||
.env.local
|
|
||||||
.env.development
|
|
||||||
.env.production
|
.env.production
|
||||||
|
|
||||||
# macOS-specific files
|
# macOS-specific files
|
||||||
|
1
.npmrc
@@ -1,2 +1,3 @@
|
|||||||
|
registry=https://registry.npmjs.org/
|
||||||
engine-strict=true
|
engine-strict=true
|
||||||
save-exact=true
|
save-exact=true
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
ARG REGISTRY=docker.io
|
ARG REGISTRY=docker.io
|
||||||
FROM ${REGISTRY}/node:22.18.0-alpine3.22 AS base
|
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"
|
LABEL description="Astro based personal website"
|
||||||
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
79
README.md
@@ -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.
|
Personal site used for information about myself and blog.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- 🚀 **Maximum Performance** - Built with Astro.js for lightning-fast static sites
|
- 🐈 Simple And Beautiful
|
||||||
- 🎨 **Minimalist Design** - Clean UI that focuses on content
|
- 🖥️️ Responsive And Light/Dark mode
|
||||||
- 🌓 **Light/Dark Mode** - Smooth theme switching
|
- 🐛 SiteMap & RSS Feed
|
||||||
- 📱 **Responsive** - Perfect experience on all devices
|
- 🐝 Category Support
|
||||||
- ⚡ **SPA Transitions** - Smooth page navigation with transition effects
|
- 🐜 SEO and Responsiveness
|
||||||
- 📝 **Markdown & MDX** - Write posts with Markdown and extend with MDX
|
- 🪲 Markdown And MDX
|
||||||
- 🔍 **SEO Optimized** - Meta tags, Open Graph, and Twitter Cards
|
- 🏂🏾 Page Compression & Image Optimization
|
||||||
- 📊 **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
|
|
||||||
|
|
||||||
## 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
|
## Thanks
|
||||||
# Clone repository
|
|
||||||
git clone https://gitea.alexlebens.dev/alexlebens/site-profile
|
|
||||||
|
|
||||||
# Navigate to project directory
|
Thanks https://github.com/mearashadowfax/ScrewFast, https://github.com/godruoyi/gblog/tree/gblog-template
|
||||||
cd site-profile
|
|
||||||
|
|
||||||
# Install dependencies
|
## License
|
||||||
pnpm install
|
|
||||||
|
|
||||||
```
|
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.
|
||||||
|
|
||||||
### 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
|
|
||||||
```
|
|
||||||
|
@@ -1,8 +1,16 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig, passthroughImageService, sharpImageService } from 'astro/config';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
|
||||||
import react from '@astrojs/react';
|
|
||||||
|
|
||||||
|
import mdx from '@astrojs/mdx';
|
||||||
import node from '@astrojs/node';
|
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 = () => {
|
const getSiteURL = () => {
|
||||||
if (process.env.SITE_URL) {
|
if (process.env.SITE_URL) {
|
||||||
@@ -13,7 +21,71 @@ const getSiteURL = () => {
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: getSiteURL(),
|
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: {
|
plugins: {
|
||||||
'@tailwindcss/postcss': {},
|
'@tailwindcss/postcss': {},
|
||||||
@@ -28,4 +100,13 @@ export default defineConfig({
|
|||||||
adapter: node({
|
adapter: node({
|
||||||
mode: 'standalone',
|
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' : '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
69
package.json
@@ -1,43 +1,84 @@
|
|||||||
{
|
{
|
||||||
"name": "site-profile",
|
"name": "site-profile",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "1.1.1",
|
"version": "2.0.0",
|
||||||
"private": true,
|
"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": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
|
"astro": "astro",
|
||||||
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,md,mdx,astro}\"",
|
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,md,mdx,astro}\"",
|
||||||
"lint": "eslint \"src/**/*.{js,ts,jsx,tsx,astro}\"",
|
"lint": "eslint \"src/**/*.{js,ts,jsx,tsx,astro}\"",
|
||||||
"lint:fix": "eslint --fix \"src/**/*.{js,ts,jsx,tsx,astro}\"",
|
"lint:fix": "eslint --fix \"src/**/*.{js,ts,jsx,tsx,astro}\""
|
||||||
"astro": "astro"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrojs/check": "^0.9.4",
|
||||||
"@astrojs/mdx": "^4.3.3",
|
"@astrojs/mdx": "^4.3.3",
|
||||||
"@astrojs/node": "^9.3.3",
|
"@astrojs/node": "^9.3.3",
|
||||||
|
"@astrojs/partytown": "^2.1.4",
|
||||||
"@astrojs/react": "^4.3.0",
|
"@astrojs/react": "^4.3.0",
|
||||||
"@astrojs/rss": "^4.0.12",
|
"@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/postcss": "^4.1.8",
|
||||||
"@tailwindcss/vite": "^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": "^5.12.8",
|
||||||
|
"astro-compressor": "^0.4.1",
|
||||||
|
"astro-icon": "^1.1.5",
|
||||||
"framer-motion": "^12.16.0",
|
"framer-motion": "^12.16.0",
|
||||||
|
"mdast-util-to-string": "^4.0.0",
|
||||||
|
"preline": "^3.1.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hotkeys-hook": "^5.1.0",
|
"reading-time": "^1.5.0",
|
||||||
"react-icons": "^5.5.0",
|
"rehype-pretty-code": "^0.14.1",
|
||||||
"sanitize-html": "^2.17.0",
|
"sharp": "^0.34.3",
|
||||||
"tailwindcss": "^4.1.8"
|
"sharp-ico": "^0.1.5",
|
||||||
|
"shiki": "^3.2.2",
|
||||||
|
"tailwindcss": "^4.1.11",
|
||||||
|
"ultrahtml": "^1.5.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@eslint-react/eslint-plugin": "^1.52.3",
|
||||||
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@typescript-eslint/parser": "8.38.0",
|
"astro-icon": "^1.1.5",
|
||||||
"eslint": "9.32.0",
|
"eslint": "^9.32.0",
|
||||||
"eslint-config-prettier": "10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-astro": "1.3.1",
|
"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": "^3.5.3",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"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"
|
"typescript-eslint": "8.38.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
8691
pnpm-lock.yaml
generated
@@ -1,5 +0,0 @@
|
|||||||
.DS_Store
|
|
||||||
.astro
|
|
||||||
.vscode
|
|
||||||
node_modules
|
|
||||||
dist
|
|
@@ -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
|
|
@@ -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 }}
|
|
@@ -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
|
|
25
site-profile-new/.gitignore
vendored
@@ -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
|
|
@@ -1,3 +0,0 @@
|
|||||||
registry=https://registry.npmjs.org/
|
|
||||||
engine-strict=true
|
|
||||||
save-exact=true
|
|
@@ -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"]
|
|
@@ -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.
|
|
@@ -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' : '',
|
|
||||||
},
|
|
||||||
});
|
|
@@ -1,11 +0,0 @@
|
|||||||
import eslintPluginAstro from 'eslint-plugin-astro';
|
|
||||||
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
|
||||||
|
|
||||||
export default [
|
|
||||||
...eslintPluginAstro.configs.recommended,
|
|
||||||
eslintConfigPrettier,
|
|
||||||
{
|
|
||||||
rules: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
14126
site-profile-new/pnpm-lock.yaml
generated
@@ -1,8 +0,0 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
|
||||||
const config = {
|
|
||||||
plugins: {
|
|
||||||
'@tailwindcss/postcss': {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
export default config;
|
|
@@ -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;
|
|
@@ -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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@@ -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">
|
|
||||||
© {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>
|
|
2
site-profile-new/src/env.d.ts
vendored
@@ -1,2 +0,0 @@
|
|||||||
/// <reference types="astro/client" />
|
|
||||||
/// <reference path="../.astro/types.d.ts" />
|
|
@@ -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>
|
|
@@ -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;
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
@@ -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;
|
|
||||||
}
|
|
@@ -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/*"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
10
site-profile.code-workspace
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
|
}
|
@@ -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>
|
|
@@ -1,130 +1,58 @@
|
|||||||
---
|
---
|
||||||
import directus from '../lib/directus';
|
|
||||||
import { readSingleton } from '@directus/sdk';
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
const global = await directus.request(readSingleton('global'));
|
import directus from '@lib/directus';
|
||||||
const links = await directus.request(readSingleton('links'));
|
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 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
|
<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"
|
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="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">
|
<div class="grid grid-cols-1 gap-10 md:grid-cols-12">
|
||||||
<!-- Brand section -->
|
<!-- Brand section -->
|
||||||
<div class="col-span-1 md:col-span-3">
|
<div class="col-span-1 md:col-span-3">
|
||||||
<a href="/" class="group inline-block">
|
<a href="/" class="group inline-block">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="mx-auto aspect-square overflow-hidden rounded-lg">
|
<div class="mx-auto aspect-square overflow-hidden rounded-lg">
|
||||||
<img
|
<BrandLogo class="max-h-[40px] max-w-[40px] rounded-full" />
|
||||||
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>
|
</div>
|
||||||
|
|
||||||
<span
|
<span class="ml-3 text-xl font-bold text-neutral-800 dark:text-neutral-200">
|
||||||
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
|
|
||||||
>
|
|
||||||
Blog
|
Blog
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<p
|
<p class="mt-4 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
|
||||||
class="theme-transition-color mt-4 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400"
|
A description of something.
|
||||||
>
|
|
||||||
{global.description}
|
|
||||||
</p>
|
</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>
|
</div>
|
||||||
|
<!-- Left links -->
|
||||||
<!-- Quick links -->
|
<div class="col-span-1 md:col-span-2">
|
||||||
<div class="col-span-1 md:col-span-3">
|
|
||||||
<h3
|
<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>
|
</h3>
|
||||||
<ul class="mt-4 space-y-3">
|
<ul class="mt-4 space-y-3">
|
||||||
{
|
{
|
||||||
navLinks.map((link) => (
|
NavigationLinks.map((link) => (
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
href={link.href}
|
href={link.url}
|
||||||
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"
|
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 inline-block overflow-hidden">
|
||||||
<span class="relative z-10">{link.text}</span>
|
<span class="relative z-10">{link.name}</span>
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -132,30 +60,63 @@ const socialLinks = [
|
|||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Bottom section -->
|
<!-- 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">
|
<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">
|
||||||
© {currentYear} All rights reserved.
|
© {currentYear} All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
|
<span class="text-xs text-neutral-500 dark:text-neutral-400">Built with </span>
|
||||||
>Built with
|
|
||||||
</span>
|
|
||||||
<a
|
<a
|
||||||
href="https://astro.build"
|
href="https://astro.build"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="group inline-flex items-center text-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
|
<svg class="mr-1 h-4 w-4 text-[#FF5D01]" viewBox="0 0 36 36" fill="none">
|
||||||
class="mr-1 h-4 w-4 text-[#FF5D01] group-hover:animate-pulse"
|
|
||||||
viewBox="0 0 36 36"
|
|
||||||
fill="none"
|
|
||||||
>
|
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
clip-rule="evenodd"
|
clip-rule="evenodd"
|
||||||
@@ -181,67 +142,3 @@ const socialLinks = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</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>
|
|
||||||
|
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@@ -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>
|
|
@@ -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>
|
|
@@ -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>
|
|
||||||
)
|
|
||||||
}
|
|
@@ -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
@@ -1,3 +1,8 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
/// <reference path="../.astro/types.d.ts" />
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
/// <reference types="astro/client" />
|
/// <reference types="astro/client" />
|
||||||
/// <reference types="astro/content" />
|
/// <reference types="astro/content" />
|
||||||
|
=======
|
||||||
|
/// <reference types="astro/client" />
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
|
>>>>>>> 184f0c7 (fix path)
|
||||||
|
Before Width: | Height: | Size: 11 MiB After Width: | Height: | Size: 11 MiB |
Before Width: | Height: | Size: 13 MiB After Width: | Height: | Size: 13 MiB |
Before Width: | Height: | Size: 7.8 MiB After Width: | Height: | Size: 7.8 MiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.9 KiB |
Before Width: | Height: | Size: 6.8 MiB After Width: | Height: | Size: 6.8 MiB |
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 76 KiB |
@@ -1,17 +1,93 @@
|
|||||||
---
|
---
|
||||||
import Layout from './Layout.astro';
|
import { ClientRouter } from 'astro:transitions';
|
||||||
|
|
||||||
import directus from '../lib/directus';
|
|
||||||
import { readSingleton } from '@directus/sdk';
|
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 {
|
import '@styles/global.css';
|
||||||
title: string;
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
description?: 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}>
|
<html lang={lang}>
|
||||||
<slot />
|
<head>
|
||||||
</Layout>
|
<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>
|
||||||
|
@@ -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>
|
|