Compare commits
204 Commits
0.8.10
...
renovate/a
Author | SHA1 | Date | |
---|---|---|---|
a0c04c7930
|
|||
b58cbdbe0a | |||
49e2376dbf | |||
6b1eaa439a | |||
f92f911360 | |||
1cdbbd4a11 | |||
da7c5c4a58 | |||
931d1009ed | |||
43ff986963 | |||
b9d85a5520 | |||
9836b40531 | |||
ea1c3d9f1a | |||
28f73be784 | |||
284f30c392 | |||
9e4a2d681b | |||
c8e250c5b2 | |||
58f05178a4
|
|||
b8966e2b88 | |||
3f6563a0d3
|
|||
4840d15101 | |||
f2cb98888a
|
|||
9d1402ee82 | |||
741338ae9f
|
|||
89d8b025d3 | |||
5e74f8b01e
|
|||
bf4835e797 | |||
fc766599e1 | |||
465bda1859
|
|||
19d2558436
|
|||
2f797ca614
|
|||
99e451a934
|
|||
1dc4ccfbc6 | |||
a484feb7cd | |||
93d11dca17 | |||
3eacf17f61 | |||
12ffcc4d72 | |||
060400183f
|
|||
31ec9908e6
|
|||
4180a2eceb | |||
fdef90e636
|
|||
c369651a70
|
|||
75fd981f10 | |||
80a4aee41c | |||
9e84de0a5a | |||
64140cce6b | |||
0733fe6a06 | |||
0f5c015932
|
|||
dc17aeb3d5 | |||
a852f22409
|
|||
130a3866bc | |||
2fb0542d36
|
|||
8a2be36f17 | |||
266d25e0f2
|
|||
34dbe6d809 | |||
3c654e19e1
|
|||
2a0142ee83
|
|||
7836f49828 | |||
25280a239c | |||
c56dc99e72 | |||
48b7a13729 | |||
ac026b0264 | |||
5332854856 | |||
2e0c2f3de5 | |||
88d510b06f | |||
7843378503 | |||
75016fdb4f
|
|||
4d74f74ab2 | |||
2c1b7f577d
|
|||
0e79b32012 | |||
c1ef2d2ba2
|
|||
020c709b43 | |||
9f346ee156 | |||
e820e4f163 | |||
796926316e | |||
bf8578045e
|
|||
f16af9a98d
|
|||
ec45ad29ed | |||
17afce6710 | |||
f83fe98b38 | |||
2f244761ed | |||
649bf4482b | |||
2028e2247e | |||
fcae7676c6 | |||
cc16b5435a | |||
27b5e6a36b | |||
bcb91972a1 | |||
b11666decb
|
|||
a947a05041 | |||
297c573281
|
|||
9093594973
|
|||
77ce0a1182
|
|||
799e6b6090 | |||
735e4b4877
|
|||
3e12a8647d | |||
e07210638e | |||
22d5b50f73 | |||
40acf8f34a | |||
543516baba | |||
e985f905f2 | |||
e1f09ca4ec | |||
0c09eb38e9 | |||
95eeb44e4f
|
|||
d47d67572e | |||
fa4841948a
|
|||
71e2b0185b | |||
7f9fb4d2b9 | |||
8420c8dd58 | |||
fa6ed18edb | |||
30860fce1e | |||
b479e0e22c | |||
cf01ebcd3c | |||
df8ccf81c2
|
|||
073911c1b9 | |||
3eeea3dd8f | |||
43fea76778
|
|||
d64df6473a | |||
63a6a00817
|
|||
54759056b3 | |||
3cc9762e0d | |||
ef757c4a14
|
|||
176f92bf67 | |||
09d411dd68 | |||
54acfcb24d
|
|||
6f3b631862
|
|||
18cd240a9b | |||
bb4fe8ef37
|
|||
e0e3c1f61a
|
|||
0b5c6ae999 | |||
a20ba4ab43 | |||
550e7dfe52 | |||
03174cfb9d
|
|||
da50c1928c
|
|||
f1d1fe979e | |||
4d6019d0b0 | |||
7dd302b3d4
|
|||
8a8f2a6216
|
|||
97775f1ceb | |||
0a437a26f1 | |||
ba67b4d0e4 | |||
0bcfa9bed4
|
|||
ada95481f7
|
|||
7c9f4acc00
|
|||
0b7b87580a
|
|||
08f076e566
|
|||
26c27b9353 | |||
ce8b3a2e19 | |||
6d34c0d407 | |||
63607bbca3 | |||
745d2553a0
|
|||
8a19559cc7 | |||
42854db0fb
|
|||
7b72e3849b | |||
6a8dbb0c7c | |||
91fdf5a83f
|
|||
073f3a7916
|
|||
38202841ca | |||
0492922cce
|
|||
a17500835b | |||
2f8b97208c | |||
d6c30d5e5b
|
|||
a7ea9db3aa
|
|||
9134e78e8a | |||
2ca7d6705d | |||
5722e8c7a1 | |||
e39fd2acb8 | |||
0313fd54bc | |||
dbb0f6d7ff | |||
20669d9766 | |||
6b2e6353d1 | |||
6d112b52df | |||
ff17af604f | |||
32ea0989d7 | |||
e4ab7d134c | |||
5fad13655c | |||
8614d40a64 | |||
8c417b93b3 | |||
1d9519831b | |||
fa57f2e93f | |||
9e01002d4e | |||
cb52c169a3 | |||
3017668cd2 | |||
1972b3bc19 | |||
af77f90a49 | |||
bdda29f369 | |||
644c5fcd6a | |||
bafd8158d3 | |||
4d9c1a3e8c | |||
4a4233ac62 | |||
c71957348d | |||
400bf16dd9 | |||
85535614a0 | |||
38fcbb635b | |||
b1e57c3f17 | |||
e22a1985be | |||
70b0b86944 | |||
ba36de8e36 | |||
d2e44fe046 | |||
36ec797d3b | |||
086d98ba50 | |||
8a05fa4d96 | |||
dbbf886de9 | |||
ae7e21eb82 | |||
ce6f476e8f | |||
0ca6be1d91 |
@@ -1,6 +1,5 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.astro
|
.astro
|
||||||
.gitea
|
|
||||||
.vscode
|
.vscode
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
@@ -3,7 +3,7 @@ name: release-image
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 0.*
|
- 2.*
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Login to Registry
|
- name: Login to Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -42,7 +42,7 @@ jobs:
|
|||||||
namespace=gitea
|
namespace=gitea
|
||||||
qemu.install=true
|
qemu.install=true
|
||||||
buildkitd-config-inline: |
|
buildkitd-config-inline: |
|
||||||
[registry."hub.docker.com"]
|
[registry."docker.io"]
|
||||||
mirrors = ["harbor.alexlebens.net/proxy-hub.docker/"]
|
mirrors = ["harbor.alexlebens.net/proxy-hub.docker/"]
|
||||||
|
|
||||||
- name: Available Platforms
|
- name: Available Platforms
|
||||||
@@ -75,7 +75,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
url: '${{ secrets.NTFY_URL }}'
|
url: '${{ secrets.NTFY_URL }}'
|
||||||
topic: '${{ secrets.NTFY_TOPIC }}'
|
topic: '${{ secrets.NTFY_TOPIC }}'
|
||||||
title: "Gitea Action"
|
title: 'Gitea Action'
|
||||||
priority: 3
|
priority: 3
|
||||||
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
|
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
|
||||||
tags: action,successfully,completed
|
tags: action,successfully,completed
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
url: '${{ secrets.NTFY_URL }}'
|
url: '${{ secrets.NTFY_URL }}'
|
||||||
topic: '${{ secrets.NTFY_TOPIC }}'
|
topic: '${{ secrets.NTFY_TOPIC }}'
|
||||||
title: "Gitea Action"
|
title: 'Gitea Action'
|
||||||
priority: 4
|
priority: 4
|
||||||
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
|
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
|
||||||
tags: action,failed
|
tags: action,failed
|
||||||
|
@@ -13,18 +13,17 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
renovate:
|
renovate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: ghcr.io/renovatebot/renovate:40
|
container: ghcr.io/renovatebot/renovate:41
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Renovate
|
- name: Renovate
|
||||||
run: renovate
|
run: renovate
|
||||||
env:
|
env:
|
||||||
RENOVATE_PLATFORM: gitea
|
RENOVATE_PLATFORM: gitea
|
||||||
RENOVATE_AUTODISCOVER: true
|
|
||||||
RENOVATE_ONBOARDING: true
|
|
||||||
RENOVATE_ENDPOINT: ${{ vars.INSTANCE_URL }}
|
RENOVATE_ENDPOINT: ${{ vars.INSTANCE_URL }}
|
||||||
|
RENOVATE_REPOSITORIES: alexlebens/site-profile
|
||||||
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net>
|
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net>
|
||||||
LOG_LEVEL: info
|
LOG_LEVEL: info
|
||||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
|
@@ -1,75 +0,0 @@
|
|||||||
name: tag-old-issues
|
|
||||||
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "@daily"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
tag-old-issues:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Tag Old Issues
|
|
||||||
env:
|
|
||||||
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
|
|
||||||
INSTANCE_URL: ${{ vars.INSTANCE_URL }}
|
|
||||||
REPO_OWNER: ${{ github.repository_owner }}
|
|
||||||
REPO_NAME: ${{ github.repository_name }}
|
|
||||||
TAG_NAME: 'stale'
|
|
||||||
DAYS_OLD: 3
|
|
||||||
EXCLUDE_TAG_NAME: ''
|
|
||||||
REQUIRED_TAG: 'automerge'
|
|
||||||
run: |
|
|
||||||
# Install necessary tools
|
|
||||||
apt-get update && apt-get install -y jq curl
|
|
||||||
|
|
||||||
# --- Conditionally build the API URL ---
|
|
||||||
API_URL="${GITEA_INSTANCE_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues?state=open"
|
|
||||||
if [[ -n "${REQUIRED_TAG}" ]]; then
|
|
||||||
echo "Filtering for issues with the required tag: ${REQUIRED_TAG}"
|
|
||||||
# URL-encode the tag to handle special characters
|
|
||||||
ENCODED_TAG=$(jq -s -R -r @uri <<< "${REQUIRED_TAG}")
|
|
||||||
API_URL="${API_URL}&labels=${ENCODED_TAG}"
|
|
||||||
else
|
|
||||||
echo "No required tag specified. Checking all open issues."
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Fetch issues using the constructed URL
|
|
||||||
ISSUES=$(curl -s -X GET \
|
|
||||||
-H "Authorization: token ${BOT_TOKEN}" \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
"${API_URL}")
|
|
||||||
|
|
||||||
# Calculate the date ${DAYS_OLD} days ago in ISO 8601 format
|
|
||||||
OLDER_THAN_DATE=$(date -d "-${DAYS_OLD} days" -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
||||||
|
|
||||||
# Filter issues older than the specified date and without the exclusion tag
|
|
||||||
echo "$ISSUES" | jq -c '.[] | select(.created_at < "'"$OLDER_THAN_DATE"'")' | while read -r issue; do
|
|
||||||
ISSUE_NUMBER=$(echo "$issue" | jq -r '.number')
|
|
||||||
LABELS=$(echo "$issue" | jq -r '.labels[].name')
|
|
||||||
|
|
||||||
# Check if the issue has the exclusion tag
|
|
||||||
if ! echo "$LABELS" | grep -q -w "${EXCLUDE_TAG_NAME}"; then
|
|
||||||
echo "Tagging issue #${ISSUE_NUMBER} as ${TAG_NAME}"
|
|
||||||
|
|
||||||
# Get existing labels for the issue
|
|
||||||
EXISTING_LABELS=$(curl -s -X GET \
|
|
||||||
-H "Authorization: token ${BOT_TOKEN}" \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
"${INSTANCE_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/labels" | jq -r '.[].name')
|
|
||||||
|
|
||||||
# Add the new tag to the list of existing labels
|
|
||||||
NEW_LABELS=$(echo -e "${EXISTING_LABELS}\n${TAG_NAME}" | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
|
|
||||||
|
|
||||||
# Update the issue with the new set of labels
|
|
||||||
curl -s -X PUT \
|
|
||||||
-H "Authorization: token ${BOT_TOKEN}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"labels\": $(echo "$NEW_LABELS" | jq -r 'map(select(. != ""))')}" \
|
|
||||||
"${INSTANCE_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/labels"
|
|
||||||
else
|
|
||||||
echo "Skipping issue #${ISSUE_NUMBER} because it has the '${EXCLUDE_TAG_NAME}' tag."
|
|
||||||
fi
|
|
||||||
done
|
|
37
.gitea/workflows/test-build.yaml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: test-build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- 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
|
4
.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
|
||||||
@@ -24,4 +23,3 @@ pnpm-debug.log*
|
|||||||
# ide
|
# ide
|
||||||
.vscode/
|
.vscode/
|
||||||
site-profile.code-workspace
|
site-profile.code-workspace
|
||||||
.pre-commit-config.yaml
|
|
||||||
|
2
.npmrc
@@ -1,3 +1,3 @@
|
|||||||
|
registry=https://registry.npmjs.org/
|
||||||
engine-strict=true
|
engine-strict=true
|
||||||
save-exact=true
|
save-exact=true
|
||||||
|
|
||||||
|
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/src/components/ui/sections/Experience.astro
|
17
.prettierrc
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
4
.vscode/extensions.json
vendored
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["astro-build.astro-vscode"],
|
|
||||||
"unwantedRecommendations": []
|
|
||||||
}
|
|
11
.vscode/launch.json
vendored
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"command": "./node_modules/.bin/astro dev",
|
|
||||||
"name": "Development server",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "node-terminal"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
@@ -1,7 +1,7 @@
|
|||||||
ARG REGISTRY=hub.docker.com
|
ARG REGISTRY=docker.io
|
||||||
FROM ${REGISTRY}/node:22.16.0-alpine3.22 AS base
|
FROM ${REGISTRY}/node:22.18.0-alpine3.22 AS base
|
||||||
|
|
||||||
LABEL version="0.8.10"
|
LABEL version="2.0.5"
|
||||||
LABEL description="Astro based personal website"
|
LABEL description="Astro based personal website"
|
||||||
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
# MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Lê Vĩnh Khang
|
Copyright (c) 2025 Alex Lebens
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
83
README.md
@@ -1,78 +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 16+ and pnpm/yarn
|
- `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 astro-blog
|
|
||||||
|
|
||||||
# Install dependencies
|
## License
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# Create .env file from template
|
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.
|
||||||
cp .env.example .env
|
|
||||||
|
|
||||||
# Edit .env with your information
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start development server
|
|
||||||
pnpm run dev
|
|
||||||
|
|
||||||
# Open browser at http://localhost:4321
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create production build
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# Preview production build
|
|
||||||
pnpm run 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,17 +21,92 @@ 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': {},
|
||||||
},
|
},
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()]
|
plugins: [tailwindcss()],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
output: 'static',
|
||||||
|
|
||||||
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' : '',
|
||||||
|
},
|
||||||
});
|
});
|
11
eslint.config.mjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import eslintPluginAstro from 'eslint-plugin-astro';
|
||||||
|
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...eslintPluginAstro.configs.recommended,
|
||||||
|
eslintConfigPrettier,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
@@ -1,58 +0,0 @@
|
|||||||
import { createDirectus, rest } from '@directus/sdk';
|
|
||||||
|
|
||||||
type Global = {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
name: string;
|
|
||||||
initals: string;
|
|
||||||
tagline: string;
|
|
||||||
email: string;
|
|
||||||
portrait: string;
|
|
||||||
portrait_alt: string;
|
|
||||||
about: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type About = {
|
|
||||||
background: string;
|
|
||||||
experience: string;
|
|
||||||
education: string;
|
|
||||||
certifications: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Links = {
|
|
||||||
github: string;
|
|
||||||
linkedin: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Skill = {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
icon: string;
|
|
||||||
level: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Post = {
|
|
||||||
slug: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
content: string;
|
|
||||||
image: string;
|
|
||||||
image_alt: string;
|
|
||||||
published_date: Date;
|
|
||||||
updated_date: Date;
|
|
||||||
tags: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Schema = {
|
|
||||||
global: Global;
|
|
||||||
about: About;
|
|
||||||
links: Links;
|
|
||||||
skills: Skill[];
|
|
||||||
posts: Post[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const directus = createDirectus<Schema>(
|
|
||||||
process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'
|
|
||||||
).with(rest());
|
|
||||||
|
|
||||||
export default directus;
|
|
79
package.json
@@ -1,39 +1,84 @@
|
|||||||
{
|
{
|
||||||
"name": "site-profile",
|
"name": "site-profile",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.8.10",
|
"version": "2.0.5",
|
||||||
"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",
|
||||||
"format": "prettier . --write",
|
"astro": "astro",
|
||||||
"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": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^4.3.0",
|
"@astrojs/check": "^0.9.4",
|
||||||
"@astrojs/node": "^9.2.2",
|
"@astrojs/mdx": "^4.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",
|
||||||
"@astrojs/sitemap": "^3.4.1",
|
"@astrojs/sitemap": "^3.4.2",
|
||||||
"@directus/sdk": "^19.1.0",
|
"@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.2.0",
|
||||||
|
"@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",
|
||||||
"astro": "^5.9.2",
|
"@directus/sdk": "^20.0.0",
|
||||||
"form-data": "4.0.3",
|
"@types/react": "^19.0.0",
|
||||||
|
"@types/unist": "^3.0.2",
|
||||||
|
"astro": "^5.12.8",
|
||||||
|
"astro-compressor": "^1.0.0",
|
||||||
|
"astro-icon": "^1.1.5",
|
||||||
"framer-motion": "^12.16.0",
|
"framer-motion": "^12.16.0",
|
||||||
"postcss-preset-env": "^10.2.1",
|
"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",
|
||||||
|
"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": "^3.5.3",
|
||||||
"prettier-plugin-astro": "^0.14.0",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.0"
|
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||||
|
"timeago.js": "^4.0.2",
|
||||||
|
"typescript": "5.9.2",
|
||||||
|
"typescript-eslint": "8.39.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
10240
pnpm-lock.yaml
generated
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- swup
|
@@ -1,13 +1,7 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
|
||||||
'postcss-preset-env': {
|
|
||||||
features: {
|
|
||||||
'nesting-rules': false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
23
prettier.config.mjs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/** @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;
|
Before Width: | Height: | Size: 713 B After Width: | Height: | Size: 9.9 KiB |
352
public/vendor/preline/collapse2.1.0.min.js
vendored
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
/**
|
||||||
|
* Skipped minification because the original files appears to be already minified.
|
||||||
|
* Original file: /npm/@preline/collapse@2.1.0/index.js
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
!(function (t, e) {
|
||||||
|
if ('object' == typeof exports && 'object' == typeof module) module.exports = e();
|
||||||
|
else if ('function' == typeof define && define.amd) define([], e);
|
||||||
|
else {
|
||||||
|
var n = e();
|
||||||
|
for (var o in n) ('object' == typeof exports ? exports : t)[o] = n[o];
|
||||||
|
}
|
||||||
|
})(self, () =>
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
var t = {
|
||||||
|
737: (t, e) => {
|
||||||
|
/*
|
||||||
|
* HSBasePlugin
|
||||||
|
* @version: 2.1.0
|
||||||
|
* @author: HTMLStream
|
||||||
|
* @license: Licensed under MIT (https://preline.co/docs/license.html)
|
||||||
|
* Copyright 2023 HTMLStream
|
||||||
|
*/
|
||||||
|
Object.defineProperty(e, '__esModule', { value: !0 });
|
||||||
|
var n = (function () {
|
||||||
|
function t(t, e, n) {
|
||||||
|
((this.el = t),
|
||||||
|
(this.options = e),
|
||||||
|
(this.events = n),
|
||||||
|
(this.el = t),
|
||||||
|
(this.options = e),
|
||||||
|
(this.events = {}));
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
(t.prototype.createCollection = function (t, e) {
|
||||||
|
var n;
|
||||||
|
t.push({
|
||||||
|
id:
|
||||||
|
(null === (n = null == e ? void 0 : e.el) || void 0 === n ? void 0 : n.id) ||
|
||||||
|
t.length + 1,
|
||||||
|
element: e,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
(t.prototype.fireEvent = function (t, e) {
|
||||||
|
if ((void 0 === e && (e = null), this.events.hasOwnProperty(t)))
|
||||||
|
return this.events[t](e);
|
||||||
|
}),
|
||||||
|
(t.prototype.on = function (t, e) {
|
||||||
|
this.events[t] = e;
|
||||||
|
}),
|
||||||
|
t
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
e.default = n;
|
||||||
|
},
|
||||||
|
652: function (t, e, n) {
|
||||||
|
/*
|
||||||
|
* HSCollapse
|
||||||
|
* @version: 2.1.0
|
||||||
|
* @author: HTMLStream
|
||||||
|
* @license: Licensed under MIT (https://preline.co/docs/license.html)
|
||||||
|
* Copyright 2023 HTMLStream
|
||||||
|
*/
|
||||||
|
var o,
|
||||||
|
i =
|
||||||
|
(this && this.__extends) ||
|
||||||
|
((o = function (t, e) {
|
||||||
|
return (
|
||||||
|
(o =
|
||||||
|
Object.setPrototypeOf ||
|
||||||
|
({ __proto__: [] } instanceof Array &&
|
||||||
|
function (t, e) {
|
||||||
|
t.__proto__ = e;
|
||||||
|
}) ||
|
||||||
|
function (t, e) {
|
||||||
|
for (var n in e) Object.prototype.hasOwnProperty.call(e, n) && (t[n] = e[n]);
|
||||||
|
}),
|
||||||
|
o(t, e)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
function (t, e) {
|
||||||
|
if ('function' != typeof e && null !== e)
|
||||||
|
throw new TypeError(
|
||||||
|
'Class extends value ' + String(e) + ' is not a constructor or null'
|
||||||
|
);
|
||||||
|
function n() {
|
||||||
|
this.constructor = t;
|
||||||
|
}
|
||||||
|
(o(t, e),
|
||||||
|
(t.prototype =
|
||||||
|
null === e ? Object.create(e) : ((n.prototype = e.prototype), new n())));
|
||||||
|
});
|
||||||
|
Object.defineProperty(e, '__esModule', { value: !0 });
|
||||||
|
var s = n(969),
|
||||||
|
r = (function (t) {
|
||||||
|
function e(e, n, o) {
|
||||||
|
var i = t.call(this, e, n, o) || this;
|
||||||
|
return (
|
||||||
|
(i.contentId = i.el.dataset.hsCollapse),
|
||||||
|
(i.content = document.querySelector(i.contentId)),
|
||||||
|
(i.animationInProcess = !1),
|
||||||
|
i.content && i.init(),
|
||||||
|
i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
i(e, t),
|
||||||
|
(e.prototype.init = function () {
|
||||||
|
var t = this;
|
||||||
|
(this.createCollection(window.$hsCollapseCollection, this),
|
||||||
|
this.el.addEventListener('click', function () {
|
||||||
|
t.content.classList.contains('open') ? t.hide() : t.show();
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
(e.prototype.hideAllMegaMenuItems = function () {
|
||||||
|
this.content
|
||||||
|
.querySelectorAll('.hs-mega-menu-content.block')
|
||||||
|
.forEach(function (t) {
|
||||||
|
(t.classList.remove('block'), t.classList.add('hidden'));
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
(e.prototype.show = function () {
|
||||||
|
var t = this;
|
||||||
|
if (this.animationInProcess || this.el.classList.contains('open')) return !1;
|
||||||
|
((this.animationInProcess = !0),
|
||||||
|
this.el.classList.add('open'),
|
||||||
|
this.content.classList.add('open'),
|
||||||
|
this.content.classList.remove('hidden'),
|
||||||
|
(this.content.style.height = '0'),
|
||||||
|
setTimeout(function () {
|
||||||
|
((t.content.style.height = ''.concat(t.content.scrollHeight, 'px')),
|
||||||
|
t.fireEvent('beforeOpen', t.el),
|
||||||
|
(0, s.dispatch)('beforeOpen.hs.collapse', t.el, t.el));
|
||||||
|
}),
|
||||||
|
(0, s.afterTransition)(this.content, function () {
|
||||||
|
((t.content.style.height = ''),
|
||||||
|
t.fireEvent('open', t.el),
|
||||||
|
(0, s.dispatch)('open.hs.collapse', t.el, t.el),
|
||||||
|
(t.animationInProcess = !1));
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
(e.prototype.hide = function () {
|
||||||
|
var t = this;
|
||||||
|
if (this.animationInProcess || !this.el.classList.contains('open')) return !1;
|
||||||
|
((this.animationInProcess = !0),
|
||||||
|
this.el.classList.remove('open'),
|
||||||
|
(this.content.style.height = ''.concat(this.content.scrollHeight, 'px')),
|
||||||
|
setTimeout(function () {
|
||||||
|
t.content.style.height = '0';
|
||||||
|
}),
|
||||||
|
this.content.classList.remove('open'),
|
||||||
|
(0, s.afterTransition)(this.content, function () {
|
||||||
|
(t.content.classList.add('hidden'),
|
||||||
|
(t.content.style.height = ''),
|
||||||
|
t.fireEvent('hide', t.el),
|
||||||
|
(0, s.dispatch)('hide.hs.collapse', t.el, t.el),
|
||||||
|
(t.animationInProcess = !1));
|
||||||
|
}),
|
||||||
|
this.content.querySelectorAll('.hs-mega-menu-content.block').length &&
|
||||||
|
this.hideAllMegaMenuItems());
|
||||||
|
}),
|
||||||
|
(e.getInstance = function (t, e) {
|
||||||
|
void 0 === e && (e = !1);
|
||||||
|
var n = window.$hsCollapseCollection.find(function (e) {
|
||||||
|
return e.element.el === ('string' == typeof t ? document.querySelector(t) : t);
|
||||||
|
});
|
||||||
|
return n ? (e ? n : n.element.el) : null;
|
||||||
|
}),
|
||||||
|
(e.autoInit = function () {
|
||||||
|
(window.$hsCollapseCollection || (window.$hsCollapseCollection = []),
|
||||||
|
document
|
||||||
|
.querySelectorAll('.hs-collapse-toggle:not(.--prevent-on-load-init)')
|
||||||
|
.forEach(function (t) {
|
||||||
|
window.$hsCollapseCollection.find(function (e) {
|
||||||
|
var n;
|
||||||
|
return (
|
||||||
|
(null === (n = null == e ? void 0 : e.element) || void 0 === n
|
||||||
|
? void 0
|
||||||
|
: n.el) === t
|
||||||
|
);
|
||||||
|
}) || new e(t);
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
(e.show = function (t) {
|
||||||
|
var e = window.$hsCollapseCollection.find(function (e) {
|
||||||
|
return e.element.el === ('string' == typeof t ? document.querySelector(t) : t);
|
||||||
|
});
|
||||||
|
e && e.element.content.classList.contains('hidden') && e.element.show();
|
||||||
|
}),
|
||||||
|
(e.hide = function (t) {
|
||||||
|
var e = window.$hsCollapseCollection.find(function (e) {
|
||||||
|
return e.element.el === ('string' == typeof t ? document.querySelector(t) : t);
|
||||||
|
});
|
||||||
|
e && !e.element.content.classList.contains('hidden') && e.element.hide();
|
||||||
|
}),
|
||||||
|
(e.on = function (t, e, n) {
|
||||||
|
var o = window.$hsCollapseCollection.find(function (t) {
|
||||||
|
return t.element.el === ('string' == typeof e ? document.querySelector(e) : e);
|
||||||
|
});
|
||||||
|
o && (o.element.events[t] = n);
|
||||||
|
}),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
})(n(737).default);
|
||||||
|
(window.addEventListener('load', function () {
|
||||||
|
r.autoInit();
|
||||||
|
}),
|
||||||
|
'undefined' != typeof window && (window.HSCollapse = r),
|
||||||
|
(e.default = r));
|
||||||
|
},
|
||||||
|
969: function (t, e) {
|
||||||
|
var n = this;
|
||||||
|
(Object.defineProperty(e, '__esModule', { value: !0 }),
|
||||||
|
(e.menuSearchHistory =
|
||||||
|
e.classToClassList =
|
||||||
|
e.htmlToElement =
|
||||||
|
e.afterTransition =
|
||||||
|
e.dispatch =
|
||||||
|
e.debounce =
|
||||||
|
e.isFormElement =
|
||||||
|
e.isParentOrElementHidden =
|
||||||
|
e.isEnoughSpace =
|
||||||
|
e.isIpadOS =
|
||||||
|
e.isIOS =
|
||||||
|
e.getClassPropertyAlt =
|
||||||
|
e.getClassProperty =
|
||||||
|
e.stringToBoolean =
|
||||||
|
void 0));
|
||||||
|
e.stringToBoolean = function (t) {
|
||||||
|
return 'true' === t;
|
||||||
|
};
|
||||||
|
e.getClassProperty = function (t, e, n) {
|
||||||
|
return (
|
||||||
|
void 0 === n && (n = ''),
|
||||||
|
(window.getComputedStyle(t).getPropertyValue(e) || n).replace(' ', '')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
e.getClassPropertyAlt = function (t, e, n) {
|
||||||
|
void 0 === n && (n = '');
|
||||||
|
var o = '';
|
||||||
|
return (
|
||||||
|
t.classList.forEach(function (t) {
|
||||||
|
t.includes(e) && (o = t);
|
||||||
|
}),
|
||||||
|
o.match(/:(.*)]/) ? o.match(/:(.*)]/)[1] : n
|
||||||
|
);
|
||||||
|
};
|
||||||
|
e.isIOS = function () {
|
||||||
|
return (
|
||||||
|
!!/iPad|iPhone|iPod/.test(navigator.platform) ||
|
||||||
|
(navigator.maxTouchPoints &&
|
||||||
|
navigator.maxTouchPoints > 2 &&
|
||||||
|
/MacIntel/.test(navigator.platform))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
e.isIpadOS = function () {
|
||||||
|
return (
|
||||||
|
navigator.maxTouchPoints &&
|
||||||
|
navigator.maxTouchPoints > 2 &&
|
||||||
|
/MacIntel/.test(navigator.platform)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
e.isEnoughSpace = function (t, e, n, o, i) {
|
||||||
|
(void 0 === n && (n = 'auto'), void 0 === o && (o = 10), void 0 === i && (i = null));
|
||||||
|
var s = e.getBoundingClientRect(),
|
||||||
|
r = i ? i.getBoundingClientRect() : null,
|
||||||
|
l = window.innerHeight,
|
||||||
|
c = r ? s.top - r.top : s.top,
|
||||||
|
a = (i ? r.bottom : l) - s.bottom,
|
||||||
|
u = t.clientHeight + o;
|
||||||
|
return 'bottom' === n ? a >= u : 'top' === n ? c >= u : c >= u || a >= u;
|
||||||
|
};
|
||||||
|
e.isFormElement = function (t) {
|
||||||
|
return (
|
||||||
|
t instanceof HTMLInputElement ||
|
||||||
|
t instanceof HTMLTextAreaElement ||
|
||||||
|
t instanceof HTMLSelectElement
|
||||||
|
);
|
||||||
|
};
|
||||||
|
var o = function (t) {
|
||||||
|
return !!t && ('none' === window.getComputedStyle(t).display || o(t.parentElement));
|
||||||
|
};
|
||||||
|
e.isParentOrElementHidden = o;
|
||||||
|
e.debounce = function (t, e) {
|
||||||
|
var o;
|
||||||
|
return (
|
||||||
|
void 0 === e && (e = 200),
|
||||||
|
function () {
|
||||||
|
for (var i = [], s = 0; s < arguments.length; s++) i[s] = arguments[s];
|
||||||
|
(clearTimeout(o),
|
||||||
|
(o = setTimeout(function () {
|
||||||
|
t.apply(n, i);
|
||||||
|
}, e)));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
e.dispatch = function (t, e, n) {
|
||||||
|
void 0 === n && (n = null);
|
||||||
|
var o = new CustomEvent(t, {
|
||||||
|
detail: { payload: n },
|
||||||
|
bubbles: !0,
|
||||||
|
cancelable: !0,
|
||||||
|
composed: !1,
|
||||||
|
});
|
||||||
|
e.dispatchEvent(o);
|
||||||
|
};
|
||||||
|
e.afterTransition = function (t, e) {
|
||||||
|
var n = function () {
|
||||||
|
(e(), t.removeEventListener('transitionend', n, !0));
|
||||||
|
};
|
||||||
|
window.getComputedStyle(t, null).getPropertyValue('transition') !==
|
||||||
|
(navigator.userAgent.includes('Firefox') ? 'all' : 'all 0s ease 0s')
|
||||||
|
? t.addEventListener('transitionend', n, !0)
|
||||||
|
: e();
|
||||||
|
};
|
||||||
|
e.htmlToElement = function (t) {
|
||||||
|
var e = document.createElement('template');
|
||||||
|
return ((t = t.trim()), (e.innerHTML = t), e.content.firstChild);
|
||||||
|
};
|
||||||
|
e.classToClassList = function (t, e, n, o) {
|
||||||
|
(void 0 === n && (n = ' '),
|
||||||
|
void 0 === o && (o = 'add'),
|
||||||
|
t.split(n).forEach(function (t) {
|
||||||
|
return 'add' === o ? e.classList.add(t) : e.classList.remove(t);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
e.menuSearchHistory = {
|
||||||
|
historyIndex: -1,
|
||||||
|
addHistory: function (t) {
|
||||||
|
this.historyIndex = t;
|
||||||
|
},
|
||||||
|
existsInHistory: function (t) {
|
||||||
|
return t > this.historyIndex;
|
||||||
|
},
|
||||||
|
clearHistory: function () {
|
||||||
|
this.historyIndex = -1;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
e = {};
|
||||||
|
var n = (function n(o) {
|
||||||
|
var i = e[o];
|
||||||
|
if (void 0 !== i) return i.exports;
|
||||||
|
var s = (e[o] = { exports: {} });
|
||||||
|
return (t[o].call(s.exports, s, s.exports, n), s.exports);
|
||||||
|
})(652);
|
||||||
|
return n;
|
||||||
|
})()
|
||||||
|
);
|
@@ -1,10 +1,40 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": ["config:recommended", "mergeConfidence:all-badges", ":rebaseStalePrs"],
|
"extends": [
|
||||||
"timezone": "US/Central",
|
"config:recommended",
|
||||||
"schedule": ["* */1 * * *"],
|
"mergeConfidence:all-badges",
|
||||||
"labels": [],
|
":rebaseStalePrs"
|
||||||
"prHourlyLimit": 0,
|
],
|
||||||
"prConcurrentLimit": 0,
|
"timezone": "US/Central",
|
||||||
"packageRules": []
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
10
site-profile.code-workspace
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
|
}
|
@@ -1,109 +0,0 @@
|
|||||||
---
|
|
||||||
// Background.astro - Dot pattern and ambient glow background with smooth theme transitions
|
|
||||||
---
|
|
||||||
|
|
||||||
<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 bg-[center_top_-1px] [mask-image:radial-gradient(white,transparent_85%)]"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ambient glow effects -->
|
|
||||||
<div
|
|
||||||
class="animate-glow theme-transition-bg absolute left-1/4 top-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 bottom-1/3 right-1/4 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('DOMContentLoaded', () => {
|
|
||||||
const themeToggle = document.querySelector('[data-theme-toggle]');
|
|
||||||
const overlay = document.getElementById('theme-transition-overlay');
|
|
||||||
|
|
||||||
if (themeToggle && overlay) {
|
|
||||||
themeToggle.addEventListener('click', () => {
|
|
||||||
// Add transitioning class to optimize performance
|
|
||||||
document.documentElement.classList.add('theme-transitioning');
|
|
||||||
|
|
||||||
// Fade in overlay
|
|
||||||
overlay.style.opacity = '0.15';
|
|
||||||
overlay.style.transition = 'opacity 0.3s ease';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// Fade out overlay
|
|
||||||
overlay.style.opacity = '0';
|
|
||||||
|
|
||||||
// Remove transitioning class after animation completes
|
|
||||||
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.15) 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.1) 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>
|
|
95
src/components/BaseHead.astro
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
import { getImage } from 'astro:assets';
|
||||||
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
import brandSrc from '@images/brand_logo.png';
|
||||||
|
import faviconSvgSrc from '@images/favicon_icon.svg';
|
||||||
|
import faviconSrc from '@images/favicon_icon.png';
|
||||||
|
import { SEO } from '@/config';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
ogImage?: any;
|
||||||
|
ogTitle?: string;
|
||||||
|
ogDescription?: string;
|
||||||
|
structuredData?: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canonicalURL = Astro.url.href;
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
ogImage,
|
||||||
|
ogTitle = title,
|
||||||
|
ogDescription = description,
|
||||||
|
structuredData = SEO.structuredData,
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
let card = 'summary_large_image';
|
||||||
|
if (!ogImage) {
|
||||||
|
ogImage = brandSrc;
|
||||||
|
card = 'summary';
|
||||||
|
}
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
|
||||||
|
const faviconSvg = await getImage({ src: faviconSvgSrc, format: 'svg' });
|
||||||
|
const appleTouchIcon = await getImage({ src: faviconSrc, width: 180, height: 180, format: 'png' });
|
||||||
|
const socialImageRes = await getImage({ src: ogImage, width: 1200, height: 600 });
|
||||||
|
|
||||||
|
let socialImage = socialImageRes.src;
|
||||||
|
if (!socialImage.startsWith('http')) {
|
||||||
|
socialImage = Astro.url.origin + socialImageRes.src;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Inject structured data https://developers.google.com/search/docs/advanced/structured-data/intro-structured-data -->{
|
||||||
|
structuredData && <script type="application/ld+json" set:html={JSON.stringify(structuredData)} />
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Global Metadata -->
|
||||||
|
<meta name="title" content={title} />
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="web_author" content={global.name} />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0"
|
||||||
|
/>
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="theme-color" content="#facc15" />
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:locale" content="en_US" />
|
||||||
|
<meta property="og:url" content={Astro.url} />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:title" content={ogTitle} />
|
||||||
|
<meta property="og:site_name" content={global.name} />
|
||||||
|
<meta property="og:description" content={ogDescription} />
|
||||||
|
<meta property="og:image" content={socialImage} />
|
||||||
|
<meta content="1200" property="og:image:width" />
|
||||||
|
<meta content="600" property="og:image:height" />
|
||||||
|
<meta content="image/png" property="og:image:type" />
|
||||||
|
|
||||||
|
<!-- Twitter -->
|
||||||
|
<meta property="twitter:card" content={card} />
|
||||||
|
<meta property="twitter:url" content={Astro.url} />
|
||||||
|
<meta property="twitter:domain" content={Astro.url} />
|
||||||
|
<meta property="twitter:title" content={ogTitle} />
|
||||||
|
<meta property="twitter:description" content={ogDescription} />
|
||||||
|
<meta property="twitter:image" content={socialImage} />
|
||||||
|
|
||||||
|
<!-- Links -->
|
||||||
|
<link href={canonicalURL} rel="canonical" />
|
||||||
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||||
|
<!--<link href="/manifest.json" rel="manifest" />-->
|
||||||
|
<link href="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" />
|
||||||
|
<link href={faviconSvg.src} rel="icon" type="image/svg+xml" sizes="any" />
|
||||||
|
<link href={appleTouchIcon.src} rel="apple-touch-icon" />
|
||||||
|
<link href={appleTouchIcon.src} rel="shortcut icon" />
|
||||||
|
<link rel="preconnect" href="https://461ZQ3AX3S-dsn.algolia.net" crossorigin />
|
@@ -1,128 +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: 'About', href: '/about' },
|
|
||||||
{ text: 'Blog', href: '/blog' },
|
|
||||||
{ text: 'Topics', href: '/topics' },
|
|
||||||
{ text: 'RSS', href: '/rss.xml' },
|
|
||||||
];
|
|
||||||
|
|
||||||
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: '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"
|
||||||
>
|
>
|
||||||
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
<div class="relative px-4 pt-16 pb-12 sm:px-6">
|
||||||
<div
|
<div class="mx-auto max-w-[85rem]">
|
||||||
class="theme-transition-all animate-float-slow absolute -right-40 -top-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 left-1/4 top-20 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 pb-12 pt-16 sm:px-6">
|
|
||||||
<div class="mx-auto max-w-4xl">
|
|
||||||
<!-- Main footer content -->
|
|
||||||
<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
|
<div class="mx-auto aspect-square overflow-hidden rounded-lg">
|
||||||
class="relative flex h-10 w-10 transform items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-zinc-800 to-zinc-600 shadow-lg transition-transform group-hover:scale-105 dark:from-zinc-200 dark:to-zinc-400"
|
<BrandLogo class="max-h-[40px] max-w-[40px] rounded-full" />
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900"
|
|
||||||
>{global.initals}</span
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<span
|
|
||||||
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
|
<span class="ml-3 text-xl font-bold text-neutral-800 dark:text-neutral-200">
|
||||||
>Blog</span
|
{global.name}
|
||||||
>
|
</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"
|
{global.about}
|
||||||
>
|
|
||||||
{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="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 relative inline-block pb-2 text-sm font-semibold uppercase tracking-wider text-zinc-900 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:bg-zinc-300 after:content-[''] dark:text-zinc-100 dark:after:bg-zinc-700"
|
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 class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -130,115 +60,85 @@ 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>
|
||||||
|
<!-- 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>
|
||||||
|
|
||||||
<!-- Bottom section -->
|
<div class="flex items-center space-x-2">
|
||||||
<div class="theme-transition-all mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
<span class="text-xs text-neutral-500 dark:text-neutral-400">Built with </span>
|
||||||
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
<a
|
||||||
<p class="theme-transition-color text-sm text-zinc-600 dark:text-zinc-400">
|
href="https://astro.build"
|
||||||
© {currentYear} All rights reserved.
|
target="_blank"
|
||||||
</p>
|
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"
|
||||||
<div class="flex items-center space-x-2">
|
>
|
||||||
<span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
|
<svg class="mr-1 h-4 w-4 text-[#FF5D01]" viewBox="0 0 36 36" fill="none">
|
||||||
>Built with</span
|
<path
|
||||||
>
|
fill-rule="evenodd"
|
||||||
<a
|
clip-rule="evenodd"
|
||||||
href="https://astro.build"
|
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"
|
||||||
target="_blank"
|
fill="currentColor"></path>
|
||||||
rel="noopener noreferrer"
|
<path
|
||||||
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"
|
fill-rule="evenodd"
|
||||||
>
|
clip-rule="evenodd"
|
||||||
<svg
|
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"
|
||||||
class="mr-1 h-4 w-4 text-[#FF5D01] group-hover:animate-pulse"
|
fill="currentColor"></path>
|
||||||
viewBox="0 0 36 36"
|
</svg>
|
||||||
fill="none"
|
<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"
|
||||||
>
|
>
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M8.833 22.958c.622-1.185 1.832-1.918 3.18-1.918 2.292 0 4.145 1.86 4.145 4.153 0 1.34-.626 2.54-1.601 3.303 1.223-1.299 1.97-3.048 1.97-4.971 0-3.994-3.243-7.233-7.242-7.233-2.818 0-5.26 1.6-6.469 3.933.78-2.912 3.428-5.06 6.577-5.06 3.75 0 6.79 3.035 6.79 6.78 0 2.606-1.468 4.868-3.616 6.002a4.163 4.163 0 0 0 2.285-3.724c0-2.293-1.853-4.153-4.145-4.153-1.348 0-2.558.733-3.18 1.918l1.306-3.03Z"
|
|
||||||
fill="currentColor"></path>
|
|
||||||
<path
|
|
||||||
fill-rule="evenodd"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
d="M22.155 12.056c-.622 1.185-1.832 1.918-3.18 1.918-2.292 0-4.145-1.86-4.145-4.153 0-1.34.626-2.54 1.601-3.303-1.223 1.299-1.97 3.048-1.97 4.971 0 3.994 3.243 7.233 7.242 7.233 2.818 0 5.26-1.6 6.469-3.933-.78 2.912-3.428 5.06-6.577 5.06-3.75 0-6.79-3.035-6.79-6.78 0-2.606 1.468-4.868 3.616-6.002a4.163 4.163 0 0 0-2.285 3.724c0 2.293 1.853 4.153 4.145 4.153 1.348 0 2.558-.733 3.18-1.918l-1.306 3.03Z"
|
|
||||||
fill="currentColor"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="relative">
|
|
||||||
Astro
|
|
||||||
<span
|
|
||||||
class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"
|
|
||||||
></span>
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</span>
|
||||||
</div>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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>
|
|
||||||
</footer>
|
</footer>
|
||||||
|
@@ -1,21 +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()}>
|
|
||||||
{parsedDate.toLocaleDateString('en-us', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
})}
|
|
||||||
</time>
|
|
||||||
)
|
|
||||||
}
|
|
100
src/components/Header.astro
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
import BrandLogo from '@components/ui/logos/BrandLogo.astro';
|
||||||
|
import ThemeToggle from '@components/ui/buttons/ThemeToggle.astro';
|
||||||
|
import { NavigationLinks } from '@/config';
|
||||||
|
|
||||||
|
const pathname = new URL(Astro.request.url).pathname;
|
||||||
|
const currentPath = pathname.slice(1);
|
||||||
|
---
|
||||||
|
|
||||||
|
<header
|
||||||
|
id="nav"
|
||||||
|
class="sticky inset-x-0 top-4 z-50 flex w-full flex-wrap text-sm transition-none md:flex-nowrap md:justify-start"
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
class="relative mx-2 w-full rounded-[36px] border border-neutral-100 bg-neutral-100 px-4 py-3 md:flex md:items-center md:justify-between md:px-6 lg:px-8 dark:border-neutral-700/40 dark:bg-neutral-800/80"
|
||||||
|
aria-label="Global"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<a
|
||||||
|
class="h-[42px] flex-none rounded-lg text-xl font-bold ring-neutral-500 outline-none focus-visible:ring dark:ring-neutral-200 dark:focus:outline-none"
|
||||||
|
href="/"
|
||||||
|
aria-label="Brand"
|
||||||
|
>
|
||||||
|
<BrandLogo class="h-full w-auto rounded-full object-cover" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<div class="ml-auto md:hidden">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="hs-collapse-toggle flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold text-neutral-600 transition duration-300 hover:bg-neutral-200 disabled:pointer-events-none disabled:opacity-50 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:focus:outline-none"
|
||||||
|
data-hs-collapse="#navbar-collapse-with-animation"
|
||||||
|
aria-controls="navbar-collapse-with-animation"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="hs-collapse-open:hidden h-[1.25rem] w-[1.25rem] flex-shrink-0"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="3" x2="21" y1="6" y2="6"></line>
|
||||||
|
<line x1="3" x2="21" y1="12" y2="12"></line>
|
||||||
|
<line x1="3" x2="21" y1="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="hs-collapse-open:block hidden h-[1.25rem] w-[1.25rem] flex-shrink-0"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M18 6 6 18"></path>
|
||||||
|
<path d="m6 6 12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="navbar-collapse-with-animation"
|
||||||
|
class="hs-collapse hidden grow basis-full overflow-hidden transition-all duration-300 md:block"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mt-5 flex flex-col gap-x-0 gap-y-4 md:mt-0 md:flex-row md:items-center md:justify-end md:gap-x-4 md:gap-y-0 md:ps-7 lg:gap-x-7"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
NavigationLinks.map((item) => {
|
||||||
|
const isActive = currentPath === (item.url === '/' ? '' : item.url.slice(1));
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={item.url}
|
||||||
|
class={`text-sm font-medium ${
|
||||||
|
isActive
|
||||||
|
? 'text-orange-500 dark:text-orange-300'
|
||||||
|
: 'text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
<span class="md:inline-block">
|
||||||
|
<ThemeToggle />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<script is:inline src="/vendor/preline/collapse2.1.0.min.js"></script>
|
@@ -1,245 +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 navItems = [
|
|
||||||
{ text: 'Home', href: '/' },
|
|
||||||
{ text: 'Blog', href: '/blog' },
|
|
||||||
{ text: 'Topics', href: '/topics' },
|
|
||||||
{ text: 'About', href: '/about' },
|
|
||||||
{ text: 'RSS', href: 'rss.xml' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const pathname = new URL(Astro.request.url).pathname;
|
|
||||||
const currentPath = pathname.slice(1);
|
|
||||||
---
|
|
||||||
|
|
||||||
<header
|
|
||||||
class="fixed left-0 right-0 top-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<div class="mx-auto flex max-w-3xl items-center justify-between px-4">
|
|
||||||
<!-- Logo -->
|
|
||||||
<a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">{global.initals}</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-white'
|
|
||||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{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">JD</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-white'
|
|
||||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
|
|
||||||
}`}
|
|
||||||
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('DOMContentLoaded', () => {
|
|
||||||
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-sm');
|
|
||||||
} else {
|
|
||||||
header.classList.remove('shadow-sm');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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(4px);
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,175 +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="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="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="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 whitespace-nowrap rounded bg-zinc-800 px-2 py-1 text-xs text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
|
|
||||||
>
|
|
||||||
Copied!
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Function to handle copy link button
|
|
||||||
function setupCopyLinkButton() {
|
|
||||||
const copyButtons = document.querySelectorAll('#copy-link-button');
|
|
||||||
|
|
||||||
copyButtons.forEach((button) => {
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
// Get the current URL
|
|
||||||
const url = window.location.href;
|
|
||||||
|
|
||||||
// Copy to clipboard
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(url)
|
|
||||||
.then(() => {
|
|
||||||
// Show tooltip
|
|
||||||
const tooltip = button.querySelector('#copy-tooltip');
|
|
||||||
if (tooltip) {
|
|
||||||
tooltip.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Hide tooltip after 2 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
tooltip.classList.remove('opacity-100');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error('Failed to copy: ', err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up the copy link button when the DOM is loaded
|
|
||||||
document.addEventListener('DOMContentLoaded', setupCopyLinkButton);
|
|
||||||
|
|
||||||
// Also set up when the page content is updated via SPA navigation
|
|
||||||
document.addEventListener('astro:page-load', setupCopyLinkButton);
|
|
||||||
|
|
||||||
// For compatibility with the custom page transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupCopyLinkButton);
|
|
||||||
|
|
||||||
// Handle SPA transitions for share links
|
|
||||||
function setupSpaTransitions() {
|
|
||||||
// Get all share links
|
|
||||||
const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]');
|
|
||||||
|
|
||||||
// Make sure external share links don't trigger page transitions
|
|
||||||
shareLinks.forEach((link) => {
|
|
||||||
link.setAttribute('data-spa-external', 'true');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize SPA transitions
|
|
||||||
document.addEventListener('DOMContentLoaded', setupSpaTransitions);
|
|
||||||
document.addEventListener('astro:page-load', setupSpaTransitions);
|
|
||||||
document.addEventListener('page-transition-complete', setupSpaTransitions);
|
|
||||||
|
|
||||||
// Dispatch custom event when share action is completed
|
|
||||||
function notifyShareComplete() {
|
|
||||||
document.dispatchEvent(new CustomEvent('share-action-complete'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add analytics tracking for share actions if needed
|
|
||||||
function trackShareAction(platform) {
|
|
||||||
// You can implement analytics tracking here
|
|
||||||
console.log(`Shared on ${platform}`);
|
|
||||||
|
|
||||||
// Notify other components that share action is complete
|
|
||||||
notifyShareComplete();
|
|
||||||
}
|
|
||||||
</script>
|
|
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
export interface Props {
|
|
||||||
tags: string[];
|
|
||||||
class?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tags = [], class: className = '' } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
{
|
|
||||||
tags.length > 0 && (
|
|
||||||
<div class={`mt-3 flex flex-wrap gap-2 ${className}`}>
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<a
|
|
||||||
href={`/tag/${tag}`}
|
|
||||||
class="inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-800 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
61
src/components/blog/BlogCard.astro
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
import type { Post } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import { getDirectusImageURL } from '@lib/directusFunctions';
|
||||||
|
import Image from '@components/ui/images/Image.astro';
|
||||||
|
import { formatDate } from '@support/time';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
post: Post;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post } = Astro.props;
|
||||||
|
|
||||||
|
const baseClasses = 'group group-hover smooth-reveal-cards rounded-xl flex flex-col';
|
||||||
|
const borderClasses = 'border border-stone-200/50 dark:border-stone-700/50';
|
||||||
|
const bgColorClasses =
|
||||||
|
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
|
||||||
|
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class={`${baseClasses}`}>
|
||||||
|
<a
|
||||||
|
class={`rounded-xl duration-300 transition-all ${borderClasses} ${shadowClasses} ${bgColorClasses}`}
|
||||||
|
href={`/blog/${post.slug}/`}
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative w-full flex-shrink-0 overflow-hidden rounded-t-xl before:absolute before:inset-x-0 before:z-[1] before:size-full"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
class="h-auto w-full rounded-t-xl"
|
||||||
|
src={getDirectusImageURL(post.image)}
|
||||||
|
alt={post.image_alt}
|
||||||
|
draggable="false"
|
||||||
|
loading="eager"
|
||||||
|
format="webp"
|
||||||
|
width="800"
|
||||||
|
height="460"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl p-4 md:p-5">
|
||||||
|
<h3 class="text-xl font-bold text-neutral-600 dark:text-neutral-200">
|
||||||
|
{post.title}
|
||||||
|
</h3>
|
||||||
|
<div
|
||||||
|
class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-[44px] items-center font-medium text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-400"
|
||||||
|
>
|
||||||
|
<span class="relative inline-block overflow-hidden"> Read more </span>
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
|
||||||
|
/>
|
||||||
|
<p class="ml-auto text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
{formatDate(post.published_date)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
70
src/components/blog/BlogCategoryCard.astro
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
count: number;
|
||||||
|
publishDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { slug, title, description, count, publishDate } = Astro.props;
|
||||||
|
|
||||||
|
const baseClasses =
|
||||||
|
'group group-hover rounded-xl flex h-full min-h-[220px] cursor-pointer flex-col overflow-hidden';
|
||||||
|
const bgColorClasses =
|
||||||
|
'bg-neutral-100/60 dark:bg-neutral-800/60 hover:bg-neutral-100 dark:hover:bg-neutral-800/90 ';
|
||||||
|
---
|
||||||
|
|
||||||
|
<a class={`rounded-xl`} href={`/categories/${slug}/`} data-astro-prefetch="false">
|
||||||
|
<div class={`${baseClasses}`}>
|
||||||
|
<div
|
||||||
|
class={`relative min-h-0 flex-grow overflow-hidden transition-all duration-300 ${bgColorClasses}`}
|
||||||
|
>
|
||||||
|
<div class="absolute inset-1 flex flex-col p-3 md:p-4 lg:p-5">
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<h2
|
||||||
|
class="group-hover:text-steel dark:group-hover:text-bermuda transition-text mb-4 text-4xl font-extrabold tracking-tight text-balance whitespace-nowrap text-neutral-800 duration-300 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<p class="mb-4 font-light text-neutral-600 sm:text-lg dark:text-neutral-400">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="mt-auto flex items-center justify-between pt-1 text-xs text-neutral-600 md:pt-2 dark:text-neutral-300"
|
||||||
|
>
|
||||||
|
<span class="inline-flex items-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"></path>
|
||||||
|
</svg>
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||||||
|
</svg>
|
||||||
|
{publishDate}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
29
src/components/blog/BlogFeaturedArticle.astro
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
import type { Post } from '@lib/directusTypes';
|
||||||
|
import BlogCard from '@components/blog/BlogCard.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
posts: Post[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { posts } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="mx-auto mb-10 max-w-[85rem] px-4 py-8 sm:px-6 lg:px-8 2xl:max-w-full">
|
||||||
|
<div class="text-left">
|
||||||
|
<h2
|
||||||
|
id="selected-articel"
|
||||||
|
class="smooth-reveal-2 mb-4 text-5xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
Older Articles
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col md:flex-row md:space-x-12 lg:space-x-16">
|
||||||
|
<div class="w-full">
|
||||||
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{posts.map((b) => <BlogCard post={b} />)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
44
src/components/blog/BlogLeftSection.astro
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||||
|
import Image from '@components/ui/images/Image.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subTitle: string;
|
||||||
|
btnExists?: boolean;
|
||||||
|
btnTitle?: string;
|
||||||
|
btnURL?: string;
|
||||||
|
img: any;
|
||||||
|
imgAlt: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, subTitle, btnExists, btnTitle, btnURL, img, imgAlt } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="mx-auto max-w-[85rem] items-center gap-8 px-4 py-10 sm:px-6 sm:py-16 md:grid md:grid-cols-2 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 xl:gap-16 2xl:max-w-full"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
class="h-full w-full rounded-xl object-cover sm:max-h-[320px] md:max-h-[360px]"
|
||||||
|
src={img}
|
||||||
|
alt={imgAlt}
|
||||||
|
draggable="false"
|
||||||
|
loading="lazy"
|
||||||
|
width="850"
|
||||||
|
height="420"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div class="mt-4 md:mt-0">
|
||||||
|
<h2
|
||||||
|
class="mb-4 text-4xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
class="mb-4 max-w-prose font-light text-pretty text-neutral-600 sm:text-lg dark:text-neutral-300"
|
||||||
|
>
|
||||||
|
{subTitle}
|
||||||
|
</p>
|
||||||
|
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
|
||||||
|
</div>
|
||||||
|
</section>
|
45
src/components/blog/BlogRecentCard.astro
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
import type { Post } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import { getDirectusImageURL } from '@lib/directusFunctions';
|
||||||
|
import BlogLeftSection from '@components/blog/BlogLeftSection.astro';
|
||||||
|
import BlogRightSection from '@components/blog/BlogRightSection.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
posts: Post[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { posts } = Astro.props;
|
||||||
|
const blogPosts = posts.slice(0, 5);
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="smooth-reveal">
|
||||||
|
{
|
||||||
|
blogPosts.map((b, index) =>
|
||||||
|
index % 2 === 0 ? (
|
||||||
|
<BlogLeftSection
|
||||||
|
title={b.title}
|
||||||
|
subTitle={b.description}
|
||||||
|
btnExists={true}
|
||||||
|
btnTitle="Read More"
|
||||||
|
btnURL={`/blog/${b.slug}`}
|
||||||
|
img={getDirectusImageURL(b.image)}
|
||||||
|
imgAlt={b.image_alt}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<BlogRightSection
|
||||||
|
title={b.title}
|
||||||
|
subTitle={b.description}
|
||||||
|
btnExists={true}
|
||||||
|
btnTitle="Read More"
|
||||||
|
btnURL={`/blog/${b.slug}`}
|
||||||
|
single={!b.image_second}
|
||||||
|
imgOne={getDirectusImageURL(b.image)}
|
||||||
|
imgOneAlt={b.image_alt}
|
||||||
|
imgTwo={getDirectusImageURL(b?.image_second)}
|
||||||
|
imgTwoAlt={b?.image_second_alt}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</section>
|
87
src/components/blog/BlogRightSection.astro
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
---
|
||||||
|
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||||
|
import Image from '@components/ui/images/Image.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subTitle: string;
|
||||||
|
btnExists?: boolean;
|
||||||
|
btnTitle?: string;
|
||||||
|
btnURL?: string;
|
||||||
|
single?: boolean;
|
||||||
|
imgOne?: any;
|
||||||
|
imgOneAlt?: any;
|
||||||
|
imgTwo?: any;
|
||||||
|
imgTwoAlt?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title,
|
||||||
|
subTitle,
|
||||||
|
btnExists,
|
||||||
|
btnTitle,
|
||||||
|
btnURL,
|
||||||
|
single,
|
||||||
|
imgOne,
|
||||||
|
imgOneAlt,
|
||||||
|
imgTwo,
|
||||||
|
imgTwoAlt,
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="mx-auto max-w-[85rem] items-center gap-16 px-4 py-10 sm:px-6 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 2xl:max-w-full"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2
|
||||||
|
class="mb-4 text-4xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
class="mb-4 max-w-prose font-light text-pretty text-neutral-600 sm:text-lg dark:text-neutral-400"
|
||||||
|
>
|
||||||
|
{subTitle}
|
||||||
|
</p>
|
||||||
|
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
single ? (
|
||||||
|
<div class="mt-8">
|
||||||
|
<Image
|
||||||
|
class="w-full rounded-lg"
|
||||||
|
src={imgOne}
|
||||||
|
alt={imgOneAlt}
|
||||||
|
format="webp"
|
||||||
|
loading="lazy"
|
||||||
|
width="850"
|
||||||
|
height="420"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="mt-8 grid grid-cols-2 gap-4">
|
||||||
|
<Image
|
||||||
|
class="w-full rounded-xl"
|
||||||
|
src={imgOne}
|
||||||
|
alt={imgOneAlt}
|
||||||
|
draggable="false"
|
||||||
|
format="webp"
|
||||||
|
loading="lazy"
|
||||||
|
width="400"
|
||||||
|
height="230"
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
class="mt-4 w-full rounded-xl lg:mt-10"
|
||||||
|
src={imgTwo}
|
||||||
|
alt={imgTwoAlt}
|
||||||
|
draggable="false"
|
||||||
|
format="webp"
|
||||||
|
loading="lazy"
|
||||||
|
width="400"
|
||||||
|
height="230"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</section>
|
85
src/components/ui/buttons/Bookmark.astro
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
import Icon from '@components/ui/icons/icon.astro';
|
||||||
|
---
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="focus-visible:ring-secondary group inline-flex items-center rounded-lg p-2.5 text-neutral-600 ring-neutral-500 transition duration-300 outline-none hover:bg-neutral-100 focus:outline-none focus-visible:ring-1 focus-visible:outline-none dark:text-neutral-400 dark:ring-neutral-200 dark:hover:bg-neutral-700"
|
||||||
|
data-bookmark-button="bookmark-button"
|
||||||
|
>
|
||||||
|
<Icon name="bookmark" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
class Bookmark {
|
||||||
|
private static readonly BOOKMARKS_KEY = 'bookmarks';
|
||||||
|
private bookmarkButton: Element | null;
|
||||||
|
|
||||||
|
constructor(private dataAttrValue: string) {
|
||||||
|
this.bookmarkButton = document.querySelector(`[data-bookmark-button="${dataAttrValue}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStoredBookmarks(): string[] {
|
||||||
|
const item = localStorage.getItem(Bookmark.BOOKMARKS_KEY);
|
||||||
|
return item ? JSON.parse(item) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
if (this.bookmarkButton && this.isStored()) {
|
||||||
|
this.markAsStored();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bookmarkButton?.addEventListener('click', () => this.toggleBookmark());
|
||||||
|
}
|
||||||
|
|
||||||
|
isStored(): boolean {
|
||||||
|
return this.getStoredBookmarks().includes(window.location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
markAsStored(): void {
|
||||||
|
if (this.bookmarkButton) {
|
||||||
|
this.bookmarkButton.classList.add('bookmarked');
|
||||||
|
const svgElement = this.bookmarkButton.querySelector('svg');
|
||||||
|
if (svgElement) {
|
||||||
|
svgElement.setAttribute('class', 'h-6 w-6 fill-red-500 dark:fill-red-500');
|
||||||
|
}
|
||||||
|
const pathElement = svgElement?.querySelector('path');
|
||||||
|
if (pathElement) {
|
||||||
|
pathElement.setAttribute('class', 'fill-current text-red-500 dark:text-red-500');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unmarkAsStored(): void {
|
||||||
|
if (this.bookmarkButton) {
|
||||||
|
this.bookmarkButton.classList.remove('bookmarked');
|
||||||
|
const svgElement = this.bookmarkButton.querySelector('svg');
|
||||||
|
if (svgElement) {
|
||||||
|
svgElement.setAttribute('class', 'h-6 w-6 fill-none');
|
||||||
|
}
|
||||||
|
const pathElement = svgElement?.querySelector('path');
|
||||||
|
if (pathElement) {
|
||||||
|
pathElement.setAttribute(
|
||||||
|
'class',
|
||||||
|
'fill-current text-neutral-500 group-hover:text-red-400 dark:text-neutral-500 group-hover:dark:text-red-400'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleBookmark(): void {
|
||||||
|
const storedBookmarks = this.getStoredBookmarks();
|
||||||
|
const index = storedBookmarks.indexOf(window.location.pathname);
|
||||||
|
if (index !== -1) {
|
||||||
|
storedBookmarks.splice(index, 1);
|
||||||
|
this.unmarkAsStored();
|
||||||
|
} else {
|
||||||
|
storedBookmarks.push(window.location.pathname);
|
||||||
|
this.markAsStored();
|
||||||
|
}
|
||||||
|
localStorage.setItem(Bookmark.BOOKMARKS_KEY, JSON.stringify(storedBookmarks));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Bookmark('bookmark-button').init();
|
||||||
|
</script>
|
32
src/components/ui/buttons/GiteaBtn.astro
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
const { title, url } = Astro.props;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseClasses =
|
||||||
|
'group group-hover inline-flex items-center justify-center gap-x-3 rounded-full px-4 py-3 text-center text-sm font-medium text-neutral-200';
|
||||||
|
const borderClasses = 'border border-transparent';
|
||||||
|
const bgColorClasses =
|
||||||
|
'bg-gitea-primary hover:bg-gitea-secondary dark:bg-gitea-secondary dark:hover:bg-gitea-primary';
|
||||||
|
const shadowClasses = 'shadow-sm';
|
||||||
|
const fontSizeClasses = '2xl:text-base';
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${shadowClasses} ${fontSizeClasses} `}
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<Icon name="pajamas:gitea" class="h-4 w-4 md:h-6 md:w-6" />
|
||||||
|
{title}
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
|
||||||
|
/>
|
||||||
|
</a>
|
35
src/components/ui/buttons/GoBack.astro
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import Icon from '@components/ui/icons/icon.astro';
|
||||||
|
|
||||||
|
const { title, noArrow } = Astro.props;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
url?: string;
|
||||||
|
noArrow?: boolean;
|
||||||
|
addHome?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseClasses =
|
||||||
|
'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-50 ring-neutral-500 transition duration-300 focus-visible:ring outline-none';
|
||||||
|
const borderClasses = 'border border-transparent';
|
||||||
|
const bgColorClasses = 'bg-steel hover:bg-sky-800 active:bg-orange-500 dark:focus:outline-none';
|
||||||
|
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
|
||||||
|
const fontSizeClasses = '2xl:text-base';
|
||||||
|
const ringClasses = 'dark:ring-neutral-200';
|
||||||
|
---
|
||||||
|
|
||||||
|
<button
|
||||||
|
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses}`}
|
||||||
|
id="back-button"
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
{noArrow ? null : <Icon name="arrowLeft" />}
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('back-button')?.addEventListener('click', () => {
|
||||||
|
window.history.back();
|
||||||
|
});
|
||||||
|
</script>
|
45
src/components/ui/buttons/PrimaryCTA.astro
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
const { title, url, noArrow, addHome, addClass } = Astro.props;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
url?: string;
|
||||||
|
noArrow?: boolean;
|
||||||
|
addHome?: boolean;
|
||||||
|
addClass?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseClasses =
|
||||||
|
'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-100 transition duration-300 ';
|
||||||
|
const borderClasses = 'border border-transparent';
|
||||||
|
const bgColorClasses = 'bg-bermuda hover:bg-turquoise dark:bg-turquoise dark:hover:bg-bermuda';
|
||||||
|
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
|
||||||
|
const fontSizeClasses = '2xl:text-base';
|
||||||
|
const ringClasses = 'dark:ring-neutral-200';
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${addClass}`}
|
||||||
|
href={url}
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
{
|
||||||
|
addHome ? (
|
||||||
|
<Icon
|
||||||
|
name="mdi:home-variant-outline"
|
||||||
|
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
{title}
|
||||||
|
{
|
||||||
|
noArrow ? null : (
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</a>
|
26
src/components/ui/buttons/SecondaryCTA.astro
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
---
|
||||||
|
const { title, url } = Astro.props;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const baseClasses =
|
||||||
|
'inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-center text-sm font-medium text-neutral-600 shadow-sm outline-none ring-neutral-500 focus-visible:ring transition duration-300';
|
||||||
|
const borderClasses = 'border border-neutral-200';
|
||||||
|
const bgColorClasses = 'bg-neutral-300';
|
||||||
|
const hoverClasses = 'hover:bg-neutral-400/50 hover:text-neutral-600 active:text-neutral-700';
|
||||||
|
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
|
||||||
|
const fontSizeClasses = '2xl:text-base';
|
||||||
|
const ringClasses = 'ring-neutral-500';
|
||||||
|
const darkClasses =
|
||||||
|
'dark:border-neutral-700 dark:bg-neutral-700 dark:text-neutral-300 dark:ring-neutral-200 dark:hover:bg-neutral-600 dark:focus:outline-none';
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${darkClasses}`}
|
||||||
|
href={url}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</a>
|
150
src/components/ui/buttons/SocialShare.astro
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
---
|
||||||
|
import Icon from '@components/ui/icons/icon.astro';
|
||||||
|
|
||||||
|
const { pageTitle, title = 'Share' } = Astro.props;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pageTitle: string;
|
||||||
|
title?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type SocialPlatform = {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
svg: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const socialPlatforms: SocialPlatform[] = [
|
||||||
|
{
|
||||||
|
name: 'Facebook',
|
||||||
|
url: `https://www.facebook.com/share.php?u=${Astro.url}&title=${pageTitle}`,
|
||||||
|
svg: 'facebook',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'X',
|
||||||
|
url: `https://twitter.com/home/?status=${pageTitle}${Astro.url}`,
|
||||||
|
svg: 'x',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'LinkedIn',
|
||||||
|
url: `https://www.linkedin.com/shareArticle?mini=true&url=${Astro.url}&title=${pageTitle}`,
|
||||||
|
svg: 'linkedIn',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="hs-dropdown relative inline-flex [--auto-close:inside] [--placement:top-left]">
|
||||||
|
<button
|
||||||
|
id="hs-dropup"
|
||||||
|
type="button"
|
||||||
|
class="hs-dropdown-toggle inline-flex items-center gap-x-2 rounded-lg px-4 py-3 text-sm font-medium text-neutral-600 ring-neutral-500 transition duration-300 outline-none hover:bg-neutral-100 hover:text-neutral-700 focus-visible:ring dark:text-neutral-400 dark:ring-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:outline-none"
|
||||||
|
>
|
||||||
|
<Icon name="share" />
|
||||||
|
|
||||||
|
{title}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="hs-dropdown-menu duration hs-dropdown-open:opacity-100 z-10 hidden w-72 divide-y divide-neutral-200 rounded-lg bg-neutral-50 p-2 opacity-0 shadow-md transition-[opacity,margin] dark:divide-neutral-700 dark:border dark:border-neutral-700 dark:bg-neutral-800"
|
||||||
|
aria-labelledby="hs-dropup"
|
||||||
|
>
|
||||||
|
<div class="py-2 first:pt-0 last:pb-0">
|
||||||
|
{
|
||||||
|
socialPlatforms.map((platform) => (
|
||||||
|
<a
|
||||||
|
class="flex items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700"
|
||||||
|
href={platform.url}
|
||||||
|
>
|
||||||
|
<Icon name={platform.svg} />
|
||||||
|
Share on {platform.name}
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div class="py-2 first:pt-0 last:pb-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="js-clipboard hover:text-dark focus-visible:ring-secondary group inline-flex w-full items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none focus-visible:ring-1 focus-visible:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700"
|
||||||
|
data-clipboard-success-text="Copied"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="js-clipboard-default h-4 w-4 transition group-hover:rotate-6"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect>
|
||||||
|
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class="js-clipboard-success hidden h-4 w-4 text-neutral-700 dark:text-neutral-300"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
|
</svg>
|
||||||
|
<span class="js-clipboard-success-text">Copy link</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!--Import the necessary Dropdown and Clipboard plugins-->
|
||||||
|
<!--https://preline.co/plugins/html/dropdown.html-->
|
||||||
|
<!--<script is:inline src="/scripts/vendor/preline/dropdown/index.js"></script>-->
|
||||||
|
|
||||||
|
<!-- https://clipboardjs.com/ -->
|
||||||
|
<!--<script is:inline src="/scripts/vendor/clipboard.min.js"></script>-->
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
(function () {
|
||||||
|
window.addEventListener('load', () => {
|
||||||
|
const $clipboards = document.querySelectorAll('.js-clipboard');
|
||||||
|
$clipboards.forEach((el) => {
|
||||||
|
const clipboard = new ClipboardJS(el, {
|
||||||
|
text: () => {
|
||||||
|
return window.location.href;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
clipboard.on('success', () => {
|
||||||
|
const $default = el.querySelector('.js-clipboard-default');
|
||||||
|
const $success = el.querySelector('.js-clipboard-success');
|
||||||
|
const $successText = el.querySelector('.js-clipboard-success-text');
|
||||||
|
const successText = el.dataset.clipboardSuccessText || '';
|
||||||
|
let oldSuccessText;
|
||||||
|
|
||||||
|
if ($successText) {
|
||||||
|
oldSuccessText = $successText.textContent;
|
||||||
|
$successText.textContent = successText;
|
||||||
|
}
|
||||||
|
if ($default && $success) {
|
||||||
|
$default.style.display = 'none';
|
||||||
|
$success.style.display = 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
if ($successText && oldSuccessText) {
|
||||||
|
$successText.textContent = oldSuccessText;
|
||||||
|
}
|
||||||
|
if ($default && $success) {
|
||||||
|
$success.style.display = '';
|
||||||
|
$default.style.display = '';
|
||||||
|
}
|
||||||
|
}, 800);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
@@ -5,14 +5,14 @@
|
|||||||
<button
|
<button
|
||||||
id="theme-toggle"
|
id="theme-toggle"
|
||||||
data-theme-toggle
|
data-theme-toggle
|
||||||
class="group relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-zinc-300 dark:hover:bg-zinc-800 dark:focus:ring-zinc-700 sm:p-2"
|
class="group dark:hover:bg-steel/30 relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-yellow-300/20 focus:outline-hidden sm:p-2"
|
||||||
aria-label="Toggle dark mode"
|
aria-label="Toggle dark mode"
|
||||||
>
|
>
|
||||||
<div class="relative z-10 flex h-5 w-5 items-center justify-center">
|
<div class="relative z-10 flex h-5 w-5 items-center justify-center">
|
||||||
<!-- Sun icon -->
|
<!-- Sun icon -->
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon-light absolute h-5 w-5 rotate-0 scale-100 text-zinc-800 transition-all duration-500 dark:-rotate-90 dark:scale-0 dark:text-zinc-200"
|
class="icon-light absolute h-5 w-5 scale-100 rotate-0 text-neutral-600 transition-all duration-500 dark:scale-0 dark:-rotate-90 dark:text-neutral-400"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<!-- Moon icon -->
|
<!-- Moon icon -->
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon-dark absolute h-5 w-5 rotate-90 scale-0 text-zinc-800 transition-all duration-500 dark:rotate-0 dark:scale-100 dark:text-zinc-200"
|
class="icon-dark absolute h-5 w-5 scale-0 rotate-90 text-neutral-600 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-neutral-400"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -40,31 +40,27 @@
|
|||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</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>
|
</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>
|
<script>
|
||||||
// Use a function to handle theme toggle to ensure it can be called from anywhere
|
// Use a function to handle theme toggle to ensure it can be called from anywhere
|
||||||
function setupThemeToggle() {
|
function setupThemeToggle() {
|
||||||
const themeToggles = document.querySelectorAll('[data-theme-toggle]');
|
const themeToggles = document.querySelectorAll('[data-theme-toggle]');
|
||||||
|
|
||||||
// Check for dark mode preference at the system level
|
|
||||||
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
|
|
||||||
// Check for saved theme preference or use the system preference
|
|
||||||
const currentTheme = localStorage.getItem('theme') || (prefersDarkMode ? 'dark' : 'light');
|
|
||||||
|
|
||||||
// Apply the theme on initial load
|
|
||||||
if (currentTheme === 'dark') {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create theme switch overlay element if it doesn't exist
|
// Create theme switch overlay element if it doesn't exist
|
||||||
if (!document.querySelector('.theme-switch-overlay')) {
|
if (!document.querySelector('.theme-switch-overlay')) {
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
@@ -117,11 +113,6 @@
|
|||||||
// Add transition class
|
// Add transition class
|
||||||
document.documentElement.classList.add('theme-switching');
|
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
|
// Force a reflow to ensure all elements update
|
||||||
document.body.offsetHeight;
|
document.body.offsetHeight;
|
||||||
|
|
||||||
@@ -154,7 +145,6 @@
|
|||||||
|
|
||||||
// Remove transition class after animation completes
|
// Remove transition class after animation completes
|
||||||
document.documentElement.classList.remove('theme-switching');
|
document.documentElement.classList.remove('theme-switching');
|
||||||
ripple.remove();
|
|
||||||
}, 300);
|
}, 300);
|
||||||
}, 50);
|
}, 50);
|
||||||
},
|
},
|
||||||
@@ -184,7 +174,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run setup on load
|
// Run setup on load
|
||||||
document.addEventListener('DOMContentLoaded', setupThemeToggle);
|
document.addEventListener('astro:page-load', setupThemeToggle);
|
||||||
|
|
||||||
// Also run on page visibility change to ensure theme is consistent
|
// Also run on page visibility change to ensure theme is consistent
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
@@ -233,30 +223,6 @@
|
|||||||
transition-property: background-color, border-color, color, fill, stroke !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 */
|
/* Subtle hover animation */
|
||||||
#theme-toggle {
|
#theme-toggle {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@@ -274,12 +240,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
#theme-toggle:hover .icon-light:not(.dark .icon-light) {
|
#theme-toggle:hover .icon-light:not(.dark .icon-light) {
|
||||||
filter: drop-shadow(0 0 2px rgba(251, 191, 36, 0.6));
|
filter: drop-shadow-sm(0 0 2px rgba(251, 191, 36, 0.6));
|
||||||
transform: scale(1.1) rotate(15deg);
|
transform: scale(1.1) rotate(15deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) {
|
#theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) {
|
||||||
filter: drop-shadow(0 0 2px rgba(129, 140, 248, 0.6));
|
filter: drop-shadow-sm(0 0 2px rgba(129, 140, 248, 0.6));
|
||||||
transform: scale(1.1) rotate(-15deg);
|
transform: scale(1.1) rotate(-15deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -302,10 +268,6 @@
|
|||||||
transform: none;
|
transform: none;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle-ripple {
|
|
||||||
animation-duration: 0.4s;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adjust size for very small screens */
|
/* Adjust size for very small screens */
|
45
src/components/ui/cards/FeaturesCard.astro
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
url?: string;
|
||||||
|
icon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, url, icon } = Astro.props;
|
||||||
|
|
||||||
|
const baseClasses = 'smooth-reveal-2 group group-hover flex flex-col ';
|
||||||
|
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
|
||||||
|
const bgColorClasses =
|
||||||
|
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
|
||||||
|
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class={`${baseClasses}`}>
|
||||||
|
<a
|
||||||
|
class={`rounded-xl duration-300 transition-all h-30 ${borderClasses} ${bgColorClasses} ${shadowClasses}`}
|
||||||
|
href={url}
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
<div class="p-4 md:p-5">
|
||||||
|
<div class="flex">
|
||||||
|
<Icon
|
||||||
|
name={icon}
|
||||||
|
class="group-hover:text-steel dark:group-hover:text-bermuda h-6 w-6 text-neutral-600 transition-all duration-300 md:h-8 md:w-8 dark:text-neutral-200"
|
||||||
|
/>
|
||||||
|
<div class="ms-5 grow">
|
||||||
|
<span
|
||||||
|
class="group-hover:text-steel dark:group-hover:text-bermuda block text-lg font-bold text-neutral-600 transition-all duration-300 dark:text-neutral-300"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<span class="mt-1 block text-neutral-500 dark:text-neutral-400">
|
||||||
|
{description}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
39
src/components/ui/icons/icon.astro
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
import { Icons } from './icons.ts';
|
||||||
|
|
||||||
|
interface Path {
|
||||||
|
d: string;
|
||||||
|
class?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name } = Astro.props;
|
||||||
|
|
||||||
|
const icon = (Icons as any)[name] || {};
|
||||||
|
const paths: Path[] = icon.paths || [];
|
||||||
|
---
|
||||||
|
|
||||||
|
{
|
||||||
|
icon ? (
|
||||||
|
<svg
|
||||||
|
class={icon.class}
|
||||||
|
height={icon.height}
|
||||||
|
viewBox={icon.viewBox}
|
||||||
|
width={icon.width}
|
||||||
|
fill={icon.fill}
|
||||||
|
clip-rule={icon.clipRule}
|
||||||
|
fill-rule={icon.fillRule}
|
||||||
|
stroke={icon.stroke}
|
||||||
|
stroke-width={icon.strokeWidth}
|
||||||
|
stroke-linecap={icon.strokeLinecap}
|
||||||
|
stroke-linejoin={icon.strokeLinejoin}
|
||||||
|
>
|
||||||
|
<title>{icon.title}</title>
|
||||||
|
<circle cx={icon.circleCx} cy={icon.circleCy} r={icon.circleR} />
|
||||||
|
{paths.map((path) => (
|
||||||
|
<path d={path.d} class={path.class || ''} />
|
||||||
|
))}
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
'Icon not found'
|
||||||
|
)
|
||||||
|
}
|
573
src/components/ui/icons/icons.ts
Normal file
@@ -0,0 +1,573 @@
|
|||||||
|
export const Icons = {
|
||||||
|
groups: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'm150-400 82-80-82-82-80 82 80 80Zm573-10 87-140 88 140H723Zm-243-70q-50 0-85-35t-35-85q0-51 35-85.5t85-34.5q51 0 85.5 34.5T600-600q0 50-34.5 85T480-480Zm.351-180Q455-660 437.5-642.851t-17.5 42.5Q420-575 437.351-557.5t43 17.5Q506-540 523-557.351t17-43Q540-626 522.851-643t-42.5-17ZM480-600ZM0-240v-53q0-39.464 42-63.232T150.398-380q12.158 0 23.38.5T196-377.273q-8 17.273-12 34.842-4 17.57-4 37.431v65H0Zm240 0v-65q0-65 66.5-105T480-450q108 0 174 40t66 105v65H240Zm570-140q67.5 0 108.75 23.768T960-293v53H780v-65q0-19.861-3.5-37.431Q773-360 765-377.273q11-1.727 22.171-2.227 11.172-.5 22.829-.5Zm-330.2-10Q400-390 350-366q-50 24-50 61v5h360v-6q0-36-49.5-60t-130.7-24Zm.2 90Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
viewBox: '0 -960 960 960',
|
||||||
|
},
|
||||||
|
books: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M343-420h225v-60H343v60Zm0-90h395v-60H343v60Zm0-90h395v-60H343v60Zm-83 400q-24 0-42-18t-18-42v-560q0-24 18-42t42-18h560q24 0 42 18t18 42v560q0 24-18 42t-42 18H260Zm0-60h560v-560H260v560ZM140-80q-24 0-42-18t-18-42v-620h60v620h620v60H140Zm120-740v560-560Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
viewBox: '0 -960 960 960',
|
||||||
|
},
|
||||||
|
verified: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'm346-60-76-130-151-31 17-147-96-112 96-111-17-147 151-31 76-131 134 62 134-62 77 131 150 31-17 147 96 111-96 112 17 147-150 31-77 130-134-62-134 62Zm27-79 107-45 110 45 67-100 117-30-12-119 81-92-81-94 12-119-117-28-69-100-108 45-110-45-67 100-117 28 12 119-81 94 81 92-12 121 117 28 70 100Zm107-341Zm-43 133 227-225-45-41-182 180-95-99-46 45 141 140Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
viewBox: '0 -960 960 960',
|
||||||
|
},
|
||||||
|
frame: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M480-480q-51 0-85.5-34.5T360-600q0-50 34.5-85t85.5-35q50 0 85 35t35 85q0 51-35 85.5T480-480Zm-.351-60Q505-540 522.5-557.149t17.5-42.5Q540-625 522.649-642.5t-43-17.5Q454-660 437-642.649t-17 43Q420-574 437.149-557t42.5 17ZM240-240v-76q0-27 17.5-47.5T300-397q42-22 86.943-32.5 44.942-10.5 93-10.5Q528-440 573-429.5t87 32.5q25 13 42.5 33.5T720-316v76H240Zm240-140q-47.546 0-92.773 13T300-328v28h360v-28q-42-26-87.227-39-45.227-13-92.773-13Zm0-220Zm0 300h180-360 180ZM140-80q-24 0-42-18t-18-42v-172h60v172h172v60H140ZM80-648v-172q0-24 18-42t42-18h172v60H140v172H80ZM648-80v-60h172v-172h60v172q0 24-18 42t-42 18H648Zm172-568v-172H648v-60h172q24 0 42 18t18 42v172h-60Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
viewBox: '0 -960 960 960',
|
||||||
|
},
|
||||||
|
tools: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M764-80q-6 0-11-2t-10-7L501-331q-5-5-7-10t-2-11q0-6 2-11t7-10l85-85q5-5 10-7t11-2q6 0 11 2t10 7l242 242q5 5 7 10t2 11q0 6-2 11t-7 10l-85 85q-5 5-10 7t-11 2Zm0-72 43-43-200-200-43 43 200 200ZM195-80q-6 0-11.5-2T173-89l-84-84q-5-5-7-10.5T80-195q0-6 2-11t7-10l225-225h85l38-38-175-175h-57L80-779l99-99 125 125v57l175 175 130-130-67-67 56-56H485l-18-18 128-128 18 18v113l56-56 169 169q15 15 23.5 34.5T870-600q0 20-6.5 38.5T845-528l-85-85-56 56-52-52-211 211v84L216-89q-5 5-10 7t-11 2Zm0-72 200-200v-43h-43L152-195l43 43Zm0 0-43-43 22 21 21 22Zm569 0 43-43-43 43Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class:
|
||||||
|
'mt-2 h-6 w-6 flex-shrink-0 fill-neutral-700 hs-tab-active:fill-orange-400 dark:fill-neutral-300 dark:hs-tab-active:fill-orange-300 md:h-7 md:w-7',
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
viewBox: '0 -960 960 960',
|
||||||
|
},
|
||||||
|
dashboard: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M510-570v-270h330v270H510ZM120-450v-390h330v390H120Zm390 330v-390h330v390H510Zm-390 0v-270h330v270H120Zm60-390h210v-270H180v270Zm390 330h210v-270H570v270Zm0-450h210v-150H570v150ZM180-180h210v-150H180v150Zm210-330Zm180-120Zm0 180ZM390-330Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class:
|
||||||
|
'mt-2 h-6 w-6 flex-shrink-0 fill-neutral-700 hs-tab-active:fill-orange-400 dark:fill-neutral-300 dark:hs-tab-active:fill-orange-300 md:h-7 md:w-7',
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
viewBox: '0 -960 960 960',
|
||||||
|
},
|
||||||
|
house: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'h-4 w-4 flex-shrink-0 md:h-5 md:w-5',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
home: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205 3 1m1.5.5-1.5-.5M6.75 7.364V3h-3v18m3-13.636 10.5-3.819',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class:
|
||||||
|
'h-6 w-6 flex-shrink-0 group-hover:text-steel dark:group-hover:text-steel transition-all duration-200 text-neutral-600 dark:text-neutral-300 md:h-7 md:w-7',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '1.5',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
arrowUp: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'm5 12 7-7 7 7',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d: 'M12 19V5',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'h-5 w-5 flex-shrink-0 text-orange-400 dark:text-orange-300',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
checkCircle: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M10 18a8 8 0 100-16 8 8 0 000 16zM13.707 8.293a1 1 0 00-1.414-1.414L9 10.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'h-5 w-5 shrink-0',
|
||||||
|
viewBox: '0 0 20 20',
|
||||||
|
fill: 'currentColor',
|
||||||
|
fillRule: 'evenodd',
|
||||||
|
clipRule: 'evenodd',
|
||||||
|
},
|
||||||
|
bookmark: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z',
|
||||||
|
class:
|
||||||
|
'fill-current text-neutral-500 transition duration-300 group-hover:text-red-400 group-hover:dark:text-red-400',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'h-6 w-6 fill-none transition duration-300',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '1.5',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
arrowRight: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'm9 18 6-6-6-6',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'h-4 w-4 flex-shrink-0 transition duration-300 group-hover:translate-x-1',
|
||||||
|
width: 20,
|
||||||
|
height: 20,
|
||||||
|
viewBox: '0 0 22 22',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
arrowLeft: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'm15 18-6-6 6-6',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'h-4 w-4 flex-shrink-0 transition duration-300 group-hover:-translate-x-1',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
facebook: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'size-4 flex-shrink-0 fill-current',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'size-4 flex-shrink-0 fill-current',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
linkedIn: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'size-4 flex-shrink-0 fill-current',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
share: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'h-4 w-4 group-hover:text-neutral-700',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '1.5',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
github: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'w-4.5 h-4.5 transition flex-shrink-0 text-neutral-700 duration-300',
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
viewBox: '0 0 16 16',
|
||||||
|
fill: 'currentColor',
|
||||||
|
},
|
||||||
|
gitea: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'w-6 h-6 transition flex-shrink-0 duration-300',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'currentColor',
|
||||||
|
},
|
||||||
|
arrowRightStatic: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'm9 18 6-6-6-6',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'size-4 flex-shrink-0',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
openInNew: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'm4.5 19.5 15-15m0 0H8.25m11.25 0v11.25',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'ml-0.5 w-3 h-3 md:w-4 md:h-4 inline pb-0.5',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '3',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
accordionNotActive: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'm6 9 6 6 6-6',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class:
|
||||||
|
'block h-5 w-5 flex-shrink-0 text-neutral-600 group-hover:text-neutral-500 hs-accordion-active:hidden dark:text-neutral-400',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
accordionActive: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'm18 15-6-6-6 6',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class:
|
||||||
|
'hidden h-5 w-5 flex-shrink-0 text-neutral-600 group-hover:text-neutral-500 hs-accordion-active:block dark:text-neutral-400',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
xFooter: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'currentColor',
|
||||||
|
title: 'Twitter',
|
||||||
|
},
|
||||||
|
facebookFooter: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'currentColor',
|
||||||
|
title: 'Facebook',
|
||||||
|
},
|
||||||
|
githubFooter: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'currentColor',
|
||||||
|
title: 'GitHub',
|
||||||
|
},
|
||||||
|
googleFooter: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'currentColor',
|
||||||
|
title: 'Google',
|
||||||
|
},
|
||||||
|
slackFooter: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'currentColor',
|
||||||
|
title: 'Slack',
|
||||||
|
},
|
||||||
|
quotation: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M7.39762 10.3C7.39762 11.0733 7.14888 11.7 6.6514 12.18C6.15392 12.6333 5.52552 12.86 4.76621 12.86C3.84979 12.86 3.09047 12.5533 2.48825 11.94C1.91222 11.3266 1.62421 10.4467 1.62421 9.29999C1.62421 8.07332 1.96459 6.87332 2.64535 5.69999C3.35231 4.49999 4.33418 3.55332 5.59098 2.85999L6.4943 4.25999C5.81354 4.73999 5.26369 5.27332 4.84476 5.85999C4.45201 6.44666 4.19017 7.12666 4.05926 7.89999C4.29491 7.79332 4.56983 7.73999 4.88403 7.73999C5.61716 7.73999 6.21938 7.97999 6.69067 8.45999C7.16197 8.93999 7.39762 9.55333 7.39762 10.3ZM14.6242 10.3C14.6242 11.0733 14.3755 11.7 13.878 12.18C13.3805 12.6333 12.7521 12.86 11.9928 12.86C11.0764 12.86 10.3171 12.5533 9.71484 11.94C9.13881 11.3266 8.85079 10.4467 8.85079 9.29999C8.85079 8.07332 9.19117 6.87332 9.87194 5.69999C10.5789 4.49999 11.5608 3.55332 12.8176 2.85999L13.7209 4.25999C13.0401 4.73999 12.4903 5.27332 12.0713 5.85999C11.6786 6.44666 11.4168 7.12666 11.2858 7.89999C11.5215 7.79332 11.7964 7.73999 12.1106 7.73999C12.8437 7.73999 13.446 7.97999 13.9173 8.45999C14.3886 8.93999 14.6242 9.55333 14.6242 10.3Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class:
|
||||||
|
'absolute start-0 top-0 h-16 w-16 -translate-x-6 -translate-y-8 transform text-neutral-300 dark:text-neutral-700',
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
viewBox: '0 0 16 16',
|
||||||
|
fill: 'currentColor',
|
||||||
|
},
|
||||||
|
question: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '1.5',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
chatBubble: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '1.5',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
mapPin: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d: 'M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '1.5',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
envelopeOpen: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M21.75 9v.906a2.25 2.25 0 0 1-1.183 1.981l-6.478 3.488M2.25 9v.906a2.25 2.25 0 0 0 1.183 1.981l6.478 3.488m8.839 2.51-4.66-2.51m0 0-1.023-.55a2.25 2.25 0 0 0-2.134 0l-1.022.55m0 0-4.661 2.51m16.5 1.615a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V8.844a2.25 2.25 0 0 1 1.183-1.981l7.5-4.039a2.25 2.25 0 0 1 2.134 0l7.5 4.039a2.25 2.25 0 0 1 1.183 1.98V19.5Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '1.5',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
earth: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'm20.893 13.393-1.135-1.135a2.252 2.252 0 0 1-.421-.585l-1.08-2.16a.414.414 0 0 0-.663-.107.827.827 0 0 1-.812.21l-1.273-.363a.89.89 0 0 0-.738 1.595l.587.39c.59.395.674 1.23.172 1.732l-.2.2c-.212.212-.33.498-.33.796v.41c0 .409-.11.809-.32 1.158l-1.315 2.191a2.11 2.11 0 0 1-1.81 1.025 1.055 1.055 0 0 1-1.055-1.055v-1.172c0-.92-.56-1.747-1.414-2.089l-.655-.261a2.25 2.25 0 0 1-1.383-2.46l.007-.042a2.25 2.25 0 0 1 .29-.787l.09-.15a2.25 2.25 0 0 1 2.37-1.048l1.178.236a1.125 1.125 0 0 0 1.302-.795l.208-.73a1.125 1.125 0 0 0-.578-1.315l-.665-.332-.091.091a2.25 2.25 0 0 1-1.591.659h-.18c-.249 0-.487.1-.662.274a.931.931 0 0 1-1.458-1.137l1.411-2.353a2.25 2.25 0 0 0 .286-.76m11.928 9.869A9 9 0 0 0 8.965 3.525m11.928 9.868A9 9 0 1 1 8.965 3.525',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class: 'w-4 h-4 flex-shrink-0',
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '1.5',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
party: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M5.8 11.3 2 22l10.7-3.79',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d: 'M4 3h.01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d: 'M22 8h.01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d: 'M15 2h.01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d: 'M22 20h.01',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d: 'm22 2-2.24.75a2.9 2.9 0 0 0-1.96 3.12v0c.1.86-.57 1.63-1.45 1.63h-.38c-.86 0-1.6.6-1.76 1.44L14 10',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d: 'm22 13-.82-.33c-.86-.34-1.82.2-1.98 1.11v0c-.11.7-.72 1.22-1.43 1.22H17',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d: 'm11 2 .33.82c.34.86-.2 1.82-1.11 1.98v0C9.52 4.9 9 5.52 9 6.23V7',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
d: 'M11 13c1.93 1.93 2.83 4.17 2 5-.83.83-3.07-.07-5-2-1.93-1.93-2.83-4.17-2-5 .83-.83 3.07.07 5 2Z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class:
|
||||||
|
'w-6 h-6 group-hover:text-steel dark:group-hover:text-steel transition-all duration-200 text-neutral-600 dark:text-neutral-300',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class:
|
||||||
|
'w-8 h-8 group-hover:text-steel dark:group-hover:text-steel transition-all duration-200 text-neutral-600 dark:text-neutral-300',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
sun: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
circleCx: '12',
|
||||||
|
circleCy: '12',
|
||||||
|
circleR: '5',
|
||||||
|
class:
|
||||||
|
'icon-light absolute h-5 w-5 scale-100 rotate-0 text-neutral-800 transition-all duration-500 dark:scale-0 dark:-rotate-90 dark:text-neutral-200',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
moon: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class:
|
||||||
|
'icon-dark absolute h-5 w-5 scale-0 rotate-90 text-neutral-800 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-neutral-200',
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
viewBox: '0 0 24 24',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
arrow: {
|
||||||
|
paths: [
|
||||||
|
{
|
||||||
|
d: 'M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
class:
|
||||||
|
'icon-dark absolute h-5 w-5 scale-0 rotate-90 text-neutral-800 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-neutral-200',
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
viewBox: '0 0 20 20',
|
||||||
|
fill: 'none',
|
||||||
|
strokeWidth: '2',
|
||||||
|
strokeLinecap: 'round',
|
||||||
|
strokeLinejoin: 'round',
|
||||||
|
stroke: 'currentColor',
|
||||||
|
},
|
||||||
|
};
|
17
src/components/ui/images/Image.astro
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
import { ImageMetadata } from 'astro';
|
||||||
|
import { blurStyle } from '@support/image';
|
||||||
|
|
||||||
|
interface FsPathImage extends ImageMetadata {
|
||||||
|
fsPath?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = Astro.props;
|
||||||
|
|
||||||
|
const image = props.src as FsPathImage;
|
||||||
|
const showBlur = !props.disableBlur;
|
||||||
|
const blurCSS = image.fsPath && showBlur ? await blurStyle(image.fsPath) : {};
|
||||||
|
---
|
||||||
|
|
||||||
|
<Image {...props} style={blurCSS} inferSize={true} />
|
11
src/components/ui/logos/BrandLogo.astro
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
|
import Image from '@components/ui/images/Image.astro';
|
||||||
|
import logo from '@images/brand_logo.png';
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
---
|
||||||
|
|
||||||
|
<Image src={logo} alt={global.name} {...Astro.props} draggable="false" loading="eager" />
|
129
src/components/ui/sections/Education.astro
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
|
import type { Education } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const education = await directus.request(
|
||||||
|
readItems('site_education', {
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-graduationDate'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const certificate = await directus.request(
|
||||||
|
readItems('site_certificate', {
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-issuerDate'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseClasses = ' rounded-xl flex flex-col';
|
||||||
|
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
|
||||||
|
const bgColorClasses =
|
||||||
|
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
|
||||||
|
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class:list={['order-first flex flex-col gap-4', Astro.props.className]}>
|
||||||
|
<h3
|
||||||
|
class="smooth-reveal-1 relative flex w-full items-center gap-3 pb-5 text-5xl text-neutral-800 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
Education
|
||||||
|
</h3>
|
||||||
|
<div class="ml-8">
|
||||||
|
<h4 class="smooth-reveal-1 pt-5 text-2xl font-semibold text-neutral-800 dark:text-neutral-200">
|
||||||
|
University
|
||||||
|
</h4>
|
||||||
|
<ul class="space-y-4 py-3">
|
||||||
|
{
|
||||||
|
education.map(({ institution, area, url }) => {
|
||||||
|
return (
|
||||||
|
<div class="smooth-reveal-cards mt-4 grid grid-cols-3 gap-4 rounded-xl">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class={`p-4 transition-all duration-300 md:p-5 ${shadowClasses} ${bgColorClasses} ${baseClasses} ${borderClasses}`}
|
||||||
|
>
|
||||||
|
<h3 class="flex flex-row text-lg font-bold text-neutral-800 dark:text-neutral-200">
|
||||||
|
<Icon
|
||||||
|
name="mdi:university-outline"
|
||||||
|
class="mr-2 h-3 w-3 translate-y-1 md:h-5 md:w-5"
|
||||||
|
/>
|
||||||
|
{institution}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-2 ml-7 text-xs font-medium text-neutral-600 uppercase dark:text-neutral-400">
|
||||||
|
{area}
|
||||||
|
</p>
|
||||||
|
<div class="ml-6 flex">
|
||||||
|
<a
|
||||||
|
class="group group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
href={url}
|
||||||
|
>
|
||||||
|
<div class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-[44px] items-center text-sm font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
|
||||||
|
<span class="relative inline-block overflow-hidden"> Visit Page </span>
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{
|
||||||
|
certificate.length > 0 && (
|
||||||
|
<div class="ml-8">
|
||||||
|
<h4 class="smooth-reveal-1 pt-8 text-2xl font-semibold text-neutral-800 dark:text-neutral-200">
|
||||||
|
Certificates
|
||||||
|
</h4>
|
||||||
|
<ul class="space-y-4 py-3">
|
||||||
|
{certificate.map(({ name, issuer, url }) => {
|
||||||
|
return (
|
||||||
|
<div class="smooth-reveal-cards mt-4 grid grid-cols-3 gap-4 rounded-xl">
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class={`p-4 transition-all duration-300 md:p-5 ${shadowClasses} ${bgColorClasses} ${baseClasses} ${borderClasses}`}
|
||||||
|
>
|
||||||
|
<h3 class="flex flex-row text-lg font-bold text-neutral-800 dark:text-neutral-200">
|
||||||
|
<Icon
|
||||||
|
name="mdi:script-text-outline"
|
||||||
|
class="mr-2 h-3 w-3 translate-y-1 md:h-5 md:w-5"
|
||||||
|
/>
|
||||||
|
{name}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-2 ml-7 text-xs font-medium text-neutral-600 uppercase dark:text-neutral-400">
|
||||||
|
{issuer}
|
||||||
|
</p>
|
||||||
|
<div class="ml-6 flex">
|
||||||
|
<a
|
||||||
|
class="group group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
href={url}
|
||||||
|
>
|
||||||
|
<div class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-[44px] items-center text-sm font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
|
||||||
|
<span class="relative inline-block overflow-hidden"> Visit Page </span>
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</section>
|
152
src/components/ui/sections/Experience.astro
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
|
import type { Experience } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const experiences = await directus.request(
|
||||||
|
readItems('site_experience', {
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-endDate'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
class:list={['flex flex-col gap-4', Astro.props.className]}
|
||||||
|
|
||||||
|
>
|
||||||
|
<h3 class="relative smooth-reveal-1 flex w-full items-center gap-3 pb-10 text-5xl text-neutral-800 dark:text-neutral-200">Experience</h3>
|
||||||
|
<ul class="ml-8 w-full flex flex-col">
|
||||||
|
{
|
||||||
|
experiences.map(
|
||||||
|
(experience: Experience) => {
|
||||||
|
const startYear = new Date(experience.startDate).getFullYear();
|
||||||
|
const endYear = experience.endDate != null ? new Date(experience.endDate).getFullYear() : 'Present';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li class="relative">
|
||||||
|
<div class="group smooth-reveal-2 relative grid pb-1 transition-all sm:grid-cols-18 sm:gap-8 md:gap-6 lg:hover:!opacity-100">
|
||||||
|
<header class="relative mt-1 text-lg font-semibold sm:col-span-3 text-neutral-800 dark:text-neutral-200">
|
||||||
|
<time datetime={experience.startDate} data-title={experience.startDate}>
|
||||||
|
{startYear}
|
||||||
|
</time>{' '}
|
||||||
|
-{' '}
|
||||||
|
<time datetime={experience.endDate} data-title={experience.endDate}>
|
||||||
|
{endYear}
|
||||||
|
</time>
|
||||||
|
</header>
|
||||||
|
<div class="relative flex flex-col pb-6 before:absolute before:mt-8 before:-ml-6 before:h-full before:w-px before:bg-stone-400 sm:col-span-12">
|
||||||
|
<div class="absolute mt-4 h-2 w-2 -translate-x-[1.71rem] rounded-full bg-stone-400" />
|
||||||
|
<h3>
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center text-2xl leading-tight font-semibold"
|
||||||
|
aria-label="{position} - {company}"
|
||||||
|
>
|
||||||
|
<span class="text-neutral-800 dark:text-neutral-200">
|
||||||
|
{experience.position} <span>@</span>
|
||||||
|
{experience.url ? (
|
||||||
|
<a
|
||||||
|
class="hover:text-steel dark:hover:text-bermuda"
|
||||||
|
href={experience.url}
|
||||||
|
title={`Ver ${experience.name}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{experience.name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span>{experience.name}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
{(experience.location || experience.location_type) && (
|
||||||
|
<div class="text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
{experience.location} {experience.location && experience.location_type && '-'} {experience.location_type}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="text-md mt-4 flex flex-col gap-4" x-data="{ expanded: false }">
|
||||||
|
{experience.summary && (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Summary:</h4>
|
||||||
|
<ul class="flex list-disc flex-col gap-2 text-neutral-700 dark:text-neutral-400 [&>li]:ml-4">
|
||||||
|
{Array.isArray(experience.summary) ? (
|
||||||
|
experience.summary.map((item) => ({ item }))
|
||||||
|
) : (
|
||||||
|
<li class="marker:text-steel dark:marker:text-bermuda">{experience.summary}</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(experience.responsibilities || experience.achievements) && (
|
||||||
|
<div class="relative flex flex-col gap-4 max-sm:!h-auto md:after:absolute md:after:bottom-0 md:after:h-12 md:after:w-full md:after:bg-gradient-to-t md:after:from-neutral-200 dark:md:after:from-stone-700 md:after:content-[''] " :class="expanded ? 'after:hidden' : ''" x-show="expanded" x-collapse.min.50px>
|
||||||
|
{experience.responsibilities && (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Responsibilities:</h4>
|
||||||
|
<ul class="text-neutral-700 dark:text-neutral-400 [&>li]:ml-4 flex list-disc flex-col gap-2">
|
||||||
|
{experience.responsibilities.map(responsibility => (
|
||||||
|
<li class="marker:text-steel dark:marker:text-bermuda">{responsibility}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{experience.achievements && (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Achievements:</h4>
|
||||||
|
<ul class="text-neutral-700 dark:text-neutral-400 [&>li]:ml-4 flex list-disc flex-col gap-2">
|
||||||
|
{experience.achievements.map(achievement => (
|
||||||
|
<li class="marker:text-steel dark:marker:text-bermuda">{achievement}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="expanded = ! expanded" class="group/more w-fit cursor-pointer items-center justify-center gap-1.5 text-xs underline text-neutral-700 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-neutral-400 transition-all flex">
|
||||||
|
<span x-text="expanded ? 'Show less' : 'Show more'">Show more</span>
|
||||||
|
<svg
|
||||||
|
class="h-4 w-4 duration-200 ease-out group-hover/more:translate-y-0.5"
|
||||||
|
:class="{ 'rotate-180': expanded }"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="6 9 12 15 18 9" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul class="flex print:hidden flex-wrap gap-2" aria-label="Technologies used">
|
||||||
|
{experience.skills && experience.skills.map(skill => {
|
||||||
|
const iconName = skill.toLowerCase();
|
||||||
|
return (
|
||||||
|
<li class="bg-steel/20 border-steel/20 text-neutral-800 dark:bg-bermuda/20 dark:border-bermuda/20 dark:text-neutral-200 flex gap-1 items-center border-solid border rounded-md px-2 py-0.5 text-xs">
|
||||||
|
<Icon name={`mdi:${iconName}`} /> <span>{skill}</span>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Alpine Plugins -->
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Alpine Core -->
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
37
src/components/ui/sections/FeaturesSection.astro
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
import FeaturesCard from '@components/ui/cards/FeaturesCard.astro';
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="mx-auto mb-20 max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full">
|
||||||
|
<div
|
||||||
|
class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:gap-x-12 sm:gap-y-0 lg:gap-x-24"
|
||||||
|
>
|
||||||
|
<div class="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="grid gap-3 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3">
|
||||||
|
<FeaturesCard
|
||||||
|
title="Cloud Engineer"
|
||||||
|
description="Full stack and cloud engineer."
|
||||||
|
url="/about"
|
||||||
|
icon="mdi:cloud-outline"
|
||||||
|
/>
|
||||||
|
<FeaturesCard
|
||||||
|
title="Homelab"
|
||||||
|
description="Tinkering, testing, deploying, etc, etc ..."
|
||||||
|
url="/categories/homelab/"
|
||||||
|
icon="mdi:home-variant-outline"
|
||||||
|
/>
|
||||||
|
<FeaturesCard
|
||||||
|
title="Email"
|
||||||
|
description={`Send me a message.`}
|
||||||
|
url=`mailto:${global.email}`
|
||||||
|
icon="mdi:email-fast"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
35
src/components/ui/sections/HeaderSection.astro
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subTitle: string;
|
||||||
|
btnExists?: boolean;
|
||||||
|
btnTitle?: string;
|
||||||
|
btnURL?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, subTitle, btnExists, btnTitle, btnURL } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="mx-auto mt-10 px-4 sm:px-6 lg:px-8 lg:pt-10 2xl:max-w-full">
|
||||||
|
<div class="flex-wrap md:flex md:items-center md:justify-between">
|
||||||
|
<div class="w-full md:w-auto">
|
||||||
|
<h1
|
||||||
|
class="smooth-reveal block text-4xl font-bold tracking-tight text-balance text-neutral-800 md:text-5xl lg:text-6xl dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p class="smooth-reveal mt-4 text-lg text-pretty text-neutral-600 dark:text-neutral-400">
|
||||||
|
{subTitle}
|
||||||
|
</p>
|
||||||
|
{
|
||||||
|
btnExists ? (
|
||||||
|
<div class="smooth-reveal mt-4 md:mt-8">
|
||||||
|
<PrimaryCTA title={btnTitle} url={btnURL} />
|
||||||
|
</div>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
63
src/components/ui/sections/HeroSection.astro
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
|
||||||
|
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
|
||||||
|
import Image from '@components/ui/images/Image.astro';
|
||||||
|
|
||||||
|
const { title, subTitle, primaryBtn, primaryBtnURL, secondaryBtn, secondaryBtnURL, src, alt } =
|
||||||
|
Astro.props;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subTitle?: string;
|
||||||
|
primaryBtn?: string;
|
||||||
|
primaryBtnURL?: string;
|
||||||
|
secondaryBtn?: string;
|
||||||
|
secondaryBtnURL?: string;
|
||||||
|
src?: any;
|
||||||
|
alt?: string;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<section
|
||||||
|
class="mx-auto grid max-w-[85rem] gap-4 px-4 py-14 sm:px-6 md:grid-cols-2 md:items-center md:gap-8 lg:px-8 2xl:max-w-full"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1
|
||||||
|
class="smooth-reveal block text-3xl font-bold tracking-tight text-balance text-neutral-800 sm:text-4xl lg:text-7xl lg:leading-tight dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
<Fragment set:html={title} />
|
||||||
|
</h1>
|
||||||
|
{
|
||||||
|
subTitle && (
|
||||||
|
<p class="smooth-reveal mt-6 text-lg leading-relaxed text-pretty text-neutral-700 lg:w-4/5 dark:text-neutral-300">
|
||||||
|
{subTitle}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="smooth-reveal mt-7 grid w-full gap-3 sm:inline-flex">
|
||||||
|
{primaryBtn && <PrimaryCTA title={primaryBtn} url={primaryBtnURL} />}
|
||||||
|
{secondaryBtn && <SecondaryCTA title={secondaryBtn} url={secondaryBtnURL} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="smooth-reveal-fade hidden w-full md:block">
|
||||||
|
<div class="top-12 flex w-full justify-center overflow-hidden md:ml-4">
|
||||||
|
{
|
||||||
|
src && alt && (
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
class="h-full w-[420px] scale-100 object-cover object-center"
|
||||||
|
draggable="false"
|
||||||
|
loading="eager"
|
||||||
|
format="webp"
|
||||||
|
quality="low"
|
||||||
|
widths={[840]}
|
||||||
|
disableBlur={true}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
164
src/components/ui/sections/HeroSectionAlt.astro
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
---
|
||||||
|
import GiteaBtn from '@components/ui/buttons/GiteaBtn.astro';
|
||||||
|
|
||||||
|
const { title, subTitle, url } = Astro.props;
|
||||||
|
const btnTitle = 'Continue to Gitea';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subTitle?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="lg:px- relative mx-auto mb-20 max-w-[85rem] px-4 pt-30 pb-30 sm:px-6">
|
||||||
|
<div
|
||||||
|
class="smooth-reveal absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="animate-hover animate-hover-1"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1.5"
|
||||||
|
color="#ea580c"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#ea580c"
|
||||||
|
stroke="#ea580c"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 23a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM3 8a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM3 18a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke="#ea580c"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M21 7.353v9.294a.6.6 0 0 1-.309.525l-8.4 4.666a.6.6 0 0 1-.582 0l-8.4-4.666A.6.6 0 0 1 3 16.647V7.353a.6.6 0 0 1 .309-.524l8.4-4.667a.6.6 0 0 1 .582 0l8.4 4.667a.6.6 0 0 1 .309.524Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke="#ea580c"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="m3.528 7.294 8.18 4.544a.6.6 0 0 0 .583 0l8.209-4.56M12 21v-9"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="smooth-reveal absolute top-0 left-[85%] scale-75">
|
||||||
|
<svg
|
||||||
|
class="animate-hover animate-hover-2"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1.5"
|
||||||
|
color="#fbbf24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="#fbbf24"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"></path>
|
||||||
|
<path
|
||||||
|
fill="#fbbf24"
|
||||||
|
stroke="#fbbf24"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M5 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"></path>
|
||||||
|
<path stroke="#fbbf24" stroke-linecap="round" stroke-linejoin="round" d="M5 10.5V9M5 15v-1.5"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="#fbbf24"
|
||||||
|
stroke="#fbbf24"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M5 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM19 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"></path>
|
||||||
|
<path
|
||||||
|
stroke="#fbbf24"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M10.5 19H9M15 19h-1.5"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="smooth-reveal absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="animate-hover animate-hover-3"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1.5"
|
||||||
|
color="#a3a3a3"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="#a3a3a3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M5.164 17c.29-1.049.67-2.052 1.132-3M11.5 7.794A16.838 16.838 0 0 1 14 6.296M4.5 22a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke="#a3a3a3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.5 12a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5ZM19.5 7a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- Hero Section Heading -->
|
||||||
|
<div class="smooth-reveal-2 mx-auto mt-5 max-w-xl text-center">
|
||||||
|
<h2
|
||||||
|
class="block text-4xl leading-tight font-bold tracking-tight text-balance text-neutral-800 md:text-5xl lg:text-5xl dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<!-- Hero Section Sub-heading -->
|
||||||
|
<div class="smooth-reveal-2 mx-auto mt-5 max-w-3xl text-center">
|
||||||
|
{
|
||||||
|
subTitle && (
|
||||||
|
<p class="text-lg text-pretty text-neutral-600 dark:text-neutral-400">{subTitle}</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<!-- Github Button -->
|
||||||
|
{
|
||||||
|
url && (
|
||||||
|
<div class="smooth-reveal-2 mt-8 flex justify-center gap-3">
|
||||||
|
<GiteaBtn url={url} title={btnTitle} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes animate-hover {
|
||||||
|
from {
|
||||||
|
transform: translateY(15px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(-15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-hover {
|
||||||
|
animation: animate-hover ease-in-out;
|
||||||
|
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-direction: alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-hover-1 {
|
||||||
|
animation-duration: 5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-hover-2 {
|
||||||
|
animation-duration: 5.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-hover-3 {
|
||||||
|
animation-duration: 6s;
|
||||||
|
}
|
||||||
|
</style>
|
35
src/components/ui/sections/LatestPosts.astro
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
import type { Post } from '@lib/directusTypes';
|
||||||
|
import BlogCard from '@components/blog/BlogCard.astro';
|
||||||
|
|
||||||
|
const posts = await directus.request(
|
||||||
|
readItems('posts', {
|
||||||
|
filter: { published: { _eq: true } },
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-published_date'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const recentPosts = posts
|
||||||
|
.sort((a: Post, b: Post) => b.published_date.getTime() - a.published_date.getTime())
|
||||||
|
.slice(0, 3);
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="mx-auto mb-20 max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full">
|
||||||
|
<div class="mx-auto mb-10 max-w-2xl text-center lg:mb-14">
|
||||||
|
<h1
|
||||||
|
class="smooth-reveal block text-4xl font-bold text-neutral-800 md:text-5xl md:leading-tight lg:text-5xl dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
Latest Posts
|
||||||
|
</h1>
|
||||||
|
<p class="smooth-reveal mt-1 text-pretty text-neutral-600 dark:text-neutral-300">
|
||||||
|
More recent posts.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{recentPosts.map((b) => <BlogCard post={b} />)}
|
||||||
|
</div>
|
||||||
|
</section>
|
75
src/components/ui/sections/Projects.astro
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
|
import type { Project } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const projects = await directus.request(
|
||||||
|
readItems('site_projects', {
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-isActive'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseClasses = 'smooth-reveal-cards rounded-xl flex flex-col';
|
||||||
|
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
|
||||||
|
const bgColorClasses =
|
||||||
|
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
|
||||||
|
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
|
||||||
|
<h3
|
||||||
|
class="relative flex w-full items-center gap-3 pb-10 text-5xl text-neutral-800 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
Projects
|
||||||
|
</h3>
|
||||||
|
<div class="ml-8 grid grid-cols-1 gap-3 md:grid-cols-2 print:flex print:flex-col">
|
||||||
|
{
|
||||||
|
projects.map((project: Project) => {
|
||||||
|
return (
|
||||||
|
<div class={`${baseClasses}`}>
|
||||||
|
<div
|
||||||
|
class={`rounded-xl transition-all duration-300 ${borderClasses} ${bgColorClasses} ${shadowClasses}`}
|
||||||
|
>
|
||||||
|
<div class="p-4 md:p-10">
|
||||||
|
<h3 class="text-lg font-bold text-gray-800 dark:text-white">{project.name}</h3>
|
||||||
|
<p class="mt-2 text-gray-500 dark:text-neutral-400">{project.description}</p>
|
||||||
|
<ul class="mt-1 flex list-disc flex-col gap-2 text-sm text-gray-500 dark:text-neutral-400 [&>li]:ml-4">
|
||||||
|
{project.highlights.map((highlight) => {
|
||||||
|
return <li class="marker:text-yellow-500">{highlight}</li>;
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
<div class="flex">
|
||||||
|
<a
|
||||||
|
class="group group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
href={project.url}
|
||||||
|
>
|
||||||
|
<div class="group-hover:text-steel dark:group-hover:text-bermuda transition-text text-md relative z-10 mx-auto flex min-h-[44px] items-center font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
|
||||||
|
<span class="relative inline-block overflow-hidden"> Visit Page </span>
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
class="group group-hover relative ml-auto inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
|
||||||
|
href={project.source}
|
||||||
|
>
|
||||||
|
<div class="group-hover:text-gitea-primary dark:group-hover:text-gitea-primary transition-text text-md relative z-10 mx-auto flex min-h-[44px] items-center font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
|
||||||
|
<span class="relative inline-block overflow-hidden"> Source </span>
|
||||||
|
<Icon name="pajamas:gitea" class="ml-2 translate-y-0.5" />
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
250
src/components/ui/sections/Skills.astro
Normal file
@@ -0,0 +1,250 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
|
import type { Skill } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const skills = await directus.request(
|
||||||
|
readItems('site_skills', {
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-date_created'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseClasses = 'mx-2 min-w-[220px] sm:mx-4 sm:min-w-[280px]';
|
||||||
|
const borderClasses =
|
||||||
|
'border border-neutral-100 hover:border-neutral-200 dark:border-stone-500/20 dark:hover:border-neutral-800';
|
||||||
|
const bgColorClasses = 'bg-neutral-100/80 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
|
||||||
|
const hoverClasses = 'hover:-translate-y-2 hover:scale-105 ';
|
||||||
|
const shadowClasses = 'shadow-xs hover:shadow-lg';
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
|
||||||
|
<h3
|
||||||
|
class="relative flex w-full items-center gap-3 pb-4 text-5xl text-neutral-800 dark:text-neutral-200"
|
||||||
|
>
|
||||||
|
Skills
|
||||||
|
</h3>
|
||||||
|
<div class="">
|
||||||
|
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
|
||||||
|
<!-- Main slider container -->
|
||||||
|
<div class="slider-track animate-slide flex">
|
||||||
|
{
|
||||||
|
[...skills, ...skills, ...skills].map((skill: Skill) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={`skill-card transform rounded-xl transition-all duration-300 ${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${shadowClasses}`}
|
||||||
|
>
|
||||||
|
<div class="p-4 sm:p-6">
|
||||||
|
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
||||||
|
<div class="flex items-center gap-2 sm:gap-4">
|
||||||
|
<div class="flex transform items-center justify-center rounded-lg text-neutral-800 transition-transform group-hover:rotate-12 dark:text-neutral-200">
|
||||||
|
<Icon name={skill.icon} class="h-8 w-8 sm:h-12 sm:w-12" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-base font-semibold text-neutral-900 sm:text-xl dark:text-neutral-100">
|
||||||
|
{skill.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<span class="rounded-full bg-neutral-200 px-2 py-0.5 font-mono text-xs text-neutral-700 sm:px-2.5 sm:py-1 sm:text-sm dark:bg-neutral-800 dark:text-neutral-300">
|
||||||
|
{skill.level}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative h-1.5 w-full overflow-hidden rounded-full bg-stone-500/20 sm:h-2 dark:bg-stone-500/20">
|
||||||
|
<div
|
||||||
|
class="progress-bar-animate from-steel via-bermuda to-steel absolute top-0 left-0 h-full rounded-full bg-gradient-to-r transition-all duration-1000"
|
||||||
|
style={`width: ${skill.level}%`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-1 flex justify-between font-mono text-[10px] text-neutral-600 sm:mt-2 sm:text-xs dark:text-neutral-400">
|
||||||
|
<span>Beginner</span>
|
||||||
|
<span>Advanced</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Gradient overlays for smooth fade effect -->
|
||||||
|
<div
|
||||||
|
class="absolute top-0 bottom-0 left-0 z-10 w-12 bg-gradient-to-r from-neutral-200 to-transparent sm:w-24 dark:from-stone-700"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="absolute top-0 right-0 bottom-0 z-10 w-12 bg-gradient-to-l from-neutral-200 to-transparent sm:w-24 dark:from-stone-700"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
|
// Create seamless infinite scrolling effect
|
||||||
|
function setupInfiniteScroll() {
|
||||||
|
const cards = document.querySelectorAll('.skill-card');
|
||||||
|
if (!cards.length) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupInfiniteScroll();
|
||||||
|
|
||||||
|
// Add hover effects to cards - only on non-touch devices
|
||||||
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||||
|
const cards = document.querySelectorAll('.skill-card');
|
||||||
|
|
||||||
|
if (!isTouchDevice) {
|
||||||
|
cards.forEach((card) => {
|
||||||
|
card.addEventListener('mousemove', (e) => {
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
const centerX = rect.width / 2;
|
||||||
|
const centerY = rect.height / 2;
|
||||||
|
|
||||||
|
const angleX = (y - centerY) / 15;
|
||||||
|
const angleY = (centerX - x) / 15;
|
||||||
|
|
||||||
|
card.style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) scale(1.08) translateZ(20px)`;
|
||||||
|
|
||||||
|
// Dynamic shadow based on tilt
|
||||||
|
const shadowX = (x - centerX) / 25;
|
||||||
|
const shadowY = (y - centerY) / 25;
|
||||||
|
card.style.boxShadow = `
|
||||||
|
${shadowX}px ${shadowY}px 20px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 20px rgba(0, 0, 0, 0.05)
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('mouseleave', () => {
|
||||||
|
card.style.transform = '';
|
||||||
|
card.style.boxShadow = '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Simpler effects for touch devices
|
||||||
|
cards.forEach((card) => {
|
||||||
|
card.addEventListener('touchstart', () => {
|
||||||
|
card.classList.add('is-touched');
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('touchend', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
card.classList.remove('is-touched');
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Tech Stack Slider */
|
||||||
|
.slider-track {
|
||||||
|
width: fit-content;
|
||||||
|
animation: scroll 40s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.slider-track {
|
||||||
|
animation: scroll 80s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-stack-slider:hover .slider-track {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card {
|
||||||
|
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:hover {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce animation complexity on mobile */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.skill-card {
|
||||||
|
transition:
|
||||||
|
transform 0.3s ease,
|
||||||
|
box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:hover {
|
||||||
|
transform: translateY(-5px) !important;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -10%;
|
||||||
|
left: -10%;
|
||||||
|
width: 120%;
|
||||||
|
height: 120%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgba(255, 255, 255, 0.1) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 70%
|
||||||
|
);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:hover:before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-animate {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-animate:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
animation: progress-shine 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-shine {
|
||||||
|
0% {
|
||||||
|
left: -100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
66
src/config.ts
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
export interface NavigationLink {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
|
||||||
|
export const WorkInformation = [
|
||||||
|
{
|
||||||
|
name: 'Tech Startup',
|
||||||
|
position: 'Junior Web Developer',
|
||||||
|
location_type: 'On site',
|
||||||
|
location: 'Auckland, New Zealand',
|
||||||
|
url: 'https://techstartup.com',
|
||||||
|
startDate: '2024-01-01',
|
||||||
|
endDate: null,
|
||||||
|
summary:
|
||||||
|
'Developing and maintaining web applications using JavaScript, HTML, and CSS. Collaborating with the team to implement new features and fix bugs.',
|
||||||
|
highlights: ['Improved website performance by optimizing code'],
|
||||||
|
responsibilities: [
|
||||||
|
'Collaborated with senior developers to design and implement web applications using modern JavaScript frameworks.',
|
||||||
|
'Assisted in debugging and optimizing front-end code to ensure smooth user experiences.',
|
||||||
|
'Participated in code reviews and contributed to improving coding standards within the team.',
|
||||||
|
],
|
||||||
|
achievements: [
|
||||||
|
'Developing and maintaining web applications using JavaScript, HTML, and CSS. Collaborating with the team to implement new features and fix bugs.',
|
||||||
|
],
|
||||||
|
skills: ['React', 'Tailwind', 'GitHub'],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NavigationLinks: NavigationLink[] = [
|
||||||
|
{ name: 'Home', url: '/' },
|
||||||
|
{ name: 'Blog', url: '/blog/' },
|
||||||
|
{ name: 'Categories', url: '/categories/' },
|
||||||
|
{ name: 'About Me', url: '/about/' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FooterLinks: NavigationLink[] = [
|
||||||
|
{ name: 'RSS', url: '/rss.xml' },
|
||||||
|
{ name: 'Gitea', url: '/https://gitea.alexlebens.dev' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SEO = {
|
||||||
|
title: global.name,
|
||||||
|
description: global.about,
|
||||||
|
structuredData: {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebPage',
|
||||||
|
inLanguage: 'en-US',
|
||||||
|
'@id': global.site_url,
|
||||||
|
url: global.site_url,
|
||||||
|
name: global.name,
|
||||||
|
description: global.about,
|
||||||
|
isPartOf: {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
url: global.site_url,
|
||||||
|
name: global.name,
|
||||||
|
description: global.about,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
4
src/content/categories/books.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Books 📖'
|
||||||
|
description: 'Books I have read or listened to'
|
||||||
|
---
|
4
src/content/categories/cloud.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Cloud ☁️'
|
||||||
|
description: "Its just someone else's server"
|
||||||
|
---
|
4
src/content/categories/homelab.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Homelab 🏠'
|
||||||
|
description: 'What happens when rack servers find a home'
|
||||||
|
---
|
4
src/content/categories/kubernetes.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Kubernetes ☸️'
|
||||||
|
description: 'The container orchestration system'
|
||||||
|
---
|
4
src/content/categories/life.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Life 🏃🏻'
|
||||||
|
description: 'Just random musings on everyday stuff'
|
||||||
|
---
|
4
src/content/categories/minnesota.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Minnesota 🌳'
|
||||||
|
description: 'Land of 10,000 Lakes'
|
||||||
|
---
|
4
src/content/categories/postgresql.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'PostgreSQL'
|
||||||
|
description: 'PostgreSQL is an open-source relational database management system (RDBMS)'
|
||||||
|
---
|
4
src/content/categories/python.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Python 🐍'
|
||||||
|
description: 'Generally my go to language'
|
||||||
|
---
|
4
src/content/categories/tool.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Tool 🪜'
|
||||||
|
description: 'Usually just the software kind'
|
||||||
|
---
|
4
src/content/categories/whatis.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'What Is?'
|
||||||
|
description: 'A series on discovery'
|
||||||
|
---
|
12
src/content/config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
|
||||||
|
const categoryCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: () =>
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { categories: categoryCollection };
|
BIN
src/images/autumn_bench.png
Normal file
After Width: | Height: | Size: 11 MiB |
BIN
src/images/autumn_mountain.png
Normal file
After Width: | Height: | Size: 13 MiB |
BIN
src/images/autumn_tree.png
Normal file
After Width: | Height: | Size: 7.8 MiB |
BIN
src/images/brand_logo.png
Normal file
After Width: | Height: | Size: 98 KiB |
1
src/images/favicon.svg
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
src/images/favicon_icon.png
Normal file
After Width: | Height: | Size: 98 KiB |
1
src/images/favicon_icon.svg
Normal file
After Width: | Height: | Size: 9.9 KiB |
BIN
src/images/flowers.png
Normal file
After Width: | Height: | Size: 6.8 MiB |
BIN
src/images/portrait.avif
Normal file
After Width: | Height: | Size: 76 KiB |
@@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
import Layout from './Layout.astro';
|
|
||||||
|
|
||||||
import directus from '../../lib/directus';
|
|
||||||
import { readSingleton } from '@directus/sdk';
|
|
||||||
|
|
||||||
const global = await directus.request(readSingleton('global'));
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={global.title} description={global.description}>
|
|
||||||
<slot />
|
|
||||||
</Layout>
|
|
@@ -1,59 +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.description}>
|
<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';
|
||||||
|
})();
|
||||||
|
|
||||||
<script>
|
if (theme === 'light') {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.documentElement.classList.remove('dark');
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
} 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>
|
||||||
|
|
||||||
if (themeToggle) {
|
<style>
|
||||||
themeToggle.addEventListener('click', () => {
|
.bg-grid-pattern {
|
||||||
document.documentElement.classList.add('theme-switching');
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
const rippleElements = document.querySelectorAll('.theme-ripple');
|
:global(.dark) .bg-grid-pattern {
|
||||||
rippleElements.forEach((el) => {
|
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.25) 1px, transparent 1px);
|
||||||
el.classList.add('ripple-active');
|
}
|
||||||
setTimeout(() => {
|
</style>
|
||||||
el.classList.remove('ripple-active');
|
|
||||||
}, 600);
|
|
||||||
});
|
|
||||||
|
|
||||||
const event = new CustomEvent('themeChange', {
|
|
||||||
detail: {
|
|
||||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
document.documentElement.classList.remove('theme-switching');
|
|
||||||
}, 600);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const socialLinks = document.querySelectorAll('.social-link');
|
|
||||||
socialLinks.forEach((link) => {
|
|
||||||
link.addEventListener('mouseenter', () => {
|
|
||||||
link.classList.add('hover-active');
|
|
||||||
});
|
|
||||||
|
|
||||||
link.addEventListener('mouseleave', () => {
|
|
||||||
link.classList.remove('hover-active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
@@ -1,398 +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;
|
|
||||||
const published_date: string = post.published_date.toLocaleString();
|
|
||||||
|
|
||||||
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 mx-auto max-w-4xl dark:prose-invert lg:prose-lg">
|
|
||||||
<div class="mb-12">
|
|
||||||
<h1
|
|
||||||
class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl"
|
|
||||||
>
|
|
||||||
{post.title}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="mb-6 flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400">
|
|
||||||
<FormattedDate date={published_date} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TagList tags={post.tags} class="mt-2" />
|
|
||||||
</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} />
|
|
||||||
<!-- Convert URL to string -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
post.updated_date && (
|
|
||||||
<div class="mt-8 text-sm italic text-zinc-500 dark:text-zinc-400">
|
|
||||||
Last updated on <FormattedDate date={post.updated_date} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<slot name="after-article" />
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Blog post SPA transitions
|
|
||||||
function setupBlogPostTransitions() {
|
|
||||||
// Animate article entrance
|
|
||||||
const article = document.querySelector('article');
|
|
||||||
if (article) {
|
|
||||||
article.classList.add('article-entering');
|
|
||||||
|
|
||||||
// Remove class after animation completes
|
|
||||||
setTimeout(() => {
|
|
||||||
article.classList.remove('article-entering');
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure consistent code block styling
|
|
||||||
function updateCodeBlockStyles() {
|
|
||||||
document.querySelectorAll('pre').forEach((pre) => {
|
|
||||||
// Force the background color with !important for both light and dark mode
|
|
||||||
pre.setAttribute('style', 'background-color: #1e293b !important');
|
|
||||||
|
|
||||||
// Also apply to any nested code elements
|
|
||||||
const codeElements = pre.querySelectorAll('code');
|
|
||||||
codeElements.forEach((code) => {
|
|
||||||
code.setAttribute(
|
|
||||||
'style',
|
|
||||||
'background-color: transparent !important; color: #e5e7eb !important;'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial application
|
|
||||||
updateCodeBlockStyles();
|
|
||||||
|
|
||||||
// Watch for theme changes
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
updateCodeBlockStyles();
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
||||||
|
|
||||||
// Also run on any content changes that might add new code blocks
|
|
||||||
const contentObserver = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.addedNodes.length) {
|
|
||||||
updateCodeBlockStyles();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
contentObserver.observe(document.body, { childList: true, subtree: true });
|
|
||||||
|
|
||||||
// Clean up observers when navigating away
|
|
||||||
document.addEventListener('spa-navigation-start', () => {
|
|
||||||
observer.disconnect();
|
|
||||||
contentObserver.disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove the parallax effect for hero image
|
|
||||||
|
|
||||||
// Handle prev/next navigation links
|
|
||||||
const navLinks = document.querySelectorAll('.blog-nav-link');
|
|
||||||
navLinks.forEach((link) => {
|
|
||||||
if (!link.hasAttribute('data-spa-handled')) {
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('mouseenter', () => {
|
|
||||||
link.classList.add('nav-link-hover');
|
|
||||||
});
|
|
||||||
|
|
||||||
link.addEventListener('mouseleave', () => {
|
|
||||||
link.classList.remove('nav-link-hover');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate headings when they enter the viewport
|
|
||||||
const animateHeadings = () => {
|
|
||||||
const headings = document.querySelectorAll('article h2, article h3');
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add('heading-visible');
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
threshold: 0.2,
|
|
||||||
rootMargin: '0px 0px -100px 0px',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
headings.forEach((heading) => {
|
|
||||||
heading.classList.add('heading-animated');
|
|
||||||
observer.observe(heading);
|
|
||||||
});
|
|
||||||
|
|
||||||
return observer;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize heading animations
|
|
||||||
const headingObserver = animateHeadings();
|
|
||||||
|
|
||||||
// Enhance code blocks with syntax highlighting and copy button
|
|
||||||
function enhanceCodeBlocks() {
|
|
||||||
const codeBlocks = document.querySelectorAll('pre code');
|
|
||||||
|
|
||||||
codeBlocks.forEach((codeBlock) => {
|
|
||||||
// Skip if already processed
|
|
||||||
if (codeBlock.parentElement.classList.contains('enhanced')) return;
|
|
||||||
|
|
||||||
// Mark as enhanced
|
|
||||||
codeBlock.parentElement.classList.add('enhanced');
|
|
||||||
|
|
||||||
// Create copy button
|
|
||||||
const copyButton = document.createElement('button');
|
|
||||||
copyButton.className = 'copy-code-button';
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
||||||
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add copy functionality
|
|
||||||
copyButton.addEventListener('click', () => {
|
|
||||||
const code = codeBlock.textContent;
|
|
||||||
navigator.clipboard.writeText(code);
|
|
||||||
|
|
||||||
// Show copied feedback
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
||||||
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add copy button to pre element
|
|
||||||
codeBlock.parentElement.appendChild(copyButton);
|
|
||||||
|
|
||||||
// Fix line numbers implementation
|
|
||||||
const codeText = codeBlock.textContent;
|
|
||||||
const lines = codeText.split('\n');
|
|
||||||
|
|
||||||
const lineNumbers = document.createElement('div');
|
|
||||||
lineNumbers.className = 'line-numbers';
|
|
||||||
|
|
||||||
// Always include all lines, including empty ones
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const lineNumber = document.createElement('span');
|
|
||||||
lineNumber.textContent = i + 1;
|
|
||||||
lineNumbers.appendChild(lineNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
codeBlock.parentElement.classList.add('with-line-numbers');
|
|
||||||
codeBlock.parentElement.insertBefore(lineNumbers, codeBlock);
|
|
||||||
|
|
||||||
// Fix language label detection and display
|
|
||||||
const className = codeBlock.className;
|
|
||||||
const languageMatch = className.match(/language-(\w+)/);
|
|
||||||
|
|
||||||
if (languageMatch && languageMatch[1]) {
|
|
||||||
const language = languageMatch[1];
|
|
||||||
|
|
||||||
// Add language label at top right
|
|
||||||
const languageLabel = document.createElement('div');
|
|
||||||
languageLabel.className = 'language-label';
|
|
||||||
languageLabel.textContent = language;
|
|
||||||
codeBlock.parentElement.appendChild(languageLabel);
|
|
||||||
|
|
||||||
// Add language badge at bottom right with markdown syntax
|
|
||||||
const languageBadge = document.createElement('div');
|
|
||||||
languageBadge.className = 'language-badge';
|
|
||||||
languageBadge.textContent = `\`\`\`${language}`;
|
|
||||||
languageBadge.style.position = 'absolute';
|
|
||||||
languageBadge.style.bottom = '0.5rem';
|
|
||||||
languageBadge.style.right = '0.5rem';
|
|
||||||
languageBadge.style.fontSize = '0.7rem';
|
|
||||||
languageBadge.style.padding = '0.1rem 0.3rem';
|
|
||||||
languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)';
|
|
||||||
languageBadge.style.color = '#e5e7eb';
|
|
||||||
languageBadge.style.borderRadius = '0.25rem';
|
|
||||||
languageBadge.style.fontFamily =
|
|
||||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
|
||||||
languageBadge.style.zIndex = '10';
|
|
||||||
codeBlock.parentElement.appendChild(languageBadge);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhance tables with better styling
|
|
||||||
function enhanceTables() {
|
|
||||||
const tables = document.querySelectorAll('.markdown-content table');
|
|
||||||
|
|
||||||
tables.forEach((table) => {
|
|
||||||
if (table.classList.contains('enhanced-table')) return;
|
|
||||||
|
|
||||||
table.classList.add('enhanced-table');
|
|
||||||
|
|
||||||
// Wrap table in responsive container
|
|
||||||
const wrapper = document.createElement('div');
|
|
||||||
wrapper.className = 'table-container';
|
|
||||||
table.parentNode.insertBefore(wrapper, table);
|
|
||||||
wrapper.appendChild(table);
|
|
||||||
|
|
||||||
// Add zebra striping to rows
|
|
||||||
const rows = table.querySelectorAll('tbody tr');
|
|
||||||
rows.forEach((row, index) => {
|
|
||||||
if (index % 2 === 0) {
|
|
||||||
row.classList.add('even-row');
|
|
||||||
} else {
|
|
||||||
row.classList.add('odd-row');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhance blockquotes with icons
|
|
||||||
function enhanceBlockquotes() {
|
|
||||||
const blockquotes = document.querySelectorAll('.markdown-content blockquote');
|
|
||||||
|
|
||||||
blockquotes.forEach((blockquote) => {
|
|
||||||
if (blockquote.classList.contains('enhanced-quote')) return;
|
|
||||||
|
|
||||||
blockquote.classList.add('enhanced-quote');
|
|
||||||
|
|
||||||
// Add quote icon
|
|
||||||
const icon = document.createElement('div');
|
|
||||||
icon.className = 'quote-icon';
|
|
||||||
icon.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
blockquote.insertBefore(icon, blockquote.firstChild);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all enhancements
|
|
||||||
enhanceCodeBlocks();
|
|
||||||
enhanceTables();
|
|
||||||
enhanceBlockquotes();
|
|
||||||
|
|
||||||
// Clean up observers when navigating away
|
|
||||||
document.addEventListener('spa-navigation-start', () => {
|
|
||||||
if (headingObserver) {
|
|
||||||
headingObserver.disconnect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', setupBlogPostTransitions);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupBlogPostTransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupBlogPostTransitions);
|
|
||||||
|
|
||||||
// Also initialize when SPA navigation completes
|
|
||||||
document.addEventListener('spa-navigation-complete', setupBlogPostTransitions);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Enhanced 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Article entrance animation */
|
|
||||||
.article-entering {
|
|
||||||
animation: article-fade-in 0.8s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes article-fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Rest of the styles remain unchanged... */
|
|
||||||
</style>
|
|
@@ -1,336 +0,0 @@
|
|||||||
---
|
|
||||||
import Navigation from '../components/Navigation.astro';
|
|
||||||
import Footer from '../components/Footer.astro';
|
|
||||||
import Background from '../components/Background.astro';
|
|
||||||
import '../styles/global.css';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title?: string | undefined;
|
|
||||||
description?: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { title, description } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<meta name="generator" content={Astro.generator} />
|
|
||||||
<meta name="description" content={description} />
|
|
||||||
<title>{title}</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
</head>
|
|
||||||
<body
|
|
||||||
class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100"
|
|
||||||
>
|
|
||||||
<!-- Page transition overlay - for smooth transitions between pages -->
|
|
||||||
<div
|
|
||||||
id="page-transition"
|
|
||||||
class="pointer-events-none fixed inset-0 z-40 flex items-center justify-center bg-white opacity-0 transition-opacity duration-300 dark:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<div class="transition-spinner"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Background component with dot pattern and ambient glow -->
|
|
||||||
<Background />
|
|
||||||
|
|
||||||
<div class="mx-auto w-full max-w-3xl flex-grow px-4 sm:px-6">
|
|
||||||
<Navigation />
|
|
||||||
<main class="py-12">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
<Footer />
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// SPA transition system with history API
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
const mainContent = document.querySelector('main');
|
|
||||||
|
|
||||||
// Initialize content with entrance animation
|
|
||||||
if (mainContent) {
|
|
||||||
mainContent.classList.add('content-entering');
|
|
||||||
setTimeout(() => {
|
|
||||||
mainContent.classList.remove('content-entering');
|
|
||||||
}, 800);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to load content via fetch
|
|
||||||
async function loadContent(url) {
|
|
||||||
try {
|
|
||||||
// Show transition overlay
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0', 'pointer-events-none');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fade out current content
|
|
||||||
if (mainContent) {
|
|
||||||
mainContent.style.opacity = '0';
|
|
||||||
mainContent.style.transform = 'translateY(10px)';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the new page content
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) throw new Error(`Failed to fetch ${url}`);
|
|
||||||
const html = await response.text();
|
|
||||||
|
|
||||||
// Create a temporary element to parse the HTML
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
|
||||||
|
|
||||||
// Extract the main content
|
|
||||||
const newContent = doc.querySelector('main');
|
|
||||||
if (!newContent) throw new Error('Could not find main content in the fetched page');
|
|
||||||
|
|
||||||
// Extract the title
|
|
||||||
const newTitle = doc.querySelector('title');
|
|
||||||
if (newTitle) {
|
|
||||||
document.title = newTitle.textContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract meta description
|
|
||||||
const newDescription = doc.querySelector('meta[name="description"]');
|
|
||||||
if (newDescription) {
|
|
||||||
const currentDescription = document.querySelector('meta[name="description"]');
|
|
||||||
if (currentDescription) {
|
|
||||||
currentDescription.setAttribute(
|
|
||||||
'content',
|
|
||||||
newDescription.getAttribute('content') || ''
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait a bit for transition effect
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
|
|
||||||
// Replace the content
|
|
||||||
if (mainContent && newContent) {
|
|
||||||
mainContent.innerHTML = newContent.innerHTML;
|
|
||||||
|
|
||||||
// Run scripts in the new content
|
|
||||||
Array.from(newContent.querySelectorAll('script')).forEach((oldScript) => {
|
|
||||||
const newScript = document.createElement('script');
|
|
||||||
Array.from(oldScript.attributes).forEach((attr) => {
|
|
||||||
newScript.setAttribute(attr.name, attr.value);
|
|
||||||
});
|
|
||||||
newScript.textContent = oldScript.textContent;
|
|
||||||
if (oldScript.parentNode) {
|
|
||||||
mainContent.appendChild(newScript);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fade in new content with animation
|
|
||||||
if (mainContent) {
|
|
||||||
mainContent.style.opacity = '0';
|
|
||||||
mainContent.style.transform = 'translateY(10px)';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
mainContent.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
|
||||||
mainContent.style.opacity = '1';
|
|
||||||
mainContent.style.transform = 'translateY(0)';
|
|
||||||
|
|
||||||
// Add entrance animation class
|
|
||||||
mainContent.classList.add('content-entering');
|
|
||||||
setTimeout(() => {
|
|
||||||
mainContent.classList.remove('content-entering');
|
|
||||||
}, 800);
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide transition overlay
|
|
||||||
if (pageTransition) {
|
|
||||||
setTimeout(() => {
|
|
||||||
pageTransition.classList.add('opacity-0', 'pointer-events-none');
|
|
||||||
pageTransition.classList.remove('opacity-100');
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch custom event for content loaded
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent('spa-content-loaded', {
|
|
||||||
detail: { url },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Scroll to top or to saved position
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
|
|
||||||
// Re-attach event listeners to new content
|
|
||||||
attachLinkListeners();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading content:', error);
|
|
||||||
|
|
||||||
// Fallback to traditional navigation on error
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to attach event listeners to all links
|
|
||||||
function attachLinkListeners() {
|
|
||||||
document.querySelectorAll('a').forEach((link) => {
|
|
||||||
// Skip links that are already handled, anchor links, external links, or have special attributes
|
|
||||||
if (
|
|
||||||
link.hasAttribute('data-spa-handled') ||
|
|
||||||
!link.href.startsWith(window.location.origin) ||
|
|
||||||
link.href.includes('#') ||
|
|
||||||
link.hasAttribute('target') ||
|
|
||||||
link.hasAttribute('download') ||
|
|
||||||
link.getAttribute('rel') === 'external' ||
|
|
||||||
link.getAttribute('rel') === 'nofollow'
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.href;
|
|
||||||
|
|
||||||
// Don't transition if clicking the current page
|
|
||||||
if (targetHref === window.location.href) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update browser history
|
|
||||||
window.history.pushState({ path: targetHref }, '', targetHref);
|
|
||||||
|
|
||||||
// Load the new content
|
|
||||||
loadContent(targetHref);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial attachment of link listeners
|
|
||||||
attachLinkListeners();
|
|
||||||
|
|
||||||
// Handle browser back/forward navigation
|
|
||||||
window.addEventListener('popstate', (e) => {
|
|
||||||
if (e.state && e.state.path) {
|
|
||||||
loadContent(e.state.path);
|
|
||||||
} else {
|
|
||||||
loadContent(window.location.href);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check RSS feed availability
|
|
||||||
const checkAndGenerateRSS = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/rss.xml');
|
|
||||||
if (!response.ok) {
|
|
||||||
console.warn('RSS feed not found. Please generate it using an RSS plugin for Astro.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Could not check RSS feed status.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check RSS feed availability
|
|
||||||
checkAndGenerateRSS();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Theme handling with transition effects
|
|
||||||
function setupThemeHandling() {
|
|
||||||
// Apply theme from localStorage or system preference
|
|
||||||
const theme = localStorage.getItem('theme');
|
|
||||||
if (
|
|
||||||
theme === 'dark' ||
|
|
||||||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
||||||
) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for theme changes
|
|
||||||
document.addEventListener('themeChanged', () => {
|
|
||||||
// Add transition class to body
|
|
||||||
document.body.classList.add('theme-transitioning');
|
|
||||||
|
|
||||||
// Remove class after transition completes
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.classList.remove('theme-transitioning');
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize theme handling
|
|
||||||
document.addEventListener('DOMContentLoaded', setupThemeHandling);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Page transition effects */
|
|
||||||
#page-transition {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Transition spinner animation */
|
|
||||||
.transition-spinner {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border: 2px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-radius: 50%;
|
|
||||||
border-top-color: #3b82f6;
|
|
||||||
animation: spin 0.7s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .transition-spinner {
|
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
border-top-color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content entrance animation */
|
|
||||||
main {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
transition:
|
|
||||||
opacity 0.5s ease,
|
|
||||||
transform 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
main.content-entering {
|
|
||||||
animation: content-fade-in 0.6s ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes content-fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme transition effect */
|
|
||||||
body.theme-transitioning * {
|
|
||||||
transition-duration: 0.3s !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
import { ViewTransitions } from 'astro:transitions';
|
|
||||||
import BaseLayout from './BaseLayout.astro';
|
|
||||||
|
|
||||||
const { title, description } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title={title} description={description}>
|
|
||||||
<ViewTransitions fallback="swap" />
|
|
||||||
|
|
||||||
<div transition:animate="slide">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Custom transition styles */
|
|
||||||
::view-transition-old(root) {
|
|
||||||
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-to-left;
|
|
||||||
}
|
|
||||||
|
|
||||||
::view-transition-new(root) {
|
|
||||||
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-from-right;
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,890 +0,0 @@
|
|||||||
/* Article entrance animation */
|
|
||||||
article {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
article.article-entering {
|
|
||||||
animation: article-fade-in 0.8s ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes article-fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hero image hover effect */
|
|
||||||
article img {
|
|
||||||
transition: transform 0.7s cubic-bezier(0.33, 1, 0.68, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Heading animations */
|
|
||||||
article .heading-animated {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
transition:
|
|
||||||
opacity 0.5s ease,
|
|
||||||
transform 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
article .heading-visible {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation link hover effect */
|
|
||||||
.blog-nav-link {
|
|
||||||
transition:
|
|
||||||
transform 0.3s ease,
|
|
||||||
box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-nav-link.nav-link-hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 10px 20px -10px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure dark mode compatibility */
|
|
||||||
:global(.dark) .blog-nav-link.nav-link-hover {
|
|
||||||
box-shadow: 0 10px 20px -10px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced Markdown Content Styling */
|
|
||||||
.markdown-content {
|
|
||||||
font-family:
|
|
||||||
system-ui,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
'Segoe UI',
|
|
||||||
Roboto,
|
|
||||||
Oxygen,
|
|
||||||
Ubuntu,
|
|
||||||
Cantarell,
|
|
||||||
'Open Sans',
|
|
||||||
'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Headings */
|
|
||||||
.markdown-content h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
color: #111827;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content h1 {
|
|
||||||
color: #f9fafb;
|
|
||||||
border-bottom-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h2 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
color: #111827;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content h2 {
|
|
||||||
color: #f9fafb;
|
|
||||||
border-bottom-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h3 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 2rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content h3 {
|
|
||||||
color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h4 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content h4 {
|
|
||||||
color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h5 {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content h5 {
|
|
||||||
color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h6 {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content h6 {
|
|
||||||
color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Paragraphs */
|
|
||||||
.markdown-content p {
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Links */
|
|
||||||
.markdown-content a {
|
|
||||||
color: #2563eb;
|
|
||||||
text-decoration: none;
|
|
||||||
border-bottom: 1px solid transparent;
|
|
||||||
transition:
|
|
||||||
border-color 0.2s ease,
|
|
||||||
color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content a:hover {
|
|
||||||
color: #1d4ed8;
|
|
||||||
border-bottom-color: #1d4ed8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content a {
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content a:hover {
|
|
||||||
color: #60a5fa;
|
|
||||||
border-bottom-color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bold text styling - enhanced */
|
|
||||||
.markdown-content strong {
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f766e;
|
|
||||||
background: linear-gradient(to bottom, transparent 60%, rgba(20, 184, 166, 0.2) 40%);
|
|
||||||
padding: 0 0.2em;
|
|
||||||
border-radius: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content strong {
|
|
||||||
color: #14b8a6;
|
|
||||||
background: linear-gradient(to bottom, transparent 60%, rgba(20, 184, 166, 0.15) 40%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Lists */
|
|
||||||
.markdown-content ul,
|
|
||||||
.markdown-content ol {
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ol {
|
|
||||||
list-style-type: decimal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content li {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content li > ul,
|
|
||||||
.markdown-content li > ol {
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Blockquotes */
|
|
||||||
.markdown-content blockquote {
|
|
||||||
border-left: 4px solid #3b82f6;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
font-style: italic;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content blockquote {
|
|
||||||
background-color: #1f2937;
|
|
||||||
border-left-color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content blockquote p {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content blockquote .quote-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
opacity: 0.1;
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content blockquote .quote-icon {
|
|
||||||
color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code blocks */
|
|
||||||
.markdown-content pre {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: #1e293b !important;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
position: relative;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode code blocks - ensure consistency */
|
|
||||||
.dark .markdown-content pre {
|
|
||||||
background-color: #1e293b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre code {
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: #e5e7eb !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content pre code {
|
|
||||||
color: #e5e7eb !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre.with-line-numbers {
|
|
||||||
padding-left: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre code {
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: #e5e7eb;
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .line-numbers {
|
|
||||||
position: absolute;
|
|
||||||
top: 1rem;
|
|
||||||
left: 0;
|
|
||||||
width: 2.5rem;
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 0.75rem;
|
|
||||||
color: #6b7280;
|
|
||||||
user-select: none;
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
border-right: 1px solid #4b5563;
|
|
||||||
height: calc(100% - 2rem);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .line-numbers span {
|
|
||||||
display: block;
|
|
||||||
height: 1.7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .copy-code-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.25rem;
|
|
||||||
right: 0.25rem;
|
|
||||||
background-color: #4b5563;
|
|
||||||
color: #e5e7eb;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 0.15rem;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.6;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
z-index: 10;
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .copy-code-button:hover {
|
|
||||||
opacity: 1;
|
|
||||||
background-color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .copy-code-button svg {
|
|
||||||
width: 0.875rem;
|
|
||||||
height: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Language label */
|
|
||||||
.markdown-content .language-label {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 2.5rem;
|
|
||||||
background-color: #4b5563;
|
|
||||||
color: #e5e7eb;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
padding: 0.125rem 0.375rem;
|
|
||||||
border-bottom-left-radius: 0.25rem;
|
|
||||||
border-bottom-right-radius: 0.25rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre:hover .language-label {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Language badge at bottom right */
|
|
||||||
.markdown-content .language-badge {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
padding: 0.1rem 0.3rem;
|
|
||||||
background-color: rgba(75, 85, 99, 0.7);
|
|
||||||
color: #e5e7eb;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre:hover .language-badge {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline code */
|
|
||||||
.markdown-content code:not(pre code) {
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
font-size: 0.875em;
|
|
||||||
color: #ef4444;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
padding: 0.2em 0.4em;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content code:not(pre code) {
|
|
||||||
color: #f87171;
|
|
||||||
background-color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
.markdown-content .table-container {
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content table th {
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
color: #111827;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-bottom: 2px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content table th {
|
|
||||||
background-color: #1f2937;
|
|
||||||
color: #f9fafb;
|
|
||||||
border-bottom-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content table td {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content table td {
|
|
||||||
border-bottom-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content table tr.even-row {
|
|
||||||
background-color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content table tr.even-row {
|
|
||||||
background-color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content table tr.odd-row {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content table tr.odd-row {
|
|
||||||
background-color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content table tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Images */
|
|
||||||
.markdown-content img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Horizontal rule */
|
|
||||||
.markdown-content hr {
|
|
||||||
border: 0;
|
|
||||||
height: 1px;
|
|
||||||
background-color: #e5e7eb;
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content hr {
|
|
||||||
background-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Task lists */
|
|
||||||
.markdown-content ul li[data-task-list-item] {
|
|
||||||
list-style-type: none;
|
|
||||||
position: relative;
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul li[data-task-list-item]::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0.25rem;
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
border: 1px solid #9ca3af;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul li[data-task-list-item][data-checked]::before {
|
|
||||||
background-color: #3b82f6;
|
|
||||||
border-color: #3b82f6;
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23ffffff'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 13l4 4L19 7'%3E%3C/path%3E%3C/svg%3E");
|
|
||||||
background-size: 0.75rem;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footnotes */
|
|
||||||
.markdown-content .footnotes {
|
|
||||||
margin-top: 2rem;
|
|
||||||
padding-top: 1rem;
|
|
||||||
border-top: 1px solid #e5e7eb;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content .footnotes {
|
|
||||||
border-top-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .footnotes ol {
|
|
||||||
padding-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .footnotes li {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .footnote-backref {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
vertical-align: super;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Definition lists */
|
|
||||||
.markdown-content dl {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content dt {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #111827;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content dt {
|
|
||||||
color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content dd {
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Callouts and admonitions */
|
|
||||||
.markdown-content .callout {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
border-left: 4px solid;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content .callout {
|
|
||||||
background-color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .callout.info {
|
|
||||||
border-left-color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .callout.warning {
|
|
||||||
border-left-color: #f59e0b;
|
|
||||||
background-color: rgba(245, 158, 11, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content .callout.warning {
|
|
||||||
background-color: rgba(245, 158, 11, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .callout.danger {
|
|
||||||
border-left-color: #ef4444;
|
|
||||||
background-color: rgba(239, 68, 68, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content .callout.danger {
|
|
||||||
background-color: rgba(239, 68, 68, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .callout.tip {
|
|
||||||
border-left-color: #10b981;
|
|
||||||
background-color: rgba(16, 185, 129, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content .callout.tip {
|
|
||||||
background-color: rgba(16, 185, 129, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code syntax highlighting - Light theme */
|
|
||||||
.markdown-content .token.comment,
|
|
||||||
.markdown-content .token.prolog,
|
|
||||||
.markdown-content .token.doctype,
|
|
||||||
.markdown-content .token.cdata {
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.punctuation {
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.namespace {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.property,
|
|
||||||
.markdown-content .token.tag,
|
|
||||||
.markdown-content .token.boolean,
|
|
||||||
.markdown-content .token.number,
|
|
||||||
.markdown-content .token.constant,
|
|
||||||
.markdown-content .token.symbol {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.selector,
|
|
||||||
.markdown-content .token.attr-name,
|
|
||||||
.markdown-content .token.string,
|
|
||||||
.markdown-content .token.char,
|
|
||||||
.markdown-content .token.builtin {
|
|
||||||
color: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.operator,
|
|
||||||
.markdown-content .token.entity,
|
|
||||||
.markdown-content .token.url,
|
|
||||||
.markdown-content .language-css .token.string,
|
|
||||||
.markdown-content .style .token.string {
|
|
||||||
color: #9333ea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.atrule,
|
|
||||||
.markdown-content .token.attr-value,
|
|
||||||
.markdown-content .token.keyword {
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.function,
|
|
||||||
.markdown-content .token.class-name {
|
|
||||||
color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.regex,
|
|
||||||
.markdown-content .token.important,
|
|
||||||
.markdown-content .token.variable {
|
|
||||||
color: #ec4899;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.important,
|
|
||||||
.markdown-content .token.bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.entity {
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.markdown-content h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h3 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre.with-line-numbers {
|
|
||||||
padding-left: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .line-numbers {
|
|
||||||
width: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content blockquote {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Print styles */
|
|
||||||
@media print {
|
|
||||||
.markdown-content {
|
|
||||||
font-size: 12pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre,
|
|
||||||
.markdown-content code {
|
|
||||||
font-size: 10pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content a {
|
|
||||||
color: #000 !important;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content blockquote {
|
|
||||||
border-left: 2pt solid #000;
|
|
||||||
padding: 0.5cm 1cm;
|
|
||||||
background: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content img {
|
|
||||||
max-width: 100% !important;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h2,
|
|
||||||
.markdown-content h3,
|
|
||||||
.markdown-content h4 {
|
|
||||||
page-break-after: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content p,
|
|
||||||
.markdown-content h2,
|
|
||||||
.markdown-content h3 {
|
|
||||||
orphans: 3;
|
|
||||||
widows: 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Additional elements */
|
|
||||||
.markdown-content details {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content details {
|
|
||||||
background-color: #1f2937;
|
|
||||||
border-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content details summary {
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content details[open] summary {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content details[open] summary {
|
|
||||||
border-bottom-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Keyboard shortcuts */
|
|
||||||
.markdown-content kbd {
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
font-size: 0.8em;
|
|
||||||
padding: 0.2em 0.4em;
|
|
||||||
margin: 0 0.1em;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: 0 1px 0 #d1d5db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content kbd {
|
|
||||||
background-color: #1f2937;
|
|
||||||
border-color: #4b5563;
|
|
||||||
box-shadow: 0 1px 0 #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Abbreviations */
|
|
||||||
.markdown-content abbr {
|
|
||||||
cursor: help;
|
|
||||||
text-decoration: underline dotted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Highlight text */
|
|
||||||
.markdown-content mark {
|
|
||||||
background-color: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
padding: 0.1em 0.2em;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content mark {
|
|
||||||
background-color: rgba(254, 243, 199, 0.2);
|
|
||||||
color: #fbbf24;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Subscript and superscript */
|
|
||||||
.markdown-content sub,
|
|
||||||
.markdown-content sup {
|
|
||||||
font-size: 0.75em;
|
|
||||||
line-height: 0;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content sup {
|
|
||||||
top: -0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content sub {
|
|
||||||
bottom: -0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Diagrams and charts */
|
|
||||||
.markdown-content .mermaid {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Math equations */
|
|
||||||
.markdown-content .math {
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Embedded content */
|
|
||||||
.markdown-content iframe {
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
27
src/lib/directus.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
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;
|
12
src/lib/directusFunctions.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
const getDirectusURL = () => {
|
||||||
|
if (process.env.DIRECTUS_URL) {
|
||||||
|
return `https://${process.env.DIRECTUS_URL}`;
|
||||||
|
}
|
||||||
|
return 'https://directus.alexlebens.dev';
|
||||||
|
};
|
||||||
|
|
||||||
|
async function getDirectusImageURL(image: string) {
|
||||||
|
return `${getDirectusURL()}/assets/${image}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { getDirectusURL, getDirectusImageURL };
|
88
src/lib/directusTypes.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
export type Global = {
|
||||||
|
name: string;
|
||||||
|
about: string;
|
||||||
|
about_description: string;
|
||||||
|
initials: string;
|
||||||
|
email: string;
|
||||||
|
site_url: string;
|
||||||
|
logo: string;
|
||||||
|
portrait: string;
|
||||||
|
portrait_alt: string;
|
||||||
|
home_image: string;
|
||||||
|
home_image_alt: string;
|
||||||
|
categories_image: string;
|
||||||
|
categories_image_alt: string;
|
||||||
|
blog_image: string;
|
||||||
|
blog_image_alt: string;
|
||||||
|
footer_image: string;
|
||||||
|
footer_image_alt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Post = {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
category: string;
|
||||||
|
selected: boolean;
|
||||||
|
published: boolean;
|
||||||
|
content: string;
|
||||||
|
image: string;
|
||||||
|
image_alt: string;
|
||||||
|
image_second: string;
|
||||||
|
image_second_alt: string;
|
||||||
|
published_date: Date;
|
||||||
|
updated_date: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Experience = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
location: string;
|
||||||
|
location_type: string;
|
||||||
|
url: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
position: string;
|
||||||
|
summary: string;
|
||||||
|
responsibilities: string[];
|
||||||
|
highlights: string[];
|
||||||
|
achievements: string[];
|
||||||
|
skills: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Education = {
|
||||||
|
id: string;
|
||||||
|
institution: string;
|
||||||
|
url: string;
|
||||||
|
area: string;
|
||||||
|
studyType: string;
|
||||||
|
graduationDate: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Certificate = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
issuer: string;
|
||||||
|
issuerDate: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Project = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isActive: boolean;
|
||||||
|
description: string;
|
||||||
|
highlights: string[];
|
||||||
|
url: string;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Skill = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
level: string;
|
||||||
|
date_created: string;
|
||||||
|
};
|
@@ -1,115 +1,75 @@
|
|||||||
---
|
---
|
||||||
import Layout from '../layouts/Layout.astro';
|
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'));
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="404 - Page Not Found">
|
<BaseLayout
|
||||||
<div
|
title="Page Not Found"
|
||||||
class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
|
description="Page Not Found"
|
||||||
>
|
structuredData={{
|
||||||
<!-- Animated background elements -->
|
'@context': 'https://schema.org',
|
||||||
<div class="absolute inset-0 overflow-hidden">
|
'@type': 'WebPage',
|
||||||
<div
|
inLanguage: 'en-US',
|
||||||
class="animate-blob absolute -left-20 -top-20 h-64 w-64 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50"
|
'@id': Astro.url.href,
|
||||||
>
|
url: Astro.url.href,
|
||||||
</div>
|
name: `Page Not Found | ${global.name}`,
|
||||||
<div
|
description: 'Page Not Found',
|
||||||
class="animate-blob animation-delay-2000 absolute right-1/4 top-1/2 h-96 w-96 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30"
|
isPartOf: {
|
||||||
>
|
'@type': 'WebSite',
|
||||||
</div>
|
url: global.site_url,
|
||||||
<div
|
name: global.name,
|
||||||
class="animate-blob animation-delay-4000 absolute bottom-20 left-1/3 h-72 w-72 rounded-full bg-zinc-100 opacity-40 blur-3xl dark:bg-zinc-800/40"
|
description: global.about,
|
||||||
>
|
},
|
||||||
</div>
|
}}
|
||||||
</div>
|
>
|
||||||
|
<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>
|
||||||
|
|
||||||
<!-- Main content with animation -->
|
|
||||||
<div class="relative z-10 mx-auto max-w-xl">
|
|
||||||
<div class="glitch-wrapper">
|
|
||||||
<h1
|
<h1
|
||||||
class="glitch text-9xl font-bold leading-none text-zinc-900 dark:text-zinc-100 sm:text-[12rem]"
|
class="text-dark smooth-reveal mb-4 text-7xl font-extrabold text-yellow-500 lg:text-9xl dark:text-yellow-400"
|
||||||
data-text="404"
|
|
||||||
>
|
>
|
||||||
404
|
{`Page Not Found - ${global.name}`}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
<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"
|
||||||
<h2 class="mt-6 text-2xl font-bold text-zinc-800 dark:text-zinc-200 sm:text-3xl">
|
|
||||||
Page Not Found
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p class="mx-auto mt-6 max-w-md text-lg text-zinc-600 dark:text-zinc-400">
|
|
||||||
The page you're looking for does not exist.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
|
|
||||||
<a
|
|
||||||
href="/"
|
|
||||||
class="group relative inline-flex items-center gap-2 overflow-hidden rounded-lg bg-zinc-900 px-6 py-3 text-zinc-100 shadow-lg transition-all duration-300 hover:bg-zinc-800 hover:shadow-xl dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
|
|
||||||
>
|
>
|
||||||
<span
|
<h3
|
||||||
class="absolute inset-0 z-0 bg-gradient-to-r from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
|
class="text-sm font-medium tracking-wider text-neutral-500 uppercase dark:text-neutral-400"
|
||||||
></span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="relative z-10 h-5 w-5"
|
|
||||||
>
|
>
|
||||||
<path
|
Did you know?
|
||||||
stroke-linecap="round"
|
</h3>
|
||||||
stroke-linejoin="round"
|
<p class="mt-2 text-sm text-neutral-600 dark:text-neutral-300" id="fun-fact">
|
||||||
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
|
The 404 error code originated when CERN's web server displayed room 404 (their server
|
||||||
></path>
|
room) as the error message when a file wasn't found.
|
||||||
</svg>
|
</p>
|
||||||
<span class="relative z-10 font-medium">Return Home</span>
|
</div>
|
||||||
</a>
|
<div
|
||||||
|
class="smooth-reveal mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row"
|
||||||
<button
|
|
||||||
id="back-button"
|
|
||||||
class="group inline-flex items-center gap-2 rounded-lg border border-zinc-300 px-6 py-3 text-zinc-700 shadow-sm transition-all duration-300 hover:bg-zinc-100 hover:shadow-md dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
||||||
>
|
>
|
||||||
<svg
|
<GoBack title="Go Back" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<PrimaryCTA title="Return Home" url={global.site_url} noArrow addHome />
|
||||||
fill="none"
|
</div>
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="h-5 w-5 transition-transform duration-300 group-hover:-translate-x-1"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="font-medium">Go Back</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Random fun fact -->
|
|
||||||
<div
|
|
||||||
class="mx-auto mt-16 max-w-md rounded-xl border border-zinc-100 bg-zinc-50 p-6 shadow-sm backdrop-blur-sm dark:border-zinc-700/50 dark:bg-zinc-800/50"
|
|
||||||
>
|
|
||||||
<h3 class="text-sm font-medium uppercase tracking-wider text-zinc-500 dark:text-zinc-400">
|
|
||||||
Did you know?
|
|
||||||
</h3>
|
|
||||||
<p class="mt-2 text-sm text-zinc-700 dark:text-zinc-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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</Layout>
|
</BaseLayout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Go back functionality
|
|
||||||
document.getElementById('back-button')?.addEventListener('click', () => {
|
|
||||||
window.history.back();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Array of fun 404 facts
|
|
||||||
const funFacts = [
|
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.",
|
"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.",
|
"In internet slang, '404' has become shorthand for something that's missing or someone who's clueless.",
|
||||||
@@ -128,96 +88,26 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
funFactElement.textContent = randomFact;
|
funFactElement.textContent = randomFact;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle SPA transitions for 404 page
|
// Add smooth reveal animations for content after loading
|
||||||
function setupSPATransitions() {
|
document.addEventListener('astro:page-load', () => {
|
||||||
// Handle all internal links for SPA transitions
|
const animateContent = () => {
|
||||||
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
// Animate group 1
|
||||||
// Skip links that are anchor links, external links, or already processed
|
const smoothReveal = document.querySelectorAll('.smooth-reveal');
|
||||||
if (
|
smoothReveal.forEach((el, index) => {
|
||||||
link.getAttribute('href').includes('#') ||
|
setTimeout(
|
||||||
link.getAttribute('target') === '_blank' ||
|
() => {
|
||||||
link.hasAttribute('data-spa-handled')
|
el.classList.add('animate-reveal');
|
||||||
) {
|
},
|
||||||
return;
|
100 + index * 150
|
||||||
}
|
);
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
// Re-initialize back button after SPA navigation
|
animateContent();
|
||||||
const backButton = document.getElementById('back-button');
|
});
|
||||||
if (backButton) {
|
|
||||||
backButton.addEventListener('click', () => {
|
|
||||||
window.history.back();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Animation for floating blobs */
|
|
||||||
@keyframes blob {
|
|
||||||
0% {
|
|
||||||
transform: translate(0px, 0px) scale(1);
|
|
||||||
}
|
|
||||||
33% {
|
|
||||||
transform: translate(30px, -50px) scale(1.1);
|
|
||||||
}
|
|
||||||
66% {
|
|
||||||
transform: translate(-20px, 20px) scale(0.9);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(0px, 0px) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-blob {
|
|
||||||
animation: blob 7s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-2000 {
|
|
||||||
animation-delay: 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-4000 {
|
|
||||||
animation-delay: 4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Glitch effect for 404 text */
|
/* Glitch effect for 404 text */
|
||||||
.glitch-wrapper {
|
.glitch-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
@@ -1,583 +1,107 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import { readSingleton } from '@directus/sdk';
|
||||||
import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa';
|
|
||||||
import { SiTypescript, SiAstro } from 'react-icons/si';
|
|
||||||
|
|
||||||
import directus from '../../lib/directus';
|
import directus from '@lib/directus';
|
||||||
import { readSingleton, readItems } from '@directus/sdk';
|
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('global'));
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
const about = await directus.request(readSingleton('about'));
|
|
||||||
|
|
||||||
const skills = await directus.request(
|
const description = 'About me.';
|
||||||
readItems('skills', {
|
|
||||||
fields: ['*'],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="About Me" description={global.description}>
|
<BaseLayout
|
||||||
<div class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16">
|
title="About Me"
|
||||||
<!-- Hero Section -->
|
description={description}
|
||||||
<div class="relative mb-12 sm:mb-16 md:mb-20">
|
structuredData={{
|
||||||
<!-- Decorative elements -->
|
'@context': 'https://schema.org',
|
||||||
<div
|
'@type': 'WebPage',
|
||||||
class="animate-blob theme-transition-bg absolute -left-10 -top-10 h-36 w-36 rounded-full bg-zinc-100 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-left-20 sm:-top-20 sm:h-48 sm:w-48 md:h-72 md:w-72"
|
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>
|
</div>
|
||||||
<div
|
</main>
|
||||||
class="animate-blob animation-delay-2000 theme-transition-bg absolute -bottom-10 -right-10 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-48 sm:w-48 md:h-72 md:w-72"
|
</section>
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12">
|
|
||||||
<div class="order-2 text-center md:order-1 md:text-left">
|
|
||||||
<h1
|
|
||||||
class="theme-transition-color mb-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:mb-6 sm:text-4xl md:text-5xl"
|
|
||||||
>
|
|
||||||
Hello, I'm <span
|
|
||||||
class="theme-transition-all bg-gradient-to-r from-zinc-500 to-zinc-900 bg-clip-text text-transparent dark:from-zinc-300 dark:to-zinc-100"
|
|
||||||
>{global.name}</span
|
|
||||||
>
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p
|
|
||||||
class="theme-transition-color mb-6 text-lg leading-relaxed text-zinc-600 dark:text-zinc-400 sm:mb-8 sm:text-xl"
|
|
||||||
>
|
|
||||||
{about.background}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="social-links-container theme-transition-children flex flex-wrap justify-center gap-4 md:justify-start"
|
|
||||||
>
|
|
||||||
<!-- Social links remain the same -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative order-1 md:order-2">
|
|
||||||
<div
|
|
||||||
class="theme-transition-all mx-auto aspect-square w-full max-w-[280px] overflow-hidden rounded-3xl border-4 border-white shadow-xl dark:border-zinc-800 sm:max-w-[320px] sm:border-8 sm:shadow-2xl md:max-w-md"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.portrait}`
|
|
||||||
alt={global.portrait_alt}
|
|
||||||
class="h-full w-full object-cover"
|
|
||||||
loading="eager"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Decorative elements -->
|
|
||||||
<div
|
|
||||||
class="theme-transition-all absolute -bottom-4 -right-4 flex h-16 w-16 items-center justify-center rounded-full border-2 border-white bg-zinc-100 shadow-lg dark:border-zinc-900 dark:bg-zinc-800 sm:-bottom-6 sm:-right-6 sm:h-20 sm:w-20 sm:border-4 md:h-24 md:w-24"
|
|
||||||
>
|
|
||||||
<span class="text-2xl sm:text-3xl">👋</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- About Section -->
|
|
||||||
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
|
|
||||||
<div class="mx-auto max-w-3xl">
|
|
||||||
<h2
|
|
||||||
class="theme-transition-color mb-6 flex items-center justify-center text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-8 sm:text-3xl md:justify-start"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="theme-transition-bg mr-4 hidden h-1 w-8 bg-zinc-300 dark:bg-zinc-700 sm:inline-block sm:w-12"
|
|
||||||
></span>
|
|
||||||
About Me
|
|
||||||
<span
|
|
||||||
class="theme-transition-bg ml-4 hidden h-1 w-8 bg-zinc-300 dark:bg-zinc-700 sm:inline-block sm:w-12"
|
|
||||||
></span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="theme-transition-all prose prose-zinc max-w-none dark:prose-invert">
|
|
||||||
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
|
||||||
{about.experience}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
|
||||||
{about.education}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
|
||||||
{about.certifications}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Skills Section -->
|
|
||||||
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
|
|
||||||
<h2
|
|
||||||
class="theme-transition-color mb-8 text-center text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-12 sm:text-3xl"
|
|
||||||
>
|
|
||||||
Tech Stack
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
|
|
||||||
<!-- Main slider container -->
|
|
||||||
<div class="slider-track animate-slide flex">
|
|
||||||
{
|
|
||||||
skills.map((skill, index) => (
|
|
||||||
<div
|
|
||||||
key={`${skill.title}-${index}`}
|
|
||||||
class="skill-card theme-transition-element mx-2 min-w-[220px] transform rounded-xl border border-zinc-200 bg-white transition-all duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-300 hover:shadow-xl dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600 sm:mx-4 sm:min-w-[280px]"
|
|
||||||
>
|
|
||||||
<div class="p-4 sm:p-6">
|
|
||||||
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
|
||||||
<div class="flex items-center gap-2 sm:gap-4">
|
|
||||||
<div class="theme-transition-bg theme-transition-color flex h-8 w-8 transform items-center justify-center rounded-lg bg-zinc-100 text-zinc-800 transition-transform group-hover:rotate-12 dark:bg-zinc-800 dark:text-zinc-200 sm:h-12 sm:w-12">
|
|
||||||
<skill.icon
|
|
||||||
size={20}
|
|
||||||
className="sm:text-2xl transform transition-all hover:scale-125"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h3 class="theme-transition-color text-base font-semibold text-zinc-900 dark:text-zinc-100 sm:text-xl">
|
|
||||||
{skill.title}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<span class="theme-transition-all rounded-full bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 sm:px-2.5 sm:py-1 sm:text-sm">
|
|
||||||
{skill.level}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="theme-transition-bg relative h-1.5 w-full overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-700 sm:h-2">
|
|
||||||
<div
|
|
||||||
class="progress-bar-animate theme-transition-bg absolute left-0 top-0 h-full rounded-full bg-gradient-to-r from-zinc-700 via-zinc-600 to-zinc-800 transition-all duration-1000 dark:from-zinc-300 dark:via-zinc-400 dark:to-zinc-200"
|
|
||||||
style={`width: ${skill.level}%`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="theme-transition-color mt-1 flex justify-between font-mono text-[10px] text-zinc-400 dark:text-zinc-500 sm:mt-2 sm:text-xs">
|
|
||||||
<span>Beginner</span>
|
|
||||||
<span>Advanced</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Gradient overlays for smooth fade effect -->
|
|
||||||
<div
|
|
||||||
class="theme-transition-bg absolute bottom-0 left-0 top-0 z-10 w-12 bg-gradient-to-r from-white to-transparent dark:from-zinc-900 sm:w-24"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="theme-transition-bg absolute bottom-0 right-0 top-0 z-10 w-12 bg-gradient-to-l from-white to-transparent dark:from-zinc-900 sm:w-24"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contact Section -->
|
|
||||||
<div class="theme-transition-all mx-auto max-w-3xl text-center">
|
|
||||||
<h2
|
|
||||||
class="theme-transition-color mb-4 text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-6 sm:text-3xl"
|
|
||||||
>
|
|
||||||
Get in Touch
|
|
||||||
</h2>
|
|
||||||
<p
|
|
||||||
class="theme-transition-color mb-6 text-base text-zinc-600 dark:text-zinc-400 sm:mb-8 sm:text-lg"
|
|
||||||
>
|
|
||||||
I'm always open to new opportunities and collaborations. If you'd like to work together or
|
|
||||||
just say hello, feel free to reach out.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href=`mailto:${global.email}`
|
|
||||||
class="theme-transition-all inline-flex items-center justify-center rounded-lg bg-zinc-900 px-6 py-3 text-base font-medium text-zinc-100 transition-colors hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300 sm:px-8 sm:py-4 sm:text-lg"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
Say Hello
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Blob animation */
|
|
||||||
.animate-blob {
|
|
||||||
animation: blob-bounce 8s infinite ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-2000 {
|
|
||||||
animation-delay: 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blob-bounce {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translate(5%, 5%) scale(1.05);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translate(0, 10%) scale(1);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: translate(-5%, 5%) scale(0.95);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tech Stack Slider */
|
|
||||||
.slider-track {
|
|
||||||
width: fit-content;
|
|
||||||
animation: scroll 40s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scroll {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.slider-track {
|
|
||||||
animation: scroll 60s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scroll {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tech-stack-slider:hover .slider-track {
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card {
|
|
||||||
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:hover {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reduce animation complexity on mobile for better performance */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.skill-card {
|
|
||||||
transition:
|
|
||||||
transform 0.3s ease,
|
|
||||||
box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:hover {
|
|
||||||
transform: translateY(-5px) !important;
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -10%;
|
|
||||||
left: -10%;
|
|
||||||
width: 120%;
|
|
||||||
height: 120%;
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at center,
|
|
||||||
rgba(255, 255, 255, 0.1) 0%,
|
|
||||||
rgba(255, 255, 255, 0) 70%
|
|
||||||
);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:hover:before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar-animate {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar-animate:after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -100%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
||||||
animation: progress-shine 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes progress-shine {
|
|
||||||
0% {
|
|
||||||
left: -100%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Improved touch targets for mobile */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
a,
|
|
||||||
button {
|
|
||||||
min-height: 44px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-link {
|
|
||||||
min-width: 44px;
|
|
||||||
min-height: 44px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme transition effect */
|
|
||||||
:global(.theme-switching) .theme-transition-element {
|
|
||||||
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth card transition during theme switch */
|
|
||||||
.skill-card.theme-transition-element {
|
|
||||||
transition:
|
|
||||||
background-color var(--theme-transition),
|
|
||||||
border-color var(--theme-transition),
|
|
||||||
color var(--theme-transition),
|
|
||||||
box-shadow var(--theme-transition),
|
|
||||||
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Wait for the DOM to be fully loaded
|
// Add smooth reveal animations for content after loading
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('astro:page-load', () => {
|
||||||
const sliderTrack = document.querySelector('.slider-track');
|
const animateContent = () => {
|
||||||
|
// Animate group 1
|
||||||
// Create seamless infinite scrolling effect
|
const smoothReveal = document.querySelectorAll('.smooth-reveal');
|
||||||
function setupInfiniteScroll() {
|
smoothReveal.forEach((el, index) => {
|
||||||
const cards = document.querySelectorAll('.skill-card');
|
|
||||||
if (!cards.length) return;
|
|
||||||
|
|
||||||
// Clone the first set of cards and append to create seamless loop
|
|
||||||
const firstSetCount = cards.length / 3; // We have 3 sets in the markup
|
|
||||||
|
|
||||||
// Set proper animation based on screen size
|
|
||||||
function updateScrollAnimation() {
|
|
||||||
if (window.innerWidth >= 640) {
|
|
||||||
sliderTrack.style.animation = 'scroll 60s linear infinite';
|
|
||||||
} else {
|
|
||||||
sliderTrack.style.animation = 'scroll 40s linear infinite';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateScrollAnimation();
|
|
||||||
window.addEventListener('resize', updateScrollAnimation);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupInfiniteScroll();
|
|
||||||
|
|
||||||
// Pause animation on hover/touch
|
|
||||||
sliderTrack?.addEventListener('mouseenter', () => {
|
|
||||||
sliderTrack.style.animationPlayState = 'paused';
|
|
||||||
});
|
|
||||||
|
|
||||||
sliderTrack?.addEventListener('touchstart', () => {
|
|
||||||
sliderTrack.style.animationPlayState = 'paused';
|
|
||||||
});
|
|
||||||
|
|
||||||
sliderTrack?.addEventListener('mouseleave', () => {
|
|
||||||
sliderTrack.style.animationPlayState = 'running';
|
|
||||||
});
|
|
||||||
|
|
||||||
sliderTrack?.addEventListener('touchend', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderTrack.style.animationPlayState = 'running';
|
|
||||||
}, 1000); // Delay resuming animation after touch
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add hover effects to cards - only on non-touch devices
|
|
||||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
||||||
const cards = document.querySelectorAll('.skill-card');
|
|
||||||
|
|
||||||
if (!isTouchDevice) {
|
|
||||||
cards.forEach((card) => {
|
|
||||||
card.addEventListener('mousemove', (e) => {
|
|
||||||
const rect = card.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
|
|
||||||
const centerX = rect.width / 2;
|
|
||||||
const centerY = rect.height / 2;
|
|
||||||
|
|
||||||
const angleX = (y - centerY) / 15;
|
|
||||||
const angleY = (centerX - x) / 15;
|
|
||||||
|
|
||||||
card.style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) scale(1.08) translateZ(20px)`;
|
|
||||||
|
|
||||||
// Dynamic shadow based on tilt
|
|
||||||
const shadowX = (x - centerX) / 25;
|
|
||||||
const shadowY = (y - centerY) / 25;
|
|
||||||
card.style.boxShadow = `
|
|
||||||
${shadowX}px ${shadowY}px 20px rgba(0, 0, 0, 0.1),
|
|
||||||
0 10px 20px rgba(0, 0, 0, 0.05)
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
card.addEventListener('mouseleave', () => {
|
|
||||||
card.style.transform = '';
|
|
||||||
card.style.boxShadow = '';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Simpler effects for touch devices
|
|
||||||
cards.forEach((card) => {
|
|
||||||
card.addEventListener('touchstart', () => {
|
|
||||||
card.classList.add('is-touched');
|
|
||||||
});
|
|
||||||
|
|
||||||
card.addEventListener('touchend', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
card.classList.remove('is-touched');
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle theme transition
|
|
||||||
document.addEventListener('themeChange', () => {
|
|
||||||
// Add special effects during theme transition
|
|
||||||
cards.forEach((card, index) => {
|
|
||||||
// Add staggered animation delay
|
|
||||||
setTimeout(() => {
|
|
||||||
card.classList.add('theme-changing');
|
|
||||||
setTimeout(() => {
|
|
||||||
card.classList.remove('theme-changing');
|
|
||||||
}, 600);
|
|
||||||
}, index * 50);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Handle SPA transitions for about page
|
|
||||||
function setupSPATransitions() {
|
|
||||||
// Handle all internal links for SPA transitions
|
|
||||||
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
|
||||||
// Skip links that are anchor links, external links, or already processed
|
|
||||||
if (
|
|
||||||
link.getAttribute('href').includes('#') ||
|
|
||||||
link.getAttribute('target') === '_blank' ||
|
|
||||||
link.hasAttribute('data-spa-handled')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize animations for about page
|
|
||||||
function animateAboutContent() {
|
|
||||||
// Animate hero section elements
|
|
||||||
const heroElements = document.querySelectorAll('h1, .order-2 p, .social-links-container');
|
|
||||||
heroElements.forEach((el, index) => {
|
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
el.classList.add('animate-reveal');
|
el.classList.add('animate-reveal');
|
||||||
},
|
},
|
||||||
100 + index * 150
|
50 + index * 100
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate profile image
|
// Animate group 2
|
||||||
const profileImage = document.querySelector('.aspect-square');
|
const smoothReveal2 = document.querySelectorAll('.smooth-reveal-2');
|
||||||
if (profileImage) {
|
smoothReveal2.forEach((el, index) => {
|
||||||
setTimeout(() => {
|
|
||||||
profileImage.classList.add('animate-reveal');
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animate skill bars with staggered delay
|
|
||||||
const skillBars = document.querySelectorAll('.skill-bar');
|
|
||||||
skillBars.forEach((bar, index) => {
|
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
bar.classList.add('animate-skill');
|
el.classList.add('animate-reveal');
|
||||||
},
|
},
|
||||||
500 + index * 100
|
200 + index * 250
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate sections with staggered delay
|
// Animate topic cards with staggered delay
|
||||||
const sections = document.querySelectorAll('section');
|
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
|
||||||
sections.forEach((section, index) => {
|
smoothRevealCards.forEach((el, index) => {
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
section.classList.add('animate-reveal');
|
el.classList.add('animate-reveal');
|
||||||
},
|
},
|
||||||
300 + index * 200
|
400 + index * 250
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Run animations
|
// Animate with just fade in with staggered delay
|
||||||
animateAboutContent();
|
const smoothRevealFade = document.querySelectorAll('.smooth-reveal-fade');
|
||||||
}
|
smoothRevealFade.forEach((el, index) => {
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
el.classList.add('animate-reveal-fade');
|
||||||
|
},
|
||||||
|
100 + index * 250
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize on first load
|
animateContent();
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
});
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
|
||||||
</script>
|
</script>
|
||||||
|
@@ -1,347 +1,181 @@
|
|||||||
---
|
---
|
||||||
import BlogPost from '../../layouts/BlogPost.astro';
|
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||||
|
import getReadingTime from 'reading-time';
|
||||||
|
import { readItems, readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
import directus from '../../../lib/directus';
|
import directus from '@lib/directus';
|
||||||
import { readItems } from '@directus/sdk';
|
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() {
|
export async function getStaticPaths() {
|
||||||
const posts = await directus.request(
|
const posts = await directus.request(readItems('posts'));
|
||||||
readItems('posts', {
|
return posts.map((post) => ({
|
||||||
fields: ['*'],
|
params: { slug: post.slug },
|
||||||
})
|
props: post,
|
||||||
);
|
}));
|
||||||
|
|
||||||
const sortedEntries = [...posts].sort(
|
|
||||||
(a, b) => b.published_date.valueOf() - a.published_date.valueOf()
|
|
||||||
);
|
|
||||||
|
|
||||||
return sortedEntries.map((post, index) => {
|
|
||||||
return {
|
|
||||||
params: { slug: post.slug },
|
|
||||||
props: {
|
|
||||||
post,
|
|
||||||
nextPost: index > 0 ? sortedEntries[index - 1] : null,
|
|
||||||
prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
const post = Astro.props;
|
||||||
|
|
||||||
const { post, nextPost, prevPost } = 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);
|
||||||
---
|
---
|
||||||
|
|
||||||
<BlogPost
|
<BaseLayout
|
||||||
slug={post.slug}
|
|
||||||
title={post.title}
|
title={post.title}
|
||||||
description={post.description}
|
description={post.description}
|
||||||
content={post.content}
|
ogImage={getDirectusImageURL(post.image)}
|
||||||
image={post.image}
|
structuredData={{
|
||||||
image_alt={post.image_alt}
|
'@context': 'https://schema.org',
|
||||||
published_date={post.published_date}
|
'@type': 'NewsArticle',
|
||||||
updated_date={post.updated_date}
|
inLanguage: 'en-US',
|
||||||
tags={post.tags}
|
'@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}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<!-- Main Content - Enhanced with better typography and spacing -->
|
<section class="mx-auto max-w-6xl px-4 pt-8 pb-12 sm:px-6 lg:px-8 lg:pt-12">
|
||||||
<div
|
<div class="smooth-reveal relative w-full">
|
||||||
class="prose prose-sm prose-zinc max-w-none dark:prose-invert sm:prose-base prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:font-medium prose-a:text-zinc-800 prose-a:underline-offset-4 hover:prose-a:text-zinc-600 prose-img:rounded-xl dark:prose-a:text-zinc-300 dark:hover:prose-a:text-zinc-100"
|
<div class="mt-4 rounded-2xl shadow-none sm:mt-0 sm:shadow-sm">
|
||||||
>
|
<Image
|
||||||
<div set:html={post.content} />
|
class="max-h-[600px] w-full rounded-t-2xl object-cover"
|
||||||
</div>
|
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>
|
||||||
|
|
||||||
<!-- Next/Previous Navigation - Improved responsive design -->
|
<article
|
||||||
<div
|
class="prose prose-blog sm:prose-lg dark:prose-invert max-w-none text-justify text-neutral-800 dark:text-neutral-200"
|
||||||
class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 dark:border-zinc-800 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2"
|
>
|
||||||
>
|
<div set:html={post.content} />
|
||||||
{
|
</article>
|
||||||
prevPost && (
|
|
||||||
<a
|
<div
|
||||||
href={`/blog/${prevPost.slug}`}
|
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"
|
||||||
class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/50 sm:p-6"
|
>
|
||||||
>
|
<div class="flex flex-wrap gap-x-2 gap-y-1 sm:flex-nowrap sm:items-center sm:gap-y-0">
|
||||||
<div class="absolute inset-0 bg-gradient-to-r from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
|
{
|
||||||
<span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm">
|
post.tags.map((tag: string) => (
|
||||||
<svg
|
<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">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
{tag}
|
||||||
width="20"
|
</span>
|
||||||
height="20"
|
))
|
||||||
viewBox="0 0 24 24"
|
}
|
||||||
fill="none"
|
</div>
|
||||||
stroke="currentColor"
|
</div>
|
||||||
stroke-width="2"
|
</div>
|
||||||
stroke-linecap="round"
|
</div>
|
||||||
stroke-linejoin="round"
|
</div>
|
||||||
class="h-3 w-3 transition-transform duration-300 group-hover:-translate-x-1 sm:h-4 sm:w-4"
|
</section>
|
||||||
>
|
<style is:inline>
|
||||||
<path d="m15 18-6-6 6-6" />
|
code[data-theme*=' '],
|
||||||
</svg>
|
code[data-theme*=' '] span {
|
||||||
Previous Article
|
color: var(--shiki-light);
|
||||||
</span>
|
|
||||||
<h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg">
|
|
||||||
{prevPost.title}
|
|
||||||
</h3>
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
{
|
|
||||||
nextPost && (
|
html.dark {
|
||||||
<a
|
code[data-theme*=' '],
|
||||||
href={`/blog/${nextPost.slug}`}
|
code[data-theme*=' '] span {
|
||||||
class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/50 sm:p-6 md:text-right"
|
color: var(--shiki-dark);
|
||||||
>
|
}
|
||||||
<div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
|
|
||||||
<span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm md:justify-end">
|
|
||||||
Next Article
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="h-3 w-3 transition-transform duration-300 group-hover:translate-x-1 sm:h-4 sm:w-4"
|
|
||||||
>
|
|
||||||
<path d="m9 18 6-6-6-6" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg">
|
|
||||||
{nextPost.title}
|
|
||||||
</h3>
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</div>
|
</style>
|
||||||
</BlogPost>
|
</BaseLayout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Removing TOC-related functions
|
// Add smooth reveal animations for content after loading
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
// Add copy buttons to code blocks
|
const animateContent = () => {
|
||||||
function initializeCodeCopyButtons() {
|
const smoothReveal = document.querySelectorAll('.smooth-reveal');
|
||||||
const codeBlocks = document.querySelectorAll('pre');
|
smoothReveal.forEach((el, index) => {
|
||||||
|
setTimeout(
|
||||||
codeBlocks.forEach((block) => {
|
() => {
|
||||||
// Skip if already processed by either method
|
el.classList.add('animate-reveal');
|
||||||
if (
|
},
|
||||||
block.classList.contains('code-block-processed') ||
|
100 + index * 100
|
||||||
block.classList.contains('enhanced')
|
);
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
block.classList.add('code-block-processed');
|
|
||||||
|
|
||||||
// Create wrapper if not already wrapped
|
|
||||||
let wrapper;
|
|
||||||
if (
|
|
||||||
block.parentNode.classList.contains('relative') &&
|
|
||||||
block.parentNode.classList.contains('group')
|
|
||||||
) {
|
|
||||||
wrapper = block.parentNode;
|
|
||||||
} else {
|
|
||||||
wrapper = document.createElement('div');
|
|
||||||
wrapper.className = 'relative group';
|
|
||||||
block.parentNode.insertBefore(wrapper, block);
|
|
||||||
wrapper.appendChild(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add copy button if not already present
|
|
||||||
if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-button')) {
|
|
||||||
const copyButton = document.createElement('button');
|
|
||||||
copyButton.className =
|
|
||||||
'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
copyButton.addEventListener('click', () => {
|
|
||||||
const code = block.querySelector('code').innerText;
|
|
||||||
navigator.clipboard.writeText(code);
|
|
||||||
|
|
||||||
// Show copied feedback
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper.appendChild(copyButton);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle SPA transitions for blog post navigation
|
|
||||||
function setupSPATransitions() {
|
|
||||||
// Handle prev/next navigation links
|
|
||||||
document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
|
|
||||||
// Skip links that are anchor links or already processed
|
|
||||||
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Main initialization function
|
animateContent();
|
||||||
function initializeBlogPost() {
|
});
|
||||||
// Initialize remaining components
|
|
||||||
initializeCodeCopyButtons();
|
|
||||||
setupSPATransitions();
|
|
||||||
|
|
||||||
// Scroll to hash if present in URL
|
|
||||||
if (window.location.hash) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const element = document.querySelector(window.location.hash);
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', initializeBlogPost);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', initializeBlogPost);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', initializeBlogPost);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Removing TOC-related styles */
|
|
||||||
|
|
||||||
/* Language badge styling */
|
|
||||||
.language-badge {
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
text-transform: lowercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Extra small screens */
|
|
||||||
@media (min-width: 480px) {
|
|
||||||
.xs\:inline {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xs\:hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced typography for blog content - Responsive adjustments */
|
|
||||||
.prose {
|
|
||||||
@reference text-zinc-800 dark:text-zinc-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose h1,
|
|
||||||
.prose h2,
|
|
||||||
.prose h3,
|
|
||||||
.prose h4 {
|
|
||||||
@reference font-semibold text-zinc-900 dark:text-zinc-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose h1 {
|
|
||||||
@reference text-2xl sm:text-3xl md:text-4xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose h2 {
|
|
||||||
@reference mb-3 mt-8 border-b border-zinc-200 pb-2 text-xl dark:border-zinc-800 sm:mb-4 sm:mt-12 sm:text-2xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose h3 {
|
|
||||||
@reference mb-2 mt-6 text-lg sm:mb-3 sm:mt-8 sm:text-xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose p {
|
|
||||||
@reference mb-4 text-sm leading-relaxed sm:mb-6 sm:text-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose a {
|
|
||||||
@reference font-medium text-zinc-800 underline decoration-zinc-400 underline-offset-2 transition-colors hover:text-zinc-600 hover:decoration-zinc-600 dark:text-zinc-300 dark:decoration-zinc-600 dark:hover:text-zinc-100 dark:hover:decoration-zinc-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose blockquote {
|
|
||||||
@reference my-4 border-l-4 border-zinc-300 pl-4 italic text-zinc-700 dark:border-zinc-700 dark:text-zinc-300 sm:my-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose code {
|
|
||||||
@reference rounded bg-zinc-100 px-1.5 py-0.5 text-sm font-medium text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose pre {
|
|
||||||
@reference my-4 overflow-x-auto rounded-lg bg-[#1e293b] p-3 text-xs text-zinc-200 shadow-md dark:bg-[#1e293b] sm:my-6 sm:p-4 sm:text-sm !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose pre code {
|
|
||||||
@reference bg-transparent p-0 text-zinc-200 dark:text-zinc-200 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose img {
|
|
||||||
@reference mx-auto my-6 h-auto max-w-full rounded-lg shadow-md sm:my-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose ul,
|
|
||||||
.prose ol {
|
|
||||||
@reference my-4 pl-5 sm:my-6 sm:pl-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose li {
|
|
||||||
@reference mb-1 text-sm sm:mb-2 sm:text-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose hr {
|
|
||||||
@reference my-8 border-zinc-200 dark:border-zinc-800 sm:my-10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Line clamp for truncating text */
|
|
||||||
.line-clamp-2 {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
@@ -1,438 +1,103 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import { readItems, readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
import directus from '../../../lib/directus';
|
import type { Post } from '@lib/directusTypes';
|
||||||
import { readItems } from '@directus/sdk';
|
|
||||||
|
|
||||||
|
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(
|
const posts = await directus.request(
|
||||||
readItems('posts', {
|
readItems('posts', {
|
||||||
|
filter: { published: { _eq: true } },
|
||||||
fields: ['*'],
|
fields: ['*'],
|
||||||
sort: ['-published_date'],
|
sort: ['-published_date'],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
const selectedPosts: Post[] = posts.filter((p) => p.selected);
|
||||||
|
|
||||||
const sortedPosts = posts.sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf());
|
const description =
|
||||||
|
"Sharing what I've learned, one post at a time. I hope you find something useful.";
|
||||||
// Group posts by year for timeline effect
|
|
||||||
const postsByYear = sortedPosts.reduce((acc, post) => {
|
|
||||||
const year = new Date(post.published_date).getFullYear();
|
|
||||||
if (!acc[year]) acc[year] = [];
|
|
||||||
acc[year].push(post);
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
|
|
||||||
const years = Object.keys(postsByYear).sort((a, b) => b - a);
|
|
||||||
|
|
||||||
// Get total post count
|
|
||||||
const totalPosts = sortedPosts.length;
|
|
||||||
|
|
||||||
// Get unique tags for search suggestions
|
|
||||||
const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Blog">
|
<BaseLayout
|
||||||
<div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16">
|
title="Blog"
|
||||||
<!-- Header with search -->
|
description={description}
|
||||||
<div class="relative mb-12 sm:mb-20">
|
structuredData={{
|
||||||
<!-- Decorative elements -->
|
'@context': 'https://schema.org',
|
||||||
<div
|
'@type': 'WebPage',
|
||||||
class="animate-blob absolute -left-10 -top-10 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-left-20 sm:-top-20 sm:h-72 sm:w-72"
|
inLanguage: 'en-US',
|
||||||
>
|
'@id': Astro.url.href,
|
||||||
</div>
|
url: Astro.url.href,
|
||||||
<div
|
name: `Blog | ${global.name}`,
|
||||||
class="animate-blob animation-delay-2000 absolute -bottom-10 -right-10 h-48 w-48 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-72 sm:w-72"
|
description: description,
|
||||||
>
|
isPartOf: {
|
||||||
</div>
|
'@type': 'WebSite',
|
||||||
|
url: global.site_url,
|
||||||
|
name: global.name,
|
||||||
|
description: global.about,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HeroSection title="Blog" subTitle={description} src={blogImg} alt={global.blog_image_alt} />
|
||||||
|
|
||||||
<div class="relative text-center">
|
<BlogRecentCard posts={posts} />
|
||||||
<h1
|
<BlogFeaturedArticle posts={selectedPosts} />
|
||||||
class="mb-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-4xl md:text-5xl"
|
|
||||||
>
|
|
||||||
Blog
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p
|
|
||||||
class="mx-auto mb-6 max-w-2xl text-sm text-zinc-600 dark:text-zinc-400 sm:mb-10 sm:text-base"
|
|
||||||
>
|
|
||||||
Thoughts, ideas, and explorations on technology and selfhosting.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Grid layout for mobile experience -->
|
|
||||||
<div class="grid grid-cols-1 gap-6 sm:gap-8 md:grid-cols-12">
|
|
||||||
<!-- Featured post (if exists) -->
|
|
||||||
{
|
|
||||||
sortedPosts.length > 0 && (
|
|
||||||
<div class="mb-8 sm:mb-12 md:col-span-12">
|
|
||||||
<article class="group relative overflow-hidden rounded-none border-b border-zinc-200 pb-6 dark:border-zinc-800 sm:pb-8">
|
|
||||||
<div class="flex h-full flex-col gap-6 sm:gap-8 md:flex-row">
|
|
||||||
{sortedPosts[0].image && (
|
|
||||||
<div class="mx-auto h-60 w-full max-w-full overflow-hidden sm:h-80 sm:max-w-md md:mx-0 md:h-96 md:w-1/2">
|
|
||||||
<img
|
|
||||||
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${sortedPosts[0].image}`}
|
|
||||||
alt={sortedPosts[0].title}
|
|
||||||
class="h-full w-full object-cover grayscale transition-all duration-700 hover:grayscale-0 group-hover:scale-105"
|
|
||||||
loading="eager"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col justify-center">
|
|
||||||
<div class="mb-3 flex items-center justify-center gap-2 text-xs text-zinc-500 dark:text-zinc-400 sm:text-sm md:justify-start">
|
|
||||||
<span class="font-medium uppercase tracking-wider">Featured</span>
|
|
||||||
<span class="h-px w-6 bg-zinc-300 dark:bg-zinc-700 sm:w-8" />
|
|
||||||
{sortedPosts[0].published_date && (
|
|
||||||
<time datetime={sortedPosts[0].published_date.toLocaleString()}>
|
|
||||||
{sortedPosts[0].published_date.toLocaleString('en-US', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
})}
|
|
||||||
</time>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 class="mb-3 text-center text-2xl font-bold text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:mb-4 sm:text-3xl md:text-left">
|
|
||||||
<a
|
|
||||||
href={`/blog/${sortedPosts[0].slug}/`}
|
|
||||||
class="before:absolute before:inset-0"
|
|
||||||
>
|
|
||||||
{sortedPosts[0].title}
|
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p class="mb-4 line-clamp-3 text-center text-sm text-zinc-600 dark:text-zinc-400 sm:mb-6 sm:text-base md:text-left">
|
|
||||||
{sortedPosts[0].description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-center gap-3 sm:gap-4 md:justify-start">
|
|
||||||
{sortedPosts[0].tags && (
|
|
||||||
<div class="flex flex-wrap justify-center gap-2 md:justify-start">
|
|
||||||
{sortedPosts[0].tags.slice(0, 2).map((tag) => (
|
|
||||||
<span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<!-- Improved sidebar for mobile -->
|
|
||||||
<div class="relative md:col-span-3">
|
|
||||||
<div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0">
|
|
||||||
<h3
|
|
||||||
class="mb-4 text-center text-lg font-medium uppercase tracking-wider text-zinc-900 dark:text-zinc-100 md:text-left"
|
|
||||||
>
|
|
||||||
Archive
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<!-- Horizontal scrollable archive on mobile, vertical on desktop -->
|
|
||||||
<div
|
|
||||||
class="hide-scrollbar flex overflow-x-auto pb-4 md:flex-col md:overflow-visible md:pb-0"
|
|
||||||
>
|
|
||||||
{
|
|
||||||
years.map((year, index) => (
|
|
||||||
<a
|
|
||||||
href={`#year-${year}`}
|
|
||||||
class={`mr-3 flex items-center whitespace-nowrap rounded-full border-b border-zinc-100 px-4 py-2 transition-colors hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-900 md:mr-0 md:w-full md:whitespace-normal md:rounded-none md:px-0 md:py-3 ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`}
|
|
||||||
>
|
|
||||||
<span class="text-base font-medium text-zinc-900 dark:text-zinc-100 md:text-lg">
|
|
||||||
{year}
|
|
||||||
</span>
|
|
||||||
<span class="ml-2 text-xs text-zinc-500 dark:text-zinc-400 md:ml-auto md:text-sm">
|
|
||||||
{postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Improved post grid for mobile -->
|
|
||||||
<div class="md:col-span-9">
|
|
||||||
{
|
|
||||||
years.map((year) => (
|
|
||||||
<div id={`year-${year}`} class="mb-12 scroll-mt-16 sm:mb-20">
|
|
||||||
<h2 class="mb-6 border-b border-zinc-200 pb-3 text-center text-xl font-bold text-zinc-900 dark:border-zinc-800 dark:text-zinc-100 sm:mb-8 sm:pb-4 sm:text-2xl md:text-left">
|
|
||||||
{year}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}
|
|
||||||
>
|
|
||||||
{postsByYear[year].map((post, index) => (
|
|
||||||
<article class="group relative mx-auto flex h-full w-full max-w-sm flex-col sm:max-w-md md:mx-0">
|
|
||||||
{post.image && (
|
|
||||||
<div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56">
|
|
||||||
<img
|
|
||||||
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
|
|
||||||
alt={post.title}
|
|
||||||
class="h-full w-full object-cover grayscale transition-all duration-700 hover:grayscale-0 group-hover:scale-105"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col">
|
|
||||||
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 dark:text-zinc-400 sm:mb-3 sm:gap-4 sm:text-sm md:justify-start">
|
|
||||||
{post.pubDate && (
|
|
||||||
<time
|
|
||||||
datetime={post.published_date.toLocaleString()}
|
|
||||||
class="flex items-center"
|
|
||||||
>
|
|
||||||
{post.published_date.toLocaleString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric',
|
|
||||||
})}
|
|
||||||
</time>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="mb-2 text-center text-lg font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:mb-3 sm:text-xl md:text-left">
|
|
||||||
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
|
|
||||||
{post.title}
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p class="mb-4 line-clamp-2 flex-grow text-center text-sm text-zinc-600 dark:text-zinc-400 md:text-left">
|
|
||||||
{post.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{post.tags && (
|
|
||||||
<div class="mt-auto flex flex-wrap justify-center gap-2 md:justify-start">
|
|
||||||
{post.tags.slice(0, 2).map((tag) => (
|
|
||||||
<span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
|
|
||||||
{tag}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{post.tags.length > 2 && (
|
|
||||||
<span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
|
|
||||||
+{post.tags.length - 2}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Blob animation */
|
|
||||||
.animate-blob {
|
|
||||||
animation: blob-bounce 8s infinite ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-2000 {
|
|
||||||
animation-delay: 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blob-bounce {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translate(5%, 5%) scale(1.05);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translate(0, 10%) scale(1);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: translate(-5%, 5%) scale(0.95);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Search container hover effect */
|
|
||||||
.search-container:hover .search-pulse {
|
|
||||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input focus animation */
|
|
||||||
input:focus + div .search-pulse {
|
|
||||||
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hide scrollbar but keep functionality */
|
|
||||||
.hide-scrollbar {
|
|
||||||
-ms-overflow-style: none;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Line clamp for descriptions */
|
|
||||||
.line-clamp-2 {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-clamp-3 {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 3;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Improved touch targets for mobile */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
a,
|
|
||||||
button {
|
|
||||||
min-height: 44px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Script không thay đổi - giữ nguyên chức năng
|
// Add smooth reveal animations for content after loading
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('astro:page-load', () => {
|
||||||
const backToTopButton = document.getElementById('back-to-top');
|
const animateContent = () => {
|
||||||
|
// Animate group 1
|
||||||
if (backToTopButton) {
|
const smoothReveal = document.querySelectorAll('.smooth-reveal');
|
||||||
// Show button when scrolled down
|
smoothReveal.forEach((el, index) => {
|
||||||
const toggleBackToTopButton = () => {
|
setTimeout(
|
||||||
if (window.scrollY > 300) {
|
() => {
|
||||||
backToTopButton.classList.remove('opacity-0', 'invisible');
|
el.classList.add('animate-reveal');
|
||||||
backToTopButton.classList.add('opacity-100', 'visible');
|
},
|
||||||
} else {
|
200 + index * 300
|
||||||
backToTopButton.classList.remove('opacity-100', 'visible');
|
);
|
||||||
backToTopButton.classList.add('opacity-0', 'invisible');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Scroll to top when clicked
|
|
||||||
backToTopButton.addEventListener('click', () => {
|
|
||||||
window.scrollTo({
|
|
||||||
top: 0,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check scroll position
|
// Animate group 2
|
||||||
window.addEventListener('scroll', toggleBackToTopButton);
|
const smoothReveal2 = document.querySelectorAll('.smooth-reveal-2');
|
||||||
toggleBackToTopButton(); // Initial check
|
smoothReveal2.forEach((el, index) => {
|
||||||
}
|
setTimeout(
|
||||||
|
() => {
|
||||||
// Add smooth scrolling to year links
|
el.classList.add('animate-reveal');
|
||||||
document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
|
},
|
||||||
anchor.addEventListener('click', function (e) {
|
500 + index * 100
|
||||||
e.preventDefault();
|
);
|
||||||
const targetId = this.getAttribute('href');
|
|
||||||
const targetElement = document.querySelector(targetId);
|
|
||||||
|
|
||||||
if (targetElement) {
|
|
||||||
window.scrollTo({
|
|
||||||
top: targetElement.offsetTop - 100,
|
|
||||||
behavior: 'smooth',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update URL hash without jumping
|
|
||||||
history.pushState(null, null, targetId);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// Add touch support for hover effects
|
// Animate topic cards with staggered delay
|
||||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
|
||||||
|
smoothRevealCards.forEach((el, index) => {
|
||||||
if (isTouchDevice) {
|
setTimeout(
|
||||||
const articles = document.querySelectorAll('article');
|
() => {
|
||||||
|
el.classList.add('animate-reveal');
|
||||||
articles.forEach((article) => {
|
},
|
||||||
article.addEventListener('touchstart', () => {
|
1000 + index * 250
|
||||||
article.classList.add('is-touched');
|
);
|
||||||
});
|
|
||||||
|
|
||||||
article.addEventListener('touchend', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
article.classList.remove('is-touched');
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
// 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();
|
||||||
});
|
});
|
||||||
|
|
||||||
// SPA transition handling
|
|
||||||
function setupSPATransitions() {
|
|
||||||
// Handle all blog post links for SPA transitions
|
|
||||||
document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
|
|
||||||
// Skip links that are anchor links or already processed
|
|
||||||
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle year anchor links specially
|
|
||||||
document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
|
|
||||||
anchor.setAttribute('data-spa-internal', 'true');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
|
||||||
</script>
|
</script>
|
||||||
|
67
src/pages/categories/[...slug].astro
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import { readItems, readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
import type { Post } from '@lib/directusTypes';
|
||||||
|
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||||
|
import BlogCard from '@components/blog/BlogCard.astro';
|
||||||
|
import HeaderSection from '@components/ui/sections/HeaderSection.astro';
|
||||||
|
|
||||||
|
export async function getStaticPaths() {
|
||||||
|
const categories = await getCollection('categories');
|
||||||
|
return categories.map((category) => ({
|
||||||
|
params: { slug: category.slug },
|
||||||
|
props: { category },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
const { category } = Astro.props;
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
const posts = await directus.request(
|
||||||
|
readItems('posts', {
|
||||||
|
filter: { published: { _eq: true } },
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-published_date'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
const categoriesPosts = posts
|
||||||
|
.sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf())
|
||||||
|
.filter((b) => {
|
||||||
|
return b.category === category.slug;
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title={category.data.title}
|
||||||
|
description={category.data.description}
|
||||||
|
structuredData={{
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebPage',
|
||||||
|
inLanguage: 'en-US',
|
||||||
|
'@id': Astro.url.href,
|
||||||
|
url: Astro.url.href,
|
||||||
|
name: `${category.data.title} | ${global.name}`,
|
||||||
|
description: category.data.description,
|
||||||
|
isPartOf: {
|
||||||
|
url: `${global.site_url}/categories`,
|
||||||
|
name: global.name,
|
||||||
|
description: global.about,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HeaderSection
|
||||||
|
title={category.data.title}
|
||||||
|
subTitle={category.data.description}
|
||||||
|
btnExists
|
||||||
|
btnTitle="Back to Categories"
|
||||||
|
btnURL="/categories"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section class="mx-auto mt-10 mb-10 max-w-[85rem] px-4 py-8 sm:px-6 lg:px-8 2xl:max-w-full">
|
||||||
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{categoriesPosts.map((b) => <BlogCard post={b} />)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
182
src/pages/categories/index.astro
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import { readItems, readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
|
import type { Post } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||||
|
import BlogCategoryCard from '@components/blog/BlogCategoryCard.astro';
|
||||||
|
import HeroSection from '@components/ui/sections/HeroSection.astro';
|
||||||
|
import { timeago } from '@support/time';
|
||||||
|
import categoryImg from '@images/autumn_bench.png';
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
const posts = await directus.request(
|
||||||
|
readItems('posts', {
|
||||||
|
filter: { published: { _eq: true } },
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-published_date'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const postMap: Map<string, Post[]> = posts
|
||||||
|
.sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf())
|
||||||
|
.reduce((acc, obj) => {
|
||||||
|
let posts = acc.get(obj.category);
|
||||||
|
if (!posts) {
|
||||||
|
posts = [];
|
||||||
|
}
|
||||||
|
posts.push(obj);
|
||||||
|
|
||||||
|
acc.set(obj.category, posts);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, new Map<string, Post[]>());
|
||||||
|
|
||||||
|
const layoutPattern = [
|
||||||
|
{ col: 2, row: 2 },
|
||||||
|
{ col: 2, row: 1 },
|
||||||
|
{ col: 1, row: 1 },
|
||||||
|
{ col: 1, row: 1 },
|
||||||
|
{ col: 1, row: 2 },
|
||||||
|
{ col: 2, row: 1 },
|
||||||
|
{ col: 1, row: 1 },
|
||||||
|
{ col: 1, row: 1 },
|
||||||
|
{ col: 1, row: 1 },
|
||||||
|
{ col: 1, row: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const categories = (await getCollection('categories'))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aCount = postMap.get(a.slug)?.length ?? 0;
|
||||||
|
const bCount = postMap.get(b.slug)?.length ?? 0;
|
||||||
|
return bCount - aCount;
|
||||||
|
})
|
||||||
|
.map((c, index) => {
|
||||||
|
const posts = postMap.get(c.slug);
|
||||||
|
const pattern = layoutPattern[index % layoutPattern.length];
|
||||||
|
const smColSpan = Math.min(pattern.col, 2);
|
||||||
|
const mdColSpan = Math.min(pattern.col, 4);
|
||||||
|
const rowSpan = pattern.row;
|
||||||
|
const rowSpanClass = rowSpan > 1 ? `row-span-${rowSpan}` : 'row-span-1';
|
||||||
|
const gridItemClass = `col-span-${smColSpan} md:col-span-${mdColSpan} ${rowSpanClass} smooth-reveal-cards rounded-xl transition-all duration-300 shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg border border-stone-200/50 dark:border-stone-700/50`;
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
posts,
|
||||||
|
gridItemClass,
|
||||||
|
layoutPattern: {
|
||||||
|
smCol: smColSpan,
|
||||||
|
mdCol: mdColSpan,
|
||||||
|
row: rowSpan,
|
||||||
|
index,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const description =
|
||||||
|
'Here are some of the general categories that I am interested in, including homelabs, technology, and Minnesota.';
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="All Categories"
|
||||||
|
description={description}
|
||||||
|
structuredData={{
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebPage',
|
||||||
|
inLanguage: 'en-US',
|
||||||
|
'@id': Astro.url.href,
|
||||||
|
url: Astro.url.href,
|
||||||
|
name: `All Categories | ${global.name}`,
|
||||||
|
description: description,
|
||||||
|
isPartOf: {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
url: global.site_url,
|
||||||
|
name: global.name,
|
||||||
|
description: global.about,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<HeroSection
|
||||||
|
title="Categories"
|
||||||
|
subTitle={description}
|
||||||
|
src={categoryImg}
|
||||||
|
alt={global.categories_image_alt}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<section class="mx-auto px-4 py-10 sm:px-6 lg:px-8 lg:py-14 lg:pt-10 2xl:max-w-full">
|
||||||
|
<div class="grid grid-flow-row-dense grid-cols-2 gap-4 md:grid-cols-4">
|
||||||
|
{
|
||||||
|
categories.map((category) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={category.gridItemClass}
|
||||||
|
style={category.layoutPattern.row > 1 ? 'grid-row: span 2 / span 2;' : ''}
|
||||||
|
>
|
||||||
|
<BlogCategoryCard
|
||||||
|
slug={category.slug}
|
||||||
|
title={category.data.title}
|
||||||
|
description={category.data.description}
|
||||||
|
count={postMap.get(category.slug)?.length ?? 0}
|
||||||
|
publishDate={timeago(postMap.get(category.slug)?.[0]?.published_date)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// 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 * 150
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Animate topic cards with staggered delay
|
||||||
|
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
|
||||||
|
smoothRevealCards.forEach((el, index) => {
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
el.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
500 + index * 100
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 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>
|
24
src/pages/favicon.ico.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import path from 'node:path';
|
||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import sharp from 'sharp';
|
||||||
|
import ico from 'sharp-ico';
|
||||||
|
|
||||||
|
const faviconSrc = path.resolve('src/images/favicon_icon.png');
|
||||||
|
|
||||||
|
export const GET: APIRoute = async () => {
|
||||||
|
// Resize the image to multiple sizes
|
||||||
|
const sizes = [16, 32];
|
||||||
|
|
||||||
|
const buffers = await Promise.all(
|
||||||
|
sizes.map(async (size) => {
|
||||||
|
return await sharp(faviconSrc).resize(size).toFormat('png').toBuffer();
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Convert the image to an ICO file
|
||||||
|
const icoBuffer = ico.encode(buffers);
|
||||||
|
|
||||||
|
return new Response(icoBuffer, {
|
||||||
|
headers: { 'Content-Type': 'image/x-icon' },
|
||||||
|
});
|
||||||
|
};
|
@@ -1,591 +1,108 @@
|
|||||||
---
|
---
|
||||||
import Layout from '../layouts/Layout.astro';
|
import { readSingleton } from '@directus/sdk';
|
||||||
import FormattedDate from '../components/FormattedDate.astro';
|
|
||||||
|
|
||||||
import directus from '../../lib/directus';
|
import directus from '@lib/directus';
|
||||||
import { readItems, readSingleton } from '@directus/sdk';
|
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('global'));
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
|
||||||
const posts = await directus.request(
|
const description =
|
||||||
readItems('posts', {
|
'Engineering the cloud by day, homelab by night, and exploring Minnesota in between.';
|
||||||
fields: ['*'],
|
|
||||||
sort: ['-published_date'],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const recentPosts = posts
|
|
||||||
.sort((a, b) => b.published_date.getTime() - a.published_date.getTime())
|
|
||||||
.slice(0, 3);
|
|
||||||
|
|
||||||
const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, 5);
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title=`Home | ${global.name}`>
|
<BaseLayout
|
||||||
<!-- Hero Section with improved mobile responsiveness -->
|
title="Home"
|
||||||
<section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20">
|
description={description}
|
||||||
<div class="relative mx-auto max-w-2xl">
|
structuredData={{
|
||||||
<!-- Adjusted blob positions and sizes for better mobile appearance -->
|
'@context': 'https://schema.org',
|
||||||
<div
|
'@type': 'WebPage',
|
||||||
class="animate-blob theme-transition-bg absolute -left-10 -top-10 h-40 w-40 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50 sm:-left-20 sm:-top-20 sm:h-64 sm:w-64"
|
inLanguage: 'en-US',
|
||||||
>
|
'@id': Astro.url.href,
|
||||||
</div>
|
url: Astro.url.href,
|
||||||
<div
|
name: `Home | ${global.name}`,
|
||||||
class="animate-blob animation-delay-2000 theme-transition-bg absolute -bottom-10 -right-10 h-40 w-40 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-64 sm:w-64"
|
description: description,
|
||||||
>
|
isPartOf: {
|
||||||
</div>
|
'@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}
|
||||||
|
/>
|
||||||
|
|
||||||
<div class="relative text-center sm:text-left">
|
<FeaturesSection />
|
||||||
<h1
|
|
||||||
class="theme-transition-color hero-text text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-4xl md:text-5xl lg:text-6xl"
|
|
||||||
>
|
|
||||||
<span class="block">Writing on technology,</span>
|
|
||||||
<span class="mt-1 block">development, and</span>
|
|
||||||
<span class="relative mt-1 block">
|
|
||||||
<span class="relative inline-block">
|
|
||||||
selfhosting.
|
|
||||||
<span
|
|
||||||
class="theme-transition-bg absolute -bottom-1 left-0 h-1 w-full origin-left transform bg-zinc-800 dark:bg-zinc-200"
|
|
||||||
></span>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</h1>
|
|
||||||
<p
|
|
||||||
class="theme-transition-color mx-auto mt-4 max-w-lg text-base leading-relaxed text-zinc-600 dark:text-zinc-400 sm:mx-0 sm:mt-6 sm:text-lg md:mt-8"
|
|
||||||
>
|
|
||||||
{global.about}
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
class="mt-6 flex flex-wrap justify-center gap-3 sm:mt-8 sm:justify-start sm:gap-4 md:mt-10 md:gap-6"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
href="/about"
|
|
||||||
class="theme-transition-color group relative inline-flex min-h-[44px] items-center gap-2 text-sm font-medium text-zinc-900 transition-all duration-300 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
|
|
||||||
>
|
|
||||||
<span>More about me</span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
|
|
||||||
</svg>
|
|
||||||
<span
|
|
||||||
class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
|
|
||||||
></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Featured Post Section - Improved for mobile -->
|
<LatestPosts />
|
||||||
<section
|
|
||||||
class="theme-transition-all border-t border-zinc-100 px-4 py-10 dark:border-zinc-800 sm:px-6 sm:py-12 md:py-16"
|
|
||||||
>
|
|
||||||
<div class="mx-auto max-w-3xl">
|
|
||||||
<div
|
|
||||||
class="mb-6 flex flex-col justify-between gap-4 sm:mb-8 sm:flex-row sm:items-center md:mb-12"
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
class="theme-transition-color text-center text-xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-left sm:text-2xl md:text-3xl"
|
|
||||||
>
|
|
||||||
Recent Posts
|
|
||||||
</h2>
|
|
||||||
<a
|
|
||||||
href="/blog"
|
|
||||||
class="theme-transition-color group relative flex min-h-[44px] items-center justify-center self-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300 sm:self-auto"
|
|
||||||
>
|
|
||||||
<span class="flex items-center gap-1">
|
|
||||||
View all posts
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
|
|
||||||
></span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Improved grid for better mobile layout -->
|
<HeroSectionAlt
|
||||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3">
|
title="Follow me on Gitea"
|
||||||
{
|
subTitle="I love open source and have my code availabile on my Gitea server."
|
||||||
recentPosts.map((post, index) => (
|
url="https://gitea.alexlebens.dev"
|
||||||
<article class="hover-3d theme-transition-element group relative mx-auto flex w-full max-w-sm flex-col items-start sm:mx-0">
|
/>
|
||||||
<div class="theme-transition-bg absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" />
|
</BaseLayout>
|
||||||
|
|
||||||
{post.image && (
|
|
||||||
<div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
|
|
||||||
<img
|
|
||||||
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
|
|
||||||
alt={post.title}
|
|
||||||
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
|
||||||
loading={index === 0 ? 'eager' : 'lazy'}
|
|
||||||
width="400"
|
|
||||||
height="225"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="theme-transition-color relative z-10 flex w-full flex-wrap items-center justify-center gap-x-3 gap-y-2 text-xs text-zinc-500 dark:text-zinc-400 sm:justify-start sm:gap-x-4">
|
|
||||||
<time datetime={post.published_date.toLocaleString()} class="font-medium">
|
|
||||||
<FormattedDate date={post.published_date} />
|
|
||||||
</time>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="theme-transition-color relative z-10 mt-3 w-full text-center text-lg font-semibold tracking-tight text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:text-left sm:text-xl">
|
|
||||||
<a
|
|
||||||
href={`/blog/${post.slug}`}
|
|
||||||
class="flex min-h-[44px] items-center justify-center sm:justify-start"
|
|
||||||
>
|
|
||||||
<span class="absolute -inset-x-4 -inset-y-2.5 sm:-inset-x-6 sm:-inset-y-4" />
|
|
||||||
{post.title}
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p class="theme-transition-color relative z-10 mt-2 line-clamp-3 w-full text-center text-sm text-zinc-600 dark:text-zinc-400 sm:mt-3 sm:text-left">
|
|
||||||
{post.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{post.tags && post.tags.length > 0 && (
|
|
||||||
<div class="relative z-10 mt-3 flex w-full flex-wrap justify-center gap-2 sm:mt-4 sm:justify-start">
|
|
||||||
{post.tags.slice(0, 3).map((tag) => (
|
|
||||||
<a
|
|
||||||
href={`/topics/${tag}`}
|
|
||||||
class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-800 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 sm:px-3"
|
|
||||||
>
|
|
||||||
#{tag}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
{post.tags.length > 3 && (
|
|
||||||
<span class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
|
|
||||||
+{post.tags.length - 3} more
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<a
|
|
||||||
href={`/blog/${post.slug}`}
|
|
||||||
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 dark:text-zinc-300 dark:group-hover:text-zinc-100 sm:mx-0 sm:mt-4"
|
|
||||||
>
|
|
||||||
<span class="relative inline-block overflow-hidden">
|
|
||||||
<span class="block transition-transform duration-300 group-hover:-translate-y-full">
|
|
||||||
Read article
|
|
||||||
</span>
|
|
||||||
<span class="absolute left-0 top-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
|
|
||||||
Explore now
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
viewBox="0 0 16 16"
|
|
||||||
fill="none"
|
|
||||||
aria-hidden="true"
|
|
||||||
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 group-hover:translate-x-1"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M6.75 5.75 9.25 8l-2.5 2.25"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</article>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- Topics/Tags Section - Improved for mobile -->
|
|
||||||
{
|
|
||||||
allTags.length > 0 && (
|
|
||||||
<section class="theme-transition-all border-t border-zinc-100 px-4 py-10 dark:border-zinc-800 sm:px-6 sm:py-12 md:py-16">
|
|
||||||
<div class="mx-auto max-w-3xl">
|
|
||||||
<h2 class="theme-transition-color mb-6 text-center text-xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:mb-8 sm:text-left sm:text-2xl md:text-3xl">
|
|
||||||
Explore Topics
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="mx-auto grid max-w-xs grid-cols-1 gap-3 sm:max-w-none sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
|
|
||||||
{allTags.map((tag) => {
|
|
||||||
const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={`/topics/${tag}`}
|
|
||||||
class="theme-transition-all group flex min-h-[80px] flex-col rounded-xl border border-zinc-200 p-3 transition-all duration-300 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/70 sm:min-h-[90px] sm:p-4 md:p-6"
|
|
||||||
>
|
|
||||||
<div class="mb-2 flex items-start justify-between">
|
|
||||||
<span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
||||||
#{tag}
|
|
||||||
</span>
|
|
||||||
<span class="theme-transition-all flex-shrink-0 rounded-full bg-zinc-100 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
|
|
||||||
{tagCount} {tagCount === 1 ? 'post' : 'posts'}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<p class="theme-transition-color mt-1 text-xs text-zinc-600 dark:text-zinc-400">
|
|
||||||
Explore articles about {tag}
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-6 text-center sm:mt-8">
|
|
||||||
<a
|
|
||||||
href="/tags"
|
|
||||||
class="theme-transition-color inline-flex min-h-[44px] items-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
|
|
||||||
>
|
|
||||||
<span>View all topics</span>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Add hover effect for cards on touch devices
|
// Add smooth reveal animations for content after loading
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('astro:page-load', () => {
|
||||||
// Check if it's a touch device
|
|
||||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
||||||
|
|
||||||
if (isTouchDevice) {
|
|
||||||
const cards = document.querySelectorAll('.hover-3d');
|
|
||||||
|
|
||||||
cards.forEach((card) => {
|
|
||||||
card.addEventListener('touchstart', () => {
|
|
||||||
card.classList.add('is-touched');
|
|
||||||
});
|
|
||||||
|
|
||||||
card.addEventListener('touchend', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
card.classList.remove('is-touched');
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Disable hover animations on touch devices for better performance
|
|
||||||
document.documentElement.classList.add('touch-device');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Improved viewport height fix for mobile browsers
|
|
||||||
const setVh = () => {
|
|
||||||
const vh = window.innerHeight * 0.01;
|
|
||||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set initial value
|
|
||||||
setVh();
|
|
||||||
|
|
||||||
// Update on resize and scroll to prevent content shifting
|
|
||||||
window.addEventListener('resize', setVh);
|
|
||||||
|
|
||||||
// Use a debounced scroll handler to prevent performance issues
|
|
||||||
let scrollTimeout;
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
if (scrollTimeout) {
|
|
||||||
window.cancelAnimationFrame(scrollTimeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollTimeout = window.requestAnimationFrame(() => {
|
|
||||||
// Lock width during scroll
|
|
||||||
document.body.style.width = '100%';
|
|
||||||
document.body.style.overflowX = 'hidden';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fix for iOS Safari address bar height changes
|
|
||||||
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
|
|
||||||
// Force the layout to use the initial viewport size
|
|
||||||
document.documentElement.style.setProperty('--initial-vh', `${window.innerHeight * 0.01}px`);
|
|
||||||
|
|
||||||
// Apply fixed height to sections to prevent resizing
|
|
||||||
const sections = document.querySelectorAll('section');
|
|
||||||
sections.forEach((section) => {
|
|
||||||
section.style.width = '100%';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Improved theme change handler that preserves scroll position and provides smoother transitions
|
|
||||||
document.addEventListener('themeChanged', () => {
|
|
||||||
// Store current scroll position
|
|
||||||
const scrollPosition = window.scrollY;
|
|
||||||
|
|
||||||
// Create a temporary overlay for smoother transition
|
|
||||||
const overlay = document.createElement('div');
|
|
||||||
overlay.style.cssText = `
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
background-color: ${document.documentElement.classList.contains('dark') ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'};
|
|
||||||
z-index: 9999;
|
|
||||||
pointer-events: none;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
`;
|
|
||||||
document.body.appendChild(overlay);
|
|
||||||
|
|
||||||
// Fade in overlay
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
overlay.style.opacity = '0.5';
|
|
||||||
|
|
||||||
// Update theme-transition elements without forcing reflow of entire page
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
document
|
|
||||||
.querySelectorAll(
|
|
||||||
'.theme-transition-all, .theme-transition-bg, .theme-transition-color'
|
|
||||||
)
|
|
||||||
.forEach((el) => {
|
|
||||||
// Apply a subtle animation instead of a hard reset
|
|
||||||
el.style.transition = 'all 0.5s ease';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fade out overlay after transition completes
|
|
||||||
setTimeout(() => {
|
|
||||||
overlay.style.opacity = '0';
|
|
||||||
setTimeout(() => {
|
|
||||||
overlay.remove();
|
|
||||||
}, 300);
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restore scroll position (prevents jumping to top)
|
|
||||||
if (scrollPosition > 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
window.scrollTo({
|
|
||||||
top: scrollPosition,
|
|
||||||
behavior: 'auto', // Use 'auto' to prevent animation
|
|
||||||
});
|
|
||||||
}, 10);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fix theme inconsistency issues by checking theme on visibility change
|
|
||||||
document.addEventListener('visibilitychange', () => {
|
|
||||||
if (document.visibilityState === 'visible') {
|
|
||||||
const storedTheme = localStorage.getItem('theme');
|
|
||||||
const currentThemeIsDark = document.documentElement.classList.contains('dark');
|
|
||||||
|
|
||||||
if (storedTheme === 'dark' && !currentThemeIsDark) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else if (storedTheme === 'light' && currentThemeIsDark) {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add smooth reveal animations for content after loading
|
|
||||||
const animateContent = () => {
|
const animateContent = () => {
|
||||||
// Animate hero section
|
// Animate group 1
|
||||||
const heroElements = document.querySelectorAll(
|
const smoothReveal = document.querySelectorAll('.smooth-reveal');
|
||||||
'.hero-text span, .hero-text + p, .hero-text ~ div'
|
smoothReveal.forEach((el, index) => {
|
||||||
);
|
|
||||||
heroElements.forEach((el, index) => {
|
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
el.classList.add('animate-reveal');
|
el.classList.add('animate-reveal');
|
||||||
},
|
},
|
||||||
100 + index * 150
|
50 + index * 100
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate posts with staggered delay
|
// Animate group 2
|
||||||
const articles = document.querySelectorAll('article.group');
|
const smoothReveal2 = document.querySelectorAll('.smooth-reveal-2');
|
||||||
articles.forEach((article, index) => {
|
smoothReveal2.forEach((el, index) => {
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
article.classList.add('animate-reveal');
|
el.classList.add('animate-reveal');
|
||||||
},
|
},
|
||||||
500 + index * 150
|
200 + index * 250
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate topic cards with staggered delay
|
// Animate topic cards with staggered delay
|
||||||
const topicCards = document.querySelectorAll('a.group.flex.flex-col');
|
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
|
||||||
topicCards.forEach((card, index) => {
|
smoothRevealCards.forEach((el, index) => {
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
card.classList.add('animate-reveal');
|
el.classList.add('animate-reveal');
|
||||||
},
|
},
|
||||||
800 + index * 100
|
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
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run animations after the loading screen is hidden
|
animateContent();
|
||||||
const loadingScreen = document.getElementById('loading-screen');
|
|
||||||
if (loadingScreen) {
|
|
||||||
// Check if loading screen is already hidden (page refresh)
|
|
||||||
if (loadingScreen.style.display === 'none') {
|
|
||||||
animateContent();
|
|
||||||
} else {
|
|
||||||
// Wait for loading screen to hide
|
|
||||||
const observer = new MutationObserver((mutations) => {
|
|
||||||
mutations.forEach((mutation) => {
|
|
||||||
if (
|
|
||||||
mutation.target === loadingScreen &&
|
|
||||||
mutation.type === 'attributes' &&
|
|
||||||
mutation.attributeName === 'style' &&
|
|
||||||
loadingScreen.style.display === 'none'
|
|
||||||
) {
|
|
||||||
animateContent();
|
|
||||||
observer.disconnect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(loadingScreen, { attributes: true });
|
|
||||||
|
|
||||||
// Fallback
|
|
||||||
setTimeout(animateContent, 3500);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If loading screen doesn't exist for some reason
|
|
||||||
animateContent();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// SPA transition handling for homepage
|
|
||||||
function setupSPATransitions() {
|
|
||||||
// Handle all internal links for SPA transitions
|
|
||||||
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
|
||||||
// Skip links that are anchor links, external links, or already processed
|
|
||||||
if (
|
|
||||||
link.getAttribute('href').includes('#') ||
|
|
||||||
link.getAttribute('target') === '_blank' ||
|
|
||||||
link.hasAttribute('data-spa-handled')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Fix for theme transition issues */
|
|
||||||
:global(:root) {
|
|
||||||
--theme-transition-duration: 0.5s;
|
|
||||||
--theme-transition-timing: ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(html),
|
|
||||||
:global(body) {
|
|
||||||
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.theme-transition-all) {
|
|
||||||
transition: all var(--theme-transition-duration) var(--theme-transition-timing);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.theme-transition-bg) {
|
|
||||||
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.theme-transition-color) {
|
|
||||||
transition: color var(--theme-transition-duration) var(--theme-transition-timing);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure transitions apply to all theme-related properties */
|
|
||||||
:global(*) {
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke, opacity;
|
|
||||||
transition-duration: var(--theme-transition-duration);
|
|
||||||
transition-timing-function: var(--theme-transition-timing);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Remove the forced transition disabling which causes flickering */
|
|
||||||
:global(.theme-switching),
|
|
||||||
:global(.theme-switching *) {
|
|
||||||
/* Use a subtle transition instead of none */
|
|
||||||
transition-duration: 0.3s !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content reveal animations */
|
|
||||||
.hero-text span,
|
|
||||||
.hero-text + p,
|
|
||||||
.hero-text ~ div,
|
|
||||||
article.group,
|
|
||||||
a.group.flex.flex-col {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
transition:
|
|
||||||
opacity 0.8s ease,
|
|
||||||
transform 0.8s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-reveal {
|
|
||||||
opacity: 1 !important;
|
|
||||||
transform: translateY(0) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Rest of your existing styles... */
|
|
||||||
</style>
|
|
||||||
|
31
src/pages/robots.txt.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
// https://docs.astro.build/en/guides/integrations-guide/sitemap/#usage
|
||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
|
||||||
|
const robotsTxt = `
|
||||||
|
User-agent: Googlebot
|
||||||
|
Disallow:
|
||||||
|
Allow: /
|
||||||
|
Crawl-delay: 10
|
||||||
|
|
||||||
|
User-agent: Yandex
|
||||||
|
Disallow:
|
||||||
|
Allow: /
|
||||||
|
Crawl-delay: 2
|
||||||
|
|
||||||
|
User-agent: archive.org_bot
|
||||||
|
Disallow:
|
||||||
|
Allow: /
|
||||||
|
Crawl-delay: 2
|
||||||
|
|
||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: ${new URL('sitemap-index.xml', import.meta.env.SITE).href}`.trim();
|
||||||
|
|
||||||
|
export const GET: APIRoute = () => {
|
||||||
|
return new Response(robotsTxt, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'text/plain; charset=utf-8',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|