Compare commits
105 Commits
086d98ba50
...
0.10.0
Author | SHA1 | Date | |
---|---|---|---|
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 |
40
.gitea/workflows/process-repository.yaml
Normal file
40
.gitea/workflows/process-repository.yaml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
name: process-repository
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: "@daily"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
process-repository:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout Python Script
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
repository: alexlebens/workflow-scripts
|
||||||
|
ref: main
|
||||||
|
token: ${{ secrets.BOT_TOKEN }}
|
||||||
|
path: workflow-scripts
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install requests immutabledict
|
||||||
|
|
||||||
|
- name: Run Script
|
||||||
|
env:
|
||||||
|
INSTANCE_URL: ${{ vars.INSTANCE_URL }}
|
||||||
|
OWNER: ${{ gitea.owner }}
|
||||||
|
REPOSITORY: ${{ gitea.repository }}
|
||||||
|
TOKEN: ${{ secrets.BOT_TOKEN }}
|
||||||
|
LOG_LEVEL: DEBUG
|
||||||
|
ISSUE_STALE_DAYS: 3
|
||||||
|
ISSUE_STALE_TAG: 23
|
||||||
|
ISSUE_EXCLUDE_TAG: 17
|
||||||
|
PULL_REQUEST_STALE_DAYS: 3
|
||||||
|
PULL_REQUEST_STALE_TAG: 23
|
||||||
|
PULL_REQUEST_REQUIRED_TAG: 22
|
||||||
|
run: python ./workflow-scripts/process-repository.py
|
@@ -13,7 +13,7 @@ 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@v4
|
||||||
|
@@ -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
|
|
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22.16.x
|
node-version: 22.17.x
|
||||||
cache: pnpm
|
cache: pnpm
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Install Dependencies
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
ARG REGISTRY=docker.io
|
ARG REGISTRY=docker.io
|
||||||
FROM ${REGISTRY}/node:22.16.0-alpine3.22 AS base
|
FROM ${REGISTRY}/node:22.17.0-alpine3.22 AS base
|
||||||
|
|
||||||
LABEL version="0.8.10"
|
LABEL version="0.10.0"
|
||||||
LABEL description="Astro based personal website"
|
LABEL description="Astro based personal website"
|
||||||
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
|
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
Copyright (c) 2025 Lê Vĩnh Khang
|
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
|
||||||
in the Software without restriction, including without limitation the rights
|
in the Software without restriction, including without limitation the rights
|
||||||
|
@@ -22,6 +22,7 @@ type About = {
|
|||||||
type Links = {
|
type Links = {
|
||||||
github: string;
|
github: string;
|
||||||
linkedin: string;
|
linkedin: string;
|
||||||
|
gitea: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Skill = {
|
type Skill = {
|
||||||
|
15
package.json
15
package.json
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "site-profile",
|
"name": "site-profile",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.8.10",
|
"version": "0.10.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
@@ -17,14 +17,11 @@
|
|||||||
"@astrojs/node": "^9.2.2",
|
"@astrojs/node": "^9.2.2",
|
||||||
"@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",
|
"@directus/sdk": "^20.0.0",
|
||||||
"@directus/sdk": "^19.1.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",
|
"astro": "^5.10.1",
|
||||||
"form-data": "4.0.3",
|
|
||||||
"framer-motion": "^12.16.0",
|
"framer-motion": "^12.16.0",
|
||||||
"postcss-preset-env": "^10.2.1",
|
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hotkeys-hook": "^5.1.0",
|
"react-hotkeys-hook": "^5.1.0",
|
||||||
@@ -34,13 +31,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
"@typescript-eslint/parser": "8.34.0",
|
"@typescript-eslint/parser": "8.36.0",
|
||||||
"eslint": "9.28.0",
|
"eslint": "9.31.0",
|
||||||
"eslint-config-prettier": "10.1.5",
|
"eslint-config-prettier": "10.1.5",
|
||||||
"eslint-plugin-astro": "1.3.1",
|
"eslint-plugin-astro": "1.3.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-astro": "^0.14.1",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.12",
|
"prettier-plugin-tailwindcss": "^0.6.12",
|
||||||
"typescript-eslint": "8.34.0"
|
"typescript-eslint": "8.36.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2377
pnpm-lock.yaml
generated
2377
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -2,12 +2,6 @@
|
|||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
'@tailwindcss/postcss': {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
|
||||||
'postcss-preset-env': {
|
|
||||||
features: {
|
|
||||||
'nesting-rules': false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@@ -1,8 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
|
||||||
<path fill="#000" d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
|
||||||
<style>
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
path { fill: #FFF; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 712 B |
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
// Background.astro - Dot pattern and ambient glow background with smooth theme transitions
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden">
|
<div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden">
|
||||||
@@ -29,24 +29,19 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Theme transition script
|
// Theme transition script
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('astro:page-load', () => {
|
||||||
const themeToggle = document.querySelector('[data-theme-toggle]');
|
const themeToggle = document.querySelector('[data-theme-toggle]');
|
||||||
const overlay = document.getElementById('theme-transition-overlay');
|
const overlay = document.getElementById('theme-transition-overlay');
|
||||||
|
|
||||||
if (themeToggle && overlay) {
|
if (themeToggle && overlay) {
|
||||||
themeToggle.addEventListener('click', () => {
|
themeToggle.addEventListener('click', () => {
|
||||||
// Add transitioning class to optimize performance
|
|
||||||
document.documentElement.classList.add('theme-transitioning');
|
document.documentElement.classList.add('theme-transitioning');
|
||||||
|
|
||||||
// Fade in overlay
|
|
||||||
overlay.style.opacity = '0.15';
|
overlay.style.opacity = '0.15';
|
||||||
overlay.style.transition = 'opacity 0.3s ease';
|
overlay.style.transition = 'opacity 0.3s ease';
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
// Fade out overlay
|
|
||||||
overlay.style.opacity = '0';
|
overlay.style.opacity = '0';
|
||||||
|
|
||||||
// Remove transitioning class after animation completes
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.documentElement.classList.remove('theme-transitioning');
|
document.documentElement.classList.remove('theme-transitioning');
|
||||||
}, 700);
|
}, 700);
|
||||||
|
@@ -8,10 +8,10 @@ const links = await directus.request(readSingleton('links'));
|
|||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
const navLinks = [
|
const navLinks = [
|
||||||
{ text: 'About', href: '/about' },
|
{ text: 'Home', href: '/' },
|
||||||
{ text: 'Blog', href: '/blog' },
|
{ text: 'Blog', href: '/blog' },
|
||||||
{ text: 'Topics', href: '/topics' },
|
{ text: 'Topics', href: '/topics' },
|
||||||
{ text: 'RSS', href: '/rss.xml' },
|
{ text: 'About', href: '/about' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const socialLinks = [
|
const socialLinks = [
|
||||||
@@ -20,6 +20,11 @@ const socialLinks = [
|
|||||||
href: links.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>`,
|
icon: `<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path>`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Gitea',
|
||||||
|
href: links.gitea,
|
||||||
|
icon: `<path d="M7 5C7 3.89543 7.89543 3 9 3C10.1046 3 11 3.89543 11 5C11 5.34168 10.9143 5.66336 10.7633 5.9447H11.3438C13.5529 5.9447 15.3438 7.73556 15.3438 9.9447V11.2244C15.9301 11.5731 16.323 12.213 16.323 12.9447C16.323 14.0493 15.4276 14.9447 14.323 14.9447C13.2184 14.9447 12.323 14.0493 12.323 12.9447C12.323 12.1959 12.7345 11.5432 13.3438 11.2004V9.9447C13.3438 8.84013 12.4483 7.9447 11.3438 7.9447H10V17.2676C10.5978 17.6134 11 18.2597 11 19C11 20.1046 10.1046 21 9 21C7.89543 21 7 20.1046 7 19C7 18.2597 7.4022 17.6134 8 17.2676V6.73244C7.4022 6.38663 7 5.74028 7 5Z" fill="currentColor"/>`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'LinkedIn',
|
name: 'LinkedIn',
|
||||||
href: links.linkedin,
|
href: links.linkedin,
|
||||||
@@ -30,6 +35,7 @@ const socialLinks = [
|
|||||||
|
|
||||||
<footer
|
<footer
|
||||||
class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
|
class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
|
||||||
|
transition:animate="none"
|
||||||
>
|
>
|
||||||
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
<div
|
<div
|
||||||
@@ -48,7 +54,6 @@ const socialLinks = [
|
|||||||
|
|
||||||
<div class="relative px-4 pt-16 pb-12 sm:px-6">
|
<div class="relative px-4 pt-16 pb-12 sm:px-6">
|
||||||
<div class="mx-auto max-w-4xl">
|
<div class="mx-auto max-w-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">
|
||||||
@@ -59,8 +64,9 @@ const socialLinks = [
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900"
|
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
|
|
||||||
>
|
>
|
||||||
|
{global.initals}
|
||||||
|
</span>
|
||||||
<div
|
<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"
|
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"
|
||||||
>
|
>
|
||||||
@@ -68,8 +74,9 @@ const socialLinks = [
|
|||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
|
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
|
||||||
>Blog</span
|
|
||||||
>
|
>
|
||||||
|
Blog
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
@@ -87,7 +94,7 @@ const socialLinks = [
|
|||||||
href={social.href}
|
href={social.href}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
class="hover group relative flex h-10 w-10 transform items-center justify-center rounded-full bg-zinc-100 text-zinc-500 transition-all duration-300 hover:-translate-y-1 hover:text-zinc-900 hover:ring-2 hover:ring-zinc-300 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100 dark:hover:ring-zinc-700"
|
||||||
aria-label={social.name}
|
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" />
|
<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" />
|
||||||
@@ -130,115 +137,116 @@ const socialLinks = [
|
|||||||
}
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Bottom section -->
|
<!-- Bottom section -->
|
||||||
<div class="theme-transition-all mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
<div class="theme-transition-all mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
||||||
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
||||||
<p class="theme-transition-color text-sm text-zinc-600 dark:text-zinc-400">
|
<p class="theme-transition-color text-sm text-zinc-600 dark:text-zinc-400">
|
||||||
© {currentYear} All rights reserved.
|
© {currentYear} All rights reserved.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
|
<span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
|
||||||
>Built with</span
|
>Built with
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="https://astro.build"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="group inline-flex items-center text-xs text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="mr-1 h-4 w-4 text-[#FF5D01] group-hover:animate-pulse"
|
||||||
|
viewBox="0 0 36 36"
|
||||||
|
fill="none"
|
||||||
>
|
>
|
||||||
<a
|
<path
|
||||||
href="https://astro.build"
|
fill-rule="evenodd"
|
||||||
target="_blank"
|
clip-rule="evenodd"
|
||||||
rel="noopener noreferrer"
|
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"
|
||||||
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="currentColor"></path>
|
||||||
>
|
<path
|
||||||
<svg
|
fill-rule="evenodd"
|
||||||
class="mr-1 h-4 w-4 text-[#FF5D01] group-hover:animate-pulse"
|
clip-rule="evenodd"
|
||||||
viewBox="0 0 36 36"
|
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="none"
|
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"
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
@@ -20,6 +20,7 @@ const currentPath = pathname.slice(1);
|
|||||||
|
|
||||||
<header
|
<header
|
||||||
class="fixed top-0 right-0 left-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
|
class="fixed top-0 right-0 left-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
|
||||||
|
transition:animate="none"
|
||||||
>
|
>
|
||||||
<div class="mx-auto flex max-w-3xl items-center justify-between px-4">
|
<div class="mx-auto flex max-w-3xl items-center justify-between px-4">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
@@ -72,7 +73,7 @@ const currentPath = pathname.slice(1);
|
|||||||
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"
|
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">
|
<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>
|
<a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">{global.initals}</a>
|
||||||
<button
|
<button
|
||||||
id="close-menu-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"
|
class="rounded-md p-2 text-zinc-900 transition-colors hover:bg-zinc-100 dark:text-white dark:hover:bg-zinc-800"
|
||||||
@@ -121,7 +122,7 @@ const currentPath = pathname.slice(1);
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Mobile menu toggle with animations
|
// Mobile menu toggle with animations
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('astro:page-load', () => {
|
||||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||||
const closeMenuButton = document.getElementById('close-menu-button');
|
const closeMenuButton = document.getElementById('close-menu-button');
|
||||||
const mobileMenu = document.getElementById('mobile-menu');
|
const mobileMenu = document.getElementById('mobile-menu');
|
||||||
@@ -200,9 +201,9 @@ const currentPath = pathname.slice(1);
|
|||||||
|
|
||||||
// Add shadow on scroll
|
// Add shadow on scroll
|
||||||
if (currentScrollY > 10) {
|
if (currentScrollY > 10) {
|
||||||
header.classList.add('shadow-sm');
|
header.classList.add('shadow-xs');
|
||||||
} else {
|
} else {
|
||||||
header.classList.remove('shadow-sm');
|
header.classList.remove('shadow-xs');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update last scroll position
|
// Update last scroll position
|
||||||
@@ -240,6 +241,6 @@ const currentPath = pathname.slice(1);
|
|||||||
/* Mobile menu transition */
|
/* Mobile menu transition */
|
||||||
#mobile-menu {
|
#mobile-menu {
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur-sm(4px);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -17,7 +17,7 @@ const encodedUrl = encodeURIComponent(url);
|
|||||||
href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
|
href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
class="hover rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
|
||||||
aria-label="Share on Twitter"
|
aria-label="Share on Twitter"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -29,16 +29,18 @@ const encodedUrl = encodeURIComponent(url);
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="h-4 w-4"
|
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
|
|
||||||
>
|
>
|
||||||
|
<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>
|
||||||
<a
|
<a
|
||||||
href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`}
|
href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
class="hover rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
|
||||||
aria-label="Share on Facebook"
|
aria-label="Share on Facebook"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -50,14 +52,15 @@ const encodedUrl = encodeURIComponent(url);
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="h-4 w-4"
|
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
|
|
||||||
>
|
>
|
||||||
|
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"> </path>
|
||||||
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`}
|
href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
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"
|
class="hover rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
|
||||||
aria-label="Share on LinkedIn"
|
aria-label="Share on LinkedIn"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
@@ -69,10 +72,12 @@ const encodedUrl = encodeURIComponent(url);
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="h-4 w-4"
|
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
|
|
||||||
>
|
>
|
||||||
|
<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>
|
</a>
|
||||||
<button
|
<button
|
||||||
id="copy-link-button"
|
id="copy-link-button"
|
||||||
@@ -89,87 +94,16 @@ const encodedUrl = encodeURIComponent(url);
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
class="h-4 w-4"
|
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
|
|
||||||
>
|
>
|
||||||
|
<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
|
<span
|
||||||
id="copy-tooltip"
|
id="copy-tooltip"
|
||||||
class="absolute -top-8 left-1/2 -translate-x-1/2 transform rounded bg-zinc-800 px-2 py-1 text-xs whitespace-nowrap text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
|
class="absolute -top-8 left-1/2 -translate-x-1/2 transform rounded-sm bg-zinc-800 px-2 py-1 text-xs whitespace-nowrap text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
|
||||||
>
|
>
|
||||||
Copied!
|
Copied!
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
|
||||||
|
@@ -5,7 +5,7 @@
|
|||||||
<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:ring-2 focus:ring-zinc-300 focus:outline-none sm:p-2 dark:hover:bg-zinc-800 dark:focus:ring-zinc-700"
|
class="group relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-zinc-100 focus:ring-2 focus:ring-zinc-300 focus:outline-hidden sm:p-2 dark:hover:bg-zinc-800 dark:focus:ring-zinc-700"
|
||||||
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">
|
||||||
@@ -47,24 +47,25 @@
|
|||||||
></span>
|
></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');
|
||||||
@@ -184,7 +185,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', () => {
|
||||||
@@ -274,12 +275,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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>
|
|
@@ -12,48 +12,6 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={global.title} description={global.description}>
|
<Layout title={global.title} description={global.title}>
|
||||||
<slot />
|
<slot />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
|
||||||
|
|
||||||
if (themeToggle) {
|
|
||||||
themeToggle.addEventListener('click', () => {
|
|
||||||
document.documentElement.classList.add('theme-switching');
|
|
||||||
|
|
||||||
const rippleElements = document.querySelectorAll('.theme-ripple');
|
|
||||||
rippleElements.forEach((el) => {
|
|
||||||
el.classList.add('ripple-active');
|
|
||||||
setTimeout(() => {
|
|
||||||
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>
|
|
||||||
|
@@ -70,7 +70,6 @@ try {
|
|||||||
<div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
<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">
|
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
||||||
<ShareButtons url={canonicalURL.toString()} title={post.title} />
|
<ShareButtons url={canonicalURL.toString()} title={post.title} />
|
||||||
<!-- Convert URL to string -->
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -86,286 +85,8 @@ try {
|
|||||||
<slot name="after-article" />
|
<slot name="after-article" />
|
||||||
</Layout>
|
</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>
|
<style>
|
||||||
/* Enhanced hero image styling */
|
/* Hero image styling */
|
||||||
article img:first-of-type {
|
article img:first-of-type {
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
box-shadow:
|
box-shadow:
|
||||||
@@ -377,22 +98,4 @@ try {
|
|||||||
article img:first-of-type:hover {
|
article img:first-of-type:hover {
|
||||||
transform: scale(1.01);
|
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>
|
</style>
|
||||||
|
@@ -1,7 +1,10 @@
|
|||||||
---
|
---
|
||||||
|
import { ClientRouter } from 'astro:transitions';
|
||||||
|
|
||||||
import Navigation from '../components/Navigation.astro';
|
import Navigation from '../components/Navigation.astro';
|
||||||
import Footer from '../components/Footer.astro';
|
import Footer from '../components/Footer.astro';
|
||||||
import Background from '../components/Background.astro';
|
import Background from '../components/Background.astro';
|
||||||
|
|
||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -17,7 +20,7 @@ const { title, description } = Astro.props;
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width" />
|
<meta name="viewport" content="width=device-width" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
|
||||||
<meta name="generator" content={Astro.generator} />
|
<meta name="generator" content={Astro.generator} />
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
@@ -27,284 +30,43 @@ const { title, description } = Astro.props;
|
|||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
|
<!-- Load theme early to prevent flashes between light and dark modes -->
|
||||||
|
<script is:inline>
|
||||||
|
const theme = (() => {
|
||||||
|
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
||||||
|
return localStorage.getItem('theme');
|
||||||
|
}
|
||||||
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
return 'light';
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (theme === 'light') {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
window.localStorage.setItem('theme', theme);
|
||||||
|
</script>
|
||||||
|
<ClientRouter />
|
||||||
</head>
|
</head>
|
||||||
<body
|
<body
|
||||||
class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100"
|
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 />
|
<Background />
|
||||||
|
|
||||||
<div class="mx-auto w-full max-w-3xl flex-grow px-4 sm:px-6">
|
<div class="mx-auto w-full max-w-3xl grow px-4 sm:px-6">
|
||||||
<Navigation />
|
<Navigation />
|
||||||
<main class="py-12">
|
<main class="py-12">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
<style>
|
<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 */
|
/* Content entrance animation */
|
||||||
main {
|
main {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
@@ -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>
|
|
@@ -5,6 +5,7 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
<Layout title="404 - Page Not Found">
|
<Layout title="404 - Page Not Found">
|
||||||
<div
|
<div
|
||||||
class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
|
class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
|
||||||
|
transition:animate="slide"
|
||||||
>
|
>
|
||||||
<!-- Animated background elements -->
|
<!-- Animated background elements -->
|
||||||
<div class="absolute inset-0 overflow-hidden">
|
<div class="absolute inset-0 overflow-hidden">
|
||||||
@@ -48,7 +49,8 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
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="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"
|
||||||
></span>
|
>
|
||||||
|
</span>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
@@ -61,14 +63,15 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
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"
|
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"
|
||||||
></path>
|
>
|
||||||
|
</path>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="relative z-10 font-medium">Return Home</span>
|
<span class="relative z-10 font-medium">Return Home</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
id="back-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"
|
class="group inline-flex items-center gap-2 rounded-lg border border-zinc-300 px-6 py-3 text-zinc-700 shadow-xs 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
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -81,7 +84,9 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
<path
|
<path
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path>
|
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
|
||||||
|
>
|
||||||
|
</path>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="font-medium">Go Back</span>
|
<span class="font-medium">Go Back</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -89,7 +94,7 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
|
|
||||||
<!-- Random fun fact -->
|
<!-- Random fun fact -->
|
||||||
<div
|
<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"
|
class="mx-auto mt-16 max-w-md rounded-xl border border-zinc-100 bg-zinc-50 p-6 shadow-xs backdrop-blur-xs dark:border-zinc-700/50 dark:bg-zinc-800/50"
|
||||||
>
|
>
|
||||||
<h3 class="text-sm font-medium tracking-wider text-zinc-500 uppercase dark:text-zinc-400">
|
<h3 class="text-sm font-medium tracking-wider text-zinc-500 uppercase dark:text-zinc-400">
|
||||||
Did you know?
|
Did you know?
|
||||||
@@ -127,66 +132,6 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)];
|
const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)];
|
||||||
funFactElement.textContent = randomFact;
|
funFactElement.textContent = randomFact;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle SPA transitions for 404 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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-initialize back button after SPA navigation
|
|
||||||
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>
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa';
|
import DynamicIcon from '../utils/DynamicIcon.tsx';
|
||||||
import { SiTypescript, SiAstro } from 'react-icons/si';
|
|
||||||
|
|
||||||
import directus from '../../lib/directus';
|
import directus from '../../lib/directus';
|
||||||
import { readSingleton, readItems } from '@directus/sdk';
|
import { readSingleton, readItems } from '@directus/sdk';
|
||||||
@@ -17,7 +16,10 @@ const skills = await directus.request(
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="About Me" description={global.description}>
|
<BaseLayout title="About Me" description={global.description}>
|
||||||
<div class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16">
|
<div
|
||||||
|
class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16"
|
||||||
|
transition:animate="slide"
|
||||||
|
>
|
||||||
<!-- Hero Section -->
|
<!-- Hero Section -->
|
||||||
<div class="relative mb-12 sm:mb-16 md:mb-20">
|
<div class="relative mb-12 sm:mb-16 md:mb-20">
|
||||||
<!-- Decorative elements -->
|
<!-- Decorative elements -->
|
||||||
@@ -119,7 +121,7 @@ const skills = await directus.request(
|
|||||||
<!-- Main slider container -->
|
<!-- Main slider container -->
|
||||||
<div class="slider-track animate-slide flex">
|
<div class="slider-track animate-slide flex">
|
||||||
{
|
{
|
||||||
skills.map((skill, index) => (
|
[...skills, ...skills, ...skills].map((skill, index) => (
|
||||||
<div
|
<div
|
||||||
key={`${skill.title}-${index}`}
|
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 sm:mx-4 sm:min-w-[280px] dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600"
|
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 sm:mx-4 sm:min-w-[280px] dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600"
|
||||||
@@ -128,10 +130,7 @@ const skills = await directus.request(
|
|||||||
<div class="mb-4 flex items-center justify-between sm:mb-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 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 sm:h-12 sm:w-12 dark:bg-zinc-800 dark:text-zinc-200">
|
<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 sm:h-12 sm:w-12 dark:bg-zinc-800 dark:text-zinc-200">
|
||||||
<skill.icon
|
<DynamicIcon name={skill.icon} />
|
||||||
size={20}
|
|
||||||
className="sm:text-2xl transform transition-all hover:scale-125"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h3 class="theme-transition-color text-base font-semibold text-zinc-900 sm:text-xl dark:text-zinc-100">
|
<h3 class="theme-transition-color text-base font-semibold text-zinc-900 sm:text-xl dark:text-zinc-100">
|
||||||
{skill.title}
|
{skill.title}
|
||||||
@@ -170,7 +169,6 @@ const skills = await directus.request(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact Section -->
|
<!-- Contact Section -->
|
||||||
<div class="theme-transition-all mx-auto max-w-3xl text-center">
|
<div class="theme-transition-all mx-auto max-w-3xl text-center">
|
||||||
<h2
|
<h2
|
||||||
@@ -187,7 +185,7 @@ const skills = await directus.request(
|
|||||||
|
|
||||||
<a
|
<a
|
||||||
href=`mailto:${global.email}`
|
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 sm:px-8 sm:py-4 sm:text-lg dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
|
class="hover 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 sm:px-8 sm:py-4 sm:text-lg dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -279,7 +277,7 @@ const skills = await directus.request(
|
|||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Reduce animation complexity on mobile for better performance */
|
/* Reduce animation complexity on mobile */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.skill-card {
|
.skill-card {
|
||||||
transition:
|
transition:
|
||||||
@@ -339,7 +337,7 @@ const skills = await directus.request(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Improved touch targets for mobile */
|
/* Touch targets for mobile */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
a,
|
a,
|
||||||
button {
|
button {
|
||||||
@@ -371,8 +369,7 @@ const skills = await directus.request(
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Wait for the DOM to be fully loaded
|
document.addEventListener('astro:page-load', () => {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const sliderTrack = document.querySelector('.slider-track');
|
const sliderTrack = document.querySelector('.slider-track');
|
||||||
|
|
||||||
// Create seamless infinite scrolling effect
|
// Create seamless infinite scrolling effect
|
||||||
@@ -380,9 +377,6 @@ const skills = await directus.request(
|
|||||||
const cards = document.querySelectorAll('.skill-card');
|
const cards = document.querySelectorAll('.skill-card');
|
||||||
if (!cards.length) return;
|
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
|
// Set proper animation based on screen size
|
||||||
function updateScrollAnimation() {
|
function updateScrollAnimation() {
|
||||||
if (window.innerWidth >= 640) {
|
if (window.innerWidth >= 640) {
|
||||||
@@ -467,9 +461,7 @@ const skills = await directus.request(
|
|||||||
|
|
||||||
// Handle theme transition
|
// Handle theme transition
|
||||||
document.addEventListener('themeChange', () => {
|
document.addEventListener('themeChange', () => {
|
||||||
// Add special effects during theme transition
|
|
||||||
cards.forEach((card, index) => {
|
cards.forEach((card, index) => {
|
||||||
// Add staggered animation delay
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
card.classList.add('theme-changing');
|
card.classList.add('theme-changing');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -480,104 +472,3 @@ const skills = await directus.request(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</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(
|
|
||||||
() => {
|
|
||||||
el.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
100 + index * 150
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate profile image
|
|
||||||
const profileImage = document.querySelector('.aspect-square');
|
|
||||||
if (profileImage) {
|
|
||||||
setTimeout(() => {
|
|
||||||
profileImage.classList.add('animate-reveal');
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animate skill bars with staggered delay
|
|
||||||
const skillBars = document.querySelectorAll('.skill-bar');
|
|
||||||
skillBars.forEach((bar, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
bar.classList.add('animate-skill');
|
|
||||||
},
|
|
||||||
500 + index * 100
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate sections with staggered delay
|
|
||||||
const sections = document.querySelectorAll('section');
|
|
||||||
sections.forEach((section, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
section.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
300 + index * 200
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run animations
|
|
||||||
animateAboutContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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>
|
|
||||||
|
@@ -116,8 +116,6 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
</BlogPost>
|
</BlogPost>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Removing TOC-related functions
|
|
||||||
|
|
||||||
// Add copy buttons to code blocks
|
// Add copy buttons to code blocks
|
||||||
function initializeCodeCopyButtons() {
|
function initializeCodeCopyButtons() {
|
||||||
const codeBlocks = document.querySelectorAll('pre');
|
const codeBlocks = document.querySelectorAll('pre');
|
||||||
@@ -183,50 +181,9 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
// Main initialization function
|
||||||
function initializeBlogPost() {
|
function initializeBlogPost() {
|
||||||
// Initialize remaining components
|
|
||||||
initializeCodeCopyButtons();
|
initializeCodeCopyButtons();
|
||||||
setupSPATransitions();
|
|
||||||
|
|
||||||
// Scroll to hash if present in URL
|
// Scroll to hash if present in URL
|
||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
@@ -239,19 +196,11 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', initializeBlogPost);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
// Re-initialize when content changes via Astro's view transitions
|
||||||
document.addEventListener('astro:page-load', initializeBlogPost);
|
document.addEventListener('astro:page-load', initializeBlogPost);
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', initializeBlogPost);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Removing TOC-related styles */
|
|
||||||
|
|
||||||
/* Language badge styling */
|
/* Language badge styling */
|
||||||
.language-badge {
|
.language-badge {
|
||||||
font-family:
|
font-family:
|
||||||
@@ -309,7 +258,7 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prose code {
|
.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;
|
@reference rounded-sm 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 {
|
.prose pre {
|
||||||
|
@@ -22,19 +22,11 @@ const postsByYear = sortedPosts.reduce((acc, post) => {
|
|||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const years = Object.keys(postsByYear).sort((a, b) => b - a);
|
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 title="Blog">
|
||||||
<div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16">
|
<div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16" transition:animate="slide">
|
||||||
<!-- Header with search -->
|
|
||||||
<div class="relative mb-12 sm:mb-20">
|
<div class="relative mb-12 sm:mb-20">
|
||||||
<!-- Decorative elements -->
|
|
||||||
<div
|
<div
|
||||||
class="animate-blob absolute -top-10 -left-10 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:-top-20 sm:-left-20 sm:h-72 sm:w-72 dark:bg-zinc-800/30"
|
class="animate-blob absolute -top-10 -left-10 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:-top-20 sm:-left-20 sm:h-72 sm:w-72 dark:bg-zinc-800/30"
|
||||||
>
|
>
|
||||||
@@ -124,7 +116,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
<!-- Improved sidebar for mobile -->
|
<!-- Sidebar for mobile -->
|
||||||
<div class="relative md:col-span-3">
|
<div class="relative md:col-span-3">
|
||||||
<div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0">
|
<div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0">
|
||||||
<h3
|
<h3
|
||||||
@@ -141,7 +133,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
|
|||||||
years.map((year, index) => (
|
years.map((year, index) => (
|
||||||
<a
|
<a
|
||||||
href={`#year-${year}`}
|
href={`#year-${year}`}
|
||||||
class={`mr-3 flex items-center rounded-full border-b border-zinc-100 px-4 py-2 whitespace-nowrap transition-colors hover:bg-zinc-50 md:mr-0 md:w-full md:rounded-none md:px-0 md:py-3 md:whitespace-normal dark:border-zinc-800 dark:hover:bg-zinc-900 ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`}
|
class={`hover mr-3 flex items-center rounded-full border-b border-zinc-100 px-4 py-2 whitespace-nowrap transition-colors hover:bg-zinc-50 md:mr-0 md:w-full md:rounded-none md:px-0 md:py-3 md:whitespace-normal dark:border-zinc-800 dark:hover:bg-zinc-900 ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`}
|
||||||
>
|
>
|
||||||
<span class="text-base font-medium text-zinc-900 md:text-lg dark:text-zinc-100">
|
<span class="text-base font-medium text-zinc-900 md:text-lg dark:text-zinc-100">
|
||||||
{year}
|
{year}
|
||||||
@@ -156,7 +148,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Improved post grid for mobile -->
|
<!-- Post grid for mobile -->
|
||||||
<div class="md:col-span-9">
|
<div class="md:col-span-9">
|
||||||
{
|
{
|
||||||
years.map((year) => (
|
years.map((year) => (
|
||||||
@@ -168,7 +160,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
|
|||||||
<div
|
<div
|
||||||
class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}
|
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) => (
|
{postsByYear[year].map((post) => (
|
||||||
<article class="group relative mx-auto flex h-full w-full max-w-sm flex-col sm:max-w-md md:mx-0">
|
<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 && (
|
{post.image && (
|
||||||
<div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56">
|
<div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56">
|
||||||
@@ -202,7 +194,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
|
|||||||
</a>
|
</a>
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
<p class="mb-4 line-clamp-2 flex-grow text-center text-sm text-zinc-600 md:text-left dark:text-zinc-400">
|
<p class="mb-4 line-clamp-2 grow text-center text-sm text-zinc-600 md:text-left dark:text-zinc-400">
|
||||||
{post.description}
|
{post.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -303,7 +295,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Improved touch targets for mobile */
|
/* Touch targets for mobile */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
a,
|
a,
|
||||||
button {
|
button {
|
||||||
@@ -315,8 +307,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Script không thay đổi - giữ nguyên chức năng
|
document.addEventListener('astro:page-load', () => {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const backToTopButton = document.getElementById('back-to-top');
|
const backToTopButton = document.getElementById('back-to-top');
|
||||||
|
|
||||||
if (backToTopButton) {
|
if (backToTopButton) {
|
||||||
@@ -341,7 +332,7 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
|
|||||||
|
|
||||||
// Check scroll position
|
// Check scroll position
|
||||||
window.addEventListener('scroll', toggleBackToTopButton);
|
window.addEventListener('scroll', toggleBackToTopButton);
|
||||||
toggleBackToTopButton(); // Initial check
|
toggleBackToTopButton();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add smooth scrolling to year links
|
// Add smooth scrolling to year links
|
||||||
@@ -382,57 +373,4 @@ const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 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>
|
||||||
|
@@ -22,10 +22,13 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout title=`Home | ${global.name}`>
|
<Layout title=`Home | ${global.name}`>
|
||||||
<!-- Hero Section with improved mobile responsiveness -->
|
<!-- Hero Section with mobile responsiveness -->
|
||||||
<section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20">
|
<section
|
||||||
|
class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20"
|
||||||
|
transition:animate="slide"
|
||||||
|
>
|
||||||
<div class="relative mx-auto max-w-2xl">
|
<div class="relative mx-auto max-w-2xl">
|
||||||
<!-- Adjusted blob positions and sizes for better mobile appearance -->
|
<!-- Adjusted blob positions and sizes for mobile appearance -->
|
||||||
<div
|
<div
|
||||||
class="animate-blob theme-transition-bg absolute -top-10 -left-10 h-40 w-40 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:-top-20 sm:-left-20 sm:h-64 sm:w-64 dark:bg-zinc-800/50"
|
class="animate-blob theme-transition-bg absolute -top-10 -left-10 h-40 w-40 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:-top-20 sm:-left-20 sm:h-64 sm:w-64 dark:bg-zinc-800/50"
|
||||||
>
|
>
|
||||||
@@ -85,7 +88,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Featured Post Section - Improved for mobile -->
|
<!-- Featured post section -->
|
||||||
<section
|
<section
|
||||||
class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800"
|
class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800"
|
||||||
>
|
>
|
||||||
@@ -124,7 +127,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Improved grid for better mobile layout -->
|
<!-- Grid for mobile layout -->
|
||||||
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3">
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3">
|
||||||
{
|
{
|
||||||
recentPosts.map((post, index) => (
|
recentPosts.map((post, index) => (
|
||||||
@@ -215,7 +218,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Topics/Tags Section - Improved for mobile -->
|
<!-- Topics section -->
|
||||||
{
|
{
|
||||||
allTags.length > 0 && (
|
allTags.length > 0 && (
|
||||||
<section class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800">
|
<section class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800">
|
||||||
@@ -236,7 +239,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
<span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
<span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||||
#{tag}
|
#{tag}
|
||||||
</span>
|
</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">
|
<span class="theme-transition-all 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'}
|
{tagCount} {tagCount === 1 ? 'post' : 'posts'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -278,8 +281,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Add hover effect for cards on touch devices
|
// Add hover effect for cards on touch devices
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('astro:page-load', () => {
|
||||||
// Check if it's a touch device
|
|
||||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||||
|
|
||||||
if (isTouchDevice) {
|
if (isTouchDevice) {
|
||||||
@@ -297,11 +299,11 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disable hover animations on touch devices for better performance
|
// Disable hover animations on touch devices
|
||||||
document.documentElement.classList.add('touch-device');
|
document.documentElement.classList.add('touch-device');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Improved viewport height fix for mobile browsers
|
// Viewport height fix for mobile browsers
|
||||||
const setVh = () => {
|
const setVh = () => {
|
||||||
const vh = window.innerHeight * 0.01;
|
const vh = window.innerHeight * 0.01;
|
||||||
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
document.documentElement.style.setProperty('--vh', `${vh}px`);
|
||||||
@@ -339,7 +341,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Improved theme change handler that preserves scroll position and provides smoother transitions
|
// Theme change handler that preserves scroll position and provides smoother transitions
|
||||||
document.addEventListener('themeChanged', () => {
|
document.addEventListener('themeChanged', () => {
|
||||||
// Store current scroll position
|
// Store current scroll position
|
||||||
const scrollPosition = window.scrollY;
|
const scrollPosition = window.scrollY;
|
||||||
@@ -477,58 +479,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
animateContent();
|
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>
|
<style>
|
||||||
@@ -586,6 +536,4 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
|
|||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
transform: translateY(0) !important;
|
transform: translateY(0) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rest of your existing styles... */
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -14,7 +14,6 @@ export async function getStaticPaths() {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Get all unique tags
|
|
||||||
const uniqueTags = [...new Set(posts.flatMap((post) => post.tags || []))];
|
const uniqueTags = [...new Set(posts.flatMap((post) => post.tags || []))];
|
||||||
|
|
||||||
// Create a path for each tag
|
// Create a path for each tag
|
||||||
@@ -41,7 +40,6 @@ const sortedPosts =
|
|||||||
: [];
|
: [];
|
||||||
console.log(`Sorted posts length: ${sortedPosts.length}`);
|
console.log(`Sorted posts length: ${sortedPosts.length}`);
|
||||||
|
|
||||||
const tagHue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
|
|
||||||
const relatedTags = [
|
const relatedTags = [
|
||||||
...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)),
|
...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)),
|
||||||
].slice(0, 5);
|
].slice(0, 5);
|
||||||
@@ -49,7 +47,6 @@ const relatedTags = [
|
|||||||
|
|
||||||
<BaseLayout title={`Posts tagged with "${tag}"`}>
|
<BaseLayout title={`Posts tagged with "${tag}"`}>
|
||||||
<div class="mx-auto max-w-5xl px-4 py-10 sm:py-16">
|
<div class="mx-auto max-w-5xl px-4 py-10 sm:py-16">
|
||||||
<!-- Header section -->
|
|
||||||
<div class="relative mb-10 sm:mb-16">
|
<div class="relative mb-10 sm:mb-16">
|
||||||
<div
|
<div
|
||||||
class="animate-blob absolute -top-20 -left-20 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:h-64 sm:w-64 dark:bg-zinc-900/30"
|
class="animate-blob absolute -top-20 -left-20 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:h-64 sm:w-64 dark:bg-zinc-900/30"
|
||||||
@@ -88,7 +85,7 @@ const relatedTags = [
|
|||||||
class="mb-2 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:justify-start"
|
class="mb-2 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:justify-start"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="tag-icon mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-zinc-100 shadow-sm sm:mx-0 dark:bg-zinc-800"
|
class="tag-icon mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-zinc-100 shadow-xs sm:mx-0 dark:bg-zinc-800"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@@ -144,7 +141,7 @@ const relatedTags = [
|
|||||||
{relatedTags.map((relatedTag) => (
|
{relatedTags.map((relatedTag) => (
|
||||||
<a
|
<a
|
||||||
href={`/topics/${relatedTag}`}
|
href={`/topics/${relatedTag}`}
|
||||||
class="inline-flex flex-shrink-0 items-center rounded-full bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-900 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
class="inline-flex shrink-0 items-center rounded-full bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-900 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
||||||
>
|
>
|
||||||
#{relatedTag}
|
#{relatedTag}
|
||||||
</a>
|
</a>
|
||||||
@@ -167,7 +164,7 @@ const relatedTags = [
|
|||||||
|
|
||||||
<div class="flex flex-col gap-5 sm:flex-row sm:gap-6">
|
<div class="flex flex-col gap-5 sm:flex-row sm:gap-6">
|
||||||
{post.image && (
|
{post.image && (
|
||||||
<div class="mx-auto h-40 w-full flex-shrink-0 overflow-hidden rounded-xl shadow-sm transition-all duration-300 group-hover:shadow-md sm:mx-0 sm:w-56">
|
<div class="mx-auto h-40 w-full shrink-0 overflow-hidden rounded-xl shadow-xs transition-all duration-300 group-hover:shadow-md sm:mx-0 sm:w-56">
|
||||||
<img
|
<img
|
||||||
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
|
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
|
||||||
alt={post.image_alt}
|
alt={post.image_alt}
|
||||||
@@ -277,7 +274,7 @@ const relatedTags = [
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state với màu zinc -->
|
<!-- Empty state -->
|
||||||
{
|
{
|
||||||
sortedPosts.length === 0 && (
|
sortedPosts.length === 0 && (
|
||||||
<div class="py-12 text-center sm:py-20">
|
<div class="py-12 text-center sm:py-20">
|
||||||
@@ -423,98 +420,3 @@ const relatedTags = [
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
|
||||||
// Handle SPA transitions for tag pages
|
|
||||||
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 tag page
|
|
||||||
function animateTagContent() {
|
|
||||||
// Animate header elements
|
|
||||||
const headerElements = document.querySelectorAll('h1, .tag-icon, .tag-description');
|
|
||||||
headerElements.forEach((el, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
el.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
100 + index * 150
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate posts with staggered delay
|
|
||||||
const articles = document.querySelectorAll('article');
|
|
||||||
articles.forEach((article, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
article.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
400 + index * 100
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate related tags
|
|
||||||
const relatedTags = document.querySelectorAll('.related-tags a');
|
|
||||||
relatedTags.forEach((tag, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
tag.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
600 + index * 50
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run animations
|
|
||||||
animateTagContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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>
|
|
||||||
|
|
||||||
<!-- Add this at the end of your page -->
|
|
||||||
|
@@ -30,8 +30,10 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Explore Tags">
|
<BaseLayout title="Explore Tags">
|
||||||
<div class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16">
|
<div
|
||||||
<!-- Enhanced header section with animated elements - improved for mobile -->
|
class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16"
|
||||||
|
transition:animate="slide"
|
||||||
|
>
|
||||||
<div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16">
|
<div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16">
|
||||||
<div
|
<div
|
||||||
class="animate-blob theme-transition-bg absolute -top-16 -left-16 h-36 w-36 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/50"
|
class="animate-blob theme-transition-bg absolute -top-16 -left-16 h-36 w-36 rounded-full bg-zinc-100 opacity-50 blur-3xl sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/50"
|
||||||
@@ -52,7 +54,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
<span class="relative inline-block">
|
<span class="relative inline-block">
|
||||||
<span class="relative inline-block">
|
<span class="relative inline-block">
|
||||||
<span
|
<span
|
||||||
class="theme-transition-bg absolute -inset-1 rounded-lg bg-gradient-to-r from-zinc-200/50 to-zinc-300/50 blur-sm dark:from-zinc-800/50 dark:to-zinc-700/50"
|
class="theme-transition-bg absolute -inset-1 rounded-lg bg-gradient-to-r from-zinc-200/50 to-zinc-300/50 blur-xs dark:from-zinc-800/50 dark:to-zinc-700/50"
|
||||||
></span>
|
></span>
|
||||||
<span class="relative">Explore</span>
|
<span class="relative">Explore</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -101,7 +103,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div class="flex w-full justify-center">
|
<div class="flex w-full justify-center">
|
||||||
<div class="tag-cloud hover-3d glass theme-transition-all relative w-full rounded-lg border border-zinc-100 bg-white/50 p-3 backdrop-blur-sm sm:rounded-xl sm:p-4 md:rounded-2xl md:p-6 lg:rounded-3xl lg:p-8 dark:border-zinc-800 dark:bg-zinc-900/50">
|
<div class="tag-cloud hover-3d glass theme-transition-all relative w-full rounded-lg border border-zinc-100 bg-white/50 p-3 backdrop-blur-xs sm:rounded-xl sm:p-4 md:rounded-2xl md:p-6 lg:rounded-3xl lg:p-8 dark:border-zinc-800 dark:bg-zinc-900/50">
|
||||||
<div class="bg-grid-pattern theme-transition-bg absolute inset-0 opacity-5 dark:opacity-10" />
|
<div class="bg-grid-pattern theme-transition-bg absolute inset-0 opacity-5 dark:opacity-10" />
|
||||||
<div class="theme-transition-bg absolute -top-8 -right-8 h-20 w-20 rounded-full bg-gradient-to-br from-zinc-200/30 to-zinc-300/20 blur-xl sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40 dark:from-zinc-700/20 dark:to-zinc-800/10" />
|
<div class="theme-transition-bg absolute -top-8 -right-8 h-20 w-20 rounded-full bg-gradient-to-br from-zinc-200/30 to-zinc-300/20 blur-xl sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40 dark:from-zinc-700/20 dark:to-zinc-800/10" />
|
||||||
<div class="theme-transition-bg absolute -bottom-8 -left-8 h-20 w-20 rounded-full bg-gradient-to-tl from-zinc-200/30 to-zinc-300/20 blur-xl sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40 dark:from-zinc-700/20 dark:to-zinc-800/10" />
|
<div class="theme-transition-bg absolute -bottom-8 -left-8 h-20 w-20 rounded-full bg-gradient-to-tl from-zinc-200/30 to-zinc-300/20 blur-xl sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40 dark:from-zinc-700/20 dark:to-zinc-800/10" />
|
||||||
@@ -114,13 +116,13 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
{sortedTags.map((tag) => (
|
{sortedTags.map((tag) => (
|
||||||
<a
|
<a
|
||||||
href={`/topics/${tag.name}`}
|
href={`/topics/${tag.name}`}
|
||||||
class="theme-transition-element theme-ripple group relative min-w-0 flex-grow overflow-hidden rounded-md border border-zinc-200 transition-all duration-300 hover:scale-[1.03] hover:border-zinc-300 hover:shadow-md active:scale-95 sm:rounded-lg sm:hover:shadow-lg md:rounded-xl dark:border-zinc-800 dark:hover:border-zinc-700"
|
class="theme-transition-element theme-ripple group relative min-w-0 grow overflow-hidden rounded-md border border-zinc-200 transition-all duration-300 hover:scale-[1.03] hover:border-zinc-300 hover:shadow-md active:scale-95 sm:rounded-lg sm:hover:shadow-lg md:rounded-xl dark:border-zinc-800 dark:hover:border-zinc-700"
|
||||||
style={`--tag-hue: ${tag.hue};`}
|
style={`--tag-hue: ${tag.hue};`}
|
||||||
>
|
>
|
||||||
<div class="theme-transition-bg absolute inset-0 bg-gradient-to-br from-zinc-50/90 to-zinc-100/90 opacity-100 transition-opacity group-hover:opacity-95 dark:from-zinc-800/90 dark:to-zinc-900/90" />
|
<div class="theme-transition-bg absolute inset-0 bg-gradient-to-br from-zinc-50/90 to-zinc-100/90 opacity-100 transition-opacity group-hover:opacity-95 dark:from-zinc-800/90 dark:to-zinc-900/90" />
|
||||||
|
|
||||||
<div class="xxxs:px-2 xxs:px-2 xs:px-2 xxxs:py-2 xxs:py-2 xs:py-2 xxs:gap-2 relative flex w-full items-center gap-1.5 px-1.5 py-1.5 sm:px-3 sm:py-3 md:px-4 md:py-4">
|
<div class="xxxs:px-2 xxs:px-2 xs:px-2 xxxs:py-2 xxs:py-2 xs:py-2 xxs:gap-2 relative flex w-full items-center gap-1.5 px-1.5 py-1.5 sm:px-3 sm:py-3 md:px-4 md:py-4">
|
||||||
<div class="xxxs:w-6 xxxs:h-6 xxs:w-6 xxs:h-6 xs:w-7 xs:h-7 group-hover:bg-accent/20 dark:group-hover:bg-accent/20 group-hover:text-accent-dark dark:group-hover:text-accent-light theme-transition-all flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-zinc-100 text-zinc-700 shadow-sm transition-all duration-300 sm:h-8 sm:w-8 md:h-10 md:w-10 dark:bg-zinc-800 dark:text-zinc-300">
|
<div class="xxxs:w-6 xxxs:h-6 xxs:w-6 xxs:h-6 xs:w-7 xs:h-7 group-hover:bg-accent/20 dark:group-hover:bg-accent/20 group-hover:text-accent-dark dark:group-hover:text-accent-light theme-transition-all flex h-5 w-5 shrink-0 items-center justify-center rounded-full bg-zinc-100 text-zinc-700 shadow-xs transition-all duration-300 sm:h-8 sm:w-8 md:h-10 md:w-10 dark:bg-zinc-800 dark:text-zinc-300">
|
||||||
<span class="xxxs:text-xs xxs:text-xs xs:text-sm text-xs font-semibold sm:text-base md:text-lg">
|
<span class="xxxs:text-xs xxs:text-xs xs:text-sm text-xs font-semibold sm:text-base md:text-lg">
|
||||||
#
|
#
|
||||||
</span>
|
</span>
|
||||||
@@ -146,9 +148,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Ultra-reliable responsiveness handling
|
document.addEventListener('astro:page-load', () => {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
// Fix viewport width issues on mobile
|
|
||||||
const fixViewportWidth = () => {
|
const fixViewportWidth = () => {
|
||||||
// Force the viewport to be exactly the width of the device
|
// Force the viewport to be exactly the width of the device
|
||||||
const viewport = document.querySelector('meta[name="viewport"]');
|
const viewport = document.querySelector('meta[name="viewport"]');
|
||||||
@@ -378,7 +378,6 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ultra-responsive breakpoints for extreme reliability */
|
|
||||||
/* Micro screens (below 240px) */
|
/* Micro screens (below 240px) */
|
||||||
@media (max-width: 239px) {
|
@media (max-width: 239px) {
|
||||||
.tag-cloud {
|
.tag-cloud {
|
||||||
@@ -545,7 +544,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Improved shadow for dark mode */
|
/* Shadow for dark mode */
|
||||||
:global(.dark) .tag-cloud {
|
:global(.dark) .tag-cloud {
|
||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 0 1px rgba(255, 255, 255, 0.05),
|
0 0 0 1px rgba(255, 255, 255, 0.05),
|
||||||
@@ -554,8 +553,8 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent layout shifts */
|
/* Prevent layout shifts */
|
||||||
.flex-grow {
|
.grow {
|
||||||
flex-grow: 1;
|
grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.min-w-0 {
|
.min-w-0 {
|
||||||
@@ -628,87 +627,3 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
|
||||||
// Handle SPA transitions for tags index 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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add hover effect for tag cards on touch devices
|
|
||||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
||||||
|
|
||||||
if (isTouchDevice) {
|
|
||||||
const tagCards = document.querySelectorAll('.tag-cloud a');
|
|
||||||
|
|
||||||
tagCards.forEach((card) => {
|
|
||||||
card.addEventListener('touchstart', () => {
|
|
||||||
card.classList.add('is-touched');
|
|
||||||
});
|
|
||||||
|
|
||||||
card.addEventListener('touchend', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
card.classList.remove('is-touched');
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animate tag cards with staggered delay
|
|
||||||
const tagCards = document.querySelectorAll('.tag-cloud a');
|
|
||||||
tagCards.forEach((card, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
card.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
100 + index * 50
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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>
|
|
||||||
|
@@ -1,6 +1,9 @@
|
|||||||
/* Remove all the complex mobile menu styles and keep only what's necessary */
|
|
||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
/* Dark mode support for Tailwind CSS v4 */
|
||||||
|
/* https://tailwindcss.com/docs/dark-mode */
|
||||||
|
@custom-variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
font-family: 'Inter', sans-serif;
|
font-family: 'Inter', sans-serif;
|
||||||
@@ -12,10 +15,11 @@
|
|||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
scroll-padding-top: 5rem;
|
scroll-padding-top: 5rem;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@reference min-h-screen bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100;
|
@apply min-h-screen bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
@@ -38,10 +42,10 @@
|
|||||||
scroll-padding-top: 4rem;
|
scroll-padding-top: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Better touch targets on mobile */
|
/* Touch targets on mobile */
|
||||||
button,
|
button,
|
||||||
a {
|
a {
|
||||||
@reference min-h-[44px];
|
@apply min-h-[44px];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,27 +129,10 @@
|
|||||||
/* Smooth hover transitions */
|
/* Smooth hover transitions */
|
||||||
a,
|
a,
|
||||||
button {
|
button {
|
||||||
transition: all 0.2s ease;
|
transition: all 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover,
|
a.hover:hover,
|
||||||
button:hover {
|
button:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-2px);
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth page transitions */
|
|
||||||
.page-transition {
|
|
||||||
transition:
|
|
||||||
opacity 0.3s ease,
|
|
||||||
transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-entering {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-entered {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
}
|
||||||
|
52
src/utils/DynamicIcon.tsx
Normal file
52
src/utils/DynamicIcon.tsx
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import * as FaIcons from 'react-icons/fa';
|
||||||
|
import * as MdIcons from 'react-icons/md';
|
||||||
|
import * as AiIcons from 'react-icons/ai';
|
||||||
|
import * as GiIcons from 'react-icons/gi';
|
||||||
|
import * as IoIcons from 'react-icons/io';
|
||||||
|
import * as CiIcons from 'react-icons/ci';
|
||||||
|
import * as FiIcons from 'react-icons/fi';
|
||||||
|
import * as LuIcons from 'react-icons/lu';
|
||||||
|
import * as SiIcons from 'react-icons/si';
|
||||||
|
|
||||||
|
// Load React Icon library dynamically from attributes in Directus
|
||||||
|
|
||||||
|
const iconSets = {
|
||||||
|
fa: FaIcons,
|
||||||
|
md: MdIcons,
|
||||||
|
ai: AiIcons,
|
||||||
|
gi: GiIcons,
|
||||||
|
io: IoIcons,
|
||||||
|
ci: CiIcons,
|
||||||
|
fi: FiIcons,
|
||||||
|
lu: LuIcons,
|
||||||
|
si: SiIcons,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DynamicIcon = ({
|
||||||
|
name,
|
||||||
|
set = 'fa',
|
||||||
|
size = 20,
|
||||||
|
color = 'currentColor',
|
||||||
|
className = '',
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
set: string;
|
||||||
|
size: number;
|
||||||
|
color: string;
|
||||||
|
className: string;
|
||||||
|
}) => {
|
||||||
|
let IconComponent = FaIcons.FaAlignCenter;
|
||||||
|
|
||||||
|
if (name.startsWith('Fa')) {
|
||||||
|
IconComponent = iconSets['fa'][name];
|
||||||
|
} else if (name.startsWith('Si')) {
|
||||||
|
IconComponent = iconSets['si'][name];
|
||||||
|
} else {
|
||||||
|
IconComponent = iconSets[set][name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return <IconComponent size={size} color={color} className={className} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DynamicIcon;
|
@@ -3,6 +3,7 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
"target": "ES6",
|
"target": "ES6",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
Reference in New Issue
Block a user