Compare commits
134 Commits
Author | SHA1 | Date | |
---|---|---|---|
71e2b0185b | |||
7f9fb4d2b9 | |||
8420c8dd58 | |||
fa6ed18edb | |||
30860fce1e | |||
b479e0e22c | |||
cf01ebcd3c | |||
df8ccf81c2
|
|||
073911c1b9 | |||
3eeea3dd8f | |||
43fea76778
|
|||
d64df6473a | |||
63a6a00817
|
|||
54759056b3 | |||
3cc9762e0d | |||
ef757c4a14
|
|||
176f92bf67 | |||
09d411dd68 | |||
54acfcb24d
|
|||
6f3b631862
|
|||
18cd240a9b | |||
bb4fe8ef37
|
|||
e0e3c1f61a
|
|||
0b5c6ae999 | |||
a20ba4ab43 | |||
550e7dfe52 | |||
03174cfb9d
|
|||
da50c1928c
|
|||
f1d1fe979e | |||
4d6019d0b0 | |||
7dd302b3d4
|
|||
8a8f2a6216
|
|||
97775f1ceb | |||
0a437a26f1 | |||
ba67b4d0e4 | |||
0bcfa9bed4
|
|||
ada95481f7
|
|||
7c9f4acc00
|
|||
0b7b87580a
|
|||
08f076e566
|
|||
26c27b9353 | |||
ce8b3a2e19 | |||
6d34c0d407 | |||
63607bbca3 | |||
745d2553a0
|
|||
8a19559cc7 | |||
42854db0fb
|
|||
7b72e3849b | |||
6a8dbb0c7c | |||
91fdf5a83f
|
|||
073f3a7916
|
|||
38202841ca | |||
0492922cce
|
|||
a17500835b | |||
2f8b97208c | |||
d6c30d5e5b
|
|||
a7ea9db3aa
|
|||
9134e78e8a | |||
2ca7d6705d | |||
5722e8c7a1 | |||
e39fd2acb8 | |||
0313fd54bc | |||
dbb0f6d7ff | |||
20669d9766 | |||
6b2e6353d1 | |||
6d112b52df | |||
ff17af604f | |||
32ea0989d7 | |||
e4ab7d134c | |||
5fad13655c | |||
8614d40a64 | |||
8c417b93b3 | |||
1d9519831b | |||
fa57f2e93f | |||
9e01002d4e | |||
cb52c169a3 | |||
3017668cd2 | |||
1972b3bc19 | |||
af77f90a49 | |||
bdda29f369 | |||
644c5fcd6a | |||
bafd8158d3 | |||
4d9c1a3e8c | |||
4a4233ac62 | |||
c71957348d | |||
400bf16dd9 | |||
85535614a0 | |||
38fcbb635b | |||
b1e57c3f17 | |||
e22a1985be | |||
70b0b86944 | |||
ba36de8e36 | |||
d2e44fe046 | |||
36ec797d3b | |||
086d98ba50 | |||
8a05fa4d96 | |||
dbbf886de9 | |||
ae7e21eb82 | |||
ce6f476e8f | |||
0ca6be1d91 | |||
cedcae02ce | |||
4ef6e85ed9 | |||
1ad039e9ff | |||
034d6d1120 | |||
2c436100c5 | |||
6ea1467653 | |||
1ba76ab5cf | |||
478482ab01 | |||
f1e3e4ecaa | |||
05eb8a092c | |||
633e374a17 | |||
cd75440a6d | |||
3354975e2e | |||
1ffe933d6e | |||
90318aad14 | |||
e454a510c6 | |||
a6d3ec5052 | |||
1d134d43da | |||
54c7c9e259 | |||
0d8cf28be4 | |||
d78a8d8c45 | |||
5b6abeb9f9 | |||
a3b0301d23 | |||
06f7546212 | |||
abd1d43f79 | |||
07f2f5f0e1 | |||
91b53a33c2 | |||
b3e23f3e6c | |||
ab68b6248f | |||
37d1f1d1f2 | |||
89e1c59e37 | |||
7153f29022 | |||
51041f6ae9 | |||
67f12ecf72 |
5
.dockerignore
Normal file
5
.dockerignore
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.DS_Store
|
||||||
|
.astro
|
||||||
|
.vscode
|
||||||
|
node_modules
|
||||||
|
dist
|
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
|
@@ -1,67 +0,0 @@
|
|||||||
name: release-image-gitea
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 0.*
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Create Kubeconfig
|
|
||||||
run: |
|
|
||||||
mkdir $HOME/.kube
|
|
||||||
echo "${{ secrets.KUBECONFIG_BUILDX }}" > $HOME/.kube/config
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
id: buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
with:
|
|
||||||
driver: kubernetes
|
|
||||||
driver-opts: |
|
|
||||||
namespace=gitea
|
|
||||||
qemu.install=true
|
|
||||||
|
|
||||||
- name: Available Platforms
|
|
||||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
|
||||||
|
|
||||||
- name: Login to Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ vars.REPOSITORY_HOST }}
|
|
||||||
username: ${{ gitea.actor }}
|
|
||||||
password: ${{ secrets.REPOSITORY_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract Metadata
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=tag
|
|
||||||
images: ${{ vars.REPOSITORY_HOST }}/${{ gitea.repository }}
|
|
||||||
|
|
||||||
- name: Build and Push Image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
file: ./Dockerfile
|
|
||||||
|
|
||||||
- name: Actions Ntfy
|
|
||||||
run: |
|
|
||||||
curl \
|
|
||||||
-H "Authorization: Bearer ${{ secrets.NTFY_CRED }}" \
|
|
||||||
-H "Title: Site-Profile Image Released to Gitea: ${{ steps.meta.outputs.tags }}" \
|
|
||||||
-H "Content-Type: text/plain" \
|
|
||||||
-d $'Repo: ${{ gitea.repository }}\nCommit: ${{ gitea.sha }}\nRef: ${{ gitea.ref }}\nStatus: ${{ job.status}}' \
|
|
||||||
${{ secrets.NTFY_URL }}
|
|
@@ -1,67 +0,0 @@
|
|||||||
name: release-image-harbor
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- 0.*
|
|
||||||
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
release:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Create Kubeconfig
|
|
||||||
run: |
|
|
||||||
mkdir $HOME/.kube
|
|
||||||
echo "${{ secrets.KUBECONFIG_BUILDX }}" > $HOME/.kube/config
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
id: buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
with:
|
|
||||||
driver: kubernetes
|
|
||||||
driver-opts: |
|
|
||||||
namespace=gitea
|
|
||||||
qemu.install=true
|
|
||||||
|
|
||||||
- name: Available Platforms
|
|
||||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
|
||||||
|
|
||||||
- name: Login to Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ vars.REGISTRY_HOST }}
|
|
||||||
username: ${{ vars.REGISTRY_USER }}
|
|
||||||
password: ${{ secrets.REGISTRY_SECRET }}
|
|
||||||
|
|
||||||
- name: Extract Metadata
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=tag
|
|
||||||
images: ${{ vars.REGISTRY_HOST }}/images/site-profile
|
|
||||||
|
|
||||||
- name: Build and Push Image
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
push: true
|
|
||||||
platforms: linux/amd64
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
file: ./Dockerfile
|
|
||||||
|
|
||||||
- name: Actions Ntfy
|
|
||||||
run: |
|
|
||||||
curl \
|
|
||||||
-H "Authorization: Bearer ${{ secrets.NTFY_CRED }}" \
|
|
||||||
-H "Title: Site-Profile Image Released to Gitea: ${{ steps.meta.outputs.tags }}" \
|
|
||||||
-H "Content-Type: text/plain" \
|
|
||||||
-d $'Repo: ${{ gitea.repository }}\nCommit: ${{ gitea.sha }}\nRef: ${{ gitea.ref }}\nStatus: ${{ job.status}}' \
|
|
||||||
${{ secrets.NTFY_URL }}
|
|
98
.gitea/workflows/release-image.yml
Normal file
98
.gitea/workflows/release-image.yml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
name: release-image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 0.*
|
||||||
|
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Login to Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.REPOSITORY_HOST }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.REPOSITORY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.REGISTRY_HOST }}
|
||||||
|
username: ${{ vars.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_SECRET }}
|
||||||
|
|
||||||
|
- name: Create Kubeconfig
|
||||||
|
run: |
|
||||||
|
mkdir $HOME/.kube
|
||||||
|
echo "${{ secrets.KUBECONFIG_BUILDX }}" > $HOME/.kube/config
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
with:
|
||||||
|
driver: kubernetes
|
||||||
|
driver-opts: |
|
||||||
|
namespace=gitea
|
||||||
|
qemu.install=true
|
||||||
|
buildkitd-config-inline: |
|
||||||
|
[registry."docker.io"]
|
||||||
|
mirrors = ["harbor.alexlebens.net/proxy-hub.docker/"]
|
||||||
|
|
||||||
|
- name: Available Platforms
|
||||||
|
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||||
|
|
||||||
|
- name: Extract Metadata
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
images: |
|
||||||
|
${{ vars.REPOSITORY_HOST }}/${{ gitea.repository }}
|
||||||
|
${{ vars.REGISTRY_HOST }}/images/site-profile
|
||||||
|
|
||||||
|
- name: Build and Push Image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
file: ./Dockerfile
|
||||||
|
|
||||||
|
- name: ntfy Success
|
||||||
|
uses: niniyas/ntfy-action@master
|
||||||
|
if: success()
|
||||||
|
with:
|
||||||
|
url: '${{ secrets.NTFY_URL }}'
|
||||||
|
topic: '${{ secrets.NTFY_TOPIC }}'
|
||||||
|
title: 'Gitea Action'
|
||||||
|
priority: 3
|
||||||
|
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
|
||||||
|
tags: action,successfully,completed
|
||||||
|
details: 'Site Profile build workflow has successfully completed!'
|
||||||
|
icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png'
|
||||||
|
|
||||||
|
- name: ntfy Failed
|
||||||
|
uses: niniyas/ntfy-action@master
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
url: '${{ secrets.NTFY_URL }}'
|
||||||
|
topic: '${{ secrets.NTFY_TOPIC }}'
|
||||||
|
title: 'Gitea Action'
|
||||||
|
priority: 4
|
||||||
|
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
|
||||||
|
tags: action,failed
|
||||||
|
details: 'Site Profile build workflow has failed!'
|
||||||
|
icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png'
|
||||||
|
actions: '[{"action": "view", "label": "Open Gitea", "url": "https://gitea.alexlebens.dev/alexlebens/site-profile/actions?workflow=release-image.yml", "clear": true}]'
|
||||||
|
image: true
|
@@ -2,7 +2,7 @@ name: renovate
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "@daily"
|
- cron: '@daily'
|
||||||
|
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
@@ -13,18 +13,20 @@ 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:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout
|
||||||
- run: renovate
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Renovate
|
||||||
|
run: renovate
|
||||||
env:
|
env:
|
||||||
RENOVATE_PLATFORM: gitea
|
RENOVATE_PLATFORM: gitea
|
||||||
RENOVATE_AUTODISCOVER: true
|
RENOVATE_ENDPOINT: ${{ vars.INSTANCE_URL }}
|
||||||
RENOVATE_ONBOARDING: true
|
RENOVATE_REPOSITORIES: alexlebens/site-profile
|
||||||
RENOVATE_ENDPOINT: http://gitea-http.gitea:3000
|
|
||||||
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net>
|
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net>
|
||||||
LOG_LEVEL: debug
|
LOG_LEVEL: info
|
||||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
RENOVATE_GIT_PRIVATE_KEY: ${{ secrets.RENOVATE_GIT_PRIVATE_KEY }}
|
RENOVATE_GIT_PRIVATE_KEY: ${{ secrets.RENOVATE_GIT_PRIVATE_KEY }}
|
||||||
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
|
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
|
||||||
RENOVATE_REDIS_URL: redis://gitea-renovate-valkey-primary.gitea:6379
|
RENOVATE_REDIS_URL: ${{ vars.RENOVATE_REDIS_URL }}
|
||||||
|
37
.gitea/workflows/test-build.yaml
Normal file
37
.gitea/workflows/test-build.yaml
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
name: test-build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10.x
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22.17.x
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Lint Code
|
||||||
|
run: pnpm lint
|
||||||
|
|
||||||
|
- name: Build Project
|
||||||
|
run: pnpm build
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -24,4 +24,3 @@ pnpm-debug.log*
|
|||||||
# ide
|
# ide
|
||||||
.vscode/
|
.vscode/
|
||||||
site-profile.code-workspace
|
site-profile.code-workspace
|
||||||
.pre-commit-config.yaml
|
|
||||||
|
1
.npmrc
1
.npmrc
@@ -1,3 +1,2 @@
|
|||||||
engine-strict=true
|
engine-strict=true
|
||||||
save-exact=true
|
save-exact=true
|
||||||
|
|
||||||
|
18
.prettierrc
18
.prettierrc
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"useTabs": false,
|
|
||||||
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": "*.astro",
|
|
||||||
"options": {
|
|
||||||
"parser": "astro"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["astro-build.astro-vscode"],
|
|
||||||
"unwantedRecommendations": []
|
|
||||||
}
|
|
11
.vscode/launch.json
vendored
11
.vscode/launch.json
vendored
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"command": "./node_modules/.bin/astro dev",
|
|
||||||
"name": "Development server",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "node-terminal"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
11
Dockerfile
11
Dockerfile
@@ -1,7 +1,8 @@
|
|||||||
FROM node:22.15.1-alpine3.20 AS base
|
ARG REGISTRY=docker.io
|
||||||
|
FROM ${REGISTRY}/node:22.17.0-alpine3.22 AS base
|
||||||
|
|
||||||
LABEL version="0.7.0"
|
LABEL version="0.9.0"
|
||||||
LABEL description="Astro based website to use as a personal site"
|
LABEL description="Astro based personal website"
|
||||||
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
@@ -20,6 +21,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
|||||||
FROM build-deps AS build
|
FROM build-deps AS build
|
||||||
COPY . .
|
COPY . .
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
RUN pnpm prune --prod
|
||||||
|
|
||||||
FROM base AS runtime
|
FROM base AS runtime
|
||||||
COPY --from=prod-deps /app/node_modules /app/node_modules
|
COPY --from=prod-deps /app/node_modules /app/node_modules
|
||||||
@@ -29,5 +31,6 @@ ENV HOST=0.0.0.0
|
|||||||
ENV SITE_URL=https://www.alexlebens.dev
|
ENV SITE_URL=https://www.alexlebens.dev
|
||||||
ENV DIRECTUS_URL=https://directus.alexlebens.dev
|
ENV DIRECTUS_URL=https://directus.alexlebens.dev
|
||||||
ENV PORT=4321
|
ENV PORT=4321
|
||||||
|
|
||||||
EXPOSE $PORT
|
EXPOSE $PORT
|
||||||
CMD node ./dist/server/entry.mjs
|
CMD ["node", "./dist/server/entry.mjs"]
|
||||||
|
@@ -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
|
||||||
|
14
README.md
14
README.md
@@ -21,7 +21,7 @@ Personal site used for information about myself and blog.
|
|||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- Node.js 16+ and pnpm/yarn
|
- Node.js 22+ and pnpm
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
@@ -30,22 +30,18 @@ Personal site used for information about myself and blog.
|
|||||||
git clone https://gitea.alexlebens.dev/alexlebens/site-profile
|
git clone https://gitea.alexlebens.dev/alexlebens/site-profile
|
||||||
|
|
||||||
# Navigate to project directory
|
# Navigate to project directory
|
||||||
cd astro-blog
|
cd site-profile
|
||||||
|
|
||||||
# Install dependencies
|
# Install dependencies
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
# Create .env file from template
|
|
||||||
cp .env.example .env
|
|
||||||
|
|
||||||
# Edit .env with your information
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Development
|
### Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Start development server
|
# Start development server
|
||||||
pnpm run dev
|
pnpm dev
|
||||||
|
|
||||||
# Open browser at http://localhost:4321
|
# Open browser at http://localhost:4321
|
||||||
```
|
```
|
||||||
@@ -54,10 +50,10 @@ pnpm run dev
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Create production build
|
# Create production build
|
||||||
pnpm run build
|
pnpm build
|
||||||
|
|
||||||
# Preview production build
|
# Preview production build
|
||||||
pnpm run preview
|
pnpm preview
|
||||||
```
|
```
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import tailwind from '@astrojs/tailwind';
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
import react from '@astrojs/react';
|
import react from '@astrojs/react';
|
||||||
|
|
||||||
|
import node from '@astrojs/node';
|
||||||
|
|
||||||
const getSiteURL = () => {
|
const getSiteURL = () => {
|
||||||
if (process.env.SITE_URL) {
|
if (process.env.SITE_URL) {
|
||||||
return `https://${process.env.SITE_URL}`;
|
return `https://${process.env.SITE_URL}`;
|
||||||
@@ -11,8 +13,17 @@ const getSiteURL = () => {
|
|||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: getSiteURL(),
|
site: getSiteURL(),
|
||||||
integrations: [
|
integrations: [tailwindcss(), react()],
|
||||||
tailwind(),
|
|
||||||
react(),
|
plugins: {
|
||||||
],
|
'@tailwindcss/postcss': {},
|
||||||
|
},
|
||||||
|
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()],
|
||||||
|
},
|
||||||
|
|
||||||
|
adapter: node({
|
||||||
|
mode: 'standalone',
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
11
eslint.config.mjs
Normal file
11
eslint.config.mjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import eslintPluginAstro from 'eslint-plugin-astro';
|
||||||
|
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...eslintPluginAstro.configs.recommended,
|
||||||
|
eslintConfigPrettier,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
@@ -1,4 +1,4 @@
|
|||||||
import { createDirectus, rest, } from '@directus/sdk';
|
import { createDirectus, rest } from '@directus/sdk';
|
||||||
|
|
||||||
type Global = {
|
type Global = {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -10,38 +10,39 @@ type Global = {
|
|||||||
portrait: string;
|
portrait: string;
|
||||||
portrait_alt: string;
|
portrait_alt: string;
|
||||||
about: string;
|
about: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
type About = {
|
type About = {
|
||||||
background: string;
|
background: string;
|
||||||
experience: string;
|
experience: string;
|
||||||
education: string;
|
education: string;
|
||||||
certifications: string;
|
certifications: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
type Links = {
|
type Links = {
|
||||||
github: string;
|
github: string;
|
||||||
linkedin: string;
|
linkedin: string;
|
||||||
}
|
gitea: string;
|
||||||
|
};
|
||||||
|
|
||||||
type Skill = {
|
type Skill = {
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
level: string;
|
level: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Post = {
|
export type Post = {
|
||||||
slug: string;
|
slug: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
content: string;
|
content: string;
|
||||||
image: string;
|
image: string;
|
||||||
image_alt: string;
|
image_alt: string;
|
||||||
published_date: Date;
|
published_date: Date;
|
||||||
updated_date: Date;
|
updated_date: Date;
|
||||||
tags: string[];
|
tags: string[];
|
||||||
}
|
};
|
||||||
|
|
||||||
type Schema = {
|
type Schema = {
|
||||||
global: Global;
|
global: Global;
|
||||||
@@ -49,8 +50,10 @@ type Schema = {
|
|||||||
links: Links;
|
links: Links;
|
||||||
skills: Skill[];
|
skills: Skill[];
|
||||||
posts: Post[];
|
posts: Post[];
|
||||||
}
|
};
|
||||||
|
|
||||||
const directus = createDirectus<Schema>(process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev").with(rest());
|
const directus = createDirectus<Schema>(
|
||||||
|
process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'
|
||||||
|
).with(rest());
|
||||||
|
|
||||||
export default directus;
|
export default directus;
|
||||||
|
25
package.json
25
package.json
@@ -1,12 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "site-profile",
|
"name": "site-profile",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.7.0",
|
"version": "0.9.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
|
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,md,mdx,astro}\"",
|
||||||
|
"lint": "eslint \"src/**/*.{js,ts,jsx,tsx,astro}\"",
|
||||||
|
"lint:fix": "eslint --fix \"src/**/*.{js,ts,jsx,tsx,astro}\"",
|
||||||
"astro": "astro"
|
"astro": "astro"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -14,23 +17,27 @@
|
|||||||
"@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",
|
||||||
"@astrojs/tailwind": "^6.0.2",
|
"@tailwindcss/postcss": "^4.1.8",
|
||||||
"@directus/sdk": "^19.1.0",
|
"@tailwindcss/vite": "^4.1.8",
|
||||||
"astro": "^5.9.1",
|
"astro": "^5.10.1",
|
||||||
"framer-motion": "^12.16.0",
|
"framer-motion": "^12.16.0",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-hotkeys-hook": "^5.1.0",
|
"react-hotkeys-hook": "^5.1.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"reading-time": "^1.5.0",
|
|
||||||
"sanitize-html": "^2.17.0",
|
"sanitize-html": "^2.17.0",
|
||||||
"tailwindcss": "^3.0.24"
|
"tailwindcss": "^4.1.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"@typescript-eslint/parser": "8.36.0",
|
||||||
|
"eslint": "9.31.0",
|
||||||
|
"eslint-config-prettier": "10.1.5",
|
||||||
|
"eslint-plugin-astro": "1.3.1",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prettier-plugin-astro": "^0.12.3",
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.14"
|
"prettier-plugin-tailwindcss": "^0.6.12",
|
||||||
|
"typescript-eslint": "8.36.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
2283
pnpm-lock.yaml
generated
2283
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,7 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
|
||||||
'postcss-preset-env': {
|
|
||||||
features: {
|
|
||||||
'nesting-rules': false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
23
prettier.config.mjs
Normal file
23
prettier.config.mjs
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/** @type {import("prettier").Config} */
|
||||||
|
const config = {
|
||||||
|
printWidth: 100,
|
||||||
|
semi: true,
|
||||||
|
singleQuote: true,
|
||||||
|
tabWidth: 2,
|
||||||
|
trailingComma: 'es5',
|
||||||
|
useTabs: false,
|
||||||
|
plugins: [
|
||||||
|
'prettier-plugin-astro',
|
||||||
|
'prettier-plugin-tailwindcss',
|
||||||
|
],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
files: '*.astro',
|
||||||
|
options: {
|
||||||
|
parser: 'astro',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 28 KiB |
@@ -1,9 +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: 713 B |
40
renovate.json
Normal file
40
renovate.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
|
"extends": [
|
||||||
|
"config:recommended",
|
||||||
|
"mergeConfidence:all-badges",
|
||||||
|
":rebaseStalePrs"
|
||||||
|
],
|
||||||
|
"timezone": "US/Central",
|
||||||
|
"labels": [],
|
||||||
|
"prHourlyLimit": 0,
|
||||||
|
"prConcurrentLimit": 0,
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"description": "Label dependency",
|
||||||
|
"matchDatasources": [
|
||||||
|
"npm"
|
||||||
|
],
|
||||||
|
"addLabels": [
|
||||||
|
"dependency"
|
||||||
|
],
|
||||||
|
"automerge": false,
|
||||||
|
"minimumReleaseAge": "1 days"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Automerge dependency patch",
|
||||||
|
"matchDatasources": [
|
||||||
|
"npm"
|
||||||
|
],
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"patch"
|
||||||
|
],
|
||||||
|
"addLabels": [
|
||||||
|
"dependency",
|
||||||
|
"automerge"
|
||||||
|
],
|
||||||
|
"automerge": true,
|
||||||
|
"minimumReleaseAge": "1 days"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@@ -2,16 +2,29 @@
|
|||||||
// Background.astro - Dot pattern and ambient glow background with smooth theme transitions
|
// Background.astro - Dot pattern and ambient glow background with smooth theme transitions
|
||||||
---
|
---
|
||||||
|
|
||||||
<div class="fixed inset-0 -z-10 overflow-hidden theme-transition-all">
|
<div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden">
|
||||||
<!-- Dot pattern background -->
|
<!-- Dot pattern background -->
|
||||||
<div class="absolute inset-0 bg-grid-pattern bg-[center_top_-1px] [mask-image:radial-gradient(white,transparent_85%)] theme-transition-bg"></div>
|
<div
|
||||||
|
class="bg-grid-pattern theme-transition-bg absolute inset-0 [mask-image:radial-gradient(white,transparent_85%)] bg-[center_top_-1px]"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Ambient glow effects -->
|
<!-- Ambient glow effects -->
|
||||||
<div class="absolute left-1/4 top-1/4 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-zinc-400/20 dark:bg-zinc-500/20 rounded-full blur-3xl opacity-50 animate-glow theme-transition-bg"></div>
|
<div
|
||||||
<div class="absolute right-1/4 bottom-1/3 translate-x-1/2 translate-y-1/2 w-64 h-64 bg-zinc-300/20 dark:bg-zinc-600/20 rounded-full blur-3xl opacity-40 animate-glow animation-delay-1000 theme-transition-bg"></div>
|
class="animate-glow theme-transition-bg absolute top-1/4 left-1/4 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-zinc-400/20 opacity-50 blur-3xl dark:bg-zinc-500/20"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="animate-glow animation-delay-1000 theme-transition-bg absolute right-1/4 bottom-1/3 h-64 w-64 translate-x-1/2 translate-y-1/2 rounded-full bg-zinc-300/20 opacity-40 blur-3xl dark:bg-zinc-600/20"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Theme transition overlay -->
|
<!-- Theme transition overlay -->
|
||||||
<div id="theme-transition-overlay" class="absolute inset-0 bg-white dark:bg-zinc-900 opacity-0 pointer-events-none"></div>
|
<div
|
||||||
|
id="theme-transition-overlay"
|
||||||
|
class="pointer-events-none absolute inset-0 bg-white opacity-0 dark:bg-zinc-900"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -19,20 +32,20 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
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
|
// Add transitioning class to optimize performance
|
||||||
document.documentElement.classList.add('theme-transitioning');
|
document.documentElement.classList.add('theme-transitioning');
|
||||||
|
|
||||||
// Fade in overlay
|
// 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
|
// Fade out overlay
|
||||||
overlay.style.opacity = '0';
|
overlay.style.opacity = '0';
|
||||||
|
|
||||||
// Remove transitioning class after animation completes
|
// Remove transitioning class after animation completes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.documentElement.classList.remove('theme-transitioning');
|
document.documentElement.classList.remove('theme-transitioning');
|
||||||
@@ -50,24 +63,27 @@
|
|||||||
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px);
|
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px);
|
||||||
transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1);
|
transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Dark mode version */
|
/* Dark mode version */
|
||||||
:global(.dark) .bg-grid-pattern {
|
:global(.dark) .bg-grid-pattern {
|
||||||
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
|
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ambient glow animations */
|
/* Ambient glow animations */
|
||||||
.animate-glow {
|
.animate-glow {
|
||||||
animation: glow 12s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
animation: glow 12s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
transition: background-color 0.7s cubic-bezier(0.65, 0, 0.35, 1), opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1);
|
transition:
|
||||||
|
background-color 0.7s cubic-bezier(0.65, 0, 0.35, 1),
|
||||||
|
opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-delay-1000 {
|
.animation-delay-1000 {
|
||||||
animation-delay: 1s;
|
animation-delay: 1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes glow {
|
@keyframes glow {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
transform: translate(0, 0) scale(1);
|
transform: translate(0, 0) scale(1);
|
||||||
}
|
}
|
||||||
@@ -84,7 +100,7 @@
|
|||||||
transform: translate(-5%, 5%) scale(1.05);
|
transform: translate(-5%, 5%) scale(1.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Theme transition overlay */
|
/* Theme transition overlay */
|
||||||
#theme-transition-overlay {
|
#theme-transition-overlay {
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
|
38
src/components/DynamicIcon.tsx
Normal file
38
src/components/DynamicIcon.tsx
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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;
|
@@ -1,9 +1,9 @@
|
|||||||
---
|
---
|
||||||
import directus from "../../lib/directus"
|
import directus from '../../lib/directus';
|
||||||
import { readSingleton } from "@directus/sdk";
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
const global = await directus.request(readSingleton("global"));
|
const global = await directus.request(readSingleton('global'));
|
||||||
const links = await directus.request(readSingleton("links"));
|
const links = await directus.request(readSingleton('links'));
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
@@ -18,105 +18,163 @@ const socialLinks = [
|
|||||||
{
|
{
|
||||||
name: 'GitHub',
|
name: 'GitHub',
|
||||||
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,
|
||||||
icon: `<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path>`
|
icon: `<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path>`,
|
||||||
}
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer class="relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800 theme-transition-all">
|
<footer
|
||||||
<div class="absolute inset-0 pointer-events-none overflow-hidden">
|
class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
|
||||||
<div class="absolute -top-40 -right-40 w-80 h-80 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-50 theme-transition-all animate-float-slow"></div>
|
>
|
||||||
<div class="absolute -bottom-40 -left-40 w-80 h-80 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-50 theme-transition-all animate-float-slow animation-delay-2000"></div>
|
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
||||||
<div class="absolute top-20 left-1/4 w-40 h-40 bg-zinc-200/50 dark:bg-zinc-700/20 rounded-full blur-2xl opacity-30 theme-transition-all animate-float-slow animation-delay-1000"></div>
|
<div
|
||||||
|
class="theme-transition-all animate-float-slow absolute -top-40 -right-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="theme-transition-all animate-float-slow animation-delay-2000 absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="theme-transition-all animate-float-slow animation-delay-1000 absolute top-20 left-1/4 h-40 w-40 rounded-full bg-zinc-200/50 opacity-30 blur-2xl dark:bg-zinc-700/20"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="relative pt-16 pb-12 px-4 sm:px-6">
|
<div class="relative px-4 pt-16 pb-12 sm:px-6">
|
||||||
<div class="max-w-4xl mx-auto">
|
<div class="mx-auto max-w-4xl">
|
||||||
<!-- Main footer content -->
|
<!-- Main footer content -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-10">
|
<div class="grid grid-cols-1 gap-10 md:grid-cols-12">
|
||||||
<!-- Brand section -->
|
<!-- Brand section -->
|
||||||
<div class="col-span-1 md:col-span-3">
|
<div class="col-span-1 md:col-span-3">
|
||||||
<a href="/" class="inline-block group">
|
<a href="/" class="group inline-block">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="relative w-10 h-10 rounded-lg bg-gradient-to-br from-zinc-800 to-zinc-600 dark:from-zinc-200 dark:to-zinc-400 flex items-center justify-center overflow-hidden shadow-lg transform transition-transform group-hover:scale-105">
|
<div
|
||||||
<span class="text-white dark:text-zinc-900 text-xl font-bold theme-transition-all group-hover:scale-110 transition-transform duration-300">{global.initals}</span>
|
class="relative flex h-10 w-10 transform items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-zinc-800 to-zinc-600 shadow-lg transition-transform group-hover:scale-105 dark:from-zinc-200 dark:to-zinc-400"
|
||||||
<div class="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900 dark:from-zinc-300 dark:to-zinc-100 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
>
|
||||||
|
<span
|
||||||
|
class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900"
|
||||||
|
>{global.initals}</span
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100 theme-transition-color">Blog</span>
|
<span
|
||||||
|
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
|
||||||
|
>Blog</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<p class="mt-4 text-sm text-zinc-600 dark:text-zinc-400 theme-transition-color leading-relaxed">
|
<p
|
||||||
|
class="theme-transition-color mt-4 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400"
|
||||||
|
>
|
||||||
{global.description}
|
{global.description}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Social links -->
|
<!-- Social links -->
|
||||||
<div class="mt-6 flex items-center space-x-4">
|
<div class="mt-6 flex items-center space-x-4">
|
||||||
{socialLinks.map(social => (
|
{
|
||||||
<a
|
socialLinks.map((social) => (
|
||||||
href={social.href}
|
<a
|
||||||
target="_blank"
|
href={social.href}
|
||||||
rel="noopener noreferrer"
|
target="_blank"
|
||||||
class="group relative flex items-center justify-center w-10 h-10 rounded-full bg-zinc-100 dark:bg-zinc-800 text-zinc-500 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-all duration-300 hover:ring-2 hover:ring-zinc-300 dark:hover:ring-zinc-700 transform hover:-translate-y-1"
|
rel="noopener noreferrer"
|
||||||
aria-label={social.name}
|
class="hover group relative flex h-10 w-10 transform items-center justify-center rounded-full bg-zinc-100 text-zinc-500 transition-all duration-300 hover:-translate-y-1 hover:text-zinc-900 hover:ring-2 hover:ring-zinc-300 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100 dark:hover:ring-zinc-700"
|
||||||
>
|
aria-label={social.name}
|
||||||
<span class="absolute inset-0 rounded-full bg-gradient-to-br from-zinc-200 to-zinc-300 dark:from-zinc-700 dark:to-zinc-600 opacity-0 group-hover:opacity-100 transition-opacity duration-300"></span>
|
>
|
||||||
<svg class="w-5 h-5 relative z-10 transition-transform duration-300 group-hover:scale-110" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
<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" />
|
||||||
<Fragment set:html={social.icon} />
|
<svg
|
||||||
</svg>
|
class="relative z-10 h-5 w-5 transition-transform duration-300 group-hover:scale-110"
|
||||||
</a>
|
fill="currentColor"
|
||||||
))}
|
viewBox="0 0 24 24"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<Fragment set:html={social.icon} />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Quick links -->
|
<!-- Quick links -->
|
||||||
<div class="col-span-1 md:col-span-3">
|
<div class="col-span-1 md:col-span-3">
|
||||||
<h3 class="text-sm font-semibold text-zinc-900 dark:text-zinc-100 uppercase tracking-wider theme-transition-color relative inline-block after:content-[''] after:absolute after:w-8 after:h-0.5 after:bg-zinc-300 dark:after:bg-zinc-700 after:bottom-0 after:left-0 pb-2">Navigation</h3>
|
<h3
|
||||||
|
class="theme-transition-color relative inline-block pb-2 text-sm font-semibold tracking-wider text-zinc-900 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:bg-zinc-300 after:content-[''] dark:text-zinc-100 dark:after:bg-zinc-700"
|
||||||
|
>
|
||||||
|
Navigation
|
||||||
|
</h3>
|
||||||
<ul class="mt-4 space-y-3">
|
<ul class="mt-4 space-y-3">
|
||||||
{navLinks.map(link => (
|
{
|
||||||
<li>
|
navLinks.map((link) => (
|
||||||
<a
|
<li>
|
||||||
href={link.href}
|
<a
|
||||||
class="group flex items-center text-base text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors"
|
href={link.href}
|
||||||
>
|
class="group flex items-center text-base text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
|
||||||
<span class="relative overflow-hidden inline-block">
|
>
|
||||||
<span class="relative z-10">{link.text}</span>
|
<span class="relative inline-block overflow-hidden">
|
||||||
<span class="absolute left-0 bottom-0 w-0 h-0.5 bg-zinc-800 dark:bg-zinc-200 transition-all duration-300 group-hover:w-full"></span>
|
<span class="relative z-10">{link.text}</span>
|
||||||
</span>
|
<span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
|
||||||
</a>
|
</span>
|
||||||
</li>
|
</a>
|
||||||
))}
|
</li>
|
||||||
|
))
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Bottom section -->
|
<!-- Bottom section -->
|
||||||
<div class="mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800 theme-transition-all">
|
<div class="theme-transition-all mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
||||||
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
|
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
||||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 theme-transition-color">
|
<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="text-xs text-zinc-500 dark:text-zinc-400 theme-transition-color">Built with</span>
|
<span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
|
||||||
|
>Built with</span
|
||||||
|
>
|
||||||
<a
|
<a
|
||||||
href="https://astro.build"
|
href="https://astro.build"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="group inline-flex items-center text-xs text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100 transition-colors"
|
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="h-4 w-4 mr-1 text-[#FF5D01] group-hover:animate-pulse" viewBox="0 0 36 36" fill="none">
|
<svg
|
||||||
<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"/>
|
class="mr-1 h-4 w-4 text-[#FF5D01] group-hover:animate-pulse"
|
||||||
<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"/>
|
viewBox="0 0 36 36"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M8.833 22.958c.622-1.185 1.832-1.918 3.18-1.918 2.292 0 4.145 1.86 4.145 4.153 0 1.34-.626 2.54-1.601 3.303 1.223-1.299 1.97-3.048 1.97-4.971 0-3.994-3.243-7.233-7.242-7.233-2.818 0-5.26 1.6-6.469 3.933.78-2.912 3.428-5.06 6.577-5.06 3.75 0 6.79 3.035 6.79 6.78 0 2.606-1.468 4.868-3.616 6.002a4.163 4.163 0 0 0 2.285-3.724c0-2.293-1.853-4.153-4.145-4.153-1.348 0-2.558.733-3.18 1.918l1.306-3.03Z"
|
||||||
|
fill="currentColor"></path>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M22.155 12.056c-.622 1.185-1.832 1.918-3.18 1.918-2.292 0-4.145-1.86-4.145-4.153 0-1.34.626-2.54 1.601-3.303-1.223 1.299-1.97 3.048-1.97 4.971 0 3.994 3.243 7.233 7.242 7.233 2.818 0 5.26-1.6 6.469-3.933-.78 2.912-3.428 5.06-6.577 5.06-3.75 0-6.79-3.035-6.79-6.78 0-2.606 1.468-4.868 3.616-6.002a4.163 4.163 0 0 0-2.285 3.724c0 2.293 1.853 4.153 4.145 4.153 1.348 0 2.558-.733 3.18-1.918l-1.306 3.03Z"
|
||||||
|
fill="currentColor"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="relative">
|
<span class="relative">
|
||||||
Astro
|
Astro
|
||||||
<span class="absolute left-0 bottom-0 w-0 h-0.5 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"></span>
|
<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>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -146,7 +204,8 @@ const socialLinks = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: scale(1);
|
transform: scale(1);
|
||||||
}
|
}
|
||||||
@@ -157,7 +216,8 @@ const socialLinks = [
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes float-slow {
|
@keyframes float-slow {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translateY(0) translateX(0);
|
transform: translateY(0) translateX(0);
|
||||||
}
|
}
|
||||||
25% {
|
25% {
|
||||||
@@ -186,5 +246,4 @@ const socialLinks = [
|
|||||||
.animation-delay-2000 {
|
.animation-delay-2000 {
|
||||||
animation-delay: 2s;
|
animation-delay: 2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -8,12 +8,14 @@ const { date } = Astro.props;
|
|||||||
const parsedDate = typeof date === 'string' ? new Date(date) : date;
|
const parsedDate = typeof date === 'string' ? new Date(date) : date;
|
||||||
---
|
---
|
||||||
|
|
||||||
{parsedDate && (
|
{
|
||||||
<time datetime={parsedDate.toISOString()}>
|
parsedDate && (
|
||||||
{parsedDate.toLocaleDateString('en-us', {
|
<time datetime={parsedDate.toISOString()}>
|
||||||
year: 'numeric',
|
{parsedDate.toLocaleDateString('en-us', {
|
||||||
month: 'long',
|
year: 'numeric',
|
||||||
day: 'numeric',
|
month: 'long',
|
||||||
})}
|
day: 'numeric',
|
||||||
</time>
|
})}
|
||||||
)}
|
</time>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
---
|
---
|
||||||
import ThemeToggle from './ThemeToggle.astro';
|
import ThemeToggle from './ThemeToggle.astro';
|
||||||
|
|
||||||
import directus from "../../lib/directus"
|
import directus from '../../lib/directus';
|
||||||
import { readSingleton } from "@directus/sdk";
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
const global = await directus.request(readSingleton("global"));
|
const global = await directus.request(readSingleton('global'));
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{ text: 'Home', href: '/' },
|
{ text: 'Home', href: '/' },
|
||||||
@@ -15,68 +15,102 @@ const navItems = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const pathname = new URL(Astro.request.url).pathname;
|
const pathname = new URL(Astro.request.url).pathname;
|
||||||
const currentPath = pathname.slice(1); // remove the first "/"
|
const currentPath = pathname.slice(1);
|
||||||
---
|
---
|
||||||
|
|
||||||
<header class="py-4 fixed top-0 left-0 right-0 z-40 bg-white dark:bg-zinc-900 border-b border-zinc-100 dark:border-zinc-800">
|
<header
|
||||||
<div class="max-w-3xl mx-auto px-4 flex items-center justify-between">
|
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"
|
||||||
|
>
|
||||||
|
<div class="mx-auto flex max-w-3xl items-center justify-between px-4">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<a href="/" class="font-bold text-xl text-zinc-900 dark:text-white">{global.initals}</a>
|
<a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">{global.initals}</a>
|
||||||
|
|
||||||
<!-- Desktop navigation -->
|
<!-- Desktop navigation -->
|
||||||
<nav class="hidden sm:flex items-center space-x-6">
|
<nav class="hidden items-center space-x-6 sm:flex">
|
||||||
{navItems.map(item => {
|
{
|
||||||
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
|
navItems.map((item) => {
|
||||||
return (
|
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
|
||||||
<a
|
return (
|
||||||
href={item.href}
|
<a
|
||||||
class={`text-sm font-medium ${isActive
|
href={item.href}
|
||||||
? 'text-zinc-900 dark:text-white'
|
class={`text-sm font-medium ${
|
||||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'}`}
|
isActive
|
||||||
>
|
? 'text-zinc-900 dark:text-white'
|
||||||
{item.text}
|
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
|
||||||
</a>
|
}`}
|
||||||
)
|
>
|
||||||
})}
|
{item.text}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Mobile menu button -->
|
<!-- Mobile menu button -->
|
||||||
<button id="mobile-menu-button" class="sm:hidden flex items-center" aria-label="Menu">
|
<button id="mobile-menu-button" class="flex items-center sm:hidden" aria-label="Menu">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6 text-zinc-900 dark:text-white">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-6 w-6 text-zinc-900 dark:text-white"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Mobile menu overlay -->
|
<!-- Mobile menu overlay -->
|
||||||
<div id="mobile-menu" class="fixed inset-0 z-50 bg-white dark:bg-zinc-900 flex flex-col opacity-0 pointer-events-none transition-all duration-300 ease-in-out">
|
<div
|
||||||
<div class="flex justify-between items-center p-4 border-b border-zinc-100 dark:border-zinc-800">
|
id="mobile-menu"
|
||||||
<a href="/" class="font-bold text-xl text-zinc-900 dark:text-white">JD</a>
|
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"
|
||||||
<button id="close-menu-button" class="text-zinc-900 dark:text-white p-2 rounded-md hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-colors" aria-label="Close menu">
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
<div class="flex items-center justify-between border-b border-zinc-100 p-4 dark:border-zinc-800">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
<a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">{global.initals}</a>
|
||||||
|
<button
|
||||||
|
id="close-menu-button"
|
||||||
|
class="rounded-md p-2 text-zinc-900 transition-colors hover:bg-zinc-100 dark:text-white dark:hover:bg-zinc-800"
|
||||||
|
aria-label="Close menu"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-6 w-6"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<nav class="flex-1 flex flex-col items-center justify-center space-y-6 text-center">
|
<nav class="flex flex-1 flex-col items-center justify-center space-y-6 text-center">
|
||||||
{navItems.map((item, index) => {
|
{
|
||||||
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
|
navItems.map((item, index) => {
|
||||||
return (
|
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
|
||||||
<a
|
return (
|
||||||
href={item.href}
|
<a
|
||||||
class={`text-lg font-medium mobile-nav-item opacity-0 translate-y-4 ${isActive
|
href={item.href}
|
||||||
? 'text-zinc-900 dark:text-white'
|
class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
|
||||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'}`}
|
isActive
|
||||||
style={`transition-delay: ${index * 0.05}s;`}
|
? 'text-zinc-900 dark:text-white'
|
||||||
>
|
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
|
||||||
{item.text}
|
}`}
|
||||||
</a>
|
style={`transition-delay: ${index * 0.05}s;`}
|
||||||
)
|
>
|
||||||
})}
|
{item.text}
|
||||||
<div class="pt-4 mobile-nav-item opacity-0 translate-y-4" style="transition-delay: 0.25s;">
|
</a>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
<div class="mobile-nav-item translate-y-4 pt-4 opacity-0" style="transition-delay: 0.25s;">
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
@@ -92,44 +126,44 @@ const currentPath = pathname.slice(1); // remove the first "/"
|
|||||||
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');
|
||||||
const navItems = document.querySelectorAll('.mobile-nav-item');
|
const navItems = document.querySelectorAll('.mobile-nav-item');
|
||||||
|
|
||||||
// Open menu with animations
|
// Open menu with animations
|
||||||
mobileMenuButton?.addEventListener('click', () => {
|
mobileMenuButton?.addEventListener('click', () => {
|
||||||
if (!mobileMenu) return;
|
if (!mobileMenu) return;
|
||||||
|
|
||||||
// Prevent body scrolling
|
// Prevent body scrolling
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
|
|
||||||
// Show menu with fade in
|
// Show menu with fade in
|
||||||
mobileMenu.classList.remove('pointer-events-none');
|
mobileMenu.classList.remove('pointer-events-none');
|
||||||
mobileMenu.classList.add('pointer-events-auto');
|
mobileMenu.classList.add('pointer-events-auto');
|
||||||
|
|
||||||
// Animate opacity
|
// Animate opacity
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mobileMenu.style.opacity = '1';
|
mobileMenu.style.opacity = '1';
|
||||||
|
|
||||||
// Animate each nav item with staggered delay
|
// Animate each nav item with staggered delay
|
||||||
navItems.forEach(item => {
|
navItems.forEach((item) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
item.classList.remove('opacity-0', 'translate-y-4');
|
item.classList.remove('opacity-0', 'translate-y-4');
|
||||||
}, 150);
|
}, 150);
|
||||||
});
|
});
|
||||||
}, 50);
|
}, 50);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close menu with animations
|
// Close menu with animations
|
||||||
const closeMenu = () => {
|
const closeMenu = () => {
|
||||||
if (!mobileMenu) return;
|
if (!mobileMenu) return;
|
||||||
|
|
||||||
// Fade out nav items first
|
// Fade out nav items first
|
||||||
navItems.forEach(item => {
|
navItems.forEach((item) => {
|
||||||
item.classList.add('opacity-0', 'translate-y-4');
|
item.classList.add('opacity-0', 'translate-y-4');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Then fade out the menu
|
// Then fade out the menu
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mobileMenu.style.opacity = '0';
|
mobileMenu.style.opacity = '0';
|
||||||
|
|
||||||
// After animation completes, hide menu and restore scrolling
|
// After animation completes, hide menu and restore scrolling
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mobileMenu.classList.remove('pointer-events-auto');
|
mobileMenu.classList.remove('pointer-events-auto');
|
||||||
@@ -138,39 +172,39 @@ const currentPath = pathname.slice(1); // remove the first "/"
|
|||||||
}, 300);
|
}, 300);
|
||||||
}, 100);
|
}, 100);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Close button event
|
// Close button event
|
||||||
closeMenuButton?.addEventListener('click', closeMenu);
|
closeMenuButton?.addEventListener('click', closeMenu);
|
||||||
|
|
||||||
// Close menu when clicking a link
|
// Close menu when clicking a link
|
||||||
const mobileLinks = mobileMenu?.querySelectorAll('a');
|
const mobileLinks = mobileMenu?.querySelectorAll('a');
|
||||||
mobileLinks?.forEach(link => {
|
mobileLinks?.forEach((link) => {
|
||||||
link.addEventListener('click', closeMenu);
|
link.addEventListener('click', closeMenu);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Close menu on escape key
|
// Close menu on escape key
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape' && mobileMenu?.classList.contains('pointer-events-auto')) {
|
if (e.key === 'Escape' && mobileMenu?.classList.contains('pointer-events-auto')) {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add smooth animation to header on scroll
|
// Add smooth animation to header on scroll
|
||||||
const header = document.querySelector('header');
|
const header = document.querySelector('header');
|
||||||
let lastScrollY = window.scrollY;
|
let lastScrollY = window.scrollY;
|
||||||
|
|
||||||
window.addEventListener('scroll', () => {
|
window.addEventListener('scroll', () => {
|
||||||
if (!header) return;
|
if (!header) return;
|
||||||
|
|
||||||
const currentScrollY = window.scrollY;
|
const currentScrollY = window.scrollY;
|
||||||
|
|
||||||
// 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
|
||||||
lastScrollY = currentScrollY;
|
lastScrollY = currentScrollY;
|
||||||
});
|
});
|
||||||
@@ -180,26 +214,32 @@ const currentPath = pathname.slice(1); // remove the first "/"
|
|||||||
<style>
|
<style>
|
||||||
/* Smooth animations for mobile navigation */
|
/* Smooth animations for mobile navigation */
|
||||||
.mobile-nav-item {
|
.mobile-nav-item {
|
||||||
transition: opacity 0.5s ease, transform 0.5s ease, color 0.3s ease;
|
transition:
|
||||||
|
opacity 0.5s ease,
|
||||||
|
transform 0.5s ease,
|
||||||
|
color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Header transition */
|
/* Header transition */
|
||||||
header {
|
header {
|
||||||
transition: box-shadow 0.3s ease, transform 0.3s ease, background-color 0.3s ease;
|
transition:
|
||||||
|
box-shadow 0.3s ease,
|
||||||
|
transform 0.3s ease,
|
||||||
|
background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile menu button hover effect */
|
/* Mobile menu button hover effect */
|
||||||
#mobile-menu-button {
|
#mobile-menu-button {
|
||||||
transition: transform 0.2s ease;
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
#mobile-menu-button:hover {
|
#mobile-menu-button:hover {
|
||||||
transform: scale(1.05);
|
transform: scale(1.05);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 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>
|
||||||
|
@@ -13,41 +13,89 @@ const encodedUrl = encodeURIComponent(url);
|
|||||||
<div class={`flex items-center gap-4 mt-8 ${className}`}>
|
<div class={`flex items-center gap-4 mt-8 ${className}`}>
|
||||||
<span class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Share:</span>
|
<span class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Share:</span>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<a
|
<a
|
||||||
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="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-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>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="h-4 w-4"
|
||||||
|
><path
|
||||||
|
d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"
|
||||||
|
></path></svg
|
||||||
|
>
|
||||||
</a>
|
</a>
|
||||||
<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="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4"><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="h-4 w-4"
|
||||||
|
><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg
|
||||||
|
>
|
||||||
</a>
|
</a>
|
||||||
<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="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-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 xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-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>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="h-4 w-4"
|
||||||
|
><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"
|
||||||
|
></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"
|
||||||
|
></circle></svg
|
||||||
|
>
|
||||||
</a>
|
</a>
|
||||||
<button
|
<button
|
||||||
id="copy-link-button"
|
id="copy-link-button"
|
||||||
class="p-2 text-zinc-500 hover:text-zinc-700 dark:text-zinc-400 dark:hover:text-zinc-300 rounded-full hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300 relative"
|
class="relative rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
|
||||||
aria-label="Copy link"
|
aria-label="Copy link"
|
||||||
title="Copy link to clipboard"
|
title="Copy link to clipboard"
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-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>
|
<svg
|
||||||
<span id="copy-tooltip" class="absolute -top-8 left-1/2 transform -translate-x-1/2 bg-zinc-800 dark:bg-zinc-700 text-white text-xs py-1 px-2 rounded opacity-0 transition-opacity duration-300 whitespace-nowrap">
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="h-4 w-4"
|
||||||
|
><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path
|
||||||
|
d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
id="copy-tooltip"
|
||||||
|
class="absolute -top-8 left-1/2 -translate-x-1/2 transform rounded-sm bg-zinc-800 px-2 py-1 text-xs whitespace-nowrap text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
|
||||||
|
>
|
||||||
Copied!
|
Copied!
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -58,66 +106,69 @@ const encodedUrl = encodeURIComponent(url);
|
|||||||
// Function to handle copy link button
|
// Function to handle copy link button
|
||||||
function setupCopyLinkButton() {
|
function setupCopyLinkButton() {
|
||||||
const copyButtons = document.querySelectorAll('#copy-link-button');
|
const copyButtons = document.querySelectorAll('#copy-link-button');
|
||||||
|
|
||||||
copyButtons.forEach(button => {
|
copyButtons.forEach((button) => {
|
||||||
button.addEventListener('click', () => {
|
button.addEventListener('click', () => {
|
||||||
// Get the current URL
|
// Get the current URL
|
||||||
const url = window.location.href;
|
const url = window.location.href;
|
||||||
|
|
||||||
// Copy to clipboard
|
// Copy to clipboard
|
||||||
navigator.clipboard.writeText(url).then(() => {
|
navigator.clipboard
|
||||||
// Show tooltip
|
.writeText(url)
|
||||||
const tooltip = button.querySelector('#copy-tooltip');
|
.then(() => {
|
||||||
if (tooltip) {
|
// Show tooltip
|
||||||
tooltip.classList.add('opacity-100');
|
const tooltip = button.querySelector('#copy-tooltip');
|
||||||
|
if (tooltip) {
|
||||||
// Hide tooltip after 2 seconds
|
tooltip.classList.add('opacity-100');
|
||||||
setTimeout(() => {
|
|
||||||
tooltip.classList.remove('opacity-100');
|
// Hide tooltip after 2 seconds
|
||||||
}, 2000);
|
setTimeout(() => {
|
||||||
}
|
tooltip.classList.remove('opacity-100');
|
||||||
}).catch(err => {
|
}, 2000);
|
||||||
console.error('Failed to copy: ', err);
|
}
|
||||||
});
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up the copy link button when the DOM is loaded
|
// Set up the copy link button when the DOM is loaded
|
||||||
document.addEventListener('DOMContentLoaded', setupCopyLinkButton);
|
document.addEventListener('DOMContentLoaded', setupCopyLinkButton);
|
||||||
|
|
||||||
// Also set up when the page content is updated via SPA navigation
|
// Also set up when the page content is updated via SPA navigation
|
||||||
document.addEventListener('astro:page-load', setupCopyLinkButton);
|
document.addEventListener('astro:page-load', setupCopyLinkButton);
|
||||||
|
|
||||||
// For compatibility with the custom page transition system
|
// For compatibility with the custom page transition system
|
||||||
document.addEventListener('page-transition-complete', setupCopyLinkButton);
|
document.addEventListener('page-transition-complete', setupCopyLinkButton);
|
||||||
|
|
||||||
// Handle SPA transitions for share links
|
// Handle SPA transitions for share links
|
||||||
function setupSpaTransitions() {
|
function setupSpaTransitions() {
|
||||||
// Get all share links
|
// Get all share links
|
||||||
const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]');
|
const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]');
|
||||||
|
|
||||||
// Make sure external share links don't trigger page transitions
|
// Make sure external share links don't trigger page transitions
|
||||||
shareLinks.forEach(link => {
|
shareLinks.forEach((link) => {
|
||||||
link.setAttribute('data-spa-external', 'true');
|
link.setAttribute('data-spa-external', 'true');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize SPA transitions
|
// Initialize SPA transitions
|
||||||
document.addEventListener('DOMContentLoaded', setupSpaTransitions);
|
document.addEventListener('DOMContentLoaded', setupSpaTransitions);
|
||||||
document.addEventListener('astro:page-load', setupSpaTransitions);
|
document.addEventListener('astro:page-load', setupSpaTransitions);
|
||||||
document.addEventListener('page-transition-complete', setupSpaTransitions);
|
document.addEventListener('page-transition-complete', setupSpaTransitions);
|
||||||
|
|
||||||
// Dispatch custom event when share action is completed
|
// Dispatch custom event when share action is completed
|
||||||
function notifyShareComplete() {
|
function notifyShareComplete() {
|
||||||
document.dispatchEvent(new CustomEvent('share-action-complete'));
|
document.dispatchEvent(new CustomEvent('share-action-complete'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add analytics tracking for share actions if needed
|
// Add analytics tracking for share actions if needed
|
||||||
function trackShareAction(platform) {
|
function trackShareAction(platform) {
|
||||||
// You can implement analytics tracking here
|
// You can implement analytics tracking here
|
||||||
console.log(`Shared on ${platform}`);
|
console.log(`Shared on ${platform}`);
|
||||||
|
|
||||||
// Notify other components that share action is complete
|
// Notify other components that share action is complete
|
||||||
notifyShareComplete();
|
notifyShareComplete();
|
||||||
}
|
}
|
||||||
|
@@ -7,15 +7,17 @@ export interface Props {
|
|||||||
const { tags = [], class: className = '' } = Astro.props;
|
const { tags = [], class: className = '' } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
{tags.length > 0 && (
|
{
|
||||||
<div class={`flex flex-wrap gap-2 mt-3 ${className}`}>
|
tags.length > 0 && (
|
||||||
{tags.map(tag => (
|
<div class={`mt-3 flex flex-wrap gap-2 ${className}`}>
|
||||||
<a
|
{tags.map((tag) => (
|
||||||
href={`/tag/${tag}`}
|
<a
|
||||||
class="inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-800 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
href={`/tag/${tag}`}
|
||||||
>
|
class="inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-800 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
||||||
{tag}
|
>
|
||||||
</a>
|
{tag}
|
||||||
))}
|
</a>
|
||||||
</div>
|
))}
|
||||||
)}
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@@ -1,65 +1,70 @@
|
|||||||
---
|
---
|
||||||
---
|
|
||||||
|
|
||||||
<button
|
---
|
||||||
id="theme-toggle"
|
|
||||||
|
<button
|
||||||
|
id="theme-toggle"
|
||||||
data-theme-toggle
|
data-theme-toggle
|
||||||
class="relative overflow-hidden rounded-full p-1.5 sm:p-2 transition-all duration-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 focus:outline-none focus:ring-2 focus:ring-zinc-300 dark:focus:ring-zinc-700 group touch-manipulation"
|
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">
|
||||||
<!-- Sun icon -->
|
<!-- Sun icon -->
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon-light absolute h-5 w-5 rotate-0 scale-100 transition-all duration-500 dark:-rotate-90 dark:scale-0 text-zinc-800 dark:text-zinc-200"
|
class="icon-light absolute h-5 w-5 scale-100 rotate-0 text-zinc-800 transition-all duration-500 dark:scale-0 dark:-rotate-90 dark:text-zinc-200"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<circle cx="12" cy="12" r="5"/>
|
<circle cx="12" cy="12" r="5"></circle>
|
||||||
<path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/>
|
<path
|
||||||
|
d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
|
|
||||||
<!-- Moon icon -->
|
<!-- Moon icon -->
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon-dark absolute h-5 w-5 rotate-90 scale-0 transition-all duration-500 dark:rotate-0 dark:scale-100 text-zinc-800 dark:text-zinc-200"
|
class="icon-dark absolute h-5 w-5 scale-0 rotate-90 text-zinc-800 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-zinc-200"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
stroke-width="2"
|
stroke-width="2"
|
||||||
stroke-linecap="round"
|
stroke-linecap="round"
|
||||||
stroke-linejoin="round"
|
stroke-linejoin="round"
|
||||||
>
|
>
|
||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ripple effect -->
|
<!-- Ripple effect -->
|
||||||
<span class="absolute inset-0 h-full w-full bg-zinc-200 dark:bg-zinc-700 opacity-0 transition-opacity duration-300 group-active:opacity-20"></span>
|
<span
|
||||||
|
class="absolute inset-0 h-full w-full bg-zinc-200 opacity-0 transition-opacity duration-300 group-active:opacity-20 dark:bg-zinc-700"
|
||||||
|
></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<script>
|
<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
|
// Check for dark mode preference at the system level
|
||||||
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
|
|
||||||
// Check for saved theme preference or use the system preference
|
// Check for saved theme preference or use the system preference
|
||||||
const currentTheme = localStorage.getItem('theme') || (prefersDarkMode ? 'dark' : 'light');
|
const currentTheme = localStorage.getItem('theme') || (prefersDarkMode ? 'dark' : 'light');
|
||||||
|
|
||||||
// Apply the theme on initial load
|
// Apply the theme on initial load
|
||||||
if (currentTheme === 'dark') {
|
if (currentTheme === 'dark') {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.classList.remove('dark');
|
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');
|
||||||
@@ -68,104 +73,119 @@
|
|||||||
overlay.style.transition = 'opacity 0.3s ease-out';
|
overlay.style.transition = 'opacity 0.3s ease-out';
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle theme when any theme toggle button is clicked
|
// Toggle theme when any theme toggle button is clicked
|
||||||
themeToggles.forEach(toggle => {
|
themeToggles.forEach((toggle) => {
|
||||||
// Add event listeners for both click and touch events
|
// Add event listeners for both click and touch events
|
||||||
['click', 'touchend'].forEach(eventType => {
|
['click', 'touchend'].forEach((eventType) => {
|
||||||
toggle.addEventListener(eventType, (e) => {
|
toggle.addEventListener(
|
||||||
e.preventDefault();
|
eventType,
|
||||||
e.stopPropagation();
|
(e) => {
|
||||||
|
e.preventDefault();
|
||||||
// Get click/touch position for radial animation
|
e.stopPropagation();
|
||||||
let x, y;
|
|
||||||
if (e.type === 'touchend' && e.changedTouches && e.changedTouches[0]) {
|
// Get click/touch position for radial animation
|
||||||
const rect = toggle.getBoundingClientRect();
|
let x, y;
|
||||||
x = e.changedTouches[0].clientX - rect.left;
|
if (e.type === 'touchend' && e.changedTouches && e.changedTouches[0]) {
|
||||||
y = e.changedTouches[0].clientY - rect.top;
|
const rect = toggle.getBoundingClientRect();
|
||||||
} else {
|
x = e.changedTouches[0].clientX - rect.left;
|
||||||
const rect = toggle.getBoundingClientRect();
|
y = e.changedTouches[0].clientY - rect.top;
|
||||||
x = e.clientX - rect.left;
|
|
||||||
y = e.clientY - rect.top;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the position variables for the radial gradient
|
|
||||||
document.documentElement.style.setProperty('--x', `${x}px`);
|
|
||||||
document.documentElement.style.setProperty('--y', `${y}px`);
|
|
||||||
|
|
||||||
// Get the overlay element
|
|
||||||
const overlay = document.querySelector('.theme-switch-overlay');
|
|
||||||
|
|
||||||
// Determine the new theme
|
|
||||||
const isDark = document.documentElement.classList.contains('dark');
|
|
||||||
const newTheme = isDark ? 'light' : 'dark';
|
|
||||||
|
|
||||||
// Show overlay during transition
|
|
||||||
if (overlay) {
|
|
||||||
overlay.style.backgroundColor = newTheme === 'dark' ? 'rgba(24, 24, 27, 0.3)' : 'rgba(255, 255, 255, 0.3)';
|
|
||||||
overlay.style.opacity = '1';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add transition class
|
|
||||||
document.documentElement.classList.add('theme-switching');
|
|
||||||
|
|
||||||
// Add ripple effect
|
|
||||||
const ripple = document.createElement('span');
|
|
||||||
ripple.className = 'theme-toggle-ripple';
|
|
||||||
toggle.appendChild(ripple);
|
|
||||||
|
|
||||||
// Force a reflow to ensure all elements update
|
|
||||||
document.body.offsetHeight;
|
|
||||||
|
|
||||||
// Toggle dark mode with a slight delay to allow overlay to appear
|
|
||||||
setTimeout(() => {
|
|
||||||
if (isDark) {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.classList.add('dark');
|
const rect = toggle.getBoundingClientRect();
|
||||||
|
x = e.clientX - rect.left;
|
||||||
|
y = e.clientY - rect.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the preference
|
// Set the position variables for the radial gradient
|
||||||
localStorage.setItem('theme', newTheme);
|
document.documentElement.style.setProperty('--x', `${x}px`);
|
||||||
|
document.documentElement.style.setProperty('--y', `${y}px`);
|
||||||
// Dispatch a custom event for other components to react to
|
|
||||||
document.dispatchEvent(new CustomEvent('themeChanged', {
|
// Get the overlay element
|
||||||
detail: { isDark: newTheme === 'dark' }
|
const overlay = document.querySelector('.theme-switch-overlay');
|
||||||
}));
|
|
||||||
|
// Determine the new theme
|
||||||
// Force another reflow to ensure all elements update
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
|
const newTheme = isDark ? 'light' : 'dark';
|
||||||
|
|
||||||
|
// Show overlay during transition
|
||||||
|
if (overlay) {
|
||||||
|
overlay.style.backgroundColor =
|
||||||
|
newTheme === 'dark' ? 'rgba(24, 24, 27, 0.3)' : 'rgba(255, 255, 255, 0.3)';
|
||||||
|
overlay.style.opacity = '1';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add transition class
|
||||||
|
document.documentElement.classList.add('theme-switching');
|
||||||
|
|
||||||
|
// Add ripple effect
|
||||||
|
const ripple = document.createElement('span');
|
||||||
|
ripple.className = 'theme-toggle-ripple';
|
||||||
|
toggle.appendChild(ripple);
|
||||||
|
|
||||||
|
// Force a reflow to ensure all elements update
|
||||||
document.body.offsetHeight;
|
document.body.offsetHeight;
|
||||||
|
|
||||||
// Hide overlay after theme has changed
|
// Toggle dark mode with a slight delay to allow overlay to appear
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (overlay) {
|
if (isDark) {
|
||||||
overlay.style.opacity = '0';
|
document.documentElement.classList.remove('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove transition class after animation completes
|
// Store the preference
|
||||||
document.documentElement.classList.remove('theme-switching');
|
localStorage.setItem('theme', newTheme);
|
||||||
ripple.remove();
|
|
||||||
}, 300);
|
// Dispatch a custom event for other components to react to
|
||||||
}, 50);
|
document.dispatchEvent(
|
||||||
}, { passive: false });
|
new CustomEvent('themeChanged', {
|
||||||
|
detail: { isDark: newTheme === 'dark' },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Force another reflow to ensure all elements update
|
||||||
|
document.body.offsetHeight;
|
||||||
|
|
||||||
|
// Hide overlay after theme has changed
|
||||||
|
setTimeout(() => {
|
||||||
|
if (overlay) {
|
||||||
|
overlay.style.opacity = '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove transition class after animation completes
|
||||||
|
document.documentElement.classList.remove('theme-switching');
|
||||||
|
ripple.remove();
|
||||||
|
}, 300);
|
||||||
|
}, 50);
|
||||||
|
},
|
||||||
|
{ passive: false }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add touch feedback
|
// Add touch feedback
|
||||||
toggle.addEventListener('touchstart', () => {
|
toggle.addEventListener(
|
||||||
toggle.classList.add('active-touch');
|
'touchstart',
|
||||||
}, { passive: true });
|
() => {
|
||||||
|
toggle.classList.add('active-touch');
|
||||||
toggle.addEventListener('touchend', () => {
|
},
|
||||||
setTimeout(() => {
|
{ passive: true }
|
||||||
toggle.classList.remove('active-touch');
|
);
|
||||||
}, 150);
|
|
||||||
}, { passive: true });
|
toggle.addEventListener(
|
||||||
|
'touchend',
|
||||||
|
() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
toggle.classList.remove('active-touch');
|
||||||
|
}, 150);
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run setup on load
|
// Run setup on load
|
||||||
document.addEventListener('DOMContentLoaded', setupThemeToggle);
|
document.addEventListener('DOMContentLoaded', 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', () => {
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
@@ -177,7 +197,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen for system preference changes
|
// Listen for system preference changes
|
||||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches }) => {
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches }) => {
|
||||||
if (!localStorage.getItem('theme')) {
|
if (!localStorage.getItem('theme')) {
|
||||||
@@ -193,9 +213,11 @@
|
|||||||
<style>
|
<style>
|
||||||
/* Smooth transition for the entire page when theme changes */
|
/* Smooth transition for the entire page when theme changes */
|
||||||
:global(body) {
|
:global(body) {
|
||||||
transition: background-color 0.5s ease, color 0.5s ease;
|
transition:
|
||||||
|
background-color 0.5s ease,
|
||||||
|
color 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Theme transition overlay */
|
/* Theme transition overlay */
|
||||||
:global(.theme-switch-overlay) {
|
:global(.theme-switch-overlay) {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -204,13 +226,13 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: opacity 0.3s ease-out;
|
transition: opacity 0.3s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure theme transitions apply to all elements */
|
/* Ensure theme transitions apply to all elements */
|
||||||
:global(.theme-switching *) {
|
:global(.theme-switching *) {
|
||||||
transition-duration: 0.5s !important;
|
transition-duration: 0.5s !important;
|
||||||
transition-property: background-color, border-color, color, fill, stroke !important;
|
transition-property: background-color, border-color, color, fill, stroke !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ripple animation */
|
/* Ripple animation */
|
||||||
.theme-toggle-ripple {
|
.theme-toggle-ripple {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -223,7 +245,7 @@
|
|||||||
background-color: rgba(161, 161, 170, 0.3);
|
background-color: rgba(161, 161, 170, 0.3);
|
||||||
animation: ripple 0.8s ease-out;
|
animation: ripple 0.8s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes ripple {
|
@keyframes ripple {
|
||||||
0% {
|
0% {
|
||||||
transform: translate(-50%, -50%) scale(0);
|
transform: translate(-50%, -50%) scale(0);
|
||||||
@@ -234,7 +256,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Subtle hover animation */
|
/* Subtle hover animation */
|
||||||
#theme-toggle {
|
#theme-toggle {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@@ -243,47 +265,49 @@
|
|||||||
min-height: 32px; /* Ensure minimum touch target size */
|
min-height: 32px; /* Ensure minimum touch target size */
|
||||||
min-width: 32px; /* Ensure minimum touch target size */
|
min-width: 32px; /* Ensure minimum touch target size */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Only apply hover effects on non-touch devices */
|
/* Only apply hover effects on non-touch devices */
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
#theme-toggle:hover {
|
#theme-toggle:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Touch feedback */
|
/* Touch feedback */
|
||||||
#theme-toggle.active-touch {
|
#theme-toggle.active-touch {
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
transition: transform 0.15s ease-in-out;
|
transition: transform 0.15s ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optimize animations for mobile */
|
/* Optimize animations for mobile */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.icon-light, .icon-dark {
|
.icon-light,
|
||||||
|
.icon-dark {
|
||||||
transition: all 0.2s ease-out !important;
|
transition: all 0.2s ease-out !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
#theme-toggle, #theme-toggle:hover {
|
#theme-toggle,
|
||||||
|
#theme-toggle:hover {
|
||||||
transform: none;
|
transform: none;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle-ripple {
|
.theme-toggle-ripple {
|
||||||
animation-duration: 0.4s;
|
animation-duration: 0.4s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adjust size for very small screens */
|
/* Adjust size for very small screens */
|
||||||
@media (max-width: 320px) {
|
@media (max-width: 320px) {
|
||||||
#theme-toggle {
|
#theme-toggle {
|
||||||
|
1
src/env.d.ts
vendored
1
src/env.d.ts
vendored
@@ -1,4 +1,3 @@
|
|||||||
/// <reference path="../.astro/types.d.ts" />
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
/// <reference types="astro/client" />
|
/// <reference types="astro/client" />
|
||||||
/// <reference types="astro/content" />
|
/// <reference types="astro/content" />
|
||||||
|
|
||||||
|
@@ -1,18 +1,17 @@
|
|||||||
---
|
---
|
||||||
import Layout from './Layout.astro';
|
import Layout from './Layout.astro';
|
||||||
|
|
||||||
import directus from "../../lib/directus"
|
import directus from '../../lib/directus';
|
||||||
import { readSingleton } from "@directus/sdk";
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
const global = await directus.request(readSingleton("global"));
|
const global = await directus.request(readSingleton('global'));
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={global.title} description={global.description}>
|
<Layout title={global.title} description={global.title}>
|
||||||
<slot />
|
<slot />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@@ -1,19 +1,18 @@
|
|||||||
---
|
---
|
||||||
import Layout from './Layout.astro';
|
import Layout from './Layout.astro';
|
||||||
|
|
||||||
import directus from "../../lib/directus"
|
import directus from '../../lib/directus';
|
||||||
import { readSingleton } from "@directus/sdk";
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
const global = await directus.request(readSingleton("global"));
|
const global = await directus.request(readSingleton('global'));
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={global.title} description={global.description}>
|
<Layout title={global.title} description={global.title}>
|
||||||
<slot />
|
<slot />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ export interface Props {
|
|||||||
document.documentElement.classList.add('theme-switching');
|
document.documentElement.classList.add('theme-switching');
|
||||||
|
|
||||||
const rippleElements = document.querySelectorAll('.theme-ripple');
|
const rippleElements = document.querySelectorAll('.theme-ripple');
|
||||||
rippleElements.forEach(el => {
|
rippleElements.forEach((el) => {
|
||||||
el.classList.add('ripple-active');
|
el.classList.add('ripple-active');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
el.classList.remove('ripple-active');
|
el.classList.remove('ripple-active');
|
||||||
@@ -35,8 +34,8 @@ export interface Props {
|
|||||||
|
|
||||||
const event = new CustomEvent('themeChange', {
|
const event = new CustomEvent('themeChange', {
|
||||||
detail: {
|
detail: {
|
||||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
document.dispatchEvent(event);
|
document.dispatchEvent(event);
|
||||||
|
|
||||||
@@ -47,8 +46,7 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const socialLinks = document.querySelectorAll('.social-link');
|
const socialLinks = document.querySelectorAll('.social-link');
|
||||||
socialLinks.forEach(link => {
|
socialLinks.forEach((link) => {
|
||||||
|
|
||||||
link.addEventListener('mouseenter', () => {
|
link.addEventListener('mouseenter', () => {
|
||||||
link.classList.add('hover-active');
|
link.classList.add('hover-active');
|
||||||
});
|
});
|
||||||
|
@@ -5,13 +5,15 @@ import ShareButtons from '../components/ShareButtons.astro';
|
|||||||
import TagList from '../components/TagList.astro';
|
import TagList from '../components/TagList.astro';
|
||||||
import './styles/markdown.css';
|
import './styles/markdown.css';
|
||||||
|
|
||||||
import directus from "../../lib/directus"
|
import directus from '../../lib/directus';
|
||||||
import { readItems } from "@directus/sdk";
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await directus.request(readItems("posts", {
|
const posts = await directus.request(
|
||||||
fields: ['*'],
|
readItems('posts', {
|
||||||
}));
|
fields: ['*'],
|
||||||
|
})
|
||||||
|
);
|
||||||
return posts.map((post) => ({ params: { slug: post.slug }, props: post }));
|
return posts.map((post) => ({ params: { slug: post.slug }, props: post }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,58 +25,64 @@ try {
|
|||||||
canonicalURL = new URL(Astro.url.pathname, Astro.site || process.env.SITE_URL);
|
canonicalURL = new URL(Astro.url.pathname, Astro.site || process.env.SITE_URL);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error creating canonical URL:', error);
|
console.error('Error creating canonical URL:', error);
|
||||||
canonicalURL = new URL("https://www.example.com");
|
canonicalURL = new URL('https://www.example.com');
|
||||||
}
|
}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={post.title} description={post.description}>
|
<Layout title={post.title} description={post.description}>
|
||||||
<article class="prose dark:prose-invert prose-zinc lg:prose-lg mx-auto max-w-4xl">
|
<article class="prose prose-zinc dark:prose-invert lg:prose-lg mx-auto max-w-4xl">
|
||||||
<div class="mb-12">
|
<div class="mb-12">
|
||||||
<h1 class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl">
|
<h1
|
||||||
|
class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 sm:text-5xl dark:text-zinc-100"
|
||||||
|
>
|
||||||
{post.title}
|
{post.title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div class="flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400 mb-6">
|
<div class="mb-6 flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400">
|
||||||
<FormattedDate date={published_date} />
|
<FormattedDate date={published_date} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TagList tags={post.tags} class="mt-2" />
|
<TagList tags={post.tags} class="mt-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Hero image -->
|
<!-- Hero image -->
|
||||||
{post.image && (
|
{
|
||||||
<div class="relative mb-8 sm:mb-12 overflow-hidden rounded-xl shadow-lg">
|
post.image && (
|
||||||
<div class="aspect-[16/9] w-full">
|
<div class="relative mb-8 overflow-hidden rounded-xl shadow-lg sm:mb-12">
|
||||||
<img
|
<div class="aspect-[16/9] w-full">
|
||||||
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}?width=500`}
|
<img
|
||||||
alt={post.image_alt}
|
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
|
||||||
class="w-full h-full object-cover"
|
alt={post.image_alt}
|
||||||
loading="eager"
|
class="h-full w-full object-cover"
|
||||||
/>
|
loading="eager"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent" />
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent"></div>
|
)
|
||||||
</div>
|
}
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="markdown-content">
|
<div class="markdown-content">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add the like button after the content -->
|
<!-- Add the like button after the content -->
|
||||||
<div class="mt-12 pt-8 border-t border-zinc-200 dark:border-zinc-800">
|
<div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
||||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-6">
|
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
||||||
<ShareButtons url={canonicalURL.toString()} title={post.title} /> <!-- Convert URL to string -->
|
<ShareButtons url={canonicalURL.toString()} title={post.title} />
|
||||||
|
<!-- Convert URL to string -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{post.updated_date && (
|
{
|
||||||
<div class="mt-8 text-sm text-zinc-500 dark:text-zinc-400 italic">
|
post.updated_date && (
|
||||||
Last updated on <FormattedDate date={post.updated_date} />
|
<div class="mt-8 text-sm text-zinc-500 italic dark:text-zinc-400">
|
||||||
</div>
|
Last updated on <FormattedDate date={post.updated_date} />
|
||||||
)}
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
<slot name="after-article" />
|
<slot name="after-article" />
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
@@ -85,37 +93,40 @@ try {
|
|||||||
const article = document.querySelector('article');
|
const article = document.querySelector('article');
|
||||||
if (article) {
|
if (article) {
|
||||||
article.classList.add('article-entering');
|
article.classList.add('article-entering');
|
||||||
|
|
||||||
// Remove class after animation completes
|
// Remove class after animation completes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
article.classList.remove('article-entering');
|
article.classList.remove('article-entering');
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure consistent code block styling
|
// Ensure consistent code block styling
|
||||||
function updateCodeBlockStyles() {
|
function updateCodeBlockStyles() {
|
||||||
document.querySelectorAll('pre').forEach(pre => {
|
document.querySelectorAll('pre').forEach((pre) => {
|
||||||
// Force the background color with !important for both light and dark mode
|
// Force the background color with !important for both light and dark mode
|
||||||
pre.setAttribute('style', 'background-color: #1e293b !important');
|
pre.setAttribute('style', 'background-color: #1e293b !important');
|
||||||
|
|
||||||
// Also apply to any nested code elements
|
// Also apply to any nested code elements
|
||||||
const codeElements = pre.querySelectorAll('code');
|
const codeElements = pre.querySelectorAll('code');
|
||||||
codeElements.forEach(code => {
|
codeElements.forEach((code) => {
|
||||||
code.setAttribute('style', 'background-color: transparent !important; color: #e5e7eb !important;');
|
code.setAttribute(
|
||||||
|
'style',
|
||||||
|
'background-color: transparent !important; color: #e5e7eb !important;'
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial application
|
// Initial application
|
||||||
updateCodeBlockStyles();
|
updateCodeBlockStyles();
|
||||||
|
|
||||||
// Watch for theme changes
|
// Watch for theme changes
|
||||||
const observer = new MutationObserver(() => {
|
const observer = new MutationObserver(() => {
|
||||||
updateCodeBlockStyles();
|
updateCodeBlockStyles();
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
||||||
|
|
||||||
// Also run on any content changes that might add new code blocks
|
// Also run on any content changes that might add new code blocks
|
||||||
const contentObserver = new MutationObserver((mutations) => {
|
const contentObserver = new MutationObserver((mutations) => {
|
||||||
for (const mutation of mutations) {
|
for (const mutation of mutations) {
|
||||||
@@ -125,71 +136,74 @@ try {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
contentObserver.observe(document.body, { childList: true, subtree: true });
|
contentObserver.observe(document.body, { childList: true, subtree: true });
|
||||||
|
|
||||||
// Clean up observers when navigating away
|
// Clean up observers when navigating away
|
||||||
document.addEventListener('spa-navigation-start', () => {
|
document.addEventListener('spa-navigation-start', () => {
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
contentObserver.disconnect();
|
contentObserver.disconnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Remove the parallax effect for hero image
|
// Remove the parallax effect for hero image
|
||||||
|
|
||||||
// Handle prev/next navigation links
|
// Handle prev/next navigation links
|
||||||
const navLinks = document.querySelectorAll('.blog-nav-link');
|
const navLinks = document.querySelectorAll('.blog-nav-link');
|
||||||
navLinks.forEach(link => {
|
navLinks.forEach((link) => {
|
||||||
if (!link.hasAttribute('data-spa-handled')) {
|
if (!link.hasAttribute('data-spa-handled')) {
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
link.setAttribute('data-spa-handled', 'true');
|
||||||
|
|
||||||
link.addEventListener('mouseenter', () => {
|
link.addEventListener('mouseenter', () => {
|
||||||
link.classList.add('nav-link-hover');
|
link.classList.add('nav-link-hover');
|
||||||
});
|
});
|
||||||
|
|
||||||
link.addEventListener('mouseleave', () => {
|
link.addEventListener('mouseleave', () => {
|
||||||
link.classList.remove('nav-link-hover');
|
link.classList.remove('nav-link-hover');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate headings when they enter the viewport
|
// Animate headings when they enter the viewport
|
||||||
const animateHeadings = () => {
|
const animateHeadings = () => {
|
||||||
const headings = document.querySelectorAll('article h2, article h3');
|
const headings = document.querySelectorAll('article h2, article h3');
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver(
|
||||||
entries.forEach(entry => {
|
(entries) => {
|
||||||
if (entry.isIntersecting) {
|
entries.forEach((entry) => {
|
||||||
entry.target.classList.add('heading-visible');
|
if (entry.isIntersecting) {
|
||||||
observer.unobserve(entry.target);
|
entry.target.classList.add('heading-visible');
|
||||||
}
|
observer.unobserve(entry.target);
|
||||||
});
|
}
|
||||||
}, {
|
});
|
||||||
threshold: 0.2,
|
},
|
||||||
rootMargin: '0px 0px -100px 0px'
|
{
|
||||||
});
|
threshold: 0.2,
|
||||||
|
rootMargin: '0px 0px -100px 0px',
|
||||||
headings.forEach(heading => {
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
headings.forEach((heading) => {
|
||||||
heading.classList.add('heading-animated');
|
heading.classList.add('heading-animated');
|
||||||
observer.observe(heading);
|
observer.observe(heading);
|
||||||
});
|
});
|
||||||
|
|
||||||
return observer;
|
return observer;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initialize heading animations
|
// Initialize heading animations
|
||||||
const headingObserver = animateHeadings();
|
const headingObserver = animateHeadings();
|
||||||
|
|
||||||
// Enhance code blocks with syntax highlighting and copy button
|
// Enhance code blocks with syntax highlighting and copy button
|
||||||
function enhanceCodeBlocks() {
|
function enhanceCodeBlocks() {
|
||||||
const codeBlocks = document.querySelectorAll('pre code');
|
const codeBlocks = document.querySelectorAll('pre code');
|
||||||
|
|
||||||
codeBlocks.forEach(codeBlock => {
|
codeBlocks.forEach((codeBlock) => {
|
||||||
// Skip if already processed
|
// Skip if already processed
|
||||||
if (codeBlock.parentElement.classList.contains('enhanced')) return;
|
if (codeBlock.parentElement.classList.contains('enhanced')) return;
|
||||||
|
|
||||||
// Mark as enhanced
|
// Mark as enhanced
|
||||||
codeBlock.parentElement.classList.add('enhanced');
|
codeBlock.parentElement.classList.add('enhanced');
|
||||||
|
|
||||||
// Create copy button
|
// Create copy button
|
||||||
const copyButton = document.createElement('button');
|
const copyButton = document.createElement('button');
|
||||||
copyButton.className = 'copy-code-button';
|
copyButton.className = 'copy-code-button';
|
||||||
@@ -199,19 +213,19 @@ try {
|
|||||||
<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" />
|
<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>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Add copy functionality
|
// Add copy functionality
|
||||||
copyButton.addEventListener('click', () => {
|
copyButton.addEventListener('click', () => {
|
||||||
const code = codeBlock.textContent;
|
const code = codeBlock.textContent;
|
||||||
navigator.clipboard.writeText(code);
|
navigator.clipboard.writeText(code);
|
||||||
|
|
||||||
// Show copied feedback
|
// Show copied feedback
|
||||||
copyButton.innerHTML = `
|
copyButton.innerHTML = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
<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" />
|
<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>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copyButton.innerHTML = `
|
copyButton.innerHTML = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
@@ -221,40 +235,40 @@ try {
|
|||||||
`;
|
`;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add copy button to pre element
|
// Add copy button to pre element
|
||||||
codeBlock.parentElement.appendChild(copyButton);
|
codeBlock.parentElement.appendChild(copyButton);
|
||||||
|
|
||||||
// Fix line numbers implementation
|
// Fix line numbers implementation
|
||||||
const codeText = codeBlock.textContent;
|
const codeText = codeBlock.textContent;
|
||||||
const lines = codeText.split('\n');
|
const lines = codeText.split('\n');
|
||||||
|
|
||||||
const lineNumbers = document.createElement('div');
|
const lineNumbers = document.createElement('div');
|
||||||
lineNumbers.className = 'line-numbers';
|
lineNumbers.className = 'line-numbers';
|
||||||
|
|
||||||
// Always include all lines, including empty ones
|
// Always include all lines, including empty ones
|
||||||
for (let i = 0; i < lines.length; i++) {
|
for (let i = 0; i < lines.length; i++) {
|
||||||
const lineNumber = document.createElement('span');
|
const lineNumber = document.createElement('span');
|
||||||
lineNumber.textContent = i + 1;
|
lineNumber.textContent = i + 1;
|
||||||
lineNumbers.appendChild(lineNumber);
|
lineNumbers.appendChild(lineNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
codeBlock.parentElement.classList.add('with-line-numbers');
|
codeBlock.parentElement.classList.add('with-line-numbers');
|
||||||
codeBlock.parentElement.insertBefore(lineNumbers, codeBlock);
|
codeBlock.parentElement.insertBefore(lineNumbers, codeBlock);
|
||||||
|
|
||||||
// Fix language label detection and display
|
// Fix language label detection and display
|
||||||
const className = codeBlock.className;
|
const className = codeBlock.className;
|
||||||
const languageMatch = className.match(/language-(\w+)/);
|
const languageMatch = className.match(/language-(\w+)/);
|
||||||
|
|
||||||
if (languageMatch && languageMatch[1]) {
|
if (languageMatch && languageMatch[1]) {
|
||||||
const language = languageMatch[1];
|
const language = languageMatch[1];
|
||||||
|
|
||||||
// Add language label at top right
|
// Add language label at top right
|
||||||
const languageLabel = document.createElement('div');
|
const languageLabel = document.createElement('div');
|
||||||
languageLabel.className = 'language-label';
|
languageLabel.className = 'language-label';
|
||||||
languageLabel.textContent = language;
|
languageLabel.textContent = language;
|
||||||
codeBlock.parentElement.appendChild(languageLabel);
|
codeBlock.parentElement.appendChild(languageLabel);
|
||||||
|
|
||||||
// Add language badge at bottom right with markdown syntax
|
// Add language badge at bottom right with markdown syntax
|
||||||
const languageBadge = document.createElement('div');
|
const languageBadge = document.createElement('div');
|
||||||
languageBadge.className = 'language-badge';
|
languageBadge.className = 'language-badge';
|
||||||
@@ -267,28 +281,29 @@ try {
|
|||||||
languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)';
|
languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)';
|
||||||
languageBadge.style.color = '#e5e7eb';
|
languageBadge.style.color = '#e5e7eb';
|
||||||
languageBadge.style.borderRadius = '0.25rem';
|
languageBadge.style.borderRadius = '0.25rem';
|
||||||
languageBadge.style.fontFamily = 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
languageBadge.style.fontFamily =
|
||||||
|
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
||||||
languageBadge.style.zIndex = '10';
|
languageBadge.style.zIndex = '10';
|
||||||
codeBlock.parentElement.appendChild(languageBadge);
|
codeBlock.parentElement.appendChild(languageBadge);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhance tables with better styling
|
// Enhance tables with better styling
|
||||||
function enhanceTables() {
|
function enhanceTables() {
|
||||||
const tables = document.querySelectorAll('.markdown-content table');
|
const tables = document.querySelectorAll('.markdown-content table');
|
||||||
|
|
||||||
tables.forEach(table => {
|
tables.forEach((table) => {
|
||||||
if (table.classList.contains('enhanced-table')) return;
|
if (table.classList.contains('enhanced-table')) return;
|
||||||
|
|
||||||
table.classList.add('enhanced-table');
|
table.classList.add('enhanced-table');
|
||||||
|
|
||||||
// Wrap table in responsive container
|
// Wrap table in responsive container
|
||||||
const wrapper = document.createElement('div');
|
const wrapper = document.createElement('div');
|
||||||
wrapper.className = 'table-container';
|
wrapper.className = 'table-container';
|
||||||
table.parentNode.insertBefore(wrapper, table);
|
table.parentNode.insertBefore(wrapper, table);
|
||||||
wrapper.appendChild(table);
|
wrapper.appendChild(table);
|
||||||
|
|
||||||
// Add zebra striping to rows
|
// Add zebra striping to rows
|
||||||
const rows = table.querySelectorAll('tbody tr');
|
const rows = table.querySelectorAll('tbody tr');
|
||||||
rows.forEach((row, index) => {
|
rows.forEach((row, index) => {
|
||||||
@@ -300,16 +315,16 @@ try {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhance blockquotes with icons
|
// Enhance blockquotes with icons
|
||||||
function enhanceBlockquotes() {
|
function enhanceBlockquotes() {
|
||||||
const blockquotes = document.querySelectorAll('.markdown-content blockquote');
|
const blockquotes = document.querySelectorAll('.markdown-content blockquote');
|
||||||
|
|
||||||
blockquotes.forEach(blockquote => {
|
blockquotes.forEach((blockquote) => {
|
||||||
if (blockquote.classList.contains('enhanced-quote')) return;
|
if (blockquote.classList.contains('enhanced-quote')) return;
|
||||||
|
|
||||||
blockquote.classList.add('enhanced-quote');
|
blockquote.classList.add('enhanced-quote');
|
||||||
|
|
||||||
// Add quote icon
|
// Add quote icon
|
||||||
const icon = document.createElement('div');
|
const icon = document.createElement('div');
|
||||||
icon.className = 'quote-icon';
|
icon.className = 'quote-icon';
|
||||||
@@ -318,16 +333,16 @@ try {
|
|||||||
<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" />
|
<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>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
blockquote.insertBefore(icon, blockquote.firstChild);
|
blockquote.insertBefore(icon, blockquote.firstChild);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run all enhancements
|
// Run all enhancements
|
||||||
enhanceCodeBlocks();
|
enhanceCodeBlocks();
|
||||||
enhanceTables();
|
enhanceTables();
|
||||||
enhanceBlockquotes();
|
enhanceBlockquotes();
|
||||||
|
|
||||||
// Clean up observers when navigating away
|
// Clean up observers when navigating away
|
||||||
document.addEventListener('spa-navigation-start', () => {
|
document.addEventListener('spa-navigation-start', () => {
|
||||||
if (headingObserver) {
|
if (headingObserver) {
|
||||||
@@ -335,16 +350,16 @@ try {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on first load
|
// Initialize on first load
|
||||||
document.addEventListener('DOMContentLoaded', setupBlogPostTransitions);
|
document.addEventListener('DOMContentLoaded', setupBlogPostTransitions);
|
||||||
|
|
||||||
// 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', setupBlogPostTransitions);
|
document.addEventListener('astro:page-load', setupBlogPostTransitions);
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
// For compatibility with custom transition system
|
||||||
document.addEventListener('page-transition-complete', setupBlogPostTransitions);
|
document.addEventListener('page-transition-complete', setupBlogPostTransitions);
|
||||||
|
|
||||||
// Also initialize when SPA navigation completes
|
// Also initialize when SPA navigation completes
|
||||||
document.addEventListener('spa-navigation-complete', setupBlogPostTransitions);
|
document.addEventListener('spa-navigation-complete', setupBlogPostTransitions);
|
||||||
</script>
|
</script>
|
||||||
@@ -353,19 +368,21 @@ try {
|
|||||||
/* Enhanced hero image styling */
|
/* Enhanced hero image styling */
|
||||||
article img:first-of-type {
|
article img:first-of-type {
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
box-shadow:
|
||||||
|
0 10px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
article img:first-of-type:hover {
|
article img:first-of-type:hover {
|
||||||
transform: scale(1.01);
|
transform: scale(1.01);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Article entrance animation */
|
/* Article entrance animation */
|
||||||
.article-entering {
|
.article-entering {
|
||||||
animation: article-fade-in 0.8s ease-out forwards;
|
animation: article-fade-in 0.8s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes article-fade-in {
|
@keyframes article-fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -376,6 +393,6 @@ try {
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rest of the styles remain unchanged... */
|
/* Rest of the styles remain unchanged... */
|
||||||
</style>
|
</style>
|
||||||
|
@@ -5,49 +5,57 @@ import Background from '../components/Background.astro';
|
|||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
title?: string | undefined;
|
title?: string | undefined;
|
||||||
description?: string | undefined;
|
description?: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title, description } = Astro.props;
|
const { title, description } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<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>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-white dark:bg-zinc-900 text-zinc-900 dark:text-zinc-100 min-h-screen flex flex-col">
|
<body
|
||||||
|
class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100"
|
||||||
|
>
|
||||||
<!-- Page transition overlay - for smooth transitions between pages -->
|
<!-- Page transition overlay - for smooth transitions between pages -->
|
||||||
<div id="page-transition" class="fixed inset-0 z-40 bg-white dark:bg-zinc-900 opacity-0 pointer-events-none transition-opacity duration-300 flex items-center justify-center">
|
<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 class="transition-spinner"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Background component with dot pattern and ambient glow -->
|
<!-- Background component with dot pattern and ambient glow -->
|
||||||
<Background />
|
<Background />
|
||||||
|
|
||||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 w-full flex-grow">
|
<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>
|
<script>
|
||||||
// SPA transition system with history API
|
// SPA transition system with history API
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const pageTransition = document.getElementById('page-transition');
|
const pageTransition = document.getElementById('page-transition');
|
||||||
const mainContent = document.querySelector('main');
|
const mainContent = document.querySelector('main');
|
||||||
|
|
||||||
// Initialize content with entrance animation
|
// Initialize content with entrance animation
|
||||||
if (mainContent) {
|
if (mainContent) {
|
||||||
mainContent.classList.add('content-entering');
|
mainContent.classList.add('content-entering');
|
||||||
@@ -55,7 +63,7 @@ const { title, description } = Astro.props;
|
|||||||
mainContent.classList.remove('content-entering');
|
mainContent.classList.remove('content-entering');
|
||||||
}, 800);
|
}, 800);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to load content via fetch
|
// Function to load content via fetch
|
||||||
async function loadContent(url) {
|
async function loadContent(url) {
|
||||||
try {
|
try {
|
||||||
@@ -64,52 +72,55 @@ const { title, description } = Astro.props;
|
|||||||
pageTransition.classList.remove('opacity-0', 'pointer-events-none');
|
pageTransition.classList.remove('opacity-0', 'pointer-events-none');
|
||||||
pageTransition.classList.add('opacity-100');
|
pageTransition.classList.add('opacity-100');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fade out current content
|
// Fade out current content
|
||||||
if (mainContent) {
|
if (mainContent) {
|
||||||
mainContent.style.opacity = '0';
|
mainContent.style.opacity = '0';
|
||||||
mainContent.style.transform = 'translateY(10px)';
|
mainContent.style.transform = 'translateY(10px)';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the new page content
|
// Fetch the new page content
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) throw new Error(`Failed to fetch ${url}`);
|
if (!response.ok) throw new Error(`Failed to fetch ${url}`);
|
||||||
const html = await response.text();
|
const html = await response.text();
|
||||||
|
|
||||||
// Create a temporary element to parse the HTML
|
// Create a temporary element to parse the HTML
|
||||||
const parser = new DOMParser();
|
const parser = new DOMParser();
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
|
|
||||||
// Extract the main content
|
// Extract the main content
|
||||||
const newContent = doc.querySelector('main');
|
const newContent = doc.querySelector('main');
|
||||||
if (!newContent) throw new Error('Could not find main content in the fetched page');
|
if (!newContent) throw new Error('Could not find main content in the fetched page');
|
||||||
|
|
||||||
// Extract the title
|
// Extract the title
|
||||||
const newTitle = doc.querySelector('title');
|
const newTitle = doc.querySelector('title');
|
||||||
if (newTitle) {
|
if (newTitle) {
|
||||||
document.title = newTitle.textContent;
|
document.title = newTitle.textContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract meta description
|
// Extract meta description
|
||||||
const newDescription = doc.querySelector('meta[name="description"]');
|
const newDescription = doc.querySelector('meta[name="description"]');
|
||||||
if (newDescription) {
|
if (newDescription) {
|
||||||
const currentDescription = document.querySelector('meta[name="description"]');
|
const currentDescription = document.querySelector('meta[name="description"]');
|
||||||
if (currentDescription) {
|
if (currentDescription) {
|
||||||
currentDescription.setAttribute('content', newDescription.getAttribute('content') || '');
|
currentDescription.setAttribute(
|
||||||
|
'content',
|
||||||
|
newDescription.getAttribute('content') || ''
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait a bit for transition effect
|
// Wait a bit for transition effect
|
||||||
await new Promise(resolve => setTimeout(resolve, 300));
|
await new Promise((resolve) => setTimeout(resolve, 300));
|
||||||
|
|
||||||
// Replace the content
|
// Replace the content
|
||||||
if (mainContent && newContent) {
|
if (mainContent && newContent) {
|
||||||
mainContent.innerHTML = newContent.innerHTML;
|
mainContent.innerHTML = newContent.innerHTML;
|
||||||
|
|
||||||
// Run scripts in the new content
|
// Run scripts in the new content
|
||||||
Array.from(newContent.querySelectorAll('script')).forEach(oldScript => {
|
Array.from(newContent.querySelectorAll('script')).forEach((oldScript) => {
|
||||||
const newScript = document.createElement('script');
|
const newScript = document.createElement('script');
|
||||||
Array.from(oldScript.attributes).forEach(attr => {
|
Array.from(oldScript.attributes).forEach((attr) => {
|
||||||
newScript.setAttribute(attr.name, attr.value);
|
newScript.setAttribute(attr.name, attr.value);
|
||||||
});
|
});
|
||||||
newScript.textContent = oldScript.textContent;
|
newScript.textContent = oldScript.textContent;
|
||||||
@@ -118,17 +129,17 @@ const { title, description } = Astro.props;
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fade in new content with animation
|
// Fade in new content with animation
|
||||||
if (mainContent) {
|
if (mainContent) {
|
||||||
mainContent.style.opacity = '0';
|
mainContent.style.opacity = '0';
|
||||||
mainContent.style.transform = 'translateY(10px)';
|
mainContent.style.transform = 'translateY(10px)';
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
mainContent.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
mainContent.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
||||||
mainContent.style.opacity = '1';
|
mainContent.style.opacity = '1';
|
||||||
mainContent.style.transform = 'translateY(0)';
|
mainContent.style.transform = 'translateY(0)';
|
||||||
|
|
||||||
// Add entrance animation class
|
// Add entrance animation class
|
||||||
mainContent.classList.add('content-entering');
|
mainContent.classList.add('content-entering');
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -136,7 +147,7 @@ const { title, description } = Astro.props;
|
|||||||
}, 800);
|
}, 800);
|
||||||
}, 50);
|
}, 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hide transition overlay
|
// Hide transition overlay
|
||||||
if (pageTransition) {
|
if (pageTransition) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -144,33 +155,34 @@ const { title, description } = Astro.props;
|
|||||||
pageTransition.classList.remove('opacity-100');
|
pageTransition.classList.remove('opacity-100');
|
||||||
}, 200);
|
}, 200);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch custom event for content loaded
|
// Dispatch custom event for content loaded
|
||||||
document.dispatchEvent(new CustomEvent('spa-content-loaded', {
|
document.dispatchEvent(
|
||||||
detail: { url }
|
new CustomEvent('spa-content-loaded', {
|
||||||
}));
|
detail: { url },
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Scroll to top or to saved position
|
// Scroll to top or to saved position
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
|
|
||||||
// Re-attach event listeners to new content
|
// Re-attach event listeners to new content
|
||||||
attachLinkListeners();
|
attachLinkListeners();
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading content:', error);
|
console.error('Error loading content:', error);
|
||||||
|
|
||||||
// Fallback to traditional navigation on error
|
// Fallback to traditional navigation on error
|
||||||
window.location.href = url;
|
window.location.href = url;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to attach event listeners to all links
|
// Function to attach event listeners to all links
|
||||||
function attachLinkListeners() {
|
function attachLinkListeners() {
|
||||||
document.querySelectorAll('a').forEach(link => {
|
document.querySelectorAll('a').forEach((link) => {
|
||||||
// Skip links that are already handled, anchor links, external links, or have special attributes
|
// Skip links that are already handled, anchor links, external links, or have special attributes
|
||||||
if (
|
if (
|
||||||
link.hasAttribute('data-spa-handled') ||
|
link.hasAttribute('data-spa-handled') ||
|
||||||
!link.href.startsWith(window.location.origin) ||
|
!link.href.startsWith(window.location.origin) ||
|
||||||
link.href.includes('#') ||
|
link.href.includes('#') ||
|
||||||
link.hasAttribute('target') ||
|
link.hasAttribute('target') ||
|
||||||
link.hasAttribute('download') ||
|
link.hasAttribute('download') ||
|
||||||
@@ -179,36 +191,36 @@ const { title, description } = Astro.props;
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
// Mark as handled to avoid duplicate listeners
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
link.setAttribute('data-spa-handled', 'true');
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', (e) => {
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const targetHref = link.href;
|
const targetHref = link.href;
|
||||||
|
|
||||||
// Don't transition if clicking the current page
|
// Don't transition if clicking the current page
|
||||||
if (targetHref === window.location.href) {
|
if (targetHref === window.location.href) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update browser history
|
// Update browser history
|
||||||
window.history.pushState({ path: targetHref }, '', targetHref);
|
window.history.pushState({ path: targetHref }, '', targetHref);
|
||||||
|
|
||||||
// Load the new content
|
// Load the new content
|
||||||
loadContent(targetHref);
|
loadContent(targetHref);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial attachment of link listeners
|
// Initial attachment of link listeners
|
||||||
attachLinkListeners();
|
attachLinkListeners();
|
||||||
|
|
||||||
// Handle browser back/forward navigation
|
// Handle browser back/forward navigation
|
||||||
window.addEventListener('popstate', (e) => {
|
window.addEventListener('popstate', (e) => {
|
||||||
if (e.state && e.state.path) {
|
if (e.state && e.state.path) {
|
||||||
@@ -217,7 +229,7 @@ const { title, description } = Astro.props;
|
|||||||
loadContent(window.location.href);
|
loadContent(window.location.href);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check RSS feed availability
|
// Check RSS feed availability
|
||||||
const checkAndGenerateRSS = async () => {
|
const checkAndGenerateRSS = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -229,33 +241,36 @@ const { title, description } = Astro.props;
|
|||||||
console.warn('Could not check RSS feed status.');
|
console.warn('Could not check RSS feed status.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check RSS feed availability
|
// Check RSS feed availability
|
||||||
checkAndGenerateRSS();
|
checkAndGenerateRSS();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Theme handling with transition effects
|
// Theme handling with transition effects
|
||||||
function setupThemeHandling() {
|
function setupThemeHandling() {
|
||||||
// Apply theme from localStorage or system preference
|
// Apply theme from localStorage or system preference
|
||||||
const theme = localStorage.getItem('theme');
|
const theme = localStorage.getItem('theme');
|
||||||
if (theme === 'dark' || (!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
if (
|
||||||
|
theme === 'dark' ||
|
||||||
|
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
||||||
|
) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
} else {
|
} else {
|
||||||
document.documentElement.classList.remove('dark');
|
document.documentElement.classList.remove('dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen for theme changes
|
// Listen for theme changes
|
||||||
document.addEventListener('themeChanged', () => {
|
document.addEventListener('themeChanged', () => {
|
||||||
// Add transition class to body
|
// Add transition class to body
|
||||||
document.body.classList.add('theme-transitioning');
|
document.body.classList.add('theme-transitioning');
|
||||||
|
|
||||||
// Remove class after transition completes
|
// Remove class after transition completes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.body.classList.remove('theme-transitioning');
|
document.body.classList.remove('theme-transitioning');
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize theme handling
|
// Initialize theme handling
|
||||||
document.addEventListener('DOMContentLoaded', setupThemeHandling);
|
document.addEventListener('DOMContentLoaded', setupThemeHandling);
|
||||||
</script>
|
</script>
|
||||||
@@ -266,9 +281,9 @@ const { title, description } = Astro.props;
|
|||||||
/* Page transition effects */
|
/* Page transition effects */
|
||||||
#page-transition {
|
#page-transition {
|
||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
backdrop-filter: blur(4px);
|
backdrop-filter: blur-sm(4px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Transition spinner animation */
|
/* Transition spinner animation */
|
||||||
.transition-spinner {
|
.transition-spinner {
|
||||||
width: 30px;
|
width: 30px;
|
||||||
@@ -278,27 +293,31 @@ const { title, description } = Astro.props;
|
|||||||
border-top-color: #3b82f6;
|
border-top-color: #3b82f6;
|
||||||
animation: spin 0.7s linear infinite;
|
animation: spin 0.7s linear infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.dark) .transition-spinner {
|
:global(.dark) .transition-spinner {
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
border-top-color: #60a5fa;
|
border-top-color: #60a5fa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Content entrance animation */
|
/* Content entrance animation */
|
||||||
main {
|
main {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
transition: opacity 0.5s ease, transform 0.5s ease;
|
transition:
|
||||||
|
opacity 0.5s ease,
|
||||||
|
transform 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
main.content-entering {
|
main.content-entering {
|
||||||
animation: content-fade-in 0.6s ease forwards;
|
animation: content-fade-in 0.6s ease forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes content-fade-in {
|
@keyframes content-fade-in {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -309,7 +328,7 @@ const { title, description } = Astro.props;
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Theme transition effect */
|
/* Theme transition effect */
|
||||||
body.theme-transitioning * {
|
body.theme-transitioning * {
|
||||||
transition-duration: 0.3s !important;
|
transition-duration: 0.3s !important;
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
---
|
---
|
||||||
import { ViewTransitions } from 'astro:transitions';
|
import { ClientRouter } from 'astro:transitions';
|
||||||
import BaseLayout from './BaseLayout.astro';
|
import BaseLayout from './BaseLayout.astro';
|
||||||
|
|
||||||
const { title, description } = Astro.props;
|
const { title, description } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={title} description={description}>
|
<BaseLayout title={title} description={description}>
|
||||||
<ViewTransitions fallback="swap" />
|
<ClientRouter fallback="swap" />
|
||||||
|
|
||||||
<div transition:animate="slide">
|
<div transition:animate="slide">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
@@ -18,7 +18,7 @@ const { title, description } = Astro.props;
|
|||||||
::view-transition-old(root) {
|
::view-transition-old(root) {
|
||||||
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-to-left;
|
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-to-left;
|
||||||
}
|
}
|
||||||
|
|
||||||
::view-transition-new(root) {
|
::view-transition-new(root) {
|
||||||
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-from-right;
|
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-from-right;
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -3,54 +3,100 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="404 - Page Not Found">
|
<Layout title="404 - Page Not Found">
|
||||||
<div class="relative flex flex-col items-center justify-center min-h-[80vh] py-20 text-center px-4 overflow-hidden">
|
<div
|
||||||
|
class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
|
||||||
|
>
|
||||||
<!-- Animated background elements -->
|
<!-- Animated background elements -->
|
||||||
<div class="absolute inset-0 overflow-hidden">
|
<div class="absolute inset-0 overflow-hidden">
|
||||||
<div class="absolute -top-20 -left-20 w-64 h-64 bg-zinc-100 dark:bg-zinc-800/50 rounded-full blur-3xl opacity-50 animate-blob"></div>
|
<div
|
||||||
<div class="absolute top-1/2 right-1/4 w-96 h-96 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000"></div>
|
class="animate-blob absolute -top-20 -left-20 h-64 w-64 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50"
|
||||||
<div class="absolute bottom-20 left-1/3 w-72 h-72 bg-zinc-100 dark:bg-zinc-800/40 rounded-full blur-3xl opacity-40 animate-blob animation-delay-4000"></div>
|
>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Main content with animation -->
|
|
||||||
<div class="relative z-10 max-w-xl mx-auto">
|
|
||||||
<div class="glitch-wrapper">
|
|
||||||
<h1 class="glitch text-9xl sm:text-[12rem] font-bold text-zinc-900 dark:text-zinc-100 leading-none" data-text="404">404</h1>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
<h2 class="mt-6 text-2xl sm:text-3xl font-bold text-zinc-800 dark:text-zinc-200">Page Not Found</h2>
|
class="animate-blob animation-delay-2000 absolute top-1/2 right-1/4 h-96 w-96 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30"
|
||||||
|
>
|
||||||
<p class="mt-6 text-zinc-600 dark:text-zinc-400 max-w-md mx-auto text-lg">
|
</div>
|
||||||
|
<div
|
||||||
|
class="animate-blob animation-delay-4000 absolute bottom-20 left-1/3 h-72 w-72 rounded-full bg-zinc-100 opacity-40 blur-3xl dark:bg-zinc-800/40"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content with animation -->
|
||||||
|
<div class="relative z-10 mx-auto max-w-xl">
|
||||||
|
<div class="glitch-wrapper">
|
||||||
|
<h1
|
||||||
|
class="glitch text-9xl leading-none font-bold text-zinc-900 sm:text-[12rem] dark:text-zinc-100"
|
||||||
|
data-text="404"
|
||||||
|
>
|
||||||
|
404
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="mt-6 text-2xl font-bold text-zinc-800 sm:text-3xl dark:text-zinc-200">
|
||||||
|
Page Not Found
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<p class="mx-auto mt-6 max-w-md text-lg text-zinc-600 dark:text-zinc-400">
|
||||||
The page you're looking for does not exist.
|
The page you're looking for does not exist.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
|
<div class="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
|
||||||
<a
|
<a
|
||||||
href="/"
|
href="/"
|
||||||
class="group relative inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-zinc-900 text-zinc-100 hover:bg-zinc-800 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200 transition-all duration-300 overflow-hidden shadow-lg hover:shadow-xl"
|
class="group relative inline-flex items-center gap-2 overflow-hidden rounded-lg bg-zinc-900 px-6 py-3 text-zinc-100 shadow-lg transition-all duration-300 hover:bg-zinc-800 hover:shadow-xl dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
|
||||||
>
|
>
|
||||||
<span class="absolute inset-0 bg-gradient-to-r from-zinc-700 to-zinc-900 dark:from-zinc-300 dark:to-zinc-100 opacity-0 group-hover:opacity-100 transition-opacity duration-300 z-0"></span>
|
<span
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 relative z-10">
|
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"
|
||||||
<path stroke-linecap="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" />
|
></span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="relative z-10 h-5 w-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="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"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="font-medium relative z-10">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 px-6 py-3 rounded-lg border border-zinc-300 dark:border-zinc-700 text-zinc-700 dark:text-zinc-300 hover:bg-zinc-100 dark:hover:bg-zinc-800 transition-all duration-300 shadow-sm hover:shadow-md"
|
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 xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5 transition-transform duration-300 group-hover:-translate-x-1">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-5 w-5 transition-transform duration-300 group-hover:-translate-x-1"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="font-medium">Go Back</span>
|
<span class="font-medium">Go Back</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Random fun fact -->
|
<!-- Random fun fact -->
|
||||||
<div class="mt-16 p-6 bg-zinc-50 dark:bg-zinc-800/50 rounded-xl shadow-sm max-w-md mx-auto backdrop-blur-sm border border-zinc-100 dark:border-zinc-700/50">
|
<div
|
||||||
<h3 class="text-sm font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Did you know?</h3>
|
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"
|
||||||
<p class="mt-2 text-zinc-700 dark:text-zinc-300 text-sm" id="fun-fact">
|
>
|
||||||
The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found.
|
<h3 class="text-sm font-medium tracking-wider text-zinc-500 uppercase dark:text-zinc-400">
|
||||||
|
Did you know?
|
||||||
|
</h3>
|
||||||
|
<p class="mt-2 text-sm text-zinc-700 dark:text-zinc-300" id="fun-fact">
|
||||||
|
The 404 error code originated when CERN's web server displayed room 404 (their server
|
||||||
|
room) as the error message when a file wasn't found.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,55 +108,57 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
document.getElementById('back-button')?.addEventListener('click', () => {
|
document.getElementById('back-button')?.addEventListener('click', () => {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Array of fun 404 facts
|
// Array of fun 404 facts
|
||||||
const funFacts = [
|
const funFacts = [
|
||||||
"The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found.",
|
"The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found.",
|
||||||
"In internet slang, '404' has become shorthand for something that's missing or someone who's clueless.",
|
"In internet slang, '404' has become shorthand for something that's missing or someone who's clueless.",
|
||||||
"Some websites turn their 404 pages into games, like Google's Pac-Man 404 page that once existed.",
|
"Some websites turn their 404 pages into games, like Google's Pac-Man 404 page that once existed.",
|
||||||
"The first web server was a NeXT computer used by Tim Berners-Lee at CERN, where the 404 error was born.",
|
'The first web server was a NeXT computer used by Tim Berners-Lee at CERN, where the 404 error was born.',
|
||||||
"Many companies use creative 404 pages as a way to showcase their brand personality and humor.",
|
'Many companies use creative 404 pages as a way to showcase their brand personality and humor.',
|
||||||
"The HTTP 1.0 specification from 1996 officially defined the 404 error as 'Not Found'.",
|
"The HTTP 1.0 specification from 1996 officially defined the 404 error as 'Not Found'.",
|
||||||
"Studies show that well-designed 404 pages can reduce bounce rates by up to 30%.",
|
'Studies show that well-designed 404 pages can reduce bounce rates by up to 30%.',
|
||||||
"The most common cause of 404 errors is mistyped URLs."
|
'The most common cause of 404 errors is mistyped URLs.',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Display a random fun fact
|
// Display a random fun fact
|
||||||
const funFactElement = document.getElementById('fun-fact');
|
const funFactElement = document.getElementById('fun-fact');
|
||||||
if (funFactElement) {
|
if (funFactElement) {
|
||||||
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
|
// Handle SPA transitions for 404 page
|
||||||
function setupSPATransitions() {
|
function setupSPATransitions() {
|
||||||
// Handle all internal links for SPA transitions
|
// Handle all internal links for SPA transitions
|
||||||
document.querySelectorAll('a[href^="/"]').forEach(link => {
|
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
||||||
// Skip links that are anchor links, external links, or already processed
|
// Skip links that are anchor links, external links, or already processed
|
||||||
if (link.getAttribute('href').includes('#') ||
|
if (
|
||||||
link.getAttribute('target') === '_blank' ||
|
link.getAttribute('href').includes('#') ||
|
||||||
link.hasAttribute('data-spa-handled')) {
|
link.getAttribute('target') === '_blank' ||
|
||||||
|
link.hasAttribute('data-spa-handled')
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
// Mark as handled to avoid duplicate listeners
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
link.setAttribute('data-spa-handled', 'true');
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', (e) => {
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const targetHref = link.getAttribute('href');
|
const targetHref = link.getAttribute('href');
|
||||||
|
|
||||||
// Trigger page transition animation
|
// Trigger page transition animation
|
||||||
const pageTransition = document.getElementById('page-transition');
|
const pageTransition = document.getElementById('page-transition');
|
||||||
if (pageTransition) {
|
if (pageTransition) {
|
||||||
pageTransition.classList.remove('opacity-0');
|
pageTransition.classList.remove('opacity-0');
|
||||||
pageTransition.classList.add('opacity-100');
|
pageTransition.classList.add('opacity-100');
|
||||||
|
|
||||||
// Navigate after transition effect
|
// Navigate after transition effect
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = targetHref;
|
window.location.href = targetHref;
|
||||||
@@ -121,7 +169,7 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Re-initialize back button after SPA navigation
|
// Re-initialize back button after SPA navigation
|
||||||
const backButton = document.getElementById('back-button');
|
const backButton = document.getElementById('back-button');
|
||||||
if (backButton) {
|
if (backButton) {
|
||||||
@@ -130,13 +178,13 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on first load
|
// Initialize on first load
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
||||||
|
|
||||||
// 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', setupSPATransitions);
|
document.addEventListener('astro:page-load', setupSPATransitions);
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
// For compatibility with custom transition system
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
document.addEventListener('page-transition-complete', setupSPATransitions);
|
||||||
</script>
|
</script>
|
||||||
@@ -157,31 +205,31 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
transform: translate(0px, 0px) scale(1);
|
transform: translate(0px, 0px) scale(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-blob {
|
.animate-blob {
|
||||||
animation: blob 7s infinite;
|
animation: blob 7s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-delay-2000 {
|
.animation-delay-2000 {
|
||||||
animation-delay: 2s;
|
animation-delay: 2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-delay-4000 {
|
.animation-delay-4000 {
|
||||||
animation-delay: 4s;
|
animation-delay: 4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Glitch effect for 404 text */
|
/* Glitch effect for 404 text */
|
||||||
.glitch-wrapper {
|
.glitch-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glitch {
|
.glitch {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
animation: glitch-skew 1s infinite linear alternate-reverse;
|
animation: glitch-skew 1s infinite linear alternate-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glitch::before,
|
.glitch::before,
|
||||||
.glitch::after {
|
.glitch::after {
|
||||||
content: attr(data-text);
|
content: attr(data-text);
|
||||||
@@ -191,20 +239,22 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glitch::before {
|
.glitch::before {
|
||||||
left: 2px;
|
left: 2px;
|
||||||
text-shadow: -2px 0 #ff00c1;
|
text-shadow: -2px 0 #ff00c1;
|
||||||
clip: rect(44px, 450px, 56px, 0);
|
clip: rect(44px, 450px, 56px, 0);
|
||||||
animation: glitch-anim 5s infinite linear alternate-reverse;
|
animation: glitch-anim 5s infinite linear alternate-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
.glitch::after {
|
.glitch::after {
|
||||||
left: -2px;
|
left: -2px;
|
||||||
text-shadow: -2px 0 #00fff9, 2px 2px #ff00c1;
|
text-shadow:
|
||||||
|
-2px 0 #00fff9,
|
||||||
|
2px 2px #ff00c1;
|
||||||
animation: glitch-anim2 1s infinite linear alternate-reverse;
|
animation: glitch-anim2 1s infinite linear alternate-reverse;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes glitch-anim {
|
@keyframes glitch-anim {
|
||||||
0% {
|
0% {
|
||||||
clip: rect(31px, 9999px, 94px, 0);
|
clip: rect(31px, 9999px, 94px, 0);
|
||||||
@@ -291,7 +341,7 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
transform: skew(0.6deg);
|
transform: skew(0.6deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes glitch-anim2 {
|
@keyframes glitch-anim2 {
|
||||||
0% {
|
0% {
|
||||||
clip: rect(65px, 9999px, 65px, 0);
|
clip: rect(65px, 9999px, 65px, 0);
|
||||||
@@ -378,7 +428,7 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
transform: skew(0.74deg);
|
transform: skew(0.74deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes glitch-skew {
|
@keyframes glitch-skew {
|
||||||
0% {
|
0% {
|
||||||
transform: skew(-1deg);
|
transform: skew(-1deg);
|
||||||
@@ -414,4 +464,4 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
transform: skew(0deg);
|
transform: skew(0deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,56 +1,74 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa';
|
import DynamicIcon from '../components/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';
|
||||||
|
|
||||||
const global = await directus.request(readSingleton("global"));
|
const global = await directus.request(readSingleton('global'));
|
||||||
const about = await directus.request(readSingleton("about"));
|
const about = await directus.request(readSingleton('about'));
|
||||||
|
|
||||||
const skills = await directus.request(
|
const skills = await directus.request(
|
||||||
readItems("skills", {
|
readItems('skills', {
|
||||||
fields: ['*']
|
fields: ['*'],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="About Me" description={global.description}>
|
<BaseLayout title="About Me" description={global.description}>
|
||||||
<div class="max-w-6xl mx-auto px-4 sm:px-6 py-8 sm:py-12 md:py-16 theme-transition-all">
|
<div class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16">
|
||||||
<!-- 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 -->
|
||||||
<div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob theme-transition-bg"></div>
|
<div
|
||||||
<div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000 theme-transition-bg"></div>
|
class="animate-blob theme-transition-bg absolute -top-10 -left-10 h-36 w-36 rounded-full bg-zinc-100 opacity-30 blur-3xl sm:-top-20 sm:-left-20 sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-10 -bottom-10 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="relative grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 items-center">
|
<div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12">
|
||||||
<div class="order-2 md:order-1 text-center md:text-left">
|
<div class="order-2 text-center md:order-1 md:text-left">
|
||||||
<h1 class="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-4 sm:mb-6 theme-transition-color">
|
<h1
|
||||||
Hello, I'm <span class="text-transparent bg-clip-text bg-gradient-to-r from-zinc-500 to-zinc-900 dark:from-zinc-300 dark:to-zinc-100 theme-transition-all">{global.name}</span>
|
class="theme-transition-color mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-6 sm:text-4xl md:text-5xl dark:text-zinc-100"
|
||||||
|
>
|
||||||
|
Hello, I'm <span
|
||||||
|
class="theme-transition-all bg-gradient-to-r from-zinc-500 to-zinc-900 bg-clip-text text-transparent dark:from-zinc-300 dark:to-zinc-100"
|
||||||
|
>{global.name}</span
|
||||||
|
>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="text-lg sm:text-xl text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-8 leading-relaxed theme-transition-color">
|
<p
|
||||||
|
class="theme-transition-color mb-6 text-lg leading-relaxed text-zinc-600 sm:mb-8 sm:text-xl dark:text-zinc-400"
|
||||||
|
>
|
||||||
{about.background}
|
{about.background}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4 social-links-container justify-center md:justify-start theme-transition-children">
|
<div
|
||||||
|
class="social-links-container theme-transition-children flex flex-wrap justify-center gap-4 md:justify-start"
|
||||||
|
>
|
||||||
<!-- Social links remain the same -->
|
<!-- Social links remain the same -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="order-1 md:order-2 relative">
|
<div class="relative order-1 md:order-2">
|
||||||
<div class="aspect-square w-full max-w-[280px] sm:max-w-[320px] md:max-w-md mx-auto overflow-hidden rounded-3xl border-4 sm:border-8 border-white dark:border-zinc-800 shadow-xl sm:shadow-2xl theme-transition-all">
|
<div
|
||||||
|
class="theme-transition-all mx-auto aspect-square w-full max-w-[280px] overflow-hidden rounded-3xl border-4 border-white shadow-xl sm:max-w-[320px] sm:border-8 sm:shadow-2xl md:max-w-md dark:border-zinc-800"
|
||||||
|
>
|
||||||
<img
|
<img
|
||||||
src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.portrait}`
|
src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.portrait}`
|
||||||
alt={global.portrait_alt}
|
alt={global.portrait_alt}
|
||||||
class="w-full h-full object-cover"
|
class="h-full w-full object-cover"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Decorative elements -->
|
<!-- Decorative elements -->
|
||||||
<div class="absolute -bottom-4 sm:-bottom-6 -right-4 sm:-right-6 w-16 sm:w-20 md:w-24 h-16 sm:h-20 md:h-24 bg-zinc-100 dark:bg-zinc-800 rounded-full border-2 sm:border-4 border-white dark:border-zinc-900 shadow-lg flex items-center justify-center theme-transition-all">
|
<div
|
||||||
|
class="theme-transition-all absolute -right-4 -bottom-4 flex h-16 w-16 items-center justify-center rounded-full border-2 border-white bg-zinc-100 shadow-lg sm:-right-6 sm:-bottom-6 sm:h-20 sm:w-20 sm:border-4 md:h-24 md:w-24 dark:border-zinc-900 dark:bg-zinc-800"
|
||||||
|
>
|
||||||
<span class="text-2xl sm:text-3xl">👋</span>
|
<span class="text-2xl sm:text-3xl">👋</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -58,87 +76,125 @@ const skills = await directus.request(
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- About Section -->
|
<!-- About Section -->
|
||||||
<div class="mb-16 sm:mb-20 md:mb-24 theme-transition-all">
|
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
|
||||||
<div class="max-w-3xl mx-auto">
|
<div class="mx-auto max-w-3xl">
|
||||||
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-6 sm:mb-8 flex items-center justify-center md:justify-start theme-transition-color">
|
<h2
|
||||||
<span class="hidden sm:inline-block w-8 sm:w-12 h-1 bg-zinc-300 dark:bg-zinc-700 mr-4 theme-transition-bg"></span>
|
class="theme-transition-color mb-6 flex items-center justify-center text-2xl font-bold text-zinc-900 sm:mb-8 sm:text-3xl md:justify-start dark:text-zinc-100"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="theme-transition-bg mr-4 hidden h-1 w-8 bg-zinc-300 sm:inline-block sm:w-12 dark:bg-zinc-700"
|
||||||
|
></span>
|
||||||
About Me
|
About Me
|
||||||
<span class="hidden sm:inline-block w-8 sm:w-12 h-1 bg-zinc-300 dark:bg-zinc-700 ml-4 theme-transition-bg"></span>
|
<span
|
||||||
|
class="theme-transition-bg ml-4 hidden h-1 w-8 bg-zinc-300 sm:inline-block sm:w-12 dark:bg-zinc-700"
|
||||||
|
></span>
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
<div class="prose prose-zinc dark:prose-invert max-w-none theme-transition-all">
|
<div class="theme-transition-all prose prose-zinc dark:prose-invert max-w-none">
|
||||||
<p class="text-base sm:text-lg leading-relaxed mb-4 sm:mb-6 theme-transition-color">
|
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
||||||
{about.experience}
|
{about.experience}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="text-base sm:text-lg leading-relaxed mb-4 sm:mb-6 theme-transition-color">
|
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
||||||
{about.education}
|
{about.education}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p class="text-base sm:text-lg leading-relaxed mb-4 sm:mb-6 theme-transition-color">
|
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
||||||
{about.certifications}
|
{about.certifications}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Skills Section -->
|
<!-- Skills Section - Improved for mobile -->
|
||||||
<div class="mb-16 sm:mb-20 md:mb-24 theme-transition-all">
|
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
|
||||||
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-8 sm:mb-12 text-center theme-transition-color">Tech Stack</h2>
|
<h2
|
||||||
|
class="theme-transition-color mb-8 text-center text-2xl font-bold text-zinc-900 sm:mb-12 sm:text-3xl dark:text-zinc-100"
|
||||||
|
>
|
||||||
|
Tech Stack
|
||||||
|
</h2>
|
||||||
|
|
||||||
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
|
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
|
||||||
<!-- Main slider container -->
|
<!-- Main slider container -->
|
||||||
<div class="slider-track flex animate-slide">
|
<div class="slider-track animate-slide flex">
|
||||||
{ skills.map((skill, index) => (
|
{
|
||||||
<div key={`${skill.title}-${index}`} class="skill-card min-w-[220px] sm:min-w-[280px] mx-2 sm:mx-4 bg-white dark:bg-zinc-800/50 rounded-xl border border-zinc-200 dark:border-zinc-700 hover:border-zinc-300 dark:hover:border-zinc-600 transition-all duration-300 hover:shadow-xl transform hover:-translate-y-2 hover:scale-105 theme-transition-element">
|
[...skills, ...skills, ...skills].map((skill) => (
|
||||||
<div class="p-4 sm:p-6">
|
<div 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">
|
||||||
<div class="flex items-center justify-between mb-4 sm:mb-6">
|
<div class="p-4 sm:p-6">
|
||||||
<div class="flex items-center gap-2 sm:gap-4">
|
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
||||||
<div class="w-8 h-8 sm:w-12 sm:h-12 flex items-center justify-center bg-zinc-100 dark:bg-zinc-800 rounded-lg text-zinc-800 dark:text-zinc-200 transform transition-transform group-hover:rotate-12 theme-transition-bg theme-transition-color">
|
<div class="flex items-center gap-2 sm:gap-4">
|
||||||
<skill.icon size={20} className="sm:text-2xl transform transition-all hover:scale-125" />
|
<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">
|
||||||
|
<DynamicIcon name={skill.icon} />
|
||||||
|
</div>
|
||||||
|
<h3 class="theme-transition-color text-base font-semibold text-zinc-900 sm:text-xl dark:text-zinc-100">
|
||||||
|
{skill.title}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<h3 class="text-base sm:text-xl font-semibold text-zinc-900 dark:text-zinc-100 theme-transition-color">{skill.title}</h3>
|
<span class="theme-transition-all rounded-full bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-600 sm:px-2.5 sm:py-1 sm:text-sm dark:bg-zinc-800 dark:text-zinc-400">
|
||||||
|
{skill.level}%
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="text-xs sm:text-sm font-mono bg-zinc-100 dark:bg-zinc-800 text-zinc-600 dark:text-zinc-400 px-2 py-0.5 sm:px-2.5 sm:py-1 rounded-full theme-transition-all">{skill.level}%</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative h-1.5 sm:h-2 w-full bg-zinc-100 dark:bg-zinc-700 overflow-hidden rounded-full theme-transition-bg">
|
<div class="theme-transition-bg relative h-1.5 w-full overflow-hidden rounded-full bg-zinc-100 sm:h-2 dark:bg-zinc-700">
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 left-0 h-full bg-gradient-to-r from-zinc-700 via-zinc-600 to-zinc-800 dark:from-zinc-300 dark:via-zinc-400 dark:to-zinc-200 rounded-full transition-all duration-1000 progress-bar-animate theme-transition-bg"
|
class="progress-bar-animate theme-transition-bg absolute top-0 left-0 h-full rounded-full bg-gradient-to-r from-zinc-700 via-zinc-600 to-zinc-800 transition-all duration-1000 dark:from-zinc-300 dark:via-zinc-400 dark:to-zinc-200"
|
||||||
style={`width: ${skill.level}%`}
|
style={`width: ${skill.level}%`}
|
||||||
></div>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between mt-1 sm:mt-2 text-[10px] sm:text-xs text-zinc-400 dark:text-zinc-500 font-mono theme-transition-color">
|
<div class="theme-transition-color mt-1 flex justify-between font-mono text-[10px] text-zinc-400 sm:mt-2 sm:text-xs dark:text-zinc-500">
|
||||||
<span>Beginner</span>
|
<span>Beginner</span>
|
||||||
<span>Advanced</span>
|
<span>Advanced</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
))
|
||||||
))}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Gradient overlays for smooth fade effect -->
|
<!-- Gradient overlays for smooth fade effect -->
|
||||||
<div class="absolute top-0 bottom-0 left-0 w-12 sm:w-24 bg-gradient-to-r from-white dark:from-zinc-900 to-transparent z-10 theme-transition-bg"></div>
|
<div
|
||||||
<div class="absolute top-0 bottom-0 right-0 w-12 sm:w-24 bg-gradient-to-l from-white dark:from-zinc-900 to-transparent z-10 theme-transition-bg"></div>
|
class="theme-transition-bg absolute top-0 bottom-0 left-0 z-10 w-12 bg-gradient-to-r from-white to-transparent sm:w-24 dark:from-zinc-900"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="theme-transition-bg absolute top-0 right-0 bottom-0 z-10 w-12 bg-gradient-to-l from-white to-transparent sm:w-24 dark:from-zinc-900"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact Section -->
|
<!-- Contact Section -->
|
||||||
<div class="max-w-3xl mx-auto text-center theme-transition-all">
|
<div class="theme-transition-all mx-auto max-w-3xl text-center">
|
||||||
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-4 sm:mb-6 theme-transition-color">Get in Touch</h2>
|
<h2
|
||||||
<p class="text-base sm:text-lg text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-8 theme-transition-color">
|
class="theme-transition-color mb-4 text-2xl font-bold text-zinc-900 sm:mb-6 sm:text-3xl dark:text-zinc-100"
|
||||||
I'm always open to new opportunities and collaborations. If you'd like to work together or just say hello,
|
>
|
||||||
feel free to reach out.
|
Get in Touch
|
||||||
|
</h2>
|
||||||
|
<p
|
||||||
|
class="theme-transition-color mb-6 text-base text-zinc-600 sm:mb-8 sm:text-lg dark:text-zinc-400"
|
||||||
|
>
|
||||||
|
I'm always open to new opportunities and collaborations. If you'd like to work together or
|
||||||
|
just say hello, feel free to reach out.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
href=`mailto:${global.email}`
|
href=`mailto:${global.email}`
|
||||||
class="inline-flex items-center justify-center px-6 sm:px-8 py-3 sm:py-4 rounded-lg bg-zinc-900 dark:bg-zinc-100 text-zinc-100 dark:text-zinc-900 hover:bg-zinc-700 dark:hover:bg-zinc-300 transition-colors text-base sm:text-lg font-medium theme-transition-all"
|
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 xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 sm:h-5 sm:w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
Say Hello
|
Say Hello
|
||||||
</a>
|
</a>
|
||||||
@@ -157,7 +213,8 @@ const skills = await directus.request(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blob-bounce {
|
@keyframes blob-bounce {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translate(0, 0) scale(1);
|
transform: translate(0, 0) scale(1);
|
||||||
}
|
}
|
||||||
25% {
|
25% {
|
||||||
@@ -218,7 +275,9 @@ const skills = await directus.request(
|
|||||||
/* Reduce animation complexity on mobile for better performance */
|
/* Reduce animation complexity on mobile for better performance */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.skill-card {
|
.skill-card {
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
transition:
|
||||||
|
transform 0.3s ease,
|
||||||
|
box-shadow 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.skill-card:hover {
|
.skill-card:hover {
|
||||||
@@ -234,7 +293,11 @@ const skills = await directus.request(
|
|||||||
left: -10%;
|
left: -10%;
|
||||||
width: 120%;
|
width: 120%;
|
||||||
height: 120%;
|
height: 120%;
|
||||||
background: radial-gradient(circle at center, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0) 70%);
|
background: radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgba(255, 255, 255, 0.1) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 70%
|
||||||
|
);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.5s ease;
|
transition: opacity 0.5s ease;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
@@ -256,7 +319,7 @@ const skills = await directus.request(
|
|||||||
left: -100%;
|
left: -100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
animation: progress-shine 2s infinite;
|
animation: progress-shine 2s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,7 +334,8 @@ const skills = await directus.request(
|
|||||||
|
|
||||||
/* Improved touch targets for mobile */
|
/* Improved touch targets for mobile */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
a, button {
|
a,
|
||||||
|
button {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -290,11 +354,12 @@ const skills = await directus.request(
|
|||||||
|
|
||||||
/* Smooth card transition during theme switch */
|
/* Smooth card transition during theme switch */
|
||||||
.skill-card.theme-transition-element {
|
.skill-card.theme-transition-element {
|
||||||
transition: background-color var(--theme-transition),
|
transition:
|
||||||
border-color var(--theme-transition),
|
background-color var(--theme-transition),
|
||||||
color var(--theme-transition),
|
border-color var(--theme-transition),
|
||||||
box-shadow var(--theme-transition),
|
color var(--theme-transition),
|
||||||
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
box-shadow var(--theme-transition),
|
||||||
|
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
@@ -350,7 +415,7 @@ const skills = await directus.request(
|
|||||||
const cards = document.querySelectorAll('.skill-card');
|
const cards = document.querySelectorAll('.skill-card');
|
||||||
|
|
||||||
if (!isTouchDevice) {
|
if (!isTouchDevice) {
|
||||||
cards.forEach(card => {
|
cards.forEach((card) => {
|
||||||
card.addEventListener('mousemove', (e) => {
|
card.addEventListener('mousemove', (e) => {
|
||||||
const rect = card.getBoundingClientRect();
|
const rect = card.getBoundingClientRect();
|
||||||
const x = e.clientX - rect.left;
|
const x = e.clientX - rect.left;
|
||||||
@@ -380,7 +445,7 @@ const skills = await directus.request(
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// Simpler effects for touch devices
|
// Simpler effects for touch devices
|
||||||
cards.forEach(card => {
|
cards.forEach((card) => {
|
||||||
card.addEventListener('touchstart', () => {
|
card.addEventListener('touchstart', () => {
|
||||||
card.classList.add('is-touched');
|
card.classList.add('is-touched');
|
||||||
});
|
});
|
||||||
@@ -413,11 +478,13 @@ const skills = await directus.request(
|
|||||||
// Handle SPA transitions for about page
|
// Handle SPA transitions for about page
|
||||||
function setupSPATransitions() {
|
function setupSPATransitions() {
|
||||||
// Handle all internal links for SPA transitions
|
// Handle all internal links for SPA transitions
|
||||||
document.querySelectorAll('a[href^="/"]').forEach(link => {
|
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
||||||
// Skip links that are anchor links, external links, or already processed
|
// Skip links that are anchor links, external links, or already processed
|
||||||
if (link.getAttribute('href').includes('#') ||
|
if (
|
||||||
link.getAttribute('target') === '_blank' ||
|
link.getAttribute('href').includes('#') ||
|
||||||
link.hasAttribute('data-spa-handled')) {
|
link.getAttribute('target') === '_blank' ||
|
||||||
|
link.hasAttribute('data-spa-handled')
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,9 +522,12 @@ const skills = await directus.request(
|
|||||||
// Animate hero section elements
|
// Animate hero section elements
|
||||||
const heroElements = document.querySelectorAll('h1, .order-2 p, .social-links-container');
|
const heroElements = document.querySelectorAll('h1, .order-2 p, .social-links-container');
|
||||||
heroElements.forEach((el, index) => {
|
heroElements.forEach((el, index) => {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
el.classList.add('animate-reveal');
|
() => {
|
||||||
}, 100 + (index * 150));
|
el.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
100 + index * 150
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate profile image
|
// Animate profile image
|
||||||
@@ -471,17 +541,23 @@ const skills = await directus.request(
|
|||||||
// Animate skill bars with staggered delay
|
// Animate skill bars with staggered delay
|
||||||
const skillBars = document.querySelectorAll('.skill-bar');
|
const skillBars = document.querySelectorAll('.skill-bar');
|
||||||
skillBars.forEach((bar, index) => {
|
skillBars.forEach((bar, index) => {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
bar.classList.add('animate-skill');
|
() => {
|
||||||
}, 500 + (index * 100));
|
bar.classList.add('animate-skill');
|
||||||
|
},
|
||||||
|
500 + index * 100
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate sections with staggered delay
|
// Animate sections with staggered delay
|
||||||
const sections = document.querySelectorAll('section');
|
const sections = document.querySelectorAll('section');
|
||||||
sections.forEach((section, index) => {
|
sections.forEach((section, index) => {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
section.classList.add('animate-reveal');
|
() => {
|
||||||
}, 300 + (index * 200));
|
section.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
300 + index * 200
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,97 +1,144 @@
|
|||||||
---
|
---
|
||||||
import BlogPost from '../../layouts/BlogPost.astro';
|
import BlogPost from '../../layouts/BlogPost.astro';
|
||||||
|
|
||||||
import directus from "../../../lib/directus"
|
import directus from '../../../lib/directus';
|
||||||
import { readItems } from "@directus/sdk";
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await directus.request(readItems("posts", {
|
const posts = await directus.request(
|
||||||
fields: ['*'],
|
readItems('posts', {
|
||||||
}));
|
fields: ['*'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
const sortedEntries = [...posts].sort(
|
const sortedEntries = [...posts].sort(
|
||||||
(a, b) => b.published_date.valueOf() - a.published_date.valueOf()
|
(a, b) => b.published_date.valueOf() - a.published_date.valueOf()
|
||||||
);
|
);
|
||||||
|
|
||||||
return sortedEntries.map((post, index) => {
|
return sortedEntries.map((post, index) => {
|
||||||
return {
|
return {
|
||||||
params: { slug: post.slug },
|
params: { slug: post.slug },
|
||||||
props: {
|
props: {
|
||||||
post,
|
post,
|
||||||
nextPost: index > 0 ? sortedEntries[index - 1] : null,
|
nextPost: index > 0 ? sortedEntries[index - 1] : null,
|
||||||
prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null
|
prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const { post, nextPost, prevPost } = Astro.props;
|
const { post, nextPost, prevPost } = Astro.props;
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BlogPost slug={post.slug} title={post.title} description={post.description} content={post.content} image={post.image} image_alt={post.image_alt} published_date={post.published_date} updated_date={post.updated_date} tags={post.tags}>
|
<BlogPost
|
||||||
<!-- Main Content - Enhanced with better typography and spacing -->
|
slug={post.slug}
|
||||||
<div class="prose prose-zinc dark:prose-invert max-w-none prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:text-zinc-800 dark:prose-a:text-zinc-300 prose-a:font-medium prose-a:underline-offset-4 hover:prose-a:text-zinc-600 dark:hover:prose-a:text-zinc-100 prose-img:rounded-xl sm:prose-base prose-sm">
|
title={post.title}
|
||||||
<div set:html={post.content} />
|
description={post.description}
|
||||||
</div>
|
content={post.content}
|
||||||
|
image={post.image}
|
||||||
|
image_alt={post.image_alt}
|
||||||
|
published_date={post.published_date}
|
||||||
|
updated_date={post.updated_date}
|
||||||
|
tags={post.tags}
|
||||||
|
>
|
||||||
|
<!-- Main Content - Enhanced with better typography and spacing -->
|
||||||
|
<div
|
||||||
|
class="prose prose-sm prose-zinc dark:prose-invert sm:prose-base prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:font-medium prose-a:text-zinc-800 prose-a:underline-offset-4 hover:prose-a:text-zinc-600 prose-img:rounded-xl dark:prose-a:text-zinc-300 dark:hover:prose-a:text-zinc-100 max-w-none"
|
||||||
|
>
|
||||||
|
<div set:html={post.content} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Next/Previous Navigation - Improved responsive design -->
|
<!-- Next/Previous Navigation - Improved responsive design -->
|
||||||
<div class="mt-12 sm:mt-16 grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6 border-t border-zinc-200 dark:border-zinc-800 pt-8 sm:pt-12">
|
<div
|
||||||
{prevPost && (
|
class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2 dark:border-zinc-800"
|
||||||
<a
|
>
|
||||||
href={`/blog/${prevPost.slug}`}
|
{
|
||||||
class="group relative flex flex-col h-full p-4 sm:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all duration-300 hover:-translate-y-1 overflow-hidden"
|
prevPost && (
|
||||||
|
<a
|
||||||
|
href={`/blog/${prevPost.slug}`}
|
||||||
|
class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 sm:p-6 dark:border-zinc-800 dark:hover:bg-zinc-800/50"
|
||||||
>
|
>
|
||||||
<div class="absolute inset-0 bg-gradient-to-r from-zinc-100 to-transparent dark:from-zinc-800 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
<div class="absolute inset-0 bg-gradient-to-r from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
|
||||||
<span class="relative z-10 text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-1 sm:gap-2 mb-1 sm:mb-2">
|
<span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 sm:mb-2 sm:gap-2 sm:text-sm dark:text-zinc-400">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-300 group-hover:-translate-x-1">
|
<svg
|
||||||
<path d="m15 18-6-6 6-6"></path>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="h-3 w-3 transition-transform duration-300 group-hover:-translate-x-1 sm:h-4 sm:w-4"
|
||||||
|
>
|
||||||
|
<path d="m15 18-6-6 6-6" />
|
||||||
</svg>
|
</svg>
|
||||||
Previous Article
|
Previous Article
|
||||||
</span>
|
</span>
|
||||||
<h3 class="text-base sm:text-lg font-medium text-zinc-900 dark:text-white line-clamp-2 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors">
|
<h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-lg dark:text-white dark:group-hover:text-zinc-300">
|
||||||
{prevPost.title}
|
{prevPost.title}
|
||||||
</h3>
|
</h3>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)
|
||||||
{nextPost && (
|
}
|
||||||
<a
|
{
|
||||||
href={`/blog/${nextPost.slug}`}
|
nextPost && (
|
||||||
class="group relative flex flex-col h-full p-4 sm:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/50 transition-all duration-300 hover:-translate-y-1 md:text-right overflow-hidden"
|
<a
|
||||||
|
href={`/blog/${nextPost.slug}`}
|
||||||
|
class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 sm:p-6 md:text-right dark:border-zinc-800 dark:hover:bg-zinc-800/50"
|
||||||
>
|
>
|
||||||
<div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent dark:from-zinc-800 dark:to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
|
<div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
|
||||||
<span class="relative z-10 text-xs sm:text-sm font-medium text-zinc-500 dark:text-zinc-400 flex items-center gap-1 sm:gap-2 mb-1 sm:mb-2 md:justify-end">
|
<span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 sm:mb-2 sm:gap-2 sm:text-sm md:justify-end dark:text-zinc-400">
|
||||||
Next Article
|
Next Article
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-3 h-3 sm:w-4 sm:h-4 transition-transform duration-300 group-hover:translate-x-1">
|
<svg
|
||||||
<path d="m9 18 6-6-6-6"></path>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="h-3 w-3 transition-transform duration-300 group-hover:translate-x-1 sm:h-4 sm:w-4"
|
||||||
|
>
|
||||||
|
<path d="m9 18 6-6-6-6" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<h3 class="text-base sm:text-lg font-medium text-zinc-900 dark:text-white line-clamp-2 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors">
|
<h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-lg dark:text-white dark:group-hover:text-zinc-300">
|
||||||
{nextPost.title}
|
{nextPost.title}
|
||||||
</h3>
|
</h3>
|
||||||
</a>
|
</a>
|
||||||
)}
|
)
|
||||||
</div>
|
}
|
||||||
|
</div>
|
||||||
</BlogPost>
|
</BlogPost>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Removing TOC-related functions
|
// 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');
|
||||||
|
|
||||||
codeBlocks.forEach(block => {
|
codeBlocks.forEach((block) => {
|
||||||
// Skip if already processed by either method
|
// Skip if already processed by either method
|
||||||
if (block.classList.contains('code-block-processed') || block.classList.contains('enhanced')) {
|
if (
|
||||||
|
block.classList.contains('code-block-processed') ||
|
||||||
|
block.classList.contains('enhanced')
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
block.classList.add('code-block-processed');
|
block.classList.add('code-block-processed');
|
||||||
|
|
||||||
// Create wrapper if not already wrapped
|
// Create wrapper if not already wrapped
|
||||||
let wrapper;
|
let wrapper;
|
||||||
if (block.parentNode.classList.contains('relative') && block.parentNode.classList.contains('group')) {
|
if (
|
||||||
|
block.parentNode.classList.contains('relative') &&
|
||||||
|
block.parentNode.classList.contains('group')
|
||||||
|
) {
|
||||||
wrapper = block.parentNode;
|
wrapper = block.parentNode;
|
||||||
} else {
|
} else {
|
||||||
wrapper = document.createElement('div');
|
wrapper = document.createElement('div');
|
||||||
@@ -99,28 +146,29 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
block.parentNode.insertBefore(wrapper, block);
|
block.parentNode.insertBefore(wrapper, block);
|
||||||
wrapper.appendChild(block);
|
wrapper.appendChild(block);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add copy button if not already present
|
// Add copy button if not already present
|
||||||
if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-button')) {
|
if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-button')) {
|
||||||
const copyButton = document.createElement('button');
|
const copyButton = document.createElement('button');
|
||||||
copyButton.className = 'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
|
copyButton.className =
|
||||||
|
'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
|
||||||
copyButton.innerHTML = `
|
copyButton.innerHTML = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
copyButton.addEventListener('click', () => {
|
copyButton.addEventListener('click', () => {
|
||||||
const code = block.querySelector('code').innerText;
|
const code = block.querySelector('code').innerText;
|
||||||
navigator.clipboard.writeText(code);
|
navigator.clipboard.writeText(code);
|
||||||
|
|
||||||
// Show copied feedback
|
// Show copied feedback
|
||||||
copyButton.innerHTML = `
|
copyButton.innerHTML = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
copyButton.innerHTML = `
|
copyButton.innerHTML = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||||
@@ -129,39 +177,39 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
`;
|
`;
|
||||||
}, 2000);
|
}, 2000);
|
||||||
});
|
});
|
||||||
|
|
||||||
wrapper.appendChild(copyButton);
|
wrapper.appendChild(copyButton);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle SPA transitions for blog post navigation
|
// Handle SPA transitions for blog post navigation
|
||||||
function setupSPATransitions() {
|
function setupSPATransitions() {
|
||||||
// Handle prev/next navigation links
|
// Handle prev/next navigation links
|
||||||
document.querySelectorAll('a[href^="/blog/"]').forEach(link => {
|
document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
|
||||||
// Skip links that are anchor links or already processed
|
// Skip links that are anchor links or already processed
|
||||||
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
|
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
// Mark as handled to avoid duplicate listeners
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
link.setAttribute('data-spa-handled', 'true');
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', (e) => {
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const targetHref = link.getAttribute('href');
|
const targetHref = link.getAttribute('href');
|
||||||
|
|
||||||
// Trigger page transition animation
|
// Trigger page transition animation
|
||||||
const pageTransition = document.getElementById('page-transition');
|
const pageTransition = document.getElementById('page-transition');
|
||||||
if (pageTransition) {
|
if (pageTransition) {
|
||||||
pageTransition.classList.remove('opacity-0');
|
pageTransition.classList.remove('opacity-0');
|
||||||
pageTransition.classList.add('opacity-100');
|
pageTransition.classList.add('opacity-100');
|
||||||
|
|
||||||
// Navigate after transition effect
|
// Navigate after transition effect
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = targetHref;
|
window.location.href = targetHref;
|
||||||
@@ -173,13 +221,13 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main initialization function
|
// Main initialization function
|
||||||
function initializeBlogPost() {
|
function initializeBlogPost() {
|
||||||
// Initialize remaining components
|
// Initialize remaining components
|
||||||
initializeCodeCopyButtons();
|
initializeCodeCopyButtons();
|
||||||
setupSPATransitions();
|
setupSPATransitions();
|
||||||
|
|
||||||
// Scroll to hash if present in URL
|
// Scroll to hash if present in URL
|
||||||
if (window.location.hash) {
|
if (window.location.hash) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -190,100 +238,105 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on first load
|
// Initialize on first load
|
||||||
document.addEventListener('DOMContentLoaded', initializeBlogPost);
|
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
|
// For compatibility with custom transition system
|
||||||
document.addEventListener('page-transition-complete', initializeBlogPost);
|
document.addEventListener('page-transition-complete', initializeBlogPost);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Removing TOC-related styles */
|
/* Removing TOC-related styles */
|
||||||
|
|
||||||
/* Language badge styling */
|
/* Language badge styling */
|
||||||
.language-badge {
|
.language-badge {
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
font-family:
|
||||||
|
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
||||||
|
monospace;
|
||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extra small screens */
|
/* Extra small screens */
|
||||||
@media (min-width: 480px) {
|
@media (min-width: 480px) {
|
||||||
.xs\:inline {
|
.xs\:inline {
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xs\:hidden {
|
.xs\:hidden {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Enhanced typography for blog content - Responsive adjustments */
|
/* Enhanced typography for blog content - Responsive adjustments */
|
||||||
.prose {
|
.prose {
|
||||||
@apply text-zinc-800 dark:text-zinc-200;
|
@reference text-zinc-800 dark:text-zinc-200;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose h1, .prose h2, .prose h3, .prose h4 {
|
.prose h1,
|
||||||
@apply text-zinc-900 dark:text-zinc-100 font-semibold;
|
.prose h2,
|
||||||
|
.prose h3,
|
||||||
|
.prose h4 {
|
||||||
|
@reference font-semibold text-zinc-900 dark:text-zinc-100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose h1 {
|
.prose h1 {
|
||||||
@apply text-2xl sm:text-3xl md:text-4xl;
|
@reference text-2xl sm:text-3xl md:text-4xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose h2 {
|
.prose h2 {
|
||||||
@apply text-xl sm:text-2xl mt-8 sm:mt-12 mb-3 sm:mb-4 pb-2 border-b border-zinc-200 dark:border-zinc-800;
|
@reference mb-3 mt-8 border-b border-zinc-200 pb-2 text-xl dark:border-zinc-800 sm:mb-4 sm:mt-12 sm:text-2xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose h3 {
|
.prose h3 {
|
||||||
@apply text-lg sm:text-xl mt-6 sm:mt-8 mb-2 sm:mb-3;
|
@reference mb-2 mt-6 text-lg sm:mb-3 sm:mt-8 sm:text-xl;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose p {
|
.prose p {
|
||||||
@apply leading-relaxed mb-4 sm:mb-6 text-sm sm:text-base;
|
@reference mb-4 text-sm leading-relaxed sm:mb-6 sm:text-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose a {
|
.prose a {
|
||||||
@apply text-zinc-800 dark:text-zinc-300 font-medium underline decoration-zinc-400 dark:decoration-zinc-600 underline-offset-2 hover:text-zinc-600 dark:hover:text-zinc-100 hover:decoration-zinc-600 dark:hover:decoration-zinc-400 transition-colors;
|
@reference font-medium text-zinc-800 underline decoration-zinc-400 underline-offset-2 transition-colors hover:text-zinc-600 hover:decoration-zinc-600 dark:text-zinc-300 dark:decoration-zinc-600 dark:hover:text-zinc-100 dark:hover:decoration-zinc-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose blockquote {
|
.prose blockquote {
|
||||||
@apply border-l-4 border-zinc-300 dark:border-zinc-700 pl-4 italic text-zinc-700 dark:text-zinc-300 my-4 sm:my-6;
|
@reference my-4 border-l-4 border-zinc-300 pl-4 italic text-zinc-700 dark:border-zinc-700 dark:text-zinc-300 sm:my-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose code {
|
.prose code {
|
||||||
@apply bg-zinc-100 dark:bg-zinc-800 px-1.5 py-0.5 rounded text-zinc-800 dark:text-zinc-200 text-sm font-medium;
|
@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 {
|
||||||
@apply bg-[#1e293b] dark:bg-[#1e293b] text-zinc-200 p-3 sm:p-4 rounded-lg overflow-x-auto text-xs sm:text-sm my-4 sm:my-6 shadow-md !important;
|
@reference my-4 overflow-x-auto rounded-lg bg-[#1e293b] p-3 text-xs text-zinc-200 shadow-md dark:bg-[#1e293b] sm:my-6 sm:p-4 sm:text-sm !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose pre code {
|
.prose pre code {
|
||||||
@apply bg-transparent p-0 text-zinc-200 dark:text-zinc-200 !important;
|
@reference bg-transparent p-0 text-zinc-200 dark:text-zinc-200 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.prose img {
|
.prose img {
|
||||||
@apply rounded-lg shadow-md my-6 sm:my-8 mx-auto max-w-full h-auto;
|
@reference mx-auto my-6 h-auto max-w-full rounded-lg shadow-md sm:my-8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose ul, .prose ol {
|
.prose ul,
|
||||||
@apply my-4 sm:my-6 pl-5 sm:pl-6;
|
.prose ol {
|
||||||
|
@reference my-4 pl-5 sm:my-6 sm:pl-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose li {
|
.prose li {
|
||||||
@apply mb-1 sm:mb-2 text-sm sm:text-base;
|
@reference mb-1 text-sm sm:mb-2 sm:text-base;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prose hr {
|
.prose hr {
|
||||||
@apply my-8 sm:my-10 border-zinc-200 dark:border-zinc-800;
|
@reference my-8 border-zinc-200 dark:border-zinc-800 sm:my-10;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Line clamp for truncating text */
|
/* Line clamp for truncating text */
|
||||||
.line-clamp-2 {
|
.line-clamp-2 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
@@ -291,4 +344,4 @@ const { post, nextPost, prevPost } = Astro.props;
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,19 +1,17 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
|
||||||
import directus from "../../../lib/directus"
|
import directus from '../../../lib/directus';
|
||||||
import { readItems } from "@directus/sdk";
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
const posts = await directus.request(
|
const posts = await directus.request(
|
||||||
readItems("posts", {
|
readItems('posts', {
|
||||||
fields: ['*'],
|
fields: ['*'],
|
||||||
sort: ["-published_date"],
|
sort: ['-published_date'],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const sortedPosts = posts.sort(
|
const sortedPosts = posts.sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf());
|
||||||
(a, b) => b.published_date.valueOf() - a.published_date.valueOf()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Group posts by year for timeline effect
|
// Group posts by year for timeline effect
|
||||||
const postsByYear = sortedPosts.reduce((acc, post) => {
|
const postsByYear = sortedPosts.reduce((acc, post) => {
|
||||||
@@ -29,176 +27,206 @@ const years = Object.keys(postsByYear).sort((a, b) => b - a);
|
|||||||
const totalPosts = sortedPosts.length;
|
const totalPosts = sortedPosts.length;
|
||||||
|
|
||||||
// Get unique tags for search suggestions
|
// Get unique tags for search suggestions
|
||||||
const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Blog">
|
<BaseLayout title="Blog">
|
||||||
<div class="w-full max-w-6xl mx-auto px-4 sm:px-6 py-10 sm:py-16">
|
<div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16">
|
||||||
<!-- Header with search -->
|
<!-- Header with search -->
|
||||||
<div class="relative mb-12 sm:mb-20">
|
<div class="relative mb-12 sm:mb-20">
|
||||||
<!-- Decorative elements -->
|
<!-- Decorative elements -->
|
||||||
<div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-48 sm:w-72 h-48 sm:h-72 bg-zinc-100 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob"></div>
|
<div
|
||||||
<div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-48 sm:w-72 h-48 sm:h-72 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000"></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"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="animate-blob animation-delay-2000 absolute -right-10 -bottom-10 h-48 w-48 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-72 sm:w-72 dark:bg-zinc-800/30"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="relative text-center">
|
<div class="relative text-center">
|
||||||
<h1 class="text-3xl sm:text-4xl md:text-5xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-4">
|
<h1
|
||||||
|
class="mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl dark:text-zinc-100"
|
||||||
|
>
|
||||||
Blog
|
Blog
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-6 sm:mb-10 max-w-2xl mx-auto">
|
<p
|
||||||
|
class="mx-auto mb-6 max-w-2xl text-sm text-zinc-600 sm:mb-10 sm:text-base dark:text-zinc-400"
|
||||||
|
>
|
||||||
Thoughts, ideas, and explorations on technology and selfhosting.
|
Thoughts, ideas, and explorations on technology and selfhosting.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Grid layout for mobile experience -->
|
<!-- Grid layout for mobile experience -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-12 gap-6 sm:gap-8">
|
<div class="grid grid-cols-1 gap-6 sm:gap-8 md:grid-cols-12">
|
||||||
<!-- Featured post (if exists) -->
|
<!-- Featured post (if exists) -->
|
||||||
{sortedPosts.length > 0 && (
|
{
|
||||||
<div class="md:col-span-12 mb-8 sm:mb-12">
|
sortedPosts.length > 0 && (
|
||||||
<article class="group relative overflow-hidden rounded-none border-b border-zinc-200 dark:border-zinc-800 pb-6 sm:pb-8">
|
<div class="mb-8 sm:mb-12 md:col-span-12">
|
||||||
<div class="flex flex-col md:flex-row h-full gap-6 sm:gap-8">
|
<article class="group relative overflow-hidden rounded-none border-b border-zinc-200 pb-6 sm:pb-8 dark:border-zinc-800">
|
||||||
{sortedPosts[0].image && (
|
<div class="flex h-full flex-col gap-6 sm:gap-8 md:flex-row">
|
||||||
<div class="w-full md:w-1/2 h-60 sm:h-80 md:h-96 overflow-hidden mx-auto md:mx-0 max-w-full sm:max-w-md">
|
{sortedPosts[0].image && (
|
||||||
<img
|
<div class="mx-auto h-60 w-full max-w-full overflow-hidden sm:h-80 sm:max-w-md md:mx-0 md:h-96 md:w-1/2">
|
||||||
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${sortedPosts[0].image}`}
|
<img
|
||||||
alt={sortedPosts[0].title}
|
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${sortedPosts[0].image}`}
|
||||||
class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105"
|
alt={sortedPosts[0].title}
|
||||||
loading="eager"
|
class="h-full w-full object-cover grayscale transition-all duration-700 group-hover:scale-105 hover:grayscale-0"
|
||||||
/>
|
loading="eager"
|
||||||
</div>
|
/>
|
||||||
)}
|
</div>
|
||||||
|
)}
|
||||||
<div class="flex-1 flex flex-col justify-center">
|
|
||||||
<div class="flex items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-2 mb-3 justify-center md:justify-start">
|
<div class="flex flex-1 flex-col justify-center">
|
||||||
<span class="font-medium uppercase tracking-wider">Featured</span>
|
<div class="mb-3 flex items-center justify-center gap-2 text-xs text-zinc-500 sm:text-sm md:justify-start dark:text-zinc-400">
|
||||||
<span class="h-px w-6 sm:w-8 bg-zinc-300 dark:bg-zinc-700"></span>
|
<span class="font-medium tracking-wider uppercase">Featured</span>
|
||||||
{sortedPosts[0].published_date && (
|
<span class="h-px w-6 bg-zinc-300 sm:w-8 dark:bg-zinc-700" />
|
||||||
<time datetime={sortedPosts[0].published_date.toLocaleString()}>
|
{sortedPosts[0].published_date && (
|
||||||
{sortedPosts[0].published_date.toLocaleString('en-US', {
|
<time datetime={sortedPosts[0].published_date.toLocaleString()}>
|
||||||
year: 'numeric',
|
{sortedPosts[0].published_date.toLocaleString('en-US', {
|
||||||
month: 'long',
|
year: 'numeric',
|
||||||
day: 'numeric'
|
month: 'long',
|
||||||
})}
|
day: 'numeric',
|
||||||
</time>
|
})}
|
||||||
)}
|
</time>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
<h2 class="text-2xl sm:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-3 sm:mb-4 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center md:text-left">
|
|
||||||
<a href={`/blog/${sortedPosts[0].slug}/`} class="before:absolute before:inset-0">
|
<h2 class="mb-3 text-center text-2xl font-bold text-zinc-900 transition-colors group-hover:text-zinc-700 sm:mb-4 sm:text-3xl md:text-left dark:text-zinc-100 dark:group-hover:text-zinc-300">
|
||||||
{sortedPosts[0].title}
|
<a
|
||||||
</a>
|
href={`/blog/${sortedPosts[0].slug}/`}
|
||||||
</h2>
|
class="before:absolute before:inset-0"
|
||||||
|
>
|
||||||
<p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-4 sm:mb-6 line-clamp-3 text-center md:text-left">
|
{sortedPosts[0].title}
|
||||||
{sortedPosts[0].description}
|
</a>
|
||||||
</p>
|
</h2>
|
||||||
|
|
||||||
<!-- Improved mobile layout for featured post metadata -->
|
<p class="mb-4 line-clamp-3 text-center text-sm text-zinc-600 sm:mb-6 sm:text-base md:text-left dark:text-zinc-400">
|
||||||
<div class="flex items-center gap-3 sm:gap-4 justify-center md:justify-start flex-wrap">
|
{sortedPosts[0].description}
|
||||||
{sortedPosts[0].tags && (
|
</p>
|
||||||
<div class="flex flex-wrap gap-2 justify-center md:justify-start">
|
|
||||||
{sortedPosts[0].tags.slice(0, 2).map((tag) => (
|
<div class="flex flex-wrap items-center justify-center gap-3 sm:gap-4 md:justify-start">
|
||||||
<span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400">
|
{sortedPosts[0].tags && (
|
||||||
{tag}
|
<div class="flex flex-wrap justify-center gap-2 md:justify-start">
|
||||||
</span>
|
{sortedPosts[0].tags.slice(0, 2).map((tag) => (
|
||||||
))}
|
<span class="border border-zinc-200 px-2 py-1 text-xs tracking-wider text-zinc-600 uppercase sm:px-3 dark:border-zinc-800 dark:text-zinc-400">
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<!-- Improved sidebar for mobile -->
|
|
||||||
<div class="md:col-span-3 relative">
|
|
||||||
<div class="md:sticky md:top-24 space-y-4 mb-8 md:mb-0">
|
|
||||||
<h3 class="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-4 uppercase tracking-wider text-center md:text-left">Archive</h3>
|
|
||||||
|
|
||||||
<!-- Horizontal scrollable archive on mobile, vertical on desktop -->
|
|
||||||
<div class="flex md:flex-col overflow-x-auto md:overflow-visible pb-4 md:pb-0 hide-scrollbar">
|
|
||||||
{years.map((year, index) => (
|
|
||||||
<a
|
|
||||||
href={`#year-${year}`}
|
|
||||||
class={`flex items-center py-2 md:py-3 px-4 md:px-0 mr-3 md:mr-0 border-b border-zinc-100 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-900 transition-colors md:w-full whitespace-nowrap md:whitespace-normal rounded-full md:rounded-none ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`}
|
|
||||||
>
|
|
||||||
<span class="text-base md:text-lg font-medium text-zinc-900 dark:text-zinc-100">{year}</span>
|
|
||||||
<span class="ml-2 md:ml-auto text-xs md:text-sm text-zinc-500 dark:text-zinc-400">
|
|
||||||
{postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Improved post grid for mobile -->
|
|
||||||
<div class="md:col-span-9">
|
|
||||||
{years.map((year) => (
|
|
||||||
<div id={`year-${year}`} class="mb-12 sm:mb-20 scroll-mt-16">
|
|
||||||
<h2 class="text-xl sm:text-2xl font-bold text-zinc-900 dark:text-zinc-100 mb-6 sm:mb-8 border-b border-zinc-200 dark:border-zinc-800 pb-3 sm:pb-4 text-center md:text-left">
|
|
||||||
{year}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}>
|
|
||||||
{postsByYear[year].map((post, index) => (
|
|
||||||
<article class="group relative flex flex-col h-full mx-auto md:mx-0 w-full max-w-sm sm:max-w-md">
|
|
||||||
{post.image && (
|
|
||||||
<div class="h-48 sm:h-56 overflow-hidden mb-4 rounded-lg">
|
|
||||||
<img
|
|
||||||
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}`}
|
|
||||||
alt={post.title}
|
|
||||||
class="w-full h-full object-cover grayscale hover:grayscale-0 transition-all duration-700 group-hover:scale-105"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col">
|
|
||||||
<div class="flex items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-3 sm:gap-4 mb-2 sm:mb-3 justify-center md:justify-start flex-wrap">
|
|
||||||
{post.pubDate && (
|
|
||||||
<time datetime={post.published_date.toLocaleString()} class="flex items-center">
|
|
||||||
{post.published_date.toLocaleString('en-US', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric'
|
|
||||||
})}
|
|
||||||
</time>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="text-lg sm:text-xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2 sm:mb-3 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center md:text-left">
|
|
||||||
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
|
|
||||||
{post.title}
|
|
||||||
</a>
|
|
||||||
</h3>
|
|
||||||
|
|
||||||
<p class="text-sm text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-2 flex-grow text-center md:text-left">
|
|
||||||
{post.description}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{post.tags && (
|
|
||||||
<div class="flex flex-wrap gap-2 mt-auto justify-center md:justify-start">
|
|
||||||
{post.tags.slice(0, 2).map((tag) => (
|
|
||||||
<span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400">
|
|
||||||
{tag}
|
{tag}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
{post.tags.length > 2 && (
|
|
||||||
<span class="px-2 sm:px-3 py-1 text-xs uppercase tracking-wider border border-zinc-200 dark:border-zinc-800 text-zinc-600 dark:text-zinc-400">
|
|
||||||
+{post.tags.length - 2}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</div>
|
||||||
))}
|
</div>
|
||||||
</div>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
))}
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Improved sidebar for mobile -->
|
||||||
|
<div class="relative md:col-span-3">
|
||||||
|
<div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0">
|
||||||
|
<h3
|
||||||
|
class="mb-4 text-center text-lg font-medium tracking-wider text-zinc-900 uppercase md:text-left dark:text-zinc-100"
|
||||||
|
>
|
||||||
|
Archive
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Horizontal scrollable archive on mobile, vertical on desktop -->
|
||||||
|
<div
|
||||||
|
class="hide-scrollbar flex overflow-x-auto pb-4 md:flex-col md:overflow-visible md:pb-0"
|
||||||
|
>
|
||||||
|
{
|
||||||
|
years.map((year, index) => (
|
||||||
|
<a
|
||||||
|
href={`#year-${year}`}
|
||||||
|
class={`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">
|
||||||
|
{year}
|
||||||
|
</span>
|
||||||
|
<span class="ml-2 text-xs text-zinc-500 md:ml-auto md:text-sm dark:text-zinc-400">
|
||||||
|
{postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
))
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Improved post grid for mobile -->
|
||||||
|
<div class="md:col-span-9">
|
||||||
|
{
|
||||||
|
years.map((year) => (
|
||||||
|
<div id={`year-${year}`} class="mb-12 scroll-mt-16 sm:mb-20">
|
||||||
|
<h2 class="mb-6 border-b border-zinc-200 pb-3 text-center text-xl font-bold text-zinc-900 sm:mb-8 sm:pb-4 sm:text-2xl md:text-left dark:border-zinc-800 dark:text-zinc-100">
|
||||||
|
{year}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}
|
||||||
|
>
|
||||||
|
{postsByYear[year].map((post, index) => (
|
||||||
|
<article class="group relative mx-auto flex h-full w-full max-w-sm flex-col sm:max-w-md md:mx-0">
|
||||||
|
{post.image && (
|
||||||
|
<div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56">
|
||||||
|
<img
|
||||||
|
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
|
||||||
|
alt={post.title}
|
||||||
|
class="h-full w-full object-cover grayscale transition-all duration-700 group-hover:scale-105 hover:grayscale-0"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="flex flex-1 flex-col">
|
||||||
|
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:gap-4 sm:text-sm md:justify-start dark:text-zinc-400">
|
||||||
|
{post.pubDate && (
|
||||||
|
<time
|
||||||
|
datetime={post.published_date.toLocaleString()}
|
||||||
|
class="flex items-center"
|
||||||
|
>
|
||||||
|
{post.published_date.toLocaleString('en-US', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
})}
|
||||||
|
</time>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="mb-2 text-center text-lg font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 sm:mb-3 sm:text-xl md:text-left dark:text-zinc-100 dark:group-hover:text-zinc-300">
|
||||||
|
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
|
||||||
|
{post.title}
|
||||||
|
</a>
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p class="mb-4 line-clamp-2 grow text-center text-sm text-zinc-600 md:text-left dark:text-zinc-400">
|
||||||
|
{post.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{post.tags && (
|
||||||
|
<div class="mt-auto flex flex-wrap justify-center gap-2 md:justify-start">
|
||||||
|
{post.tags.slice(0, 2).map((tag) => (
|
||||||
|
<span class="border border-zinc-200 px-2 py-1 text-xs tracking-wider text-zinc-600 uppercase sm:px-3 dark:border-zinc-800 dark:text-zinc-400">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{post.tags.length > 2 && (
|
||||||
|
<span class="border border-zinc-200 px-2 py-1 text-xs tracking-wider text-zinc-600 uppercase sm:px-3 dark:border-zinc-800 dark:text-zinc-400">
|
||||||
|
+{post.tags.length - 2}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -209,13 +237,14 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
|||||||
.animate-blob {
|
.animate-blob {
|
||||||
animation: blob-bounce 8s infinite ease;
|
animation: blob-bounce 8s infinite ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-delay-2000 {
|
.animation-delay-2000 {
|
||||||
animation-delay: 2s;
|
animation-delay: 2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes blob-bounce {
|
@keyframes blob-bounce {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translate(0, 0) scale(1);
|
transform: translate(0, 0) scale(1);
|
||||||
}
|
}
|
||||||
25% {
|
25% {
|
||||||
@@ -228,36 +257,37 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
|||||||
transform: translate(-5%, 5%) scale(0.95);
|
transform: translate(-5%, 5%) scale(0.95);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search container hover effect */
|
/* Search container hover effect */
|
||||||
.search-container:hover .search-pulse {
|
.search-container:hover .search-pulse {
|
||||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Input focus animation */
|
/* Input focus animation */
|
||||||
input:focus + div .search-pulse {
|
input:focus + div .search-pulse {
|
||||||
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Hide scrollbar but keep functionality */
|
/* Hide scrollbar but keep functionality */
|
||||||
.hide-scrollbar {
|
.hide-scrollbar {
|
||||||
-ms-overflow-style: none;
|
-ms-overflow-style: none;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-scrollbar::-webkit-scrollbar {
|
.hide-scrollbar::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Line clamp for descriptions */
|
/* Line clamp for descriptions */
|
||||||
.line-clamp-2 {
|
.line-clamp-2 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
@@ -265,17 +295,18 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.line-clamp-3 {
|
.line-clamp-3 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 3;
|
-webkit-line-clamp: 3;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Improved touch targets for mobile */
|
/* Improved touch targets for mobile */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
a, button {
|
a,
|
||||||
|
button {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -287,7 +318,7 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
|||||||
// Script không thay đổi - giữ nguyên chức năng
|
// Script không thay đổi - giữ nguyên chức năng
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const backToTopButton = document.getElementById('back-to-top');
|
const backToTopButton = document.getElementById('back-to-top');
|
||||||
|
|
||||||
if (backToTopButton) {
|
if (backToTopButton) {
|
||||||
// Show button when scrolled down
|
// Show button when scrolled down
|
||||||
const toggleBackToTopButton = () => {
|
const toggleBackToTopButton = () => {
|
||||||
@@ -299,50 +330,50 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
|||||||
backToTopButton.classList.add('opacity-0', 'invisible');
|
backToTopButton.classList.add('opacity-0', 'invisible');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Scroll to top when clicked
|
// Scroll to top when clicked
|
||||||
backToTopButton.addEventListener('click', () => {
|
backToTopButton.addEventListener('click', () => {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: 0,
|
top: 0,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Check scroll position
|
// Check scroll position
|
||||||
window.addEventListener('scroll', toggleBackToTopButton);
|
window.addEventListener('scroll', toggleBackToTopButton);
|
||||||
toggleBackToTopButton(); // Initial check
|
toggleBackToTopButton(); // Initial check
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add smooth scrolling to year links
|
// Add smooth scrolling to year links
|
||||||
document.querySelectorAll('a[href^="#year-"]').forEach(anchor => {
|
document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
|
||||||
anchor.addEventListener('click', function (e) {
|
anchor.addEventListener('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const targetId = this.getAttribute('href');
|
const targetId = this.getAttribute('href');
|
||||||
const targetElement = document.querySelector(targetId);
|
const targetElement = document.querySelector(targetId);
|
||||||
|
|
||||||
if (targetElement) {
|
if (targetElement) {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: targetElement.offsetTop - 100,
|
top: targetElement.offsetTop - 100,
|
||||||
behavior: 'smooth'
|
behavior: 'smooth',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update URL hash without jumping
|
// Update URL hash without jumping
|
||||||
history.pushState(null, null, targetId);
|
history.pushState(null, null, targetId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add touch support for hover effects
|
// Add touch support for hover effects
|
||||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||||
|
|
||||||
if (isTouchDevice) {
|
if (isTouchDevice) {
|
||||||
const articles = document.querySelectorAll('article');
|
const articles = document.querySelectorAll('article');
|
||||||
|
|
||||||
articles.forEach(article => {
|
articles.forEach((article) => {
|
||||||
article.addEventListener('touchstart', () => {
|
article.addEventListener('touchstart', () => {
|
||||||
article.classList.add('is-touched');
|
article.classList.add('is-touched');
|
||||||
});
|
});
|
||||||
|
|
||||||
article.addEventListener('touchend', () => {
|
article.addEventListener('touchend', () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
article.classList.remove('is-touched');
|
article.classList.remove('is-touched');
|
||||||
@@ -351,34 +382,34 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// SPA transition handling
|
// SPA transition handling
|
||||||
function setupSPATransitions() {
|
function setupSPATransitions() {
|
||||||
// Handle all blog post links for SPA transitions
|
// Handle all blog post links for SPA transitions
|
||||||
document.querySelectorAll('a[href^="/blog/"]').forEach(link => {
|
document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
|
||||||
// Skip links that are anchor links or already processed
|
// Skip links that are anchor links or already processed
|
||||||
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
|
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
// Mark as handled to avoid duplicate listeners
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
link.setAttribute('data-spa-handled', 'true');
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', (e) => {
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const targetHref = link.getAttribute('href');
|
const targetHref = link.getAttribute('href');
|
||||||
|
|
||||||
// Trigger page transition animation
|
// Trigger page transition animation
|
||||||
const pageTransition = document.getElementById('page-transition');
|
const pageTransition = document.getElementById('page-transition');
|
||||||
if (pageTransition) {
|
if (pageTransition) {
|
||||||
pageTransition.classList.remove('opacity-0');
|
pageTransition.classList.remove('opacity-0');
|
||||||
pageTransition.classList.add('opacity-100');
|
pageTransition.classList.add('opacity-100');
|
||||||
|
|
||||||
// Navigate after transition effect
|
// Navigate after transition effect
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = targetHref;
|
window.location.href = targetHref;
|
||||||
@@ -389,19 +420,19 @@ const allTags = [...new Set(sortedPosts.flatMap(post => post.tags || []))];
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Handle year anchor links specially
|
// Handle year anchor links specially
|
||||||
document.querySelectorAll('a[href^="#year-"]').forEach(anchor => {
|
document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
|
||||||
anchor.setAttribute('data-spa-internal', 'true');
|
anchor.setAttribute('data-spa-internal', 'true');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on first load
|
// Initialize on first load
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
||||||
|
|
||||||
// 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', setupSPATransitions);
|
document.addEventListener('astro:page-load', setupSPATransitions);
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
// For compatibility with custom transition system
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
document.addEventListener('page-transition-complete', setupSPATransitions);
|
||||||
</script>
|
</script>
|
||||||
|
@@ -2,15 +2,15 @@
|
|||||||
import Layout from '../layouts/Layout.astro';
|
import Layout from '../layouts/Layout.astro';
|
||||||
import FormattedDate from '../components/FormattedDate.astro';
|
import FormattedDate from '../components/FormattedDate.astro';
|
||||||
|
|
||||||
import directus from "../../lib/directus"
|
import directus from '../../lib/directus';
|
||||||
import { readItems,readSingleton } from "@directus/sdk";
|
import { readItems, readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
const global = await directus.request(readSingleton("global"));
|
const global = await directus.request(readSingleton('global'));
|
||||||
|
|
||||||
const posts = await directus.request(
|
const posts = await directus.request(
|
||||||
readItems("posts", {
|
readItems('posts', {
|
||||||
fields: ['*'],
|
fields: ['*'],
|
||||||
sort: ["-published_date"],
|
sort: ['-published_date'],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -18,42 +18,67 @@ const recentPosts = posts
|
|||||||
.sort((a, b) => b.published_date.getTime() - a.published_date.getTime())
|
.sort((a, b) => b.published_date.getTime() - a.published_date.getTime())
|
||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
|
|
||||||
const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5);
|
const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, 5);
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title=`Home | ${global.name}`>
|
<Layout title=`Home | ${global.name}`>
|
||||||
<!-- Hero Section with improved mobile responsiveness -->
|
<!-- Hero Section with improved mobile responsiveness -->
|
||||||
<section class="py-10 sm:py-16 md:py-20 px-4 sm:px-6 theme-transition-all">
|
<section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20">
|
||||||
<div class="max-w-2xl mx-auto relative">
|
<div class="relative mx-auto max-w-2xl">
|
||||||
<!-- Adjusted blob positions and sizes for better mobile appearance -->
|
<!-- Adjusted blob positions and sizes for better mobile appearance -->
|
||||||
<div class="absolute -top-10 sm:-top-20 -left-10 sm:-left-20 w-40 sm:w-64 h-40 sm:h-64 bg-zinc-100 dark:bg-zinc-800/50 rounded-full blur-3xl opacity-50 animate-blob theme-transition-bg"></div>
|
<div
|
||||||
<div class="absolute -bottom-10 sm:-bottom-20 -right-10 sm:-right-20 w-40 sm:w-64 h-40 sm:h-64 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000 theme-transition-bg"></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"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-10 -bottom-10 h-40 w-40 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:-right-20 sm:-bottom-20 sm:h-64 sm:w-64 dark:bg-zinc-800/30"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="relative text-center sm:text-left">
|
<div class="relative text-center sm:text-left">
|
||||||
<h1 class="text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 theme-transition-color hero-text">
|
<h1
|
||||||
|
class="theme-transition-color hero-text text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl lg:text-6xl dark:text-zinc-100"
|
||||||
|
>
|
||||||
<span class="block">Writing on technology,</span>
|
<span class="block">Writing on technology,</span>
|
||||||
<span class="block mt-1">development, and</span>
|
<span class="mt-1 block">development, and</span>
|
||||||
<span class="block mt-1 relative">
|
<span class="relative mt-1 block">
|
||||||
<span class="relative inline-block">
|
<span class="relative inline-block">
|
||||||
selfhosting.
|
selfhosting.
|
||||||
<span class="absolute -bottom-1 left-0 w-full h-1 bg-zinc-800 dark:bg-zinc-200 transform origin-left theme-transition-bg"></span>
|
<span
|
||||||
|
class="theme-transition-bg absolute -bottom-1 left-0 h-1 w-full origin-left transform bg-zinc-800 dark:bg-zinc-200"
|
||||||
|
></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="mt-4 sm:mt-6 md:mt-8 text-base sm:text-lg text-zinc-600 dark:text-zinc-400 leading-relaxed theme-transition-color max-w-lg mx-auto sm:mx-0">
|
<p
|
||||||
|
class="theme-transition-color mx-auto mt-4 max-w-lg text-base leading-relaxed text-zinc-600 sm:mx-0 sm:mt-6 sm:text-lg md:mt-8 dark:text-zinc-400"
|
||||||
|
>
|
||||||
{global.about}
|
{global.about}
|
||||||
</p>
|
</p>
|
||||||
<div class="mt-6 sm:mt-8 md:mt-10 flex flex-wrap gap-3 sm:gap-4 md:gap-6 justify-center sm:justify-start">
|
<div
|
||||||
<a
|
class="mt-6 flex flex-wrap justify-center gap-3 sm:mt-8 sm:justify-start sm:gap-4 md:mt-10 md:gap-6"
|
||||||
href="/about"
|
>
|
||||||
class="group relative inline-flex items-center gap-2 text-sm font-medium text-zinc-900 dark:text-zinc-100 hover:text-zinc-700 dark:hover:text-zinc-300 transition-all duration-300 theme-transition-color min-h-[44px]"
|
<a
|
||||||
|
href="/about"
|
||||||
|
class="theme-transition-color group relative inline-flex min-h-[44px] items-center gap-2 text-sm font-medium text-zinc-900 transition-all duration-300 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
|
||||||
>
|
>
|
||||||
<span>More about me</span>
|
<span>More about me</span>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-zinc-800 dark:bg-zinc-200 transition-all duration-300 group-hover:w-full theme-transition-bg"></span>
|
<span
|
||||||
|
class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
|
||||||
|
></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,139 +86,194 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Featured Post Section - Improved for mobile -->
|
<!-- Featured Post Section - Improved for mobile -->
|
||||||
<section class="py-10 sm:py-12 md:py-16 px-4 sm:px-6 border-t border-zinc-100 dark:border-zinc-800 theme-transition-all">
|
<section
|
||||||
<div class="max-w-3xl mx-auto">
|
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"
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center justify-between gap-4 mb-6 sm:mb-8 md:mb-12">
|
>
|
||||||
<h2 class="text-xl sm:text-2xl md:text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 theme-transition-color text-center sm:text-left">Recent Posts</h2>
|
<div class="mx-auto max-w-3xl">
|
||||||
<a
|
<div
|
||||||
href="/blog"
|
class="mb-6 flex flex-col justify-between gap-4 sm:mb-8 sm:flex-row sm:items-center md:mb-12"
|
||||||
class="group relative text-sm font-medium text-zinc-900 dark:text-zinc-100 hover:text-zinc-700 dark:hover:text-zinc-300 self-center sm:self-auto theme-transition-color min-h-[44px] flex items-center justify-center"
|
>
|
||||||
|
<h2
|
||||||
|
class="theme-transition-color text-center text-xl font-bold tracking-tight text-zinc-900 sm:text-left sm:text-2xl md:text-3xl dark:text-zinc-100"
|
||||||
|
>
|
||||||
|
Recent Posts
|
||||||
|
</h2>
|
||||||
|
<a
|
||||||
|
href="/blog"
|
||||||
|
class="theme-transition-color group relative flex min-h-[44px] items-center justify-center self-center text-sm font-medium text-zinc-900 hover:text-zinc-700 sm:self-auto dark:text-zinc-100 dark:hover:text-zinc-300"
|
||||||
>
|
>
|
||||||
<span class="flex items-center gap-1">
|
<span class="flex items-center gap-1">
|
||||||
View all posts
|
View all posts
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1">
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span class="absolute -bottom-1 left-0 w-0 h-0.5 bg-zinc-800 dark:bg-zinc-200 transition-all duration-300 group-hover:w-full theme-transition-bg"></span>
|
<span
|
||||||
|
class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
|
||||||
|
></span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Improved grid for better mobile layout -->
|
<!-- Improved grid for better mobile layout -->
|
||||||
<div class="grid gap-6 sm:gap-8 md:gap-12 grid-cols-1 sm:grid-cols-2 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) => (
|
{
|
||||||
<article class="group relative flex flex-col items-start hover-3d theme-transition-element max-w-sm mx-auto sm:mx-0 w-full">
|
recentPosts.map((post, index) => (
|
||||||
<div class="absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 dark:bg-zinc-800/50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 sm:-inset-x-6 sm:rounded-2xl theme-transition-bg"></div>
|
<article class="hover-3d theme-transition-element group relative mx-auto flex w-full max-w-sm flex-col items-start sm:mx-0">
|
||||||
|
<div class="theme-transition-bg absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 sm:-inset-x-6 sm:rounded-2xl dark:bg-zinc-800/50" />
|
||||||
{post.image && (
|
|
||||||
<div class="relative z-10 w-full aspect-video mb-4 overflow-hidden rounded-lg">
|
{post.image && (
|
||||||
<img
|
<div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
|
||||||
src={`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${post.image}`}
|
<img
|
||||||
alt={post.title}
|
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
|
||||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
alt={post.title}
|
||||||
loading={index === 0 ? "eager" : "lazy"}
|
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
width="400"
|
loading={index === 0 ? 'eager' : 'lazy'}
|
||||||
height="225"
|
width="400"
|
||||||
/>
|
height="225"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="theme-transition-color relative z-10 flex w-full flex-wrap items-center justify-center gap-x-3 gap-y-2 text-xs text-zinc-500 sm:justify-start sm:gap-x-4 dark:text-zinc-400">
|
||||||
|
<time datetime={post.published_date.toLocaleString()} class="font-medium">
|
||||||
|
<FormattedDate date={post.published_date} />
|
||||||
|
</time>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
<h3 class="theme-transition-color relative z-10 mt-3 w-full text-center text-lg font-semibold tracking-tight text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-left sm:text-xl dark:text-zinc-100 dark:group-hover:text-zinc-300">
|
||||||
<div class="relative z-10 flex items-center flex-wrap gap-x-3 sm:gap-x-4 gap-y-2 text-xs text-zinc-500 dark:text-zinc-400 theme-transition-color justify-center sm:justify-start w-full">
|
<a
|
||||||
<time datetime={post.published_date.toLocaleString()} class="font-medium">
|
href={`/blog/${post.slug}`}
|
||||||
<FormattedDate date={post.published_date} />
|
class="flex min-h-[44px] items-center justify-center sm:justify-start"
|
||||||
</time>
|
>
|
||||||
</div>
|
<span class="absolute -inset-x-4 -inset-y-2.5 sm:-inset-x-6 sm:-inset-y-4" />
|
||||||
|
{post.title}
|
||||||
<h3 class="relative z-10 mt-3 text-lg sm:text-xl font-semibold tracking-tight text-zinc-900 dark:text-zinc-100 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors theme-transition-color text-center sm:text-left w-full">
|
</a>
|
||||||
<a href={`/blog/${post.slug}`} class="min-h-[44px] flex items-center justify-center sm:justify-start">
|
</h3>
|
||||||
<span class="absolute -inset-x-4 -inset-y-2.5 sm:-inset-x-6 sm:-inset-y-4"></span>
|
|
||||||
{post.title}
|
<p class="theme-transition-color relative z-10 mt-2 line-clamp-3 w-full text-center text-sm text-zinc-600 sm:mt-3 sm:text-left dark:text-zinc-400">
|
||||||
</a>
|
{post.description}
|
||||||
</h3>
|
</p>
|
||||||
|
|
||||||
<p class="relative z-10 mt-2 sm:mt-3 text-sm text-zinc-600 dark:text-zinc-400 line-clamp-3 theme-transition-color text-center sm:text-left w-full">
|
{post.tags && post.tags.length > 0 && (
|
||||||
{post.description}
|
<div class="relative z-10 mt-3 flex w-full flex-wrap justify-center gap-2 sm:mt-4 sm:justify-start">
|
||||||
</p>
|
{post.tags.slice(0, 3).map((tag) => (
|
||||||
|
<a
|
||||||
{post.tags && post.tags.length > 0 && (
|
href={`/topics/${tag}`}
|
||||||
<div class="relative z-10 mt-3 sm:mt-4 flex flex-wrap gap-2 justify-center sm:justify-start w-full">
|
class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-800 transition-colors hover:bg-zinc-200 sm:px-3 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
||||||
{post.tags.slice(0, 3).map(tag => (
|
>
|
||||||
<a
|
#{tag}
|
||||||
href={`/topics/${tag}`}
|
</a>
|
||||||
class="inline-flex items-center rounded-full bg-zinc-100 px-2 sm:px-3 py-1 text-xs font-medium text-zinc-800 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 transition-colors theme-transition-all min-h-[28px]"
|
))}
|
||||||
>
|
{post.tags.length > 3 && (
|
||||||
#{tag}
|
<span class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
|
||||||
</a>
|
+{post.tags.length - 3} more
|
||||||
))}
|
</span>
|
||||||
{post.tags.length > 3 && (
|
)}
|
||||||
<span class="inline-flex items-center rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400 theme-transition-all min-h-[28px]">
|
</div>
|
||||||
+{post.tags.length - 3} more
|
)}
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={`/blog/${post.slug}`}
|
||||||
|
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100"
|
||||||
|
>
|
||||||
|
<span class="relative inline-block overflow-hidden">
|
||||||
|
<span class="block transition-transform duration-300 group-hover:-translate-y-full">
|
||||||
|
Read article
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span class="absolute top-0 left-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
|
||||||
</div>
|
Explore now
|
||||||
)}
|
</span>
|
||||||
|
</span>
|
||||||
<a
|
<svg
|
||||||
href={`/blog/${post.slug}`}
|
viewBox="0 0 16 16"
|
||||||
class="relative z-10 mt-3 sm:mt-4 flex items-center text-sm font-medium text-zinc-700 dark:text-zinc-300 group-hover:text-zinc-900 dark:group-hover:text-zinc-100 transition-colors theme-transition-color mx-auto sm:mx-0 min-h-[44px]"
|
fill="none"
|
||||||
>
|
aria-hidden="true"
|
||||||
<span class="relative overflow-hidden inline-block">
|
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 group-hover:translate-x-1"
|
||||||
<span class="group-hover:-translate-y-full block transition-transform duration-300">Read article</span>
|
>
|
||||||
<span class="absolute top-0 left-0 translate-y-full group-hover:translate-y-0 transition-transform duration-300 whitespace-nowrap">Explore now</span>
|
<path
|
||||||
</span>
|
d="M6.75 5.75 9.25 8l-2.5 2.25"
|
||||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true" class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 group-hover:translate-x-1">
|
stroke-width="1.5"
|
||||||
<path d="M6.75 5.75 9.25 8l-2.5 2.25" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"></path>
|
stroke-linecap="round"
|
||||||
</svg>
|
stroke-linejoin="round"
|
||||||
</a>
|
/>
|
||||||
</article>
|
</svg>
|
||||||
))}
|
</a>
|
||||||
|
</article>
|
||||||
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Topics/Tags Section - Improved for mobile -->
|
<!-- Topics/Tags Section - Improved for mobile -->
|
||||||
{allTags.length > 0 && (
|
{
|
||||||
<section class="py-10 sm:py-12 md:py-16 px-4 sm:px-6 border-t border-zinc-100 dark:border-zinc-800 theme-transition-all">
|
allTags.length > 0 && (
|
||||||
<div class="max-w-3xl mx-auto">
|
<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">
|
||||||
<h2 class="text-xl sm:text-2xl md:text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-6 sm:mb-8 theme-transition-color text-center sm:text-left">Explore Topics</h2>
|
<div class="mx-auto max-w-3xl">
|
||||||
|
<h2 class="theme-transition-color mb-6 text-center text-xl font-bold tracking-tight text-zinc-900 sm:mb-8 sm:text-left sm:text-2xl md:text-3xl dark:text-zinc-100">
|
||||||
<!-- Improved grid layout for mobile -->
|
Explore Topics
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-3 sm:gap-4 max-w-xs sm:max-w-none mx-auto">
|
</h2>
|
||||||
{allTags.map(tag => {
|
|
||||||
const tagCount = posts.filter(post => post.tags && post.tags.includes(tag)).length;
|
<div class="mx-auto grid max-w-xs grid-cols-1 gap-3 sm:max-w-none sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
|
||||||
return (
|
{allTags.map((tag) => {
|
||||||
<a
|
const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
|
||||||
href={`/topics/${tag}`}
|
return (
|
||||||
class="group flex flex-col p-3 sm:p-4 md:p-6 rounded-xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50 dark:hover:bg-zinc-800/70 transition-all duration-300 theme-transition-all min-h-[80px] sm:min-h-[90px]"
|
<a
|
||||||
|
href={`/topics/${tag}`}
|
||||||
|
class="theme-transition-all group flex min-h-[80px] flex-col rounded-xl border border-zinc-200 p-3 transition-all duration-300 hover:bg-zinc-50 sm:min-h-[90px] sm:p-4 md:p-6 dark:border-zinc-800 dark:hover:bg-zinc-800/70"
|
||||||
|
>
|
||||||
|
<div class="mb-2 flex items-start justify-between">
|
||||||
|
<span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
||||||
|
#{tag}
|
||||||
|
</span>
|
||||||
|
<span class="theme-transition-all shrink-0 rounded-full bg-zinc-100 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
|
||||||
|
{tagCount} {tagCount === 1 ? 'post' : 'posts'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="theme-transition-color mt-1 text-xs text-zinc-600 dark:text-zinc-400">
|
||||||
|
Explore articles about {tag}
|
||||||
|
</p>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 text-center sm:mt-8">
|
||||||
|
<a
|
||||||
|
href="/tags"
|
||||||
|
class="theme-transition-color inline-flex min-h-[44px] items-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
|
||||||
|
>
|
||||||
|
<span>View all topics</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
||||||
>
|
>
|
||||||
<div class="flex items-start justify-between mb-2">
|
<path
|
||||||
<span class="text-sm font-medium text-zinc-900 dark:text-zinc-100 theme-transition-color mr-2">#{tag}</span>
|
stroke-linecap="round"
|
||||||
<span class="text-xs bg-zinc-100 dark:bg-zinc-800 text-zinc-500 dark:text-zinc-400 px-2 py-0.5 rounded-full flex-shrink-0 theme-transition-all">
|
stroke-linejoin="round"
|
||||||
{tagCount} {tagCount === 1 ? 'post' : 'posts'}
|
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
|
||||||
</span>
|
/>
|
||||||
</div>
|
</svg>
|
||||||
<p class="text-xs text-zinc-600 dark:text-zinc-400 mt-1 theme-transition-color">
|
</a>
|
||||||
Explore articles about {tag}
|
</div>
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
<div class="mt-6 sm:mt-8 text-center">
|
)
|
||||||
<a
|
}
|
||||||
href="/tags"
|
|
||||||
class="inline-flex items-center text-sm font-medium text-zinc-900 dark:text-zinc-100 hover:text-zinc-700 dark:hover:text-zinc-300 theme-transition-color min-h-[44px]"
|
|
||||||
>
|
|
||||||
<span>View all topics</span>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 ml-1 transition-transform duration-300 group-hover:translate-x-1">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
)}
|
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@@ -201,69 +281,69 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
// Check if it's a touch device
|
// 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) {
|
||||||
const cards = document.querySelectorAll('.hover-3d');
|
const cards = document.querySelectorAll('.hover-3d');
|
||||||
|
|
||||||
cards.forEach(card => {
|
cards.forEach((card) => {
|
||||||
card.addEventListener('touchstart', () => {
|
card.addEventListener('touchstart', () => {
|
||||||
card.classList.add('is-touched');
|
card.classList.add('is-touched');
|
||||||
});
|
});
|
||||||
|
|
||||||
card.addEventListener('touchend', () => {
|
card.addEventListener('touchend', () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
card.classList.remove('is-touched');
|
card.classList.remove('is-touched');
|
||||||
}, 300);
|
}, 300);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Disable hover animations on touch devices for better performance
|
// Disable hover animations on touch devices for better performance
|
||||||
document.documentElement.classList.add('touch-device');
|
document.documentElement.classList.add('touch-device');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Improved viewport height fix for mobile browsers
|
// Improved 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`);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set initial value
|
// Set initial value
|
||||||
setVh();
|
setVh();
|
||||||
|
|
||||||
// Update on resize and scroll to prevent content shifting
|
// Update on resize and scroll to prevent content shifting
|
||||||
window.addEventListener('resize', setVh);
|
window.addEventListener('resize', setVh);
|
||||||
|
|
||||||
// Use a debounced scroll handler to prevent performance issues
|
// Use a debounced scroll handler to prevent performance issues
|
||||||
let scrollTimeout;
|
let scrollTimeout;
|
||||||
window.addEventListener('scroll', () => {
|
window.addEventListener('scroll', () => {
|
||||||
if (scrollTimeout) {
|
if (scrollTimeout) {
|
||||||
window.cancelAnimationFrame(scrollTimeout);
|
window.cancelAnimationFrame(scrollTimeout);
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollTimeout = window.requestAnimationFrame(() => {
|
scrollTimeout = window.requestAnimationFrame(() => {
|
||||||
// Lock width during scroll
|
// Lock width during scroll
|
||||||
document.body.style.width = '100%';
|
document.body.style.width = '100%';
|
||||||
document.body.style.overflowX = 'hidden';
|
document.body.style.overflowX = 'hidden';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fix for iOS Safari address bar height changes
|
// Fix for iOS Safari address bar height changes
|
||||||
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
|
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
|
||||||
// Force the layout to use the initial viewport size
|
// Force the layout to use the initial viewport size
|
||||||
document.documentElement.style.setProperty('--initial-vh', `${window.innerHeight * 0.01}px`);
|
document.documentElement.style.setProperty('--initial-vh', `${window.innerHeight * 0.01}px`);
|
||||||
|
|
||||||
// Apply fixed height to sections to prevent resizing
|
// Apply fixed height to sections to prevent resizing
|
||||||
const sections = document.querySelectorAll('section');
|
const sections = document.querySelectorAll('section');
|
||||||
sections.forEach(section => {
|
sections.forEach((section) => {
|
||||||
section.style.width = '100%';
|
section.style.width = '100%';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Improved theme change handler that preserves scroll position and provides smoother transitions
|
// Improved 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;
|
||||||
|
|
||||||
// Create a temporary overlay for smoother transition
|
// Create a temporary overlay for smoother transition
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.style.cssText = `
|
overlay.style.cssText = `
|
||||||
@@ -276,19 +356,22 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
|||||||
transition: opacity 0.3s ease;
|
transition: opacity 0.3s ease;
|
||||||
`;
|
`;
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
// Fade in overlay
|
// Fade in overlay
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
overlay.style.opacity = '0.5';
|
overlay.style.opacity = '0.5';
|
||||||
|
|
||||||
// Update theme-transition elements without forcing reflow of entire page
|
// Update theme-transition elements without forcing reflow of entire page
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
document.querySelectorAll('.theme-transition-all, .theme-transition-bg, .theme-transition-color')
|
document
|
||||||
.forEach(el => {
|
.querySelectorAll(
|
||||||
|
'.theme-transition-all, .theme-transition-bg, .theme-transition-color'
|
||||||
|
)
|
||||||
|
.forEach((el) => {
|
||||||
// Apply a subtle animation instead of a hard reset
|
// Apply a subtle animation instead of a hard reset
|
||||||
el.style.transition = 'all 0.5s ease';
|
el.style.transition = 'all 0.5s ease';
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fade out overlay after transition completes
|
// Fade out overlay after transition completes
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
overlay.style.opacity = '0';
|
overlay.style.opacity = '0';
|
||||||
@@ -298,24 +381,24 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
|||||||
}, 300);
|
}, 300);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Restore scroll position (prevents jumping to top)
|
// Restore scroll position (prevents jumping to top)
|
||||||
if (scrollPosition > 0) {
|
if (scrollPosition > 0) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.scrollTo({
|
window.scrollTo({
|
||||||
top: scrollPosition,
|
top: scrollPosition,
|
||||||
behavior: 'auto' // Use 'auto' to prevent animation
|
behavior: 'auto', // Use 'auto' to prevent animation
|
||||||
});
|
});
|
||||||
}, 10);
|
}, 10);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fix theme inconsistency issues by checking theme on visibility change
|
// Fix theme inconsistency issues by checking theme on visibility change
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
if (document.visibilityState === 'visible') {
|
if (document.visibilityState === 'visible') {
|
||||||
const storedTheme = localStorage.getItem('theme');
|
const storedTheme = localStorage.getItem('theme');
|
||||||
const currentThemeIsDark = document.documentElement.classList.contains('dark');
|
const currentThemeIsDark = document.documentElement.classList.contains('dark');
|
||||||
|
|
||||||
if (storedTheme === 'dark' && !currentThemeIsDark) {
|
if (storedTheme === 'dark' && !currentThemeIsDark) {
|
||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
} else if (storedTheme === 'light' && currentThemeIsDark) {
|
} else if (storedTheme === 'light' && currentThemeIsDark) {
|
||||||
@@ -323,34 +406,45 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add smooth reveal animations for content after loading
|
// Add smooth reveal animations for content after loading
|
||||||
const animateContent = () => {
|
const animateContent = () => {
|
||||||
// Animate hero section
|
// Animate hero section
|
||||||
const heroElements = document.querySelectorAll('.hero-text span, .hero-text + p, .hero-text ~ div');
|
const heroElements = document.querySelectorAll(
|
||||||
|
'.hero-text span, .hero-text + p, .hero-text ~ div'
|
||||||
|
);
|
||||||
heroElements.forEach((el, index) => {
|
heroElements.forEach((el, index) => {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
el.classList.add('animate-reveal');
|
() => {
|
||||||
}, 100 + (index * 150));
|
el.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
100 + index * 150
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate posts with staggered delay
|
// Animate posts with staggered delay
|
||||||
const articles = document.querySelectorAll('article.group');
|
const articles = document.querySelectorAll('article.group');
|
||||||
articles.forEach((article, index) => {
|
articles.forEach((article, index) => {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
article.classList.add('animate-reveal');
|
() => {
|
||||||
}, 500 + (index * 150));
|
article.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
500 + index * 150
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate topic cards with staggered delay
|
// Animate topic cards with staggered delay
|
||||||
const topicCards = document.querySelectorAll('a.group.flex.flex-col');
|
const topicCards = document.querySelectorAll('a.group.flex.flex-col');
|
||||||
topicCards.forEach((card, index) => {
|
topicCards.forEach((card, index) => {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
card.classList.add('animate-reveal');
|
() => {
|
||||||
}, 800 + (index * 100));
|
card.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
800 + index * 100
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run animations after the loading screen is hidden
|
// Run animations after the loading screen is hidden
|
||||||
const loadingScreen = document.getElementById('loading-screen');
|
const loadingScreen = document.getElementById('loading-screen');
|
||||||
if (loadingScreen) {
|
if (loadingScreen) {
|
||||||
@@ -361,18 +455,20 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
|||||||
// Wait for loading screen to hide
|
// Wait for loading screen to hide
|
||||||
const observer = new MutationObserver((mutations) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
mutations.forEach((mutation) => {
|
mutations.forEach((mutation) => {
|
||||||
if (mutation.target === loadingScreen &&
|
if (
|
||||||
mutation.type === 'attributes' &&
|
mutation.target === loadingScreen &&
|
||||||
mutation.attributeName === 'style' &&
|
mutation.type === 'attributes' &&
|
||||||
loadingScreen.style.display === 'none') {
|
mutation.attributeName === 'style' &&
|
||||||
|
loadingScreen.style.display === 'none'
|
||||||
|
) {
|
||||||
animateContent();
|
animateContent();
|
||||||
observer.disconnect();
|
observer.disconnect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
observer.observe(loadingScreen, { attributes: true });
|
observer.observe(loadingScreen, { attributes: true });
|
||||||
|
|
||||||
// Fallback
|
// Fallback
|
||||||
setTimeout(animateContent, 3500);
|
setTimeout(animateContent, 3500);
|
||||||
}
|
}
|
||||||
@@ -381,36 +477,38 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
|||||||
animateContent();
|
animateContent();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// SPA transition handling for homepage
|
// SPA transition handling for homepage
|
||||||
function setupSPATransitions() {
|
function setupSPATransitions() {
|
||||||
// Handle all internal links for SPA transitions
|
// Handle all internal links for SPA transitions
|
||||||
document.querySelectorAll('a[href^="/"]').forEach(link => {
|
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
||||||
// Skip links that are anchor links, external links, or already processed
|
// Skip links that are anchor links, external links, or already processed
|
||||||
if (link.getAttribute('href').includes('#') ||
|
if (
|
||||||
link.getAttribute('target') === '_blank' ||
|
link.getAttribute('href').includes('#') ||
|
||||||
link.hasAttribute('data-spa-handled')) {
|
link.getAttribute('target') === '_blank' ||
|
||||||
|
link.hasAttribute('data-spa-handled')
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
// Mark as handled to avoid duplicate listeners
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
link.setAttribute('data-spa-handled', 'true');
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', (e) => {
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const targetHref = link.getAttribute('href');
|
const targetHref = link.getAttribute('href');
|
||||||
|
|
||||||
// Trigger page transition animation
|
// Trigger page transition animation
|
||||||
const pageTransition = document.getElementById('page-transition');
|
const pageTransition = document.getElementById('page-transition');
|
||||||
if (pageTransition) {
|
if (pageTransition) {
|
||||||
pageTransition.classList.remove('opacity-0');
|
pageTransition.classList.remove('opacity-0');
|
||||||
pageTransition.classList.add('opacity-100');
|
pageTransition.classList.add('opacity-100');
|
||||||
|
|
||||||
// Navigate after transition effect
|
// Navigate after transition effect
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = targetHref;
|
window.location.href = targetHref;
|
||||||
@@ -422,13 +520,13 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on first load
|
// Initialize on first load
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
||||||
|
|
||||||
// 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', setupSPATransitions);
|
document.addEventListener('astro:page-load', setupSPATransitions);
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
// For compatibility with custom transition system
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
document.addEventListener('page-transition-complete', setupSPATransitions);
|
||||||
</script>
|
</script>
|
||||||
@@ -439,51 +537,55 @@ const allTags = [...new Set(posts.flatMap(post => post.tags || []))].slice(0, 5)
|
|||||||
--theme-transition-duration: 0.5s;
|
--theme-transition-duration: 0.5s;
|
||||||
--theme-transition-timing: ease;
|
--theme-transition-timing: ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(html), :global(body) {
|
:global(html),
|
||||||
|
:global(body) {
|
||||||
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
|
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.theme-transition-all) {
|
:global(.theme-transition-all) {
|
||||||
transition: all var(--theme-transition-duration) var(--theme-transition-timing);
|
transition: all var(--theme-transition-duration) var(--theme-transition-timing);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.theme-transition-bg) {
|
:global(.theme-transition-bg) {
|
||||||
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
|
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.theme-transition-color) {
|
:global(.theme-transition-color) {
|
||||||
transition: color var(--theme-transition-duration) var(--theme-transition-timing);
|
transition: color var(--theme-transition-duration) var(--theme-transition-timing);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure transitions apply to all theme-related properties */
|
/* Ensure transitions apply to all theme-related properties */
|
||||||
:global(*) {
|
:global(*) {
|
||||||
transition-property: background-color, border-color, color, fill, stroke, opacity;
|
transition-property: background-color, border-color, color, fill, stroke, opacity;
|
||||||
transition-duration: var(--theme-transition-duration);
|
transition-duration: var(--theme-transition-duration);
|
||||||
transition-timing-function: var(--theme-transition-timing);
|
transition-timing-function: var(--theme-transition-timing);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove the forced transition disabling which causes flickering */
|
/* Remove the forced transition disabling which causes flickering */
|
||||||
:global(.theme-switching), :global(.theme-switching *) {
|
:global(.theme-switching),
|
||||||
|
:global(.theme-switching *) {
|
||||||
/* Use a subtle transition instead of none */
|
/* Use a subtle transition instead of none */
|
||||||
transition-duration: 0.3s !important;
|
transition-duration: 0.3s !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Content reveal animations */
|
/* Content reveal animations */
|
||||||
.hero-text span,
|
.hero-text span,
|
||||||
.hero-text + p,
|
.hero-text + p,
|
||||||
.hero-text ~ div,
|
.hero-text ~ div,
|
||||||
article.group,
|
article.group,
|
||||||
a.group.flex.flex-col {
|
a.group.flex.flex-col {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
transition: opacity 0.8s ease, transform 0.8s ease;
|
transition:
|
||||||
|
opacity 0.8s ease,
|
||||||
|
transform 0.8s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-reveal {
|
.animate-reveal {
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
transform: translateY(0) !important;
|
transform: translateY(0) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Rest of your existing styles... */
|
/* Rest of your existing styles... */
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,17 +1,17 @@
|
|||||||
import rss from '@astrojs/rss';
|
import rss from '@astrojs/rss';
|
||||||
|
|
||||||
import directus from "../../lib/directus"
|
import directus from '../../lib/directus';
|
||||||
import { readItems,readSingleton } from "@directus/sdk";
|
import { readItems, readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
export async function GET(context: any) {
|
export async function GET(context: any) {
|
||||||
const global = await directus.request(readSingleton("global"));
|
const global = await directus.request(readSingleton('global'));
|
||||||
const posts = await directus.request(
|
const posts = await directus.request(
|
||||||
readItems("posts", {
|
readItems('posts', {
|
||||||
fields: ['*'],
|
fields: ['*'],
|
||||||
sort: ["-published_date"],
|
sort: ['-published_date'],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
return rss({
|
return rss({
|
||||||
title: `${global.name}`,
|
title: `${global.name}`,
|
||||||
description: `${global.description}`,
|
description: `${global.description}`,
|
||||||
|
@@ -2,24 +2,26 @@
|
|||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
import FormattedDate from '../../components/FormattedDate.astro';
|
import FormattedDate from '../../components/FormattedDate.astro';
|
||||||
|
|
||||||
import directus from "../../../lib/directus"
|
import directus from '../../../lib/directus';
|
||||||
import { readItems } from "@directus/sdk";
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await directus.request(readItems("posts", {
|
const posts = await directus.request(
|
||||||
fields: ['*'],
|
readItems('posts', {
|
||||||
}));
|
fields: ['*'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// Get all unique tags
|
// 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
|
||||||
return uniqueTags.map(tag => {
|
return uniqueTags.map((tag) => {
|
||||||
// Make tag matching case-insensitive
|
// Make tag matching case-insensitive
|
||||||
const filteredPosts = posts.filter(post =>
|
const filteredPosts = posts.filter(
|
||||||
post.tags?.some(t => t.toLowerCase() === (tag as string).toLowerCase()) // Explicitly cast tag to string
|
(post) => post.tags?.some((t) => t.toLowerCase() === (tag as string).toLowerCase()) // Explicitly cast tag to string
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
params: { tag },
|
params: { tag },
|
||||||
@@ -33,184 +35,295 @@ const { posts = [] } = Astro.props;
|
|||||||
|
|
||||||
console.log(`Tag: ${tag}, Number of posts: ${posts.length}`);
|
console.log(`Tag: ${tag}, Number of posts: ${posts.length}`);
|
||||||
|
|
||||||
const sortedPosts = posts && posts.length > 0
|
const sortedPosts =
|
||||||
? [...posts].sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf())
|
posts && posts.length > 0
|
||||||
: [];
|
? [...posts].sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf())
|
||||||
|
: [];
|
||||||
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 tagHue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
|
||||||
const relatedTags = [...new Set(
|
const relatedTags = [
|
||||||
sortedPosts.flatMap(post => post.tags || [])
|
...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)),
|
||||||
.filter(t => t !== tag)
|
].slice(0, 5);
|
||||||
)].slice(0, 5);
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title={`Posts tagged with "${tag}"`}>
|
<BaseLayout title={`Posts tagged with "${tag}"`}>
|
||||||
<div class="max-w-5xl mx-auto px-4 py-10 sm:py-16">
|
<div class="mx-auto max-w-5xl px-4 py-10 sm:py-16">
|
||||||
<!-- Header section -->
|
<!-- Header section -->
|
||||||
<div class="relative mb-10 sm:mb-16">
|
<div class="relative mb-10 sm:mb-16">
|
||||||
<div class="absolute -top-20 -left-20 w-48 sm:w-64 h-48 sm:h-64 bg-zinc-100 dark:bg-zinc-900/30 rounded-full blur-3xl opacity-30 animate-blob"></div>
|
<div
|
||||||
<div class="absolute -bottom-10 -right-10 w-36 sm:w-48 h-36 sm:h-48 bg-zinc-200 dark:bg-zinc-900/20 rounded-full blur-2xl opacity-20 animate-blob animation-delay-2000"></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"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="animate-blob animation-delay-2000 absolute -right-10 -bottom-10 h-36 w-36 rounded-full bg-zinc-200 opacity-20 blur-2xl sm:h-48 sm:w-48 dark:bg-zinc-900/20"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="relative text-center sm:text-left">
|
<div class="relative text-center sm:text-left">
|
||||||
<a href="/tags" class="inline-flex items-center gap-2 text-sm font-medium text-zinc-600 dark:text-zinc-400 hover:text-zinc-900 dark:hover:text-zinc-100 transition-colors mb-4 group">
|
<a
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 transition-transform duration-300 group-hover:-translate-x-1">
|
href="/tags"
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18" />
|
class="group mb-4 inline-flex items-center gap-2 text-sm font-medium text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-4 w-4 transition-transform duration-300 group-hover:-translate-x-1"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Back to all topics</span>
|
<span>Back to all topics</span>
|
||||||
<span class="block max-w-0 group-hover:max-w-full transition-all duration-300 h-0.5 bg-zinc-300 dark:bg-zinc-700"></span>
|
<span
|
||||||
|
class="block h-0.5 max-w-0 bg-zinc-300 transition-all duration-300 group-hover:max-w-full dark:bg-zinc-700"
|
||||||
|
></span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row sm:items-center gap-4 mb-2 justify-center sm:justify-start">
|
<div
|
||||||
<div class="tag-icon flex items-center justify-center w-12 h-12 rounded-xl bg-zinc-100 dark:bg-zinc-800 shadow-sm mx-auto sm:mx-0">
|
class="mb-2 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:justify-start"
|
||||||
<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 text-zinc-700 dark:text-zinc-300">
|
>
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" />
|
<div
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
|
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
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-6 w-6 text-zinc-700 dark:text-zinc-300"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
|
||||||
|
></path>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h1 class="text-3xl sm:text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100">
|
<h1
|
||||||
|
class="text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl dark:text-zinc-100"
|
||||||
|
>
|
||||||
<span class="relative">
|
<span class="relative">
|
||||||
#{tag}
|
#{tag}
|
||||||
<span class="absolute -bottom-1 left-0 w-full h-1 bg-zinc-200 dark:bg-zinc-700"></span>
|
<span class="absolute -bottom-1 left-0 h-1 w-full bg-zinc-200 dark:bg-zinc-700"
|
||||||
<span class="absolute -bottom-1 left-0 w-1/2 h-1 bg-zinc-900 dark:bg-zinc-100 opacity-70 animate-expand"></span>
|
></span>
|
||||||
|
<span
|
||||||
|
class="animate-expand absolute -bottom-1 left-0 h-1 w-1/2 bg-zinc-900 opacity-70 dark:bg-zinc-100"
|
||||||
|
></span>
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-base sm:text-lg text-zinc-600 dark:text-zinc-400 mt-4 max-w-2xl mx-auto sm:mx-0">
|
<p
|
||||||
Exploring <span class="font-medium text-zinc-900 dark:text-zinc-100">{sortedPosts.length}</span> articles tagged with <span class="font-medium text-zinc-900 dark:text-zinc-100">"{tag}"</span>
|
class="mx-auto mt-4 max-w-2xl text-base text-zinc-600 sm:mx-0 sm:text-lg dark:text-zinc-400"
|
||||||
|
>
|
||||||
|
Exploring <span class="font-medium text-zinc-900 dark:text-zinc-100"
|
||||||
|
>{sortedPosts.length}</span
|
||||||
|
> articles tagged with <span class="font-medium text-zinc-900 dark:text-zinc-100"
|
||||||
|
>"{tag}"</span
|
||||||
|
>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Related tags section -->
|
<!-- Related tags section -->
|
||||||
{relatedTags.length > 0 && (
|
{
|
||||||
<div class="mb-8 sm:mb-12 overflow-x-auto pb-4 hide-scrollbar">
|
relatedTags.length > 0 && (
|
||||||
<h2 class="text-lg font-medium text-zinc-900 dark:text-zinc-100 mb-3 text-center sm:text-left">Related topics</h2>
|
<div class="hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12">
|
||||||
<div class="flex gap-2 flex-nowrap justify-center sm:justify-start">
|
<h2 class="mb-3 text-center text-lg font-medium text-zinc-900 sm:text-left dark:text-zinc-100">
|
||||||
{relatedTags.map(relatedTag => (
|
Related topics
|
||||||
<a
|
</h2>
|
||||||
href={`/topics/${relatedTag}`}
|
<div class="flex flex-nowrap justify-center gap-2 sm:justify-start">
|
||||||
class="flex-shrink-0 inline-flex items-center rounded-full px-3 py-1.5 text-sm font-medium bg-zinc-100 text-zinc-900 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 transition-colors"
|
{relatedTags.map((relatedTag) => (
|
||||||
>
|
<a
|
||||||
#{relatedTag}
|
href={`/topics/${relatedTag}`}
|
||||||
</a>
|
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}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
}
|
||||||
|
|
||||||
<!-- Posts list -->
|
<!-- Posts list -->
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<div class="absolute inset-0 bg-grid-pattern opacity-5 dark:opacity-10 pointer-events-none"></div>
|
<div class="bg-grid-pattern pointer-events-none absolute inset-0 opacity-5 dark:opacity-10">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="relative space-y-6 sm:space-y-8">
|
<div class="relative space-y-6 sm:space-y-8">
|
||||||
{sortedPosts.map((post) => (
|
{
|
||||||
<article class="group relative flex flex-col p-5 sm:p-8 rounded-2xl border border-zinc-200 dark:border-zinc-800 hover:bg-zinc-50/80 dark:hover:bg-zinc-900/50 transition-all duration-300 hover:shadow-md hover-card max-w-2xl mx-auto sm:mx-0">
|
sortedPosts.map((post) => (
|
||||||
<div class="absolute inset-0 bg-gradient-to-br from-zinc-50/0 to-zinc-100/0 dark:from-zinc-900/0 dark:to-zinc-800/0 opacity-0 group-hover:opacity-100 transition-opacity duration-500 rounded-2xl"></div>
|
<article class="hover-card group relative mx-auto flex max-w-2xl flex-col rounded-2xl border border-zinc-200 p-5 transition-all duration-300 hover:bg-zinc-50/80 hover:shadow-md sm:mx-0 sm:p-8 dark:border-zinc-800 dark:hover:bg-zinc-900/50">
|
||||||
|
<div class="absolute inset-0 rounded-2xl bg-gradient-to-br from-zinc-50/0 to-zinc-100/0 opacity-0 transition-opacity duration-500 group-hover:opacity-100 dark:from-zinc-900/0 dark:to-zinc-800/0" />
|
||||||
|
|
||||||
<div class="flex flex-col sm:flex-row gap-5 sm:gap-6">
|
<div class="flex flex-col gap-5 sm:flex-row sm:gap-6">
|
||||||
{post.image && (
|
{post.image && (
|
||||||
<div class="flex-shrink-0 w-full sm:w-56 h-40 rounded-xl overflow-hidden shadow-sm group-hover:shadow-md transition-all duration-300 mx-auto sm:mx-0">
|
<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}
|
||||||
class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105"
|
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<div class="flex flex-wrap items-center text-xs sm:text-sm text-zinc-500 dark:text-zinc-400 gap-3 sm:gap-4 mb-2 sm:mb-3 justify-center sm:justify-start">
|
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400">
|
||||||
{post.published_date && (
|
{post.published_date && (
|
||||||
<time datetime={post.published_date.toLocaleString()} class="flex items-center gap-1.5">
|
<time
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3.5 h-3.5 sm:w-4 sm:h-4">
|
datetime={post.published_date.toLocaleString()}
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0
|
class="flex items-center gap-1.5"
|
||||||
A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />
|
>
|
||||||
</svg>
|
<svg
|
||||||
<FormattedDate date={post.published_date} />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</time>
|
fill="none"
|
||||||
)}
|
viewBox="0 0 24 24"
|
||||||
</div>
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-3.5 w-3.5 sm:h-4 sm:w-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0
|
||||||
|
A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<FormattedDate date={post.published_date} />
|
||||||
|
</time>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="text-xl sm:text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2 sm:mb-3 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors text-center sm:text-left">
|
<h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 sm:mb-3 sm:text-left sm:text-2xl dark:text-zinc-100 dark:group-hover:text-zinc-300">
|
||||||
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
|
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
|
||||||
{post.title}
|
{post.title}
|
||||||
</a>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<p class="text-sm sm:text-base text-zinc-600 dark:text-zinc-400 mb-4 line-clamp-2 sm:line-clamp-3 text-center sm:text-left">
|
|
||||||
{post.description}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-center sm:justify-between items-end mt-4 pt-4 border-t border-zinc-100 dark:border-zinc-800">
|
|
||||||
{post.tags && post.tags.length > 0 && (
|
|
||||||
<div class="flex flex-wrap gap-2 mb-3 sm:mb-0 justify-center sm:justify-start">
|
|
||||||
{post.tags.slice(0, 3).map(postTag => (
|
|
||||||
<a
|
|
||||||
href={`/topics/${postTag}`}
|
|
||||||
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${
|
|
||||||
postTag === tag
|
|
||||||
? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100'
|
|
||||||
: 'bg-zinc-100 text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
#{postTag}
|
|
||||||
</a>
|
</a>
|
||||||
))}
|
</h2>
|
||||||
{post.tags.length > 3 && (
|
|
||||||
<span class="inline-flex items-center rounded-full bg-zinc-50 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
|
|
||||||
+{post.tags.length - 3}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div class="mx-auto sm:ml-auto sm:mr-0">
|
<p class="mb-4 line-clamp-2 text-center text-sm text-zinc-600 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400">
|
||||||
<a
|
{post.description}
|
||||||
href={`/blog/${post.slug}/`}
|
</p>
|
||||||
class="inline-flex items-center text-sm font-medium text-zinc-700 dark:text-zinc-300 group-hover:text-zinc-900 dark:group-hover:text-zinc-100 transition-colors"
|
</div>
|
||||||
aria-hidden="true"
|
|
||||||
tabindex="-1"
|
|
||||||
>
|
|
||||||
<span class="relative overflow-hidden inline-block">
|
|
||||||
<span class="block transition-transform duration-300 group-hover:-translate-y-full">Read article</span>
|
|
||||||
<span class="absolute top-0 left-0 translate-y-full group-hover:translate-y-0 transition-transform duration-300 whitespace-nowrap">Explore now</span>
|
|
||||||
</span>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4 ml-1 transition-transform duration-300 group-hover:translate-x-1">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3" />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</article>
|
<div class="mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800">
|
||||||
))}
|
{post.tags && post.tags.length > 0 && (
|
||||||
|
<div class="mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start">
|
||||||
|
{post.tags.slice(0, 3).map((postTag) => (
|
||||||
|
<a
|
||||||
|
href={`/topics/${postTag}`}
|
||||||
|
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${
|
||||||
|
postTag === tag
|
||||||
|
? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100'
|
||||||
|
: 'bg-zinc-100 text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
#{postTag}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
{post.tags.length > 3 && (
|
||||||
|
<span class="inline-flex items-center rounded-full bg-zinc-50 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
|
||||||
|
+{post.tags.length - 3}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div class="mx-auto sm:mr-0 sm:ml-auto">
|
||||||
|
<a
|
||||||
|
href={`/blog/${post.slug}/`}
|
||||||
|
class="inline-flex items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 dark:text-zinc-300 dark:group-hover:text-zinc-100"
|
||||||
|
aria-hidden="true"
|
||||||
|
tabindex="-1"
|
||||||
|
>
|
||||||
|
<span class="relative inline-block overflow-hidden">
|
||||||
|
<span class="block transition-transform duration-300 group-hover:-translate-y-full">
|
||||||
|
Read article
|
||||||
|
</span>
|
||||||
|
<span class="absolute top-0 left-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
|
||||||
|
Explore now
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty state với màu zinc -->
|
<!-- Empty state với màu zinc -->
|
||||||
{sortedPosts.length === 0 && (
|
{
|
||||||
<div class="text-center py-12 sm:py-20">
|
sortedPosts.length === 0 && (
|
||||||
<div class="inline-flex items-center justify-center w-16 h-16 sm:w-20 sm:h-20 rounded-full bg-zinc-100 dark:bg-zinc-800 mb-4 sm:mb-6">
|
<div class="py-12 text-center sm:py-20">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 h-8 sm:w-10 sm:h-10 text-zinc-500 dark:text-zinc-400">
|
<div class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 sm:mb-6 sm:h-20 sm:w-20 dark:bg-zinc-800">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
<svg
|
||||||
</svg>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-8 w-8 text-zinc-500 sm:h-10 sm:w-10 dark:text-zinc-400"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h2 class="mb-2 text-xl font-semibold text-zinc-900 sm:text-2xl dark:text-zinc-100">
|
||||||
|
No posts found
|
||||||
|
</h2>
|
||||||
|
<p class="text-zinc-600 dark:text-zinc-400">There are no posts with this tag yet.</p>
|
||||||
|
<a
|
||||||
|
href="/blog"
|
||||||
|
class="mt-6 inline-flex items-center gap-2 rounded-md bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-800 transition-all duration-300 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="h-4 w-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>Browse all articles</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<h2 class="text-xl sm:text-2xl font-semibold text-zinc-900 dark:text-zinc-100 mb-2">No posts found</h2>
|
)
|
||||||
<p class="text-zinc-600 dark:text-zinc-400">There are no posts with this tag yet.</p>
|
}
|
||||||
<a href="/blog" class="inline-flex items-center gap-2 mt-6 px-4 py-2 rounded-md bg-zinc-100 dark:bg-zinc-800 text-zinc-800 dark:text-zinc-200 hover:bg-zinc-200 dark:hover:bg-zinc-700 transition-all duration-300 text-sm font-medium">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75" />
|
|
||||||
</svg>
|
|
||||||
<span>Browse all articles</span>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
@@ -237,8 +350,12 @@ const relatedTags = [...new Set(
|
|||||||
|
|
||||||
/* Animated underline */
|
/* Animated underline */
|
||||||
@keyframes expand {
|
@keyframes expand {
|
||||||
from { width: 0; }
|
from {
|
||||||
to { width: 50%; }
|
width: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-expand {
|
.animate-expand {
|
||||||
@@ -272,7 +389,10 @@ const relatedTags = [...new Set(
|
|||||||
/* Hover card effect */
|
/* Hover card effect */
|
||||||
.hover-card {
|
.hover-card {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
transition: transform 0.3s ease, box-shadow 0.3s ease, background-color 0.3s ease;
|
transition:
|
||||||
|
transform 0.3s ease,
|
||||||
|
box-shadow 0.3s ease,
|
||||||
|
background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
@@ -308,11 +428,13 @@ const relatedTags = [...new Set(
|
|||||||
// Handle SPA transitions for tag pages
|
// Handle SPA transitions for tag pages
|
||||||
function setupSPATransitions() {
|
function setupSPATransitions() {
|
||||||
// Handle all internal links for SPA transitions
|
// Handle all internal links for SPA transitions
|
||||||
document.querySelectorAll('a[href^="/"]').forEach(link => {
|
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
||||||
// Skip links that are anchor links, external links, or already processed
|
// Skip links that are anchor links, external links, or already processed
|
||||||
if (link.getAttribute('href').includes('#') ||
|
if (
|
||||||
link.getAttribute('target') === '_blank' ||
|
link.getAttribute('href').includes('#') ||
|
||||||
link.hasAttribute('data-spa-handled')) {
|
link.getAttribute('target') === '_blank' ||
|
||||||
|
link.hasAttribute('data-spa-handled')
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,25 +472,34 @@ const relatedTags = [...new Set(
|
|||||||
// Animate header elements
|
// Animate header elements
|
||||||
const headerElements = document.querySelectorAll('h1, .tag-icon, .tag-description');
|
const headerElements = document.querySelectorAll('h1, .tag-icon, .tag-description');
|
||||||
headerElements.forEach((el, index) => {
|
headerElements.forEach((el, index) => {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
el.classList.add('animate-reveal');
|
() => {
|
||||||
}, 100 + (index * 150));
|
el.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
100 + index * 150
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate posts with staggered delay
|
// Animate posts with staggered delay
|
||||||
const articles = document.querySelectorAll('article');
|
const articles = document.querySelectorAll('article');
|
||||||
articles.forEach((article, index) => {
|
articles.forEach((article, index) => {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
article.classList.add('animate-reveal');
|
() => {
|
||||||
}, 400 + (index * 100));
|
article.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
400 + index * 100
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate related tags
|
// Animate related tags
|
||||||
const relatedTags = document.querySelectorAll('.related-tags a');
|
const relatedTags = document.querySelectorAll('.related-tags a');
|
||||||
relatedTags.forEach((tag, index) => {
|
relatedTags.forEach((tag, index) => {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
tag.classList.add('animate-reveal');
|
() => {
|
||||||
}, 600 + (index * 50));
|
tag.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
600 + index * 50
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,4 +518,3 @@ const relatedTags = [...new Set(
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Add this at the end of your page -->
|
<!-- Add this at the end of your page -->
|
||||||
</BaseLayout>
|
|
@@ -1,110 +1,147 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../../layouts/BaseLayout.astro';
|
import BaseLayout from '../../layouts/BaseLayout.astro';
|
||||||
|
|
||||||
import directus from "../../../lib/directus"
|
import directus from '../../../lib/directus';
|
||||||
import { readItems } from "@directus/sdk";
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
const posts = await directus.request(
|
const posts = await directus.request(
|
||||||
readItems("posts", {
|
readItems('posts', {
|
||||||
fields: ['*'],
|
fields: ['*'],
|
||||||
sort: ["-published_date"],
|
sort: ['-published_date'],
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
const tags = [...new Set(posts.flatMap(post => post.tags || []))].sort();
|
const tags = [...new Set(posts.flatMap((post) => post.tags || []))].sort();
|
||||||
|
|
||||||
// Count posts for each tag and create tag objects with additional data
|
// Count posts for each tag and create tag objects with additional data
|
||||||
const tagObjects = tags.map(tag => {
|
const tagObjects = tags.map((tag) => {
|
||||||
const count = posts.filter(post => post.tags?.includes(tag)).length;
|
const count = posts.filter((post) => post.tags?.includes(tag)).length;
|
||||||
// Generate a consistent but random-looking hue for each tag
|
// Generate a consistent but random-looking hue for each tag
|
||||||
const hue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
|
const hue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
|
||||||
return {
|
return {
|
||||||
name: tag,
|
name: tag,
|
||||||
count,
|
count,
|
||||||
size: Math.max(1, Math.min(3, Math.floor(count / 2) + 1)), // Size 1-3 based on count
|
size: Math.max(1, Math.min(3, Math.floor(count / 2) + 1)), // Size 1-3 based on count
|
||||||
hue
|
hue,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="Explore Tags">
|
<BaseLayout title="Explore Tags">
|
||||||
<div class="w-full mx-auto px-3 sm:px-6 py-6 sm:py-12 md:py-16 theme-transition-all">
|
<div class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16">
|
||||||
<!-- Enhanced header section with animated elements - improved for mobile -->
|
<!-- Enhanced header section with animated elements - improved for mobile -->
|
||||||
<div class="relative mb-8 sm:mb-12 md:mb-16 text-center theme-transition-element">
|
<div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16">
|
||||||
<div class="absolute -top-16 -left-16 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-100 dark:bg-zinc-800/50 rounded-full blur-3xl opacity-50 animate-blob theme-transition-bg"></div>
|
<div
|
||||||
<div class="absolute -bottom-16 -right-16 w-36 sm:w-48 md:w-72 h-36 sm:h-48 md:h-72 bg-zinc-200 dark:bg-zinc-800/30 rounded-full blur-3xl opacity-30 animate-blob animation-delay-2000 theme-transition-bg"></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"
|
||||||
<div class="absolute top-8 right-8 w-24 sm:w-32 md:w-40 h-24 sm:h-32 md:h-40 bg-zinc-100/30 dark:bg-zinc-700/20 rounded-full blur-2xl opacity-40 animate-blob animation-delay-4000 theme-transition-bg"></div>
|
>
|
||||||
|
</div>
|
||||||
<h1 class="relative text-3xl sm:text-4xl md:text-5xl lg:text-6xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 mb-3 sm:mb-4 md:mb-6 theme-transition-color">
|
<div
|
||||||
<span class="inline-block relative">
|
class="animate-blob animation-delay-2000 theme-transition-bg absolute -right-16 -bottom-16 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl sm:h-48 sm:w-48 md:h-72 md:w-72 dark:bg-zinc-800/30"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="animate-blob animation-delay-4000 theme-transition-bg absolute top-8 right-8 h-24 w-24 rounded-full bg-zinc-100/30 opacity-40 blur-2xl sm:h-32 sm:w-32 md:h-40 md:w-40 dark:bg-zinc-700/20"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1
|
||||||
|
class="theme-transition-color relative mb-3 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-4 sm:text-4xl md:mb-6 md:text-5xl lg:text-6xl dark:text-zinc-100"
|
||||||
|
>
|
||||||
|
<span class="relative inline-block">
|
||||||
<span class="relative inline-block">
|
<span class="relative inline-block">
|
||||||
<span class="absolute -inset-1 rounded-lg bg-gradient-to-r from-zinc-200/50 to-zinc-300/50 dark:from-zinc-800/50 dark:to-zinc-700/50 blur-sm theme-transition-bg"></span>
|
<span
|
||||||
|
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 class="relative">Explore</span>
|
<span class="relative">Explore</span>
|
||||||
</span>
|
</span>
|
||||||
{" "}
|
{' '}
|
||||||
<span class="relative inline-block">
|
<span class="relative inline-block">
|
||||||
Topics
|
Topics
|
||||||
<span class="absolute -bottom-1 sm:-bottom-2 left-0 w-full h-0.5 sm:h-1 bg-gradient-to-r from-zinc-400 to-zinc-600 dark:from-zinc-600 dark:to-zinc-400 transform origin-left animate-underline theme-transition-bg"></span>
|
<span
|
||||||
|
class="animate-underline theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-full origin-left transform bg-gradient-to-r from-zinc-400 to-zinc-600 sm:-bottom-2 sm:h-1 dark:from-zinc-600 dark:to-zinc-400"
|
||||||
|
></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p class="relative text-sm sm:text-base md:text-lg lg:text-xl text-zinc-600 dark:text-zinc-400 max-w-2xl mx-auto theme-transition-color">
|
<p
|
||||||
|
class="theme-transition-color relative mx-auto max-w-2xl text-sm text-zinc-600 sm:text-base md:text-lg lg:text-xl dark:text-zinc-400"
|
||||||
|
>
|
||||||
Discover content organized by your interests
|
Discover content organized by your interests
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{tags.length === 0 ? (
|
{
|
||||||
<div class="text-center py-8 sm:py-12 md:py-16 theme-transition-element">
|
tags.length === 0 ? (
|
||||||
<div class="inline-flex items-center justify-center w-16 sm:w-20 md:w-24 h-16 sm:h-20 md:h-24 rounded-full bg-zinc-100 dark:bg-zinc-800 mb-3 sm:mb-4 md:mb-6 shadow-inner theme-transition-bg">
|
<div class="theme-transition-element py-8 text-center sm:py-12 md:py-16">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-8 sm:w-10 md:w-12 h-8 sm:h-10 md:h-12 text-zinc-500 dark:text-zinc-400 theme-transition-color">
|
<div class="theme-transition-bg mb-3 inline-flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 shadow-inner sm:mb-4 sm:h-20 sm:w-20 md:mb-6 md:h-24 md:w-24 dark:bg-zinc-800">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z" />
|
<svg
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
</svg>
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="theme-transition-color h-8 w-8 text-zinc-500 sm:h-10 sm:w-10 md:h-12 md:w-12 dark:text-zinc-400"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
|
||||||
|
/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<p class="theme-transition-color text-lg font-medium text-zinc-800 sm:text-xl md:text-2xl dark:text-zinc-200">
|
||||||
|
No tags found yet.
|
||||||
|
</p>
|
||||||
|
<p class="theme-transition-color mt-2 text-xs text-zinc-500 sm:text-sm md:text-base dark:text-zinc-500">
|
||||||
|
Check back later for categorized content.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-lg sm:text-xl md:text-2xl font-medium text-zinc-800 dark:text-zinc-200 theme-transition-color">No tags found yet.</p>
|
) : (
|
||||||
<p class="mt-2 text-xs sm:text-sm md:text-base text-zinc-500 dark:text-zinc-500 theme-transition-color">Check back later for categorized content.</p>
|
<div class="flex w-full justify-center">
|
||||||
</div>
|
<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="flex justify-center w-full">
|
<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" />
|
||||||
<!-- Featured Tags Section - ultra-responsive design -->
|
<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="tag-cloud relative p-3 sm:p-4 md:p-6 lg:p-8 rounded-lg sm:rounded-xl md:rounded-2xl lg:rounded-3xl border border-zinc-100 dark:border-zinc-800 bg-white/50 dark:bg-zinc-900/50 backdrop-blur-sm hover-3d glass theme-transition-all w-full">
|
|
||||||
<div class="absolute inset-0 bg-grid-pattern opacity-5 dark:opacity-10 theme-transition-bg"></div>
|
<h2 class="theme-transition-color mb-3 text-center text-lg font-bold text-zinc-900 sm:mb-4 sm:text-xl md:mb-6 md:text-2xl lg:mb-8 lg:text-3xl dark:text-zinc-100">
|
||||||
<div class="absolute -top-8 -right-8 w-20 sm:w-24 md:w-32 lg:w-40 h-20 sm:h-24 md:h-32 lg:h-40 bg-gradient-to-br from-zinc-200/30 to-zinc-300/20 dark:from-zinc-700/20 dark:to-zinc-800/10 rounded-full blur-xl theme-transition-bg"></div>
|
Popular Topics
|
||||||
<div class="absolute -bottom-8 -left-8 w-20 sm:w-24 md:w-32 lg:w-40 h-20 sm:h-24 md:h-32 lg:h-40 bg-gradient-to-tl from-zinc-200/30 to-zinc-300/20 dark:from-zinc-700/20 dark:to-zinc-800/10 rounded-full blur-xl theme-transition-bg"></div>
|
</h2>
|
||||||
|
|
||||||
<h2 class="text-lg sm:text-xl md:text-2xl lg:text-3xl font-bold text-zinc-900 dark:text-zinc-100 mb-3 sm:mb-4 md:mb-6 lg:mb-8 text-center theme-transition-color">Popular Topics</h2>
|
<div class="xxxs:grid-cols-2 xxs:grid-cols-2 xs:grid-cols-3 xxxs:gap-2 xxs:gap-2 xs:gap-2 grid w-full grid-cols-2 gap-1.5 sm:grid-cols-3 sm:gap-3 md:grid-cols-4 md:gap-4 lg:grid-cols-5">
|
||||||
|
{sortedTags.map((tag) => (
|
||||||
<!-- Ultra-responsive grid layout with fallbacks -->
|
<a
|
||||||
<div class="grid grid-cols-2 xxxs:grid-cols-2 xxs:grid-cols-2 xs:grid-cols-3 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-1.5 xxxs:gap-2 xxs:gap-2 xs:gap-2 sm:gap-3 md:gap-4 w-full">
|
href={`/topics/${tag.name}`}
|
||||||
{sortedTags.map((tag) => (
|
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"
|
||||||
<a
|
style={`--tag-hue: ${tag.hue};`}
|
||||||
href={`/topics/${tag.name}`}
|
>
|
||||||
class="group relative overflow-hidden rounded-md sm:rounded-lg md:rounded-xl border border-zinc-200 dark:border-zinc-800 transition-all duration-300 hover:shadow-md sm:hover:shadow-lg hover:scale-[1.03] hover:border-zinc-300 dark:hover:border-zinc-700 active:scale-95 theme-transition-element theme-ripple flex-grow min-w-0"
|
<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" />
|
||||||
style={`--tag-hue: ${tag.hue};`}
|
|
||||||
>
|
<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="absolute inset-0 bg-gradient-to-br from-zinc-50/90 to-zinc-100/90 dark:from-zinc-800/90 dark:to-zinc-900/90 opacity-100 group-hover:opacity-95 transition-opacity theme-transition-bg"></div>
|
<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">
|
||||||
<div class="relative px-1.5 xxxs:px-2 xxs:px-2 xs:px-2 sm:px-3 md:px-4 py-1.5 xxxs:py-2 xxs:py-2 xs:py-2 sm:py-3 md:py-4 flex items-center gap-1.5 xxs:gap-2 w-full">
|
#
|
||||||
<div class="flex-shrink-0 flex items-center justify-center w-5 h-5 xxxs:w-6 xxxs:h-6 xxs:w-6 xxs:h-6 xs:w-7 xs:h-7 sm:w-8 sm:h-8 md:w-10 md:h-10 rounded-full bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 group-hover:bg-accent/20 dark:group-hover:bg-accent/20 group-hover:text-accent-dark dark:group-hover:text-accent-light transition-all duration-300 shadow-sm theme-transition-all">
|
</span>
|
||||||
<span class="text-xs xxxs:text-xs xxs:text-xs xs:text-sm sm:text-base md:text-lg font-semibold">#</span>
|
</div>
|
||||||
|
|
||||||
|
<div class="min-w-0 flex-1 overflow-hidden">
|
||||||
|
<h3 class="xxxs:text-xs xxs:text-xs xs:text-xs theme-transition-color truncate text-[10px] font-bold break-words hyphens-auto text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-sm md:text-base dark:text-zinc-100 dark:group-hover:text-zinc-300">
|
||||||
|
{tag.name}
|
||||||
|
</h3>
|
||||||
|
<p class="xxxs:text-[9px] xxs:text-[9px] xs:text-[10px] theme-transition-color truncate text-[8px] text-zinc-500 sm:text-xs md:text-xs dark:text-zinc-400">
|
||||||
|
{tag.count} article{tag.count !== 1 ? 's' : ''}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</a>
|
||||||
<div class="flex-1 min-w-0 overflow-hidden">
|
))}
|
||||||
<h3 class="text-[10px] xxxs:text-xs xxs:text-xs xs:text-xs sm:text-sm md:text-base font-bold text-zinc-900 dark:text-zinc-100 group-hover:text-zinc-700 dark:group-hover:text-zinc-300 transition-colors theme-transition-color break-words hyphens-auto truncate">
|
</div>
|
||||||
{tag.name}
|
|
||||||
</h3>
|
|
||||||
<p class="text-[8px] xxxs:text-[9px] xxs:text-[9px] xs:text-[10px] sm:text-xs md:text-xs text-zinc-500 dark:text-zinc-400 theme-transition-color truncate">{tag.count} article{tag.count !== 1 ? 's' : ''}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)
|
||||||
)}
|
}
|
||||||
</div>
|
</div>
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
@@ -121,46 +158,50 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
|
meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
|
||||||
document.getElementsByTagName('head')[0].appendChild(meta);
|
document.getElementsByTagName('head')[0].appendChild(meta);
|
||||||
} else {
|
} else {
|
||||||
viewport.setAttribute('content', 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no');
|
viewport.setAttribute(
|
||||||
|
'content',
|
||||||
|
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix for horizontal overflow
|
// Fix for horizontal overflow
|
||||||
document.body.style.overflowX = 'hidden';
|
document.body.style.overflowX = 'hidden';
|
||||||
document.documentElement.style.overflowX = 'hidden';
|
document.documentElement.style.overflowX = 'hidden';
|
||||||
document.documentElement.style.width = '100%';
|
document.documentElement.style.width = '100%';
|
||||||
document.body.style.width = '100%';
|
document.body.style.width = '100%';
|
||||||
};
|
};
|
||||||
|
|
||||||
fixViewportWidth();
|
fixViewportWidth();
|
||||||
|
|
||||||
// Adjust tag items based on screen size with extreme precision
|
// Adjust tag items based on screen size with extreme precision
|
||||||
const adjustTagItems = () => {
|
const adjustTagItems = () => {
|
||||||
const tagItems = document.querySelectorAll('.theme-ripple');
|
const tagItems = document.querySelectorAll('.theme-ripple');
|
||||||
const width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
const width =
|
||||||
|
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
|
||||||
const isVerySmall = width < 360;
|
const isVerySmall = width < 360;
|
||||||
const isExtremelySmall = width < 280;
|
const isExtremelySmall = width < 280;
|
||||||
const isMicroScreen = width < 240;
|
const isMicroScreen = width < 240;
|
||||||
|
|
||||||
// Fix container width to match viewport exactly
|
// Fix container width to match viewport exactly
|
||||||
const container = document.querySelector('.tag-cloud');
|
const container = document.querySelector('.tag-cloud');
|
||||||
if (container) {
|
if (container) {
|
||||||
container.style.maxWidth = '100vw';
|
container.style.maxWidth = '100vw';
|
||||||
container.style.width = '100%';
|
container.style.width = '100%';
|
||||||
container.style.boxSizing = 'border-box';
|
container.style.boxSizing = 'border-box';
|
||||||
|
|
||||||
// Remove any margins that might cause overflow
|
// Remove any margins that might cause overflow
|
||||||
container.style.marginLeft = '0';
|
container.style.marginLeft = '0';
|
||||||
container.style.marginRight = '0';
|
container.style.marginRight = '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fix grid width
|
// Fix grid width
|
||||||
const grid = document.querySelector('.grid');
|
const grid = document.querySelector('.grid');
|
||||||
if (grid) {
|
if (grid) {
|
||||||
grid.style.width = '100%';
|
grid.style.width = '100%';
|
||||||
grid.style.maxWidth = '100%';
|
grid.style.maxWidth = '100%';
|
||||||
}
|
}
|
||||||
|
|
||||||
tagItems.forEach(item => {
|
tagItems.forEach((item) => {
|
||||||
// Set appropriate classes based on screen size
|
// Set appropriate classes based on screen size
|
||||||
if (isMicroScreen) {
|
if (isMicroScreen) {
|
||||||
item.classList.add('micro-screen');
|
item.classList.add('micro-screen');
|
||||||
@@ -174,15 +215,15 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
} else {
|
} else {
|
||||||
item.classList.remove('very-small-screen', 'extremely-small-screen', 'micro-screen');
|
item.classList.remove('very-small-screen', 'extremely-small-screen', 'micro-screen');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure text doesn't overflow on small screens
|
// Ensure text doesn't overflow on small screens
|
||||||
const tagName = item.querySelector('h3');
|
const tagName = item.querySelector('h3');
|
||||||
const tagCount = item.querySelector('p');
|
const tagCount = item.querySelector('p');
|
||||||
|
|
||||||
if (tagName) {
|
if (tagName) {
|
||||||
// Set title for accessibility
|
// Set title for accessibility
|
||||||
tagName.title = tagName.textContent.trim();
|
tagName.title = tagName.textContent.trim();
|
||||||
|
|
||||||
// Adjust text length based on screen size
|
// Adjust text length based on screen size
|
||||||
if (isMicroScreen && tagName.textContent.length > 6) {
|
if (isMicroScreen && tagName.textContent.length > 6) {
|
||||||
tagName.dataset.fullText = tagName.textContent;
|
tagName.dataset.fullText = tagName.textContent;
|
||||||
@@ -198,32 +239,32 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
delete tagName.dataset.fullText;
|
delete tagName.dataset.fullText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the tag hue for hover effects
|
// Set the tag hue for hover effects
|
||||||
const hue = item.style.getPropertyValue('--tag-hue');
|
const hue = item.style.getPropertyValue('--tag-hue');
|
||||||
item.style.setProperty('--hover-hue', hue);
|
item.style.setProperty('--hover-hue', hue);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Run on load
|
// Run on load
|
||||||
adjustTagItems();
|
adjustTagItems();
|
||||||
|
|
||||||
// Run on resize with optimized debounce
|
// Run on resize with optimized debounce
|
||||||
let resizeTimer;
|
let resizeTimer;
|
||||||
const handleResize = () => {
|
const handleResize = () => {
|
||||||
if (resizeTimer) {
|
if (resizeTimer) {
|
||||||
window.cancelAnimationFrame(resizeTimer);
|
window.cancelAnimationFrame(resizeTimer);
|
||||||
}
|
}
|
||||||
|
|
||||||
resizeTimer = window.requestAnimationFrame(() => {
|
resizeTimer = window.requestAnimationFrame(() => {
|
||||||
fixViewportWidth();
|
fixViewportWidth();
|
||||||
adjustTagItems();
|
adjustTagItems();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.addEventListener('resize', handleResize);
|
window.addEventListener('resize', handleResize);
|
||||||
window.addEventListener('orientationchange', handleResize);
|
window.addEventListener('orientationchange', handleResize);
|
||||||
|
|
||||||
// Ensure layout is recalculated after page is fully loaded
|
// Ensure layout is recalculated after page is fully loaded
|
||||||
window.addEventListener('load', () => {
|
window.addEventListener('load', () => {
|
||||||
fixViewportWidth();
|
fixViewportWidth();
|
||||||
@@ -234,17 +275,20 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
adjustTagItems();
|
adjustTagItems();
|
||||||
}, 500);
|
}, 500);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fix for iOS Safari and other mobile browsers
|
// Fix for iOS Safari and other mobile browsers
|
||||||
if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) {
|
if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) {
|
||||||
document.documentElement.style.setProperty('--safe-area-inset-bottom', 'env(safe-area-inset-bottom)');
|
document.documentElement.style.setProperty(
|
||||||
|
'--safe-area-inset-bottom',
|
||||||
|
'env(safe-area-inset-bottom)'
|
||||||
|
);
|
||||||
|
|
||||||
// Fix for mobile viewport height issues
|
// Fix for mobile viewport height issues
|
||||||
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`);
|
||||||
};
|
};
|
||||||
|
|
||||||
setVh();
|
setVh();
|
||||||
window.addEventListener('resize', setVh);
|
window.addEventListener('resize', setVh);
|
||||||
window.addEventListener('orientationchange', () => {
|
window.addEventListener('orientationchange', () => {
|
||||||
@@ -255,39 +299,51 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add touch support for mobile devices
|
// Add touch support for mobile devices
|
||||||
const addTouchSupport = () => {
|
const addTouchSupport = () => {
|
||||||
const tagItems = document.querySelectorAll('.theme-ripple');
|
const tagItems = document.querySelectorAll('.theme-ripple');
|
||||||
|
|
||||||
tagItems.forEach(item => {
|
tagItems.forEach((item) => {
|
||||||
item.addEventListener('touchstart', () => {
|
item.addEventListener(
|
||||||
item.classList.add('touch-active');
|
'touchstart',
|
||||||
}, { passive: true });
|
() => {
|
||||||
|
item.classList.add('touch-active');
|
||||||
item.addEventListener('touchend', () => {
|
},
|
||||||
setTimeout(() => {
|
{ passive: true }
|
||||||
item.classList.remove('touch-active');
|
);
|
||||||
}, 150);
|
|
||||||
}, { passive: true });
|
item.addEventListener(
|
||||||
|
'touchend',
|
||||||
|
() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
item.classList.remove('touch-active');
|
||||||
|
}, 150);
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
|
|
||||||
// Cancel active state if touch moves away
|
// Cancel active state if touch moves away
|
||||||
item.addEventListener('touchmove', (e) => {
|
item.addEventListener(
|
||||||
const touch = e.touches[0];
|
'touchmove',
|
||||||
const rect = item.getBoundingClientRect();
|
(e) => {
|
||||||
|
const touch = e.touches[0];
|
||||||
if (
|
const rect = item.getBoundingClientRect();
|
||||||
touch.clientX < rect.left ||
|
|
||||||
touch.clientX > rect.right ||
|
if (
|
||||||
touch.clientY < rect.top ||
|
touch.clientX < rect.left ||
|
||||||
touch.clientY > rect.bottom
|
touch.clientX > rect.right ||
|
||||||
) {
|
touch.clientY < rect.top ||
|
||||||
item.classList.remove('touch-active');
|
touch.clientY > rect.bottom
|
||||||
}
|
) {
|
||||||
}, { passive: true });
|
item.classList.remove('touch-active');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ passive: true }
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
addTouchSupport();
|
addTouchSupport();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@@ -295,9 +351,10 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
<style>
|
<style>
|
||||||
/* Base styles */
|
/* Base styles */
|
||||||
.tag-cloud {
|
.tag-cloud {
|
||||||
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.03),
|
box-shadow:
|
||||||
0 2px 4px rgba(0, 0, 0, 0.03),
|
0 0 0 1px rgba(0, 0, 0, 0.03),
|
||||||
0 4px 8px rgba(0, 0, 0, 0.05);
|
0 2px 4px rgba(0, 0, 0, 0.03),
|
||||||
|
0 4px 8px rgba(0, 0, 0, 0.05);
|
||||||
transform-style: preserve-3d;
|
transform-style: preserve-3d;
|
||||||
perspective: 1000px;
|
perspective: 1000px;
|
||||||
transition: all var(--theme-transition);
|
transition: all var(--theme-transition);
|
||||||
@@ -307,19 +364,20 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
margin-left: 0 !important;
|
margin-left: 0 !important;
|
||||||
margin-right: 0 !important;
|
margin-right: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix for horizontal overflow */
|
/* Fix for horizontal overflow */
|
||||||
:global(html), :global(body) {
|
:global(html),
|
||||||
|
:global(body) {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.max-w-6xl) {
|
:global(.max-w-6xl) {
|
||||||
max-width: 100% !important;
|
max-width: 100% !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ultra-responsive breakpoints for extreme reliability */
|
/* Ultra-responsive breakpoints for extreme reliability */
|
||||||
/* Micro screens (below 240px) */
|
/* Micro screens (below 240px) */
|
||||||
@media (max-width: 239px) {
|
@media (max-width: 239px) {
|
||||||
@@ -329,146 +387,146 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
border-radius: 0.25rem !important;
|
border-radius: 0.25rem !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag-cloud h2 {
|
.tag-cloud h2 {
|
||||||
font-size: 0.875rem !important;
|
font-size: 0.875rem !important;
|
||||||
margin-bottom: 0.375rem !important;
|
margin-bottom: 0.375rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid {
|
.grid {
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
|
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
|
||||||
gap: 0.375rem !important;
|
gap: 0.375rem !important;
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.micro-screen .flex {
|
.micro-screen .flex {
|
||||||
padding: 0.25rem !important;
|
padding: 0.25rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.micro-screen h3 {
|
.micro-screen h3 {
|
||||||
font-size: 0.625rem !important;
|
font-size: 0.625rem !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.micro-screen p {
|
.micro-screen p {
|
||||||
font-size: 0.5rem !important;
|
font-size: 0.5rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extra extra extra small screens (240px-279px) */
|
/* Extra extra extra small screens (240px-279px) */
|
||||||
@media (min-width: 240px) and (max-width: 279px) {
|
@media (min-width: 240px) and (max-width: 279px) {
|
||||||
.xxxs\:grid-cols-2 {
|
.xxxs\:grid-cols-2 {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxxs\:px-2 {
|
.xxxs\:px-2 {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxxs\:py-2 {
|
.xxxs\:py-2 {
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxxs\:w-6 {
|
.xxxs\:w-6 {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxxs\:h-6 {
|
.xxxs\:h-6 {
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxxs\:text-xs {
|
.xxxs\:text-xs {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxxs\:gap-2 {
|
.xxxs\:gap-2 {
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxxs\:text-\[9px\] {
|
.xxxs\:text-\[9px\] {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extra extra small screens (280px-359px) */
|
/* Extra extra small screens (280px-359px) */
|
||||||
@media (min-width: 280px) {
|
@media (min-width: 280px) {
|
||||||
.xxs\:grid-cols-2 {
|
.xxs\:grid-cols-2 {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxs\:px-2 {
|
.xxs\:px-2 {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxs\:py-2 {
|
.xxs\:py-2 {
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxs\:w-6 {
|
.xxs\:w-6 {
|
||||||
width: 1.5rem;
|
width: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxs\:h-6 {
|
.xxs\:h-6 {
|
||||||
height: 1.5rem;
|
height: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxs\:text-xs {
|
.xxs\:text-xs {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxs\:gap-2 {
|
.xxs\:gap-2 {
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xxs\:text-\[9px\] {
|
.xxs\:text-\[9px\] {
|
||||||
font-size: 9px;
|
font-size: 9px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Extra small screens (360px-639px) */
|
/* Extra small screens (360px-639px) */
|
||||||
@media (min-width: 360px) {
|
@media (min-width: 360px) {
|
||||||
.xs\:grid-cols-3 {
|
.xs\:grid-cols-3 {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|
||||||
.xs\:px-2 {
|
.xs\:px-2 {
|
||||||
padding-left: 0.5rem;
|
padding-left: 0.5rem;
|
||||||
padding-right: 0.5rem;
|
padding-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xs\:py-2 {
|
.xs\:py-2 {
|
||||||
padding-top: 0.5rem;
|
padding-top: 0.5rem;
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xs\:w-7 {
|
.xs\:w-7 {
|
||||||
width: 1.75rem;
|
width: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xs\:h-7 {
|
.xs\:h-7 {
|
||||||
height: 1.75rem;
|
height: 1.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xs\:text-xs {
|
.xs\:text-xs {
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xs\:text-sm {
|
.xs\:text-sm {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xs\:gap-2 {
|
.xs\:gap-2 {
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.xs\:text-\[10px\] {
|
.xs\:text-\[10px\] {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure text doesn't overflow on small screens */
|
/* Ensure text doesn't overflow on small screens */
|
||||||
.truncate {
|
.truncate {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -476,7 +534,7 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure proper word breaking for long tag names */
|
/* Ensure proper word breaking for long tag names */
|
||||||
.break-words {
|
.break-words {
|
||||||
word-break: break-word;
|
word-break: break-word;
|
||||||
@@ -486,38 +544,42 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
.hyphens-auto {
|
.hyphens-auto {
|
||||||
hyphens: auto;
|
hyphens: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Improved shadow for dark mode */
|
/* Improved shadow for dark mode */
|
||||||
:global(.dark) .tag-cloud {
|
:global(.dark) .tag-cloud {
|
||||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.05),
|
box-shadow:
|
||||||
0 2px 4px rgba(0, 0, 0, 0.1),
|
0 0 0 1px rgba(255, 255, 255, 0.05),
|
||||||
0 4px 8px rgba(0, 0, 0, 0.15);
|
0 2px 4px rgba(0, 0, 0, 0.1),
|
||||||
|
0 4px 8px rgba(0, 0, 0, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prevent layout shifts */
|
/* Prevent layout shifts */
|
||||||
.flex-grow {
|
.grow {
|
||||||
flex-grow: 1;
|
grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.min-w-0 {
|
.min-w-0 {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure container doesn't overflow */
|
/* Ensure container doesn't overflow */
|
||||||
.overflow-hidden {
|
.overflow-hidden {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Touch support for mobile */
|
/* Touch support for mobile */
|
||||||
.touch-active {
|
.touch-active {
|
||||||
transform: scale(0.97) !important;
|
transform: scale(0.97) !important;
|
||||||
opacity: 0.9;
|
opacity: 0.9;
|
||||||
transition: transform 0.15s ease-in-out, opacity 0.15s ease-in-out !important;
|
transition:
|
||||||
|
transform 0.15s ease-in-out,
|
||||||
|
opacity 0.15s ease-in-out !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animation for blob */
|
/* Animation for blob */
|
||||||
@keyframes blob {
|
@keyframes blob {
|
||||||
0%, 100% {
|
0%,
|
||||||
|
100% {
|
||||||
transform: translate(0, 0) scale(1);
|
transform: translate(0, 0) scale(1);
|
||||||
}
|
}
|
||||||
25% {
|
25% {
|
||||||
@@ -530,19 +592,19 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
transform: translate(-10px, -10px) scale(1.05);
|
transform: translate(-10px, -10px) scale(1.05);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-blob {
|
.animate-blob {
|
||||||
animation: blob 20s infinite ease-in-out;
|
animation: blob 20s infinite ease-in-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-delay-2000 {
|
.animation-delay-2000 {
|
||||||
animation-delay: 2s;
|
animation-delay: 2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.animation-delay-4000 {
|
.animation-delay-4000 {
|
||||||
animation-delay: 4s;
|
animation-delay: 4s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animation for underline */
|
/* Animation for underline */
|
||||||
@keyframes underline {
|
@keyframes underline {
|
||||||
0% {
|
0% {
|
||||||
@@ -552,11 +614,11 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
transform: scaleX(1);
|
transform: scaleX(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.animate-underline {
|
.animate-underline {
|
||||||
animation: underline 1.5s ease-out forwards;
|
animation: underline 1.5s ease-out forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix for iOS Safari notch */
|
/* Fix for iOS Safari notch */
|
||||||
@supports (padding: max(0px)) {
|
@supports (padding: max(0px)) {
|
||||||
.tag-cloud {
|
.tag-cloud {
|
||||||
@@ -571,32 +633,34 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
// Handle SPA transitions for tags index page
|
// Handle SPA transitions for tags index page
|
||||||
function setupSPATransitions() {
|
function setupSPATransitions() {
|
||||||
// Handle all internal links for SPA transitions
|
// Handle all internal links for SPA transitions
|
||||||
document.querySelectorAll('a[href^="/"]').forEach(link => {
|
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
||||||
// Skip links that are anchor links, external links, or already processed
|
// Skip links that are anchor links, external links, or already processed
|
||||||
if (link.getAttribute('href').includes('#') ||
|
if (
|
||||||
link.getAttribute('target') === '_blank' ||
|
link.getAttribute('href').includes('#') ||
|
||||||
link.hasAttribute('data-spa-handled')) {
|
link.getAttribute('target') === '_blank' ||
|
||||||
|
link.hasAttribute('data-spa-handled')
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
// Mark as handled to avoid duplicate listeners
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
link.setAttribute('data-spa-handled', 'true');
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
link.addEventListener('click', (e) => {
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const targetHref = link.getAttribute('href');
|
const targetHref = link.getAttribute('href');
|
||||||
|
|
||||||
// Trigger page transition animation
|
// Trigger page transition animation
|
||||||
const pageTransition = document.getElementById('page-transition');
|
const pageTransition = document.getElementById('page-transition');
|
||||||
if (pageTransition) {
|
if (pageTransition) {
|
||||||
pageTransition.classList.remove('opacity-0');
|
pageTransition.classList.remove('opacity-0');
|
||||||
pageTransition.classList.add('opacity-100');
|
pageTransition.classList.add('opacity-100');
|
||||||
|
|
||||||
// Navigate after transition effect
|
// Navigate after transition effect
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.location.href = targetHref;
|
window.location.href = targetHref;
|
||||||
@@ -607,18 +671,18 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add hover effect for tag cards on touch devices
|
// Add hover effect for tag cards on touch devices
|
||||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||||
|
|
||||||
if (isTouchDevice) {
|
if (isTouchDevice) {
|
||||||
const tagCards = document.querySelectorAll('.tag-cloud a');
|
const tagCards = document.querySelectorAll('.tag-cloud a');
|
||||||
|
|
||||||
tagCards.forEach(card => {
|
tagCards.forEach((card) => {
|
||||||
card.addEventListener('touchstart', () => {
|
card.addEventListener('touchstart', () => {
|
||||||
card.classList.add('is-touched');
|
card.classList.add('is-touched');
|
||||||
});
|
});
|
||||||
|
|
||||||
card.addEventListener('touchend', () => {
|
card.addEventListener('touchend', () => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
card.classList.remove('is-touched');
|
card.classList.remove('is-touched');
|
||||||
@@ -626,23 +690,25 @@ const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Animate tag cards with staggered delay
|
// Animate tag cards with staggered delay
|
||||||
const tagCards = document.querySelectorAll('.tag-cloud a');
|
const tagCards = document.querySelectorAll('.tag-cloud a');
|
||||||
tagCards.forEach((card, index) => {
|
tagCards.forEach((card, index) => {
|
||||||
setTimeout(() => {
|
setTimeout(
|
||||||
card.classList.add('animate-reveal');
|
() => {
|
||||||
}, 100 + (index * 50));
|
card.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
100 + index * 50
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize on first load
|
// Initialize on first load
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
||||||
|
|
||||||
// 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', setupSPATransitions);
|
document.addEventListener('astro:page-load', setupSPATransitions);
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
// For compatibility with custom transition system
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
document.addEventListener('page-transition-complete', setupSPATransitions);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@@ -1,11 +1,13 @@
|
|||||||
/* Remove all the complex mobile menu styles and keep only what's necessary */
|
/* Remove all the complex mobile menu styles and keep only what's necessary */
|
||||||
@tailwind base;
|
@import 'tailwindcss';
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
/* 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;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
--theme-transition: 0.3s ease;
|
--theme-transition: 0.3s ease;
|
||||||
@@ -14,6 +16,7 @@
|
|||||||
html {
|
html {
|
||||||
scroll-behavior: smooth;
|
scroll-behavior: smooth;
|
||||||
scroll-padding-top: 5rem;
|
scroll-padding-top: 5rem;
|
||||||
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@@ -24,10 +27,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Simple theme transition */
|
/* Simple theme transition */
|
||||||
body, a, button {
|
body,
|
||||||
transition: background-color var(--theme-transition),
|
a,
|
||||||
color var(--theme-transition),
|
button {
|
||||||
border-color var(--theme-transition);
|
transition:
|
||||||
|
background-color var(--theme-transition),
|
||||||
|
color var(--theme-transition),
|
||||||
|
border-color var(--theme-transition);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,33 +42,55 @@
|
|||||||
html {
|
html {
|
||||||
scroll-padding-top: 4rem;
|
scroll-padding-top: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Better touch targets on mobile */
|
/* Better touch targets on mobile */
|
||||||
button, a {
|
button,
|
||||||
|
a {
|
||||||
@apply min-h-[44px];
|
@apply min-h-[44px];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Add smooth animations */
|
/* Add smooth animations */
|
||||||
@keyframes fadeIn {
|
@keyframes fadeIn {
|
||||||
from { opacity: 0; }
|
from {
|
||||||
to { opacity: 1; }
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideUp {
|
@keyframes slideUp {
|
||||||
from { transform: translateY(20px); opacity: 0; }
|
from {
|
||||||
to { transform: translateY(0); opacity: 1; }
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes slideDown {
|
@keyframes slideDown {
|
||||||
from { transform: translateY(-20px); opacity: 0; }
|
from {
|
||||||
to { transform: translateY(0); opacity: 1; }
|
transform: translateY(-20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes scaleIn {
|
@keyframes scaleIn {
|
||||||
from { transform: scale(0.95); opacity: 0; }
|
from {
|
||||||
to { transform: scale(1); opacity: 1; }
|
transform: scale(0.95);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: scale(1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply animations to elements */
|
/* Apply animations to elements */
|
||||||
@@ -100,17 +128,21 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Smooth hover transitions */
|
/* Smooth hover transitions */
|
||||||
a, button {
|
a,
|
||||||
transition: all 0.2s ease;
|
button {
|
||||||
|
transition: all 0.5s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover, button:hover {
|
a.hover:hover,
|
||||||
transform: translateY(-1px);
|
button:hover {
|
||||||
}
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
/* Smooth page transitions */
|
/* Smooth page transitions */
|
||||||
.page-transition {
|
.page-transition {
|
||||||
transition: opacity 0.3s ease, transform 0.3s ease;
|
transition:
|
||||||
|
opacity 0.3s ease,
|
||||||
|
transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-entering {
|
.page-entering {
|
||||||
|
@@ -1,4 +1,3 @@
|
|||||||
export function debugObject(obj: any): string {
|
export function debugObject(obj: any): string {
|
||||||
return JSON.stringify(obj, null, 2)
|
return JSON.stringify(obj, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}", "*.{js,ts,jsx,tsx,mdx}"],
|
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}', '*.{js,ts,jsx,tsx,mdx}'],
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
@@ -54,7 +54,5 @@ module.exports = {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [require('@tailwindcss/typography')],
|
||||||
require('@tailwindcss/typography'),
|
|
||||||
],
|
|
||||||
};
|
};
|
||||||
|
@@ -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