Compare commits

...

122 Commits

Author SHA1 Message Date
fcae7676c6 0.11.0 release
All checks were successful
renovate / renovate (push) Successful in 16s
test-build / build (push) Successful in 25s
release-image / release (push) Successful in 1m20s
2025-07-19 22:39:43 -05:00
cc16b5435a update node
All checks were successful
renovate / renovate (push) Successful in 1m16s
test-build / build (push) Successful in 1m26s
2025-07-19 22:37:05 -05:00
27b5e6a36b change layout and animations to be more common with each other 2025-07-19 22:37:05 -05:00
bcb91972a1 Merge pull request 'Update astro monorepo' (#42) from renovate/astro-monorepo into main
All checks were successful
renovate / renovate (push) Successful in 23s
test-build / build (push) Successful in 28s
Reviewed-on: #42
2025-07-20 03:34:37 +00:00
b11666decb Update astro monorepo
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 29s
2025-07-20 00:02:37 +00:00
a947a05041 Merge pull request 'Update dependency eslint-config-prettier to v10.1.8' (#43) from renovate/eslint-config-prettier-10.x into main
All checks were successful
renovate / renovate (push) Successful in 47s
test-build / build (push) Successful in 34s
2025-07-20 00:01:54 +00:00
297c573281 Update dependency eslint-config-prettier to v10.1.8
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 25s
2025-07-20 00:01:31 +00:00
9093594973 Update dependency astro to v5.11.2
All checks were successful
test-build / build (push) Successful in 37s
renovate/stability-days Updates have met minimum release age requirement
renovate / renovate (push) Successful in 21s
test-build / build (pull_request) Successful in 36s
2025-07-18 00:01:29 +00:00
77ce0a1182 Update dependency framer-motion to v12.23.6
All checks were successful
renovate / renovate (push) Successful in 28s
test-build / build (pull_request) Successful in 29s
test-build / build (push) Successful in 40s
renovate/stability-days Updates have met minimum release age requirement
2025-07-16 16:14:52 +00:00
799e6b6090 Merge pull request 'Update typescript-eslint monorepo to v8.37.0' (#38) from renovate/typescript-eslint-monorepo into main
Some checks failed
test-build / build (push) Successful in 26s
process-repository / process-repository (push) Failing after 11s
renovate / renovate (push) Successful in 1m21s
Reviewed-on: #38
2025-07-16 16:14:17 +00:00
735e4b4877 Update typescript-eslint monorepo to v8.37.0
All checks were successful
test-build / build (pull_request) Successful in 27s
renovate/stability-days Updates have met minimum release age requirement
2025-07-16 02:59:27 +00:00
3e12a8647d update tag
All checks were successful
test-build / build (push) Successful in 51s
renovate / renovate (push) Successful in 1m9s
release-image / release (push) Successful in 3m32s
2025-07-15 21:58:20 -05:00
e07210638e add astro native SPA transition 2025-07-15 21:58:20 -05:00
22d5b50f73 formatting and layout 2025-07-15 21:58:20 -05:00
40acf8f34a strip theme transition on load to use early script 2025-07-15 21:58:20 -05:00
543516baba remove SPA scripts 2025-07-15 21:58:20 -05:00
e985f905f2 formatting changes 2025-07-15 21:58:20 -05:00
e1f09ca4ec fix slider 2025-07-15 21:58:20 -05:00
0c09eb38e9 Merge pull request 'Update dependency framer-motion to v12.23.5' (#37) from renovate/framer-motion-12.x-lockfile into main
All checks were successful
renovate / renovate (push) Successful in 41s
test-build / build (push) Successful in 24s
2025-07-16 00:03:06 +00:00
95eeb44e4f Update dependency framer-motion to v12.23.5
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 34s
2025-07-16 00:02:26 +00:00
d47d67572e Merge pull request 'Update dependency astro to v5.11.1' (#36) from renovate/astro-monorepo into main
Some checks failed
renovate / renovate (push) Successful in 59s
test-build / build (push) Has been cancelled
2025-07-16 00:01:41 +00:00
fa4841948a Update dependency astro to v5.11.1
All checks were successful
test-build / build (pull_request) Successful in 30s
renovate/stability-days Updates have met minimum release age requirement
2025-07-16 00:00:52 +00:00
71e2b0185b update tag
Some checks failed
test-build / build (push) Successful in 28s
release-image / release (push) Successful in 3m31s
process-repository / process-repository (push) Failing after 1m7s
renovate / renovate (push) Successful in 1m38s
2025-07-15 01:43:31 -05:00
7f9fb4d2b9 fix scrollbar affecting layout
Some checks failed
renovate / renovate (push) Successful in 29s
test-build / build (push) Has been cancelled
2025-07-15 01:39:40 -05:00
8420c8dd58 fix tech stack slider 2025-07-15 01:37:23 -05:00
fa6ed18edb fix dark mode 2025-07-14 23:18:38 -05:00
30860fce1e change paths
All checks were successful
test-build / build (push) Successful in 30s
renovate / renovate (push) Successful in 21s
2025-07-14 22:31:12 -05:00
b479e0e22c use single workflow script
Some checks failed
process-repository / process-repository (push) Failing after 17s
renovate / renovate (push) Successful in 39s
test-build / build (push) Successful in 41s
2025-07-13 23:43:58 -05:00
cf01ebcd3c Merge pull request 'Update dependency eslint to v9.31.0' (#35) from renovate/eslint-monorepo into main
All checks were successful
test-build / build (push) Successful in 35s
renovate / renovate (push) Successful in 34s
process-pull-requests / process-pull-requests (push) Successful in 10s
process-issues / process-issues (push) Successful in 13s
Reviewed-on: #35
2025-07-13 03:33:05 +00:00
df8ccf81c2 Update dependency eslint to v9.31.0
All checks were successful
test-build / build (pull_request) Successful in 38s
renovate/stability-days Updates have met minimum release age requirement
2025-07-13 00:01:03 +00:00
073911c1b9 use tag ids
Some checks failed
test-build / build (push) Successful in 31s
process-pull-requests / process-pull-requests (push) Failing after 11s
process-issues / process-issues (push) Failing after 10s
renovate / renovate (push) Successful in 58s
2025-07-11 21:47:20 -05:00
3eeea3dd8f use tag ids
All checks were successful
renovate / renovate (push) Successful in 20s
test-build / build (push) Successful in 23s
2025-07-11 21:36:40 -05:00
43fea76778 Update dependency framer-motion to v12.23.3
All checks were successful
test-build / build (pull_request) Successful in 36s
renovate / renovate (push) Successful in 32s
test-build / build (push) Successful in 41s
renovate/stability-days Updates have met minimum release age requirement
2025-07-12 00:01:43 +00:00
d64df6473a Update dependency prettier-plugin-tailwindcss to v0.6.14
Some checks failed
test-build / build (push) Successful in 32s
process-pull-requests / process-pull-requests (push) Successful in 15s
process-issues / process-issues (push) Failing after 9s
renovate / renovate (push) Successful in 1m7s
2025-07-10 20:59:07 +00:00
63a6a00817 Update dependency framer-motion to v12.23.1
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 48s
test-build / build (push) Has been cancelled
renovate / renovate (push) Has been cancelled
2025-07-10 20:57:58 +00:00
54759056b3 update version
All checks were successful
renovate / renovate (push) Successful in 1m21s
test-build / build (push) Successful in 33s
release-image / release (push) Successful in 4m15s
2025-07-10 15:57:02 -05:00
3cc9762e0d Merge pull request 'Update typescript-eslint monorepo to v8.36.0' (#31) from renovate/typescript-eslint-monorepo into main
All checks were successful
renovate / renovate (push) Successful in 26s
test-build / build (push) Successful in 45s
Reviewed-on: #31
2025-07-10 03:28:27 +00:00
ef757c4a14 Update typescript-eslint monorepo to v8.36.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 29s
2025-07-09 00:02:57 +00:00
176f92bf67 Merge pull request 'Update dependency framer-motion to v12.23.0' (#27) from renovate/framer-motion-12.x-lockfile into main
Some checks failed
test-build / build (push) Successful in 34s
process-pull-requests / process-pull-requests (push) Successful in 8s
process-issues / process-issues (push) Failing after 13s
renovate / renovate (push) Successful in 42s
Reviewed-on: #27
2025-07-05 04:54:21 +00:00
09d411dd68 Merge pull request 'Update astro monorepo' (#30) from renovate/astro-monorepo into main
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
Reviewed-on: #30
2025-07-05 04:54:14 +00:00
54acfcb24d Update astro monorepo
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 43s
2025-07-05 00:01:04 +00:00
6f3b631862 Update dependency framer-motion to v12.23.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 1m20s
2025-07-04 00:02:50 +00:00
18cd240a9b Update dependency astro to v5.10.2
Some checks failed
test-build / build (push) Successful in 29s
process-issues / process-issues (push) Failing after 14s
process-pull-requests / process-pull-requests (push) Successful in 15s
renovate / renovate (push) Successful in 1m31s
2025-07-03 00:02:59 +00:00
bb4fe8ef37 Update dependency eslint to v9.30.1
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
renovate / renovate (push) Has been cancelled
test-build / build (pull_request) Successful in 30s
test-build / build (push) Has been cancelled
2025-07-03 00:02:05 +00:00
e0e3c1f61a Update typescript-eslint monorepo to v8.35.1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (push) Successful in 48s
test-build / build (pull_request) Successful in 56s
renovate / renovate (push) Successful in 1m7s
2025-07-02 00:01:05 +00:00
0b5c6ae999 update astro
Some checks failed
test-build / build (push) Successful in 38s
process-pull-requests / process-pull-requests (push) Failing after 10s
process-issues / process-issues (push) Failing after 10s
renovate / renovate (push) Successful in 2m14s
2025-06-28 16:41:12 -05:00
a20ba4ab43 Merge pull request 'Update dependency eslint to v9.30.0' (#25) from renovate/eslint-monorepo into main
All checks were successful
renovate / renovate (push) Successful in 22s
test-build / build (push) Successful in 25s
Reviewed-on: #25
2025-06-28 21:37:27 +00:00
550e7dfe52 Merge pull request 'Update dependency @directus/sdk to v20' (#21) from renovate/directus-sdk-20.x into main
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
Reviewed-on: #21
2025-06-28 21:37:21 +00:00
03174cfb9d Update dependency @directus/sdk to v20
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 31s
2025-06-28 21:36:32 +00:00
da50c1928c Update dependency eslint to v9.30.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 31s
2025-06-28 21:36:25 +00:00
f1d1fe979e change confi
All checks were successful
test-build / build (push) Successful in 26s
renovate / renovate (push) Successful in 1m0s
2025-06-28 16:35:35 -05:00
4d6019d0b0 update node
All checks were successful
test-build / build (push) Successful in 42s
renovate / renovate (push) Successful in 1m7s
2025-06-28 16:33:16 -05:00
7dd302b3d4 Update dependency prettier to v3.6.2
All checks were successful
test-build / build (pull_request) Successful in 34s
renovate/stability-days Updates have met minimum release age requirement
test-build / build (push) Successful in 32s
renovate / renovate (push) Successful in 45s
2025-06-28 05:21:40 +00:00
8a8f2a6216 Update dependency framer-motion to v12.19.2
Some checks are pending
test-build / build (pull_request) Successful in 34s
renovate/stability-days Updates have not met minimum release age requirement
test-build / build (push) Successful in 26s
renovate / renovate (push) Successful in 1m0s
2025-06-28 05:20:17 +00:00
97775f1ceb Merge pull request 'Update typescript-eslint monorepo to v8.35.0' (#19) from renovate/typescript-eslint-monorepo into main
All checks were successful
test-build / build (push) Successful in 25s
renovate / renovate (push) Successful in 1m22s
Reviewed-on: #19
2025-06-28 05:19:32 +00:00
0a437a26f1 Merge pull request 'Update dependency prettier to v3.6.1' (#18) from renovate/prettier-3.x-lockfile into main
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Has been cancelled
Reviewed-on: #18
2025-06-28 05:19:19 +00:00
ba67b4d0e4 Merge pull request 'Update dependency framer-motion to v12.19.1' (#17) from renovate/framer-motion-12.x-lockfile into main
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Has been cancelled
Reviewed-on: #17
2025-06-28 05:19:03 +00:00
0bcfa9bed4 Update typescript-eslint monorepo to v8.35.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 30s
2025-06-28 00:03:22 +00:00
ada95481f7 Update dependency prettier to v3.6.1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 30s
2025-06-28 00:03:06 +00:00
7c9f4acc00 Update dependency framer-motion to v12.19.1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 30s
2025-06-28 00:02:54 +00:00
0b7b87580a Update tailwindcss monorepo to v4.1.11
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 42s
test-build / build (push) Successful in 33s
renovate / renovate (push) Successful in 1m43s
2025-06-28 00:01:05 +00:00
08f076e566 Update dependency prettier-plugin-tailwindcss to v0.6.13
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 1m22s
test-build / build (push) Successful in 42s
renovate / renovate (push) Successful in 21s
2025-06-20 18:18:43 +00:00
26c27b9353 update astro
Some checks failed
test-build / build (push) Successful in 1m20s
process-pull-requests / process-pull-requests (push) Failing after 10s
process-issues / process-issues (push) Failing after 12s
renovate / renovate (push) Successful in 1m37s
2025-06-20 13:17:48 -05:00
ce8b3a2e19 update config
All checks were successful
test-build / build (push) Successful in 1m22s
renovate / renovate (push) Successful in 39s
2025-06-20 00:02:41 -05:00
6d34c0d407 update config
Some checks failed
test-build / build (push) Failing after 2s
renovate / renovate (push) Successful in 12s
2025-06-19 23:59:05 -05:00
63607bbca3 Merge pull request 'Update ghcr.io/renovatebot/renovate Docker tag to v41' (#15) from renovate/ghcr.io-renovatebot-renovate-41.x into main
All checks were successful
renovate / renovate (push) Successful in 35s
test-build / build (push) Successful in 27s
Reviewed-on: #15
2025-06-20 04:40:12 +00:00
745d2553a0 Update ghcr.io/renovatebot/renovate Docker tag to v41
All checks were successful
test-build / build (pull_request) Successful in 27s
2025-06-20 04:32:37 +00:00
8a19559cc7 Merge pull request 'Update typescript-eslint monorepo to v8.34.1' (#13) from renovate/typescript-eslint-monorepo into main
All checks were successful
test-build / build (push) Successful in 1m10s
process-issues / process-issues (push) Successful in 11s
process-pull-requests / process-pull-requests (push) Successful in 11s
renovate / renovate (push) Successful in 39s
Reviewed-on: #13
2025-06-18 05:18:07 +00:00
42854db0fb Update typescript-eslint monorepo to v8.34.1
All checks were successful
test-build / build (pull_request) Successful in 1m2s
2025-06-17 00:01:12 +00:00
7b72e3849b Merge pull request 'Update dependency framer-motion to v12.18.1' (#12) from renovate/framer-motion-12.x-lockfile into main
Some checks failed
test-build / build (push) Successful in 51s
process-pull-requests / process-pull-requests (push) Successful in 6s
process-issues / process-issues (push) Failing after 5s
renovate / renovate (push) Successful in 45s
Reviewed-on: #12
2025-06-14 19:52:45 +00:00
6a8dbb0c7c Merge pull request 'Update dependency eslint to v9.29.0' (#11) from renovate/eslint-monorepo into main
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
Reviewed-on: #11
2025-06-14 19:52:35 +00:00
91fdf5a83f Update dependency framer-motion to v12.18.1
All checks were successful
test-build / build (pull_request) Successful in 1m6s
2025-06-14 00:01:33 +00:00
073f3a7916 Update dependency eslint to v9.29.0
All checks were successful
test-build / build (pull_request) Successful in 1m21s
2025-06-14 00:01:11 +00:00
38202841ca Merge pull request 'Update dependency framer-motion to v12.17.3' (#10) from renovate/framer-motion-12.x-lockfile into main
All checks were successful
test-build / build (push) Successful in 57s
process-issues / process-issues (push) Successful in 8s
process-pull-requests / process-pull-requests (push) Successful in 13s
renovate / renovate (push) Successful in 1m26s
Reviewed-on: #10
2025-06-12 20:12:45 +00:00
0492922cce Update dependency framer-motion to v12.17.3
All checks were successful
test-build / build (pull_request) Successful in 40s
2025-06-12 20:11:41 +00:00
a17500835b Merge pull request 'Update dependency framer-motion to v12.17.0' (#9) from renovate/framer-motion-12.x-lockfile into main
All checks were successful
renovate / renovate (push) Successful in 2m29s
test-build / build (push) Successful in 55s
Reviewed-on: #9
2025-06-12 18:06:43 +00:00
2f8b97208c Merge pull request 'Update tailwindcss monorepo to v4.1.10' (#8) from renovate/tailwindcss-monorepo into main
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
Reviewed-on: #8
2025-06-12 18:06:36 +00:00
d6c30d5e5b Update dependency framer-motion to v12.17.0
All checks were successful
test-build / build (pull_request) Successful in 1m9s
2025-06-12 00:01:41 +00:00
a7ea9db3aa Update tailwindcss monorepo to v4.1.10
All checks were successful
test-build / build (pull_request) Successful in 1m19s
2025-06-12 00:01:31 +00:00
9134e78e8a remove dispatch
All checks were successful
test-build / build (push) Successful in 39s
process-issues / process-issues (push) Successful in 7s
process-pull-requests / process-pull-requests (push) Successful in 7s
renovate / renovate (push) Successful in 1m31s
2025-06-10 16:43:37 -05:00
2ca7d6705d fix
All checks were successful
renovate / renovate (push) Successful in 17s
test-build / build (push) Successful in 46s
2025-06-10 16:42:15 -05:00
5722e8c7a1 fix
All checks were successful
test-build / build (push) Successful in 38s
renovate / renovate (push) Successful in 19s
2025-06-10 16:39:55 -05:00
e39fd2acb8 change token
All checks were successful
renovate / renovate (push) Successful in 21s
test-build / build (push) Successful in 40s
2025-06-10 16:29:55 -05:00
0313fd54bc add ref
All checks were successful
renovate / renovate (push) Successful in 16s
test-build / build (push) Successful in 50s
2025-06-10 16:23:38 -05:00
dbb0f6d7ff update workflows
All checks were successful
renovate / renovate (push) Successful in 16s
test-build / build (push) Successful in 58s
2025-06-10 16:15:10 -05:00
20669d9766 fix
All checks were successful
renovate / renovate (push) Successful in 18s
test-build / build (push) Successful in 1m18s
2025-06-10 14:04:19 -05:00
6b2e6353d1 add dispatch
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Successful in 19s
2025-06-10 14:03:40 -05:00
6d112b52df remove step
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Successful in 17s
2025-06-10 14:02:58 -05:00
ff17af604f convert to python script
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Successful in 27s
2025-06-10 14:01:26 -05:00
32ea0989d7 change to pull requests
All checks were successful
renovate / renovate (push) Successful in 22s
test-build / build (push) Successful in 1m27s
2025-06-10 13:11:43 -05:00
e4ab7d134c fix repo name
All checks were successful
renovate / renovate (push) Successful in 20s
test-build / build (push) Successful in 1m44s
2025-06-10 13:06:04 -05:00
5fad13655c fix env
All checks were successful
renovate / renovate (push) Successful in 21s
test-build / build (push) Successful in 1m35s
2025-06-10 12:56:18 -05:00
8614d40a64 debugging
All checks were successful
renovate / renovate (push) Successful in 17s
test-build / build (push) Successful in 48s
2025-06-10 12:40:20 -05:00
8c417b93b3 fix url
All checks were successful
renovate / renovate (push) Successful in 17s
test-build / build (push) Successful in 41s
2025-06-10 12:36:49 -05:00
1d9519831b temp debugging
All checks were successful
renovate / renovate (push) Successful in 20s
test-build / build (push) Successful in 48s
2025-06-10 12:35:13 -05:00
fa57f2e93f temp debugging
All checks were successful
test-build / build (push) Successful in 45s
renovate / renovate (push) Successful in 18s
2025-06-10 12:29:25 -05:00
9e01002d4e add more logging
All checks were successful
renovate / renovate (push) Successful in 21s
test-build / build (push) Successful in 46s
2025-06-10 12:25:45 -05:00
cb52c169a3 add logging
All checks were successful
test-build / build (push) Successful in 1m1s
renovate / renovate (push) Successful in 19s
2025-06-10 12:15:47 -05:00
3017668cd2 fix lint
All checks were successful
test-build / build (push) Successful in 2m31s
renovate / renovate (push) Successful in 18s
2025-06-10 12:05:33 -05:00
1972b3bc19 update lockfile
Some checks failed
renovate / renovate (push) Successful in 19s
test-build / build (push) Failing after 48s
release-image / release (push) Successful in 1m43s
2025-06-10 11:58:31 -05:00
af77f90a49 bump versions to 0.8.11
Some checks failed
test-build / build (push) Failing after 23s
renovate / renovate (push) Successful in 43s
release-image / release (push) Failing after 48s
2025-06-09 23:55:31 -05:00
bdda29f369 fix favicon path 2025-06-09 23:44:22 -05:00
644c5fcd6a fix footer 2025-06-09 23:37:47 -05:00
bafd8158d3 remove 2025-06-09 23:11:38 -05:00
4d9c1a3e8c changes for v4 tailwind 2025-06-09 23:03:04 -05:00
4a4233ac62 change module 2025-06-09 22:33:40 -05:00
c71957348d add pubilc image 2025-06-09 22:21:38 -05:00
400bf16dd9 use global value 2025-06-09 22:21:25 -05:00
85535614a0 remove dependencies 2025-06-09 22:21:09 -05:00
38fcbb635b change description 2025-06-09 21:47:43 -05:00
b1e57c3f17 use apply 2025-06-09 21:47:29 -05:00
e22a1985be remove unused keys
All checks were successful
renovate / renovate (push) Successful in 18s
test-build / build (push) Successful in 2m14s
2025-06-09 21:32:45 -05:00
70b0b86944 change transitions
All checks were successful
renovate / renovate (push) Successful in 1m8s
test-build / build (push) Successful in 1m16s
2025-06-09 21:31:20 -05:00
ba36de8e36 remove translate on hover for links and buttons 2025-06-09 21:21:13 -05:00
d2e44fe046 add gitea link 2025-06-09 21:20:48 -05:00
36ec797d3b change favicon 2025-06-09 19:34:30 -05:00
086d98ba50 remove ignored
Some checks failed
test-build / build (push) Successful in 47s
tag-old-issues / tag-old-issues (push) Failing after 1m11s
renovate / renovate (push) Successful in 24s
2025-06-09 15:59:59 -05:00
8a05fa4d96 update prettier and add eslint
All checks were successful
renovate / renovate (push) Successful in 18s
test-build / build (push) Successful in 2m11s
2025-06-09 15:54:56 -05:00
dbbf886de9 add test build
All checks were successful
test-build / build (push) Successful in 2m14s
renovate / renovate (push) Successful in 17s
2025-06-09 13:14:56 -05:00
ae7e21eb82 change registry
All checks were successful
renovate / renovate (push) Successful in 18s
2025-06-09 13:02:52 -05:00
ce6f476e8f fix repo
All checks were successful
renovate / renovate (push) Successful in 18s
2025-06-09 12:57:28 -05:00
0ca6be1d91 limit repo
All checks were successful
renovate / renovate (push) Successful in 55s
2025-06-09 12:56:12 -05:00
46 changed files with 3236 additions and 4628 deletions

View File

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

View 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

View File

@@ -42,7 +42,7 @@ jobs:
namespace=gitea namespace=gitea
qemu.install=true qemu.install=true
buildkitd-config-inline: | buildkitd-config-inline: |
[registry."hub.docker.com"] [registry."docker.io"]
mirrors = ["harbor.alexlebens.net/proxy-hub.docker/"] mirrors = ["harbor.alexlebens.net/proxy-hub.docker/"]
- name: Available Platforms - name: Available Platforms
@@ -75,7 +75,7 @@ jobs:
with: with:
url: '${{ secrets.NTFY_URL }}' url: '${{ secrets.NTFY_URL }}'
topic: '${{ secrets.NTFY_TOPIC }}' topic: '${{ secrets.NTFY_TOPIC }}'
title: "Gitea Action" title: 'Gitea Action'
priority: 3 priority: 3
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}' headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
tags: action,successfully,completed tags: action,successfully,completed
@@ -88,7 +88,7 @@ jobs:
with: with:
url: '${{ secrets.NTFY_URL }}' url: '${{ secrets.NTFY_URL }}'
topic: '${{ secrets.NTFY_TOPIC }}' topic: '${{ secrets.NTFY_TOPIC }}'
title: "Gitea Action" title: 'Gitea Action'
priority: 4 priority: 4
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}' headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
tags: action,failed tags: action,failed

View File

@@ -13,7 +13,7 @@ on:
jobs: jobs:
renovate: renovate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: ghcr.io/renovatebot/renovate:40 container: ghcr.io/renovatebot/renovate:41
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -22,9 +22,8 @@ jobs:
run: renovate run: renovate
env: env:
RENOVATE_PLATFORM: gitea RENOVATE_PLATFORM: gitea
RENOVATE_AUTODISCOVER: true
RENOVATE_ONBOARDING: true
RENOVATE_ENDPOINT: ${{ vars.INSTANCE_URL }} RENOVATE_ENDPOINT: ${{ vars.INSTANCE_URL }}
RENOVATE_REPOSITORIES: alexlebens/site-profile
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net> RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net>
LOG_LEVEL: info LOG_LEVEL: info
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }} RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}

View File

@@ -1,75 +0,0 @@
name: tag-old-issues
on:
schedule:
- cron: "@daily"
jobs:
tag-old-issues:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Tag Old Issues
env:
BOT_TOKEN: ${{ secrets.BOT_TOKEN }}
INSTANCE_URL: ${{ vars.INSTANCE_URL }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.repository_name }}
TAG_NAME: 'stale'
DAYS_OLD: 3
EXCLUDE_TAG_NAME: ''
REQUIRED_TAG: 'automerge'
run: |
# Install necessary tools
apt-get update && apt-get install -y jq curl
# --- Conditionally build the API URL ---
API_URL="${GITEA_INSTANCE_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues?state=open"
if [[ -n "${REQUIRED_TAG}" ]]; then
echo "Filtering for issues with the required tag: ${REQUIRED_TAG}"
# URL-encode the tag to handle special characters
ENCODED_TAG=$(jq -s -R -r @uri <<< "${REQUIRED_TAG}")
API_URL="${API_URL}&labels=${ENCODED_TAG}"
else
echo "No required tag specified. Checking all open issues."
fi
# Fetch issues using the constructed URL
ISSUES=$(curl -s -X GET \
-H "Authorization: token ${BOT_TOKEN}" \
-H "Accept: application/json" \
"${API_URL}")
# Calculate the date ${DAYS_OLD} days ago in ISO 8601 format
OLDER_THAN_DATE=$(date -d "-${DAYS_OLD} days" -u +"%Y-%m-%dT%H:%M:%SZ")
# Filter issues older than the specified date and without the exclusion tag
echo "$ISSUES" | jq -c '.[] | select(.created_at < "'"$OLDER_THAN_DATE"'")' | while read -r issue; do
ISSUE_NUMBER=$(echo "$issue" | jq -r '.number')
LABELS=$(echo "$issue" | jq -r '.labels[].name')
# Check if the issue has the exclusion tag
if ! echo "$LABELS" | grep -q -w "${EXCLUDE_TAG_NAME}"; then
echo "Tagging issue #${ISSUE_NUMBER} as ${TAG_NAME}"
# Get existing labels for the issue
EXISTING_LABELS=$(curl -s -X GET \
-H "Authorization: token ${BOT_TOKEN}" \
-H "Accept: application/json" \
"${INSTANCE_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/labels" | jq -r '.[].name')
# Add the new tag to the list of existing labels
NEW_LABELS=$(echo -e "${EXISTING_LABELS}\n${TAG_NAME}" | sort -u | jq -R -s -c 'split("\n") | map(select(length > 0))')
# Update the issue with the new set of labels
curl -s -X PUT \
-H "Authorization: token ${BOT_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"labels\": $(echo "$NEW_LABELS" | jq -r 'map(select(. != ""))')}" \
"${INSTANCE_URL}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}/issues/${ISSUE_NUMBER}/labels"
else
echo "Skipping issue #${ISSUE_NUMBER} because it has the '${EXCLUDE_TAG_NAME}' tag."
fi
done

View 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.1
cache: pnpm
- name: Install Dependencies
run: pnpm install
- name: Lint Code
run: pnpm lint
- name: Build Project
run: pnpm build

1
.gitignore vendored
View File

@@ -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
View File

@@ -1,3 +1,2 @@
engine-strict=true engine-strict=true
save-exact=true save-exact=true

View File

@@ -1,17 +0,0 @@
{
"printWidth": 100,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"useTabs": false,
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "*.astro",
"options": {
"parser": "astro"
}
}
]
}

View File

@@ -1,4 +0,0 @@
{
"recommendations": ["astro-build.astro-vscode"],
"unwantedRecommendations": []
}

11
.vscode/launch.json vendored
View File

@@ -1,11 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"command": "./node_modules/.bin/astro dev",
"name": "Development server",
"request": "launch",
"type": "node-terminal"
}
]
}

View File

@@ -1,7 +1,7 @@
ARG REGISTRY=hub.docker.com ARG REGISTRY=docker.io
FROM ${REGISTRY}/node:22.16.0-alpine3.22 AS base FROM ${REGISTRY}/node:22.17.1-alpine3.22 AS base
LABEL version="0.8.10" LABEL version="0.11.0"
LABEL description="Astro based personal website" LABEL description="Astro based personal website"
ENV PNPM_HOME="/pnpm" ENV PNPM_HOME="/pnpm"
@@ -33,4 +33,4 @@ 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"]

View File

@@ -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

View File

@@ -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

View File

@@ -16,14 +16,14 @@ export default defineConfig({
integrations: [tailwindcss(), react()], integrations: [tailwindcss(), react()],
plugins: { plugins: {
"@tailwindcss/postcss": {}, '@tailwindcss/postcss': {},
}, },
vite: { vite: {
plugins: [tailwindcss()] plugins: [tailwindcss()],
}, },
adapter: node({ adapter: node({
mode: 'standalone' mode: 'standalone',
}) }),
}); });

11
eslint.config.mjs Normal file
View 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: {
}
}
];

View File

@@ -22,6 +22,7 @@ type About = {
type Links = { type Links = {
github: string; github: string;
linkedin: string; linkedin: string;
gitea: string;
}; };
type Skill = { type Skill = {

View File

@@ -1,13 +1,15 @@
{ {
"name": "site-profile", "name": "site-profile",
"type": "module", "type": "module",
"version": "0.8.10", "version": "0.11.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", "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": {
@@ -15,14 +17,11 @@
"@astrojs/node": "^9.2.2", "@astrojs/node": "^9.2.2",
"@astrojs/react": "^4.3.0", "@astrojs/react": "^4.3.0",
"@astrojs/rss": "^4.0.12", "@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.4.1", "@directus/sdk": "^20.0.0",
"@directus/sdk": "^19.1.0",
"@tailwindcss/postcss": "^4.1.8", "@tailwindcss/postcss": "^4.1.8",
"@tailwindcss/vite": "^4.1.8", "@tailwindcss/vite": "^4.1.8",
"astro": "^5.9.2", "astro": "^5.10.1",
"form-data": "4.0.3",
"framer-motion": "^12.16.0", "framer-motion": "^12.16.0",
"postcss-preset-env": "^10.2.1",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-hotkeys-hook": "^5.1.0", "react-hotkeys-hook": "^5.1.0",
@@ -32,8 +31,13 @@
}, },
"devDependencies": { "devDependencies": {
"@tailwindcss/typography": "^0.5.16", "@tailwindcss/typography": "^0.5.16",
"@typescript-eslint/parser": "8.37.0",
"eslint": "9.31.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-astro": "1.3.1",
"prettier": "^3.5.3", "prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.0", "prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.0" "prettier-plugin-tailwindcss": "^0.6.12",
"typescript-eslint": "8.37.0"
} }
} }

3263
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,13 +1,7 @@
/** @type {import('postcss-load-config').Config} */ /** @type {import('postcss-load-config').Config} */
const config = { const config = {
plugins: { plugins: {
"@tailwindcss/postcss": {}, '@tailwindcss/postcss': {},
autoprefixer: {},
'postcss-preset-env': {
features: {
'nesting-rules': false,
},
},
}, },
}; };

23
prettier.config.mjs Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -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

View File

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

View File

@@ -1,21 +1,21 @@
--- ---
// Background.astro - Dot pattern and ambient glow background with smooth theme transitions
--- ---
<div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden"> <div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden">
<!-- Dot pattern background --> <!-- Dot pattern background -->
<div <div
class="bg-grid-pattern theme-transition-bg absolute inset-0 bg-[center_top_-1px] [mask-image:radial-gradient(white,transparent_85%)]" class="bg-grid-pattern theme-transition-bg absolute inset-0 [mask-image:radial-gradient(white,transparent_85%)] bg-[center_top_-1px]"
> >
</div> </div>
<!-- Ambient glow effects --> <!-- Ambient glow effects -->
<div <div
class="animate-glow theme-transition-bg absolute left-1/4 top-1/4 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-zinc-400/20 opacity-50 blur-3xl dark:bg-zinc-500/20" 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>
<div <div
class="animate-glow animation-delay-1000 theme-transition-bg absolute bottom-1/3 right-1/4 h-64 w-64 translate-x-1/2 translate-y-1/2 rounded-full bg-zinc-300/20 opacity-40 blur-3xl dark:bg-zinc-600/20" 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> </div>
@@ -29,24 +29,19 @@
<script> <script>
// Theme transition script // Theme transition script
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('astro:page-load', () => {
const themeToggle = document.querySelector('[data-theme-toggle]'); const themeToggle = document.querySelector('[data-theme-toggle]');
const overlay = document.getElementById('theme-transition-overlay'); const overlay = document.getElementById('theme-transition-overlay');
if (themeToggle && overlay) { if (themeToggle && overlay) {
themeToggle.addEventListener('click', () => { themeToggle.addEventListener('click', () => {
// Add transitioning class to optimize performance
document.documentElement.classList.add('theme-transitioning'); document.documentElement.classList.add('theme-transitioning');
// Fade in overlay
overlay.style.opacity = '0.15'; overlay.style.opacity = '0.15';
overlay.style.transition = 'opacity 0.3s ease'; overlay.style.transition = 'opacity 0.3s ease';
setTimeout(() => { setTimeout(() => {
// Fade out overlay
overlay.style.opacity = '0'; overlay.style.opacity = '0';
// Remove transitioning class after animation completes
setTimeout(() => { setTimeout(() => {
document.documentElement.classList.remove('theme-transitioning'); document.documentElement.classList.remove('theme-transitioning');
}, 700); }, 700);
@@ -60,13 +55,13 @@
/* Grid pattern for dots */ /* Grid pattern for dots */
.bg-grid-pattern { .bg-grid-pattern {
background-size: 24px 24px; background-size: 24px 24px;
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px); background-image: radial-gradient(circle, rgba(0, 0, 0, 0.2) 1px, transparent 1px);
transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1); 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.15) 1px, transparent 1px);
} }
/* Ambient glow animations */ /* Ambient glow animations */

View File

@@ -8,10 +8,10 @@ const links = await directus.request(readSingleton('links'));
const currentYear = new Date().getFullYear(); const currentYear = new Date().getFullYear();
const navLinks = [ const navLinks = [
{ text: 'About', href: '/about' }, { text: 'Home', href: '/' },
{ text: 'Blog', href: '/blog' }, { text: 'Blog', href: '/blog' },
{ text: 'Topics', href: '/topics' }, { text: 'About', href: '/about' },
{ text: 'RSS', href: '/rss.xml' }, { text: 'RSS', href: '/rss' },
]; ];
const socialLinks = [ const socialLinks = [
@@ -20,6 +20,11 @@ const socialLinks = [
href: links.github, href: links.github,
icon: `<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path>`, icon: `<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path>`,
}, },
{
name: 'Gitea',
href: links.gitea,
icon: `<path d="M7 5C7 3.89543 7.89543 3 9 3C10.1046 3 11 3.89543 11 5C11 5.34168 10.9143 5.66336 10.7633 5.9447H11.3438C13.5529 5.9447 15.3438 7.73556 15.3438 9.9447V11.2244C15.9301 11.5731 16.323 12.213 16.323 12.9447C16.323 14.0493 15.4276 14.9447 14.323 14.9447C13.2184 14.9447 12.323 14.0493 12.323 12.9447C12.323 12.1959 12.7345 11.5432 13.3438 11.2004V9.9447C13.3438 8.84013 12.4483 7.9447 11.3438 7.9447H10V17.2676C10.5978 17.6134 11 18.2597 11 19C11 20.1046 10.1046 21 9 21C7.89543 21 7 20.1046 7 19C7 18.2597 7.4022 17.6134 8 17.2676V6.73244C7.4022 6.38663 7 5.74028 7 5Z" fill="currentColor"/>`,
},
{ {
name: 'LinkedIn', name: 'LinkedIn',
href: links.linkedin, href: links.linkedin,
@@ -30,10 +35,11 @@ const socialLinks = [
<footer <footer
class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800" class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
transition:animate="none"
> >
<div class="pointer-events-none absolute inset-0 overflow-hidden"> <div class="pointer-events-none absolute inset-0 overflow-hidden">
<div <div
class="theme-transition-all animate-float-slow absolute -right-40 -top-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30" 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>
<div <div
@@ -41,35 +47,33 @@ const socialLinks = [
> >
</div> </div>
<div <div
class="theme-transition-all animate-float-slow animation-delay-1000 absolute left-1/4 top-20 h-40 w-40 rounded-full bg-zinc-200/50 opacity-30 blur-2xl dark:bg-zinc-700/20" 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>
<div class="relative px-4 pb-12 pt-16 sm:px-6"> <div class="relative px-4 pt-16 pb-12 sm:px-6">
<div class="mx-auto max-w-4xl"> <div class="mx-auto max-w-4xl">
<!-- Main footer content -->
<div class="grid grid-cols-1 gap-10 md:grid-cols-12"> <div class="grid grid-cols-1 gap-10 md:grid-cols-12">
<!-- Brand section --> <!-- Brand section -->
<div class="col-span-1 md:col-span-3"> <div class="col-span-1 md:col-span-3">
<a href="/" class="group inline-block"> <a href="/" class="group inline-block">
<div class="flex items-center"> <div class="flex items-center">
<div <div
class="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" 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 dark:from-zinc-200 dark:to-zinc-400"
> >
<span <span
class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900" class="theme-transition-all text-xl font-bold text-zinc-100 duration-300 dark:text-zinc-900"
>{global.initals}</span
> >
<div {global.initals}
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" </span>
> <div class="absolute inset-0"></div>
</div>
</div> </div>
<span <span
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100" class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
>Blog</span
> >
Blog
</span>
</div> </div>
</a> </a>
@@ -87,7 +91,7 @@ const socialLinks = [
href={social.href} href={social.href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="group relative flex h-10 w-10 transform items-center justify-center rounded-full bg-zinc-100 text-zinc-500 transition-all duration-300 hover:-translate-y-1 hover:text-zinc-900 hover:ring-2 hover:ring-zinc-300 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100 dark:hover:ring-zinc-700" class="hover group relative flex h-10 w-10 transform items-center justify-center rounded-full bg-zinc-100 text-zinc-500 transition-all duration-300 hover:-translate-y-1 hover:text-zinc-900 hover:ring-2 hover:ring-zinc-300 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100 dark:hover:ring-zinc-700"
aria-label={social.name} aria-label={social.name}
> >
<span class="absolute inset-0 rounded-full bg-gradient-to-br from-zinc-200 to-zinc-300 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-700 dark:to-zinc-600" /> <span class="absolute inset-0 rounded-full bg-gradient-to-br from-zinc-200 to-zinc-300 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-700 dark:to-zinc-600" />
@@ -108,7 +112,7 @@ const socialLinks = [
<!-- Quick links --> <!-- Quick links -->
<div class="col-span-1 md:col-span-3"> <div class="col-span-1 md:col-span-3">
<h3 <h3
class="theme-transition-color relative inline-block pb-2 text-sm font-semibold uppercase tracking-wider text-zinc-900 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:bg-zinc-300 after:content-[''] dark:text-zinc-100 dark:after:bg-zinc-700" class="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 Navigation
</h3> </h3>
@@ -122,7 +126,6 @@ const socialLinks = [
> >
<span class="relative inline-block overflow-hidden"> <span class="relative inline-block overflow-hidden">
<span class="relative z-10">{link.text}</span> <span class="relative z-10">{link.text}</span>
<span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
</span> </span>
</a> </a>
</li> </li>
@@ -130,115 +133,116 @@ const socialLinks = [
} }
</ul> </ul>
</div> </div>
</div>
<!-- Bottom section --> <!-- Bottom section -->
<div class="theme-transition-all mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800"> <div class="theme-transition-all mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
<div class="flex flex-col items-center justify-between gap-4 md:flex-row"> <div class="flex flex-col items-center justify-between gap-4 md:flex-row">
<p class="theme-transition-color text-sm text-zinc-600 dark:text-zinc-400"> <p class="theme-transition-color text-sm text-zinc-600 dark:text-zinc-400">
&copy; {currentYear} All rights reserved. &copy; {currentYear} All rights reserved.
</p> </p>
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400" <span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
>Built with</span >Built with
</span>
<a
href="https://astro.build"
target="_blank"
rel="noopener noreferrer"
class="group inline-flex items-center text-xs text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
>
<svg
class="mr-1 h-4 w-4 text-[#FF5D01] group-hover:animate-pulse"
viewBox="0 0 36 36"
fill="none"
> >
<a <path
href="https://astro.build" fill-rule="evenodd"
target="_blank" clip-rule="evenodd"
rel="noopener noreferrer" d="M8.833 22.958c.622-1.185 1.832-1.918 3.18-1.918 2.292 0 4.145 1.86 4.145 4.153 0 1.34-.626 2.54-1.601 3.303 1.223-1.299 1.97-3.048 1.97-4.971 0-3.994-3.243-7.233-7.242-7.233-2.818 0-5.26 1.6-6.469 3.933.78-2.912 3.428-5.06 6.577-5.06 3.75 0 6.79 3.035 6.79 6.78 0 2.606-1.468 4.868-3.616 6.002a4.163 4.163 0 0 0 2.285-3.724c0-2.293-1.853-4.153-4.145-4.153-1.348 0-2.558.733-3.18 1.918l1.306-3.03Z"
class="group inline-flex items-center text-xs text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100" fill="currentColor"></path>
> <path
<svg fill-rule="evenodd"
class="mr-1 h-4 w-4 text-[#FF5D01] group-hover:animate-pulse" clip-rule="evenodd"
viewBox="0 0 36 36" d="M22.155 12.056c-.622 1.185-1.832 1.918-3.18 1.918-2.292 0-4.145-1.86-4.145-4.153 0-1.34.626-2.54 1.601-3.303-1.223 1.299-1.97 3.048-1.97 4.971 0 3.994 3.243 7.233 7.242 7.233 2.818 0 5.26-1.6 6.469-3.933-.78 2.912-3.428 5.06-6.577 5.06-3.75 0-6.79-3.035-6.79-6.78 0-2.606 1.468-4.868 3.616-6.002a4.163 4.163 0 0 0-2.285 3.724c0 2.293 1.853 4.153 4.145 4.153 1.348 0 2.558-.733 3.18-1.918l-1.306 3.03Z"
fill="none" fill="currentColor"></path>
</svg>
<span class="relative">
Astro
<span
class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"
> >
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.833 22.958c.622-1.185 1.832-1.918 3.18-1.918 2.292 0 4.145 1.86 4.145 4.153 0 1.34-.626 2.54-1.601 3.303 1.223-1.299 1.97-3.048 1.97-4.971 0-3.994-3.243-7.233-7.242-7.233-2.818 0-5.26 1.6-6.469 3.933.78-2.912 3.428-5.06 6.577-5.06 3.75 0 6.79 3.035 6.79 6.78 0 2.606-1.468 4.868-3.616 6.002a4.163 4.163 0 0 0 2.285-3.724c0-2.293-1.853-4.153-4.145-4.153-1.348 0-2.558.733-3.18 1.918l1.306-3.03Z"
fill="currentColor"></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M22.155 12.056c-.622 1.185-1.832 1.918-3.18 1.918-2.292 0-4.145-1.86-4.145-4.153 0-1.34.626-2.54 1.601-3.303-1.223 1.299-1.97 3.048-1.97 4.971 0 3.994 3.243 7.233 7.242 7.233 2.818 0 5.26-1.6 6.469-3.933-.78 2.912-3.428 5.06-6.577 5.06-3.75 0-6.79-3.035-6.79-6.78 0-2.606 1.468-4.868 3.616-6.002a4.163 4.163 0 0 0-2.285 3.724c0 2.293 1.853 4.153 4.145 4.153 1.348 0 2.558-.733 3.18-1.918l-1.306 3.03Z"
fill="currentColor"></path>
</svg>
<span class="relative">
Astro
<span
class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"
></span>
</span> </span>
</a> </span>
</div> </a>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<style>
.theme-transition-all {
transition-property: background-color, border-color, color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
.theme-transition-color {
transition-property: color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
.theme-transition-bg {
transition-property: background-color;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.2);
}
}
@keyframes float-slow {
0%,
100% {
transform: translateY(0) translateX(0);
}
25% {
transform: translateY(-10px) translateX(10px);
}
50% {
transform: translateY(-5px) translateX(-5px);
}
75% {
transform: translateY(10px) translateX(5px);
}
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.animate-float-slow {
animation: float-slow 20s ease-in-out infinite;
}
.animation-delay-1000 {
animation-delay: 1s;
}
.animation-delay-2000 {
animation-delay: 2s;
}
</style>
</footer> </footer>
<style>
.theme-transition-all {
transition-property: background-color, border-color, color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
.theme-transition-color {
transition-property: color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
.theme-transition-bg {
transition-property: background-color;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.2);
}
}
@keyframes float-slow {
0%,
100% {
transform: translateY(0) translateX(0);
}
25% {
transform: translateY(-10px) translateX(10px);
}
50% {
transform: translateY(-5px) translateX(-5px);
}
75% {
transform: translateY(10px) translateX(5px);
}
}
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.animate-float-slow {
animation: float-slow 20s ease-in-out infinite;
}
.animation-delay-1000 {
animation-delay: 1s;
}
.animation-delay-2000 {
animation-delay: 2s;
}
</style>

View File

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

View File

@@ -5,12 +5,13 @@ 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 navItems = [ const navItems = [
{ text: 'Home', href: '/' }, { text: 'Home', href: '/' },
{ text: 'Blog', href: '/blog' }, { text: 'Blog', href: '/blog' },
{ text: 'Topics', href: '/topics' },
{ text: 'About', href: '/about' }, { text: 'About', href: '/about' },
{ text: 'Gitea', href: links.gitea },
{ text: 'RSS', href: 'rss.xml' }, { text: 'RSS', href: 'rss.xml' },
]; ];
@@ -19,7 +20,8 @@ const currentPath = pathname.slice(1);
--- ---
<header <header
class="fixed left-0 right-0 top-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900" class="fixed top-0 right-0 left-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
transition:animate="none"
> >
<div class="mx-auto flex max-w-3xl items-center justify-between px-4"> <div class="mx-auto flex max-w-3xl items-center justify-between px-4">
<!-- Logo --> <!-- Logo -->
@@ -35,8 +37,8 @@ const currentPath = pathname.slice(1);
href={item.href} href={item.href}
class={`text-sm font-medium ${ class={`text-sm font-medium ${
isActive isActive
? 'text-zinc-900 dark:text-white' ? 'text-zinc-900 dark:text-zinc-100'
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white' : 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100'
}`} }`}
> >
{item.text} {item.text}
@@ -72,7 +74,7 @@ const currentPath = pathname.slice(1);
class="pointer-events-none fixed inset-0 z-50 flex flex-col bg-white opacity-0 transition-all duration-300 ease-in-out dark:bg-zinc-900" class="pointer-events-none fixed inset-0 z-50 flex flex-col bg-white opacity-0 transition-all duration-300 ease-in-out dark:bg-zinc-900"
> >
<div class="flex items-center justify-between border-b border-zinc-100 p-4 dark:border-zinc-800"> <div class="flex items-center justify-between border-b border-zinc-100 p-4 dark:border-zinc-800">
<a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">JD</a> <a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">{global.initals}</a>
<button <button
id="close-menu-button" id="close-menu-button"
class="rounded-md p-2 text-zinc-900 transition-colors hover:bg-zinc-100 dark:text-white dark:hover:bg-zinc-800" class="rounded-md p-2 text-zinc-900 transition-colors hover:bg-zinc-100 dark:text-white dark:hover:bg-zinc-800"
@@ -100,8 +102,8 @@ const currentPath = pathname.slice(1);
href={item.href} href={item.href}
class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${ class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
isActive isActive
? 'text-zinc-900 dark:text-white' ? 'text-zinc-900 dark:text-zinc-100'
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white' : 'text-zinc-600 group-hover:text-zinc-900 dark:text-zinc-400 dark:group-hover:text-zinc-100'
}`} }`}
style={`transition-delay: ${index * 0.05}s;`} style={`transition-delay: ${index * 0.05}s;`}
> >
@@ -121,7 +123,7 @@ const currentPath = pathname.slice(1);
<script> <script>
// Mobile menu toggle with animations // Mobile menu toggle with animations
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('astro:page-load', () => {
const mobileMenuButton = document.getElementById('mobile-menu-button'); const mobileMenuButton = document.getElementById('mobile-menu-button');
const closeMenuButton = document.getElementById('close-menu-button'); const closeMenuButton = document.getElementById('close-menu-button');
const mobileMenu = document.getElementById('mobile-menu'); const mobileMenu = document.getElementById('mobile-menu');
@@ -200,9 +202,9 @@ const currentPath = pathname.slice(1);
// Add shadow on scroll // Add shadow on scroll
if (currentScrollY > 10) { if (currentScrollY > 10) {
header.classList.add('shadow-sm'); header.classList.add('shadow-xs');
} else { } else {
header.classList.remove('shadow-sm'); header.classList.remove('shadow-xs');
} }
// Update last scroll position // Update last scroll position
@@ -240,6 +242,6 @@ const currentPath = pathname.slice(1);
/* Mobile menu transition */ /* Mobile menu transition */
#mobile-menu { #mobile-menu {
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
backdrop-filter: blur(4px); backdrop-filter: blur-sm(4px);
} }
</style> </style>

View File

@@ -17,7 +17,7 @@ const encodedUrl = encodeURIComponent(url);
href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`} href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300" class="hover rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on Twitter" aria-label="Share on Twitter"
> >
<svg <svg
@@ -29,16 +29,18 @@ const encodedUrl = encodeURIComponent(url);
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class="h-4 w-4" class="h-4 w-4"
><path
d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"
></path></svg
> >
<path
d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"
>
</path>
</svg>
</a> </a>
<a <a
href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`} href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300" class="hover rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on Facebook" aria-label="Share on Facebook"
> >
<svg <svg
@@ -50,14 +52,15 @@ const encodedUrl = encodeURIComponent(url);
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class="h-4 w-4" class="h-4 w-4"
><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg
> >
<path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"> </path>
</svg>
</a> </a>
<a <a
href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`} href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300" class="hover rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on LinkedIn" aria-label="Share on LinkedIn"
> >
<svg <svg
@@ -69,10 +72,12 @@ const encodedUrl = encodeURIComponent(url);
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class="h-4 w-4" class="h-4 w-4"
><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"
></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"
></circle></svg
> >
<path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z">
</path>
<rect x="2" y="9" width="4" height="12"></rect>
<circle cx="4" cy="4" r="2"></circle>
</svg>
</a> </a>
<button <button
id="copy-link-button" id="copy-link-button"
@@ -89,87 +94,16 @@ const encodedUrl = encodeURIComponent(url);
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
class="h-4 w-4" class="h-4 w-4"
><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path
d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg
> >
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"> </path>
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"> </path>
</svg>
<span <span
id="copy-tooltip" id="copy-tooltip"
class="absolute -top-8 left-1/2 -translate-x-1/2 transform whitespace-nowrap rounded bg-zinc-800 px-2 py-1 text-xs text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700" class="absolute -top-8 left-1/2 -translate-x-1/2 transform rounded-sm bg-zinc-800 px-2 py-1 text-xs whitespace-nowrap text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
> >
Copied! Copied!
</span> </span>
</button> </button>
</div> </div>
</div> </div>
<script>
// Function to handle copy link button
function setupCopyLinkButton() {
const copyButtons = document.querySelectorAll('#copy-link-button');
copyButtons.forEach((button) => {
button.addEventListener('click', () => {
// Get the current URL
const url = window.location.href;
// Copy to clipboard
navigator.clipboard
.writeText(url)
.then(() => {
// Show tooltip
const tooltip = button.querySelector('#copy-tooltip');
if (tooltip) {
tooltip.classList.add('opacity-100');
// Hide tooltip after 2 seconds
setTimeout(() => {
tooltip.classList.remove('opacity-100');
}, 2000);
}
})
.catch((err) => {
console.error('Failed to copy: ', err);
});
});
});
}
// Set up the copy link button when the DOM is loaded
document.addEventListener('DOMContentLoaded', setupCopyLinkButton);
// Also set up when the page content is updated via SPA navigation
document.addEventListener('astro:page-load', setupCopyLinkButton);
// For compatibility with the custom page transition system
document.addEventListener('page-transition-complete', setupCopyLinkButton);
// Handle SPA transitions for share links
function setupSpaTransitions() {
// Get all share links
const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]');
// Make sure external share links don't trigger page transitions
shareLinks.forEach((link) => {
link.setAttribute('data-spa-external', 'true');
});
}
// Initialize SPA transitions
document.addEventListener('DOMContentLoaded', setupSpaTransitions);
document.addEventListener('astro:page-load', setupSpaTransitions);
document.addEventListener('page-transition-complete', setupSpaTransitions);
// Dispatch custom event when share action is completed
function notifyShareComplete() {
document.dispatchEvent(new CustomEvent('share-action-complete'));
}
// Add analytics tracking for share actions if needed
function trackShareAction(platform) {
// You can implement analytics tracking here
console.log(`Shared on ${platform}`);
// Notify other components that share action is complete
notifyShareComplete();
}
</script>

View File

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

View File

@@ -5,14 +5,14 @@
<button <button
id="theme-toggle" id="theme-toggle"
data-theme-toggle data-theme-toggle
class="group relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-zinc-300 dark:hover:bg-zinc-800 dark:focus:ring-zinc-700 sm:p-2" class="group 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 text-zinc-800 transition-all duration-500 dark:-rotate-90 dark:scale-0 dark:text-zinc-200" class="icon-light absolute h-5 w-5 scale-100 rotate-0 text-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"
@@ -29,7 +29,7 @@
<!-- Moon icon --> <!-- Moon icon -->
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
class="icon-dark absolute h-5 w-5 rotate-90 scale-0 text-zinc-800 transition-all duration-500 dark:rotate-0 dark:scale-100 dark:text-zinc-200" class="icon-dark absolute h-5 w-5 scale-0 rotate-90 text-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"
@@ -47,24 +47,25 @@
></span> ></span>
</button> </button>
<script is:inline>
// Use a function to persist theme when using SPA transitions
// https://docs.astro.build/en/guides/view-transitions/#script-re-execution
function applyTheme() {
localStorage.theme === 'dark'
? document.documentElement.classList.add('dark')
: document.documentElement.classList.remove('dark');
}
document.addEventListener('astro:after-swap', applyTheme);
applyTheme();
</script>
<script> <script>
// Use a function to handle theme toggle to ensure it can be called from anywhere // Use a function to handle theme toggle to ensure it can be called from anywhere
function setupThemeToggle() { function setupThemeToggle() {
const themeToggles = document.querySelectorAll('[data-theme-toggle]'); const themeToggles = document.querySelectorAll('[data-theme-toggle]');
// Check for dark mode preference at the system level
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
// Check for saved theme preference or use the system preference
const currentTheme = localStorage.getItem('theme') || (prefersDarkMode ? 'dark' : 'light');
// Apply the theme on initial load
if (currentTheme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Create theme switch overlay element if it doesn't exist // Create theme switch overlay element if it doesn't exist
if (!document.querySelector('.theme-switch-overlay')) { if (!document.querySelector('.theme-switch-overlay')) {
const overlay = document.createElement('div'); const overlay = document.createElement('div');
@@ -184,7 +185,7 @@
} }
// Run setup on load // Run setup on load
document.addEventListener('DOMContentLoaded', setupThemeToggle); document.addEventListener('astro:page-load', setupThemeToggle);
// Also run on page visibility change to ensure theme is consistent // Also run on page visibility change to ensure theme is consistent
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
@@ -274,12 +275,12 @@
} }
#theme-toggle:hover .icon-light:not(.dark .icon-light) { #theme-toggle:hover .icon-light:not(.dark .icon-light) {
filter: drop-shadow(0 0 2px rgba(251, 191, 36, 0.6)); filter: drop-shadow-sm(0 0 2px rgba(251, 191, 36, 0.6));
transform: scale(1.1) rotate(15deg); transform: scale(1.1) rotate(15deg);
} }
#theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) { #theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) {
filter: drop-shadow(0 0 2px rgba(129, 140, 248, 0.6)); filter: drop-shadow-sm(0 0 2px rgba(129, 140, 248, 0.6));
transform: scale(1.1) rotate(-15deg); transform: scale(1.1) rotate(-15deg);
} }
} }

View File

@@ -1,17 +0,0 @@
---
import Layout from './Layout.astro';
import directus from '../../lib/directus';
import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton('global'));
export interface Props {
title: string;
description?: string;
}
---
<Layout title={global.title} description={global.description}>
<slot />
</Layout>

View File

@@ -12,48 +12,6 @@ export interface Props {
} }
--- ---
<Layout title={global.title} description={global.description}> <Layout title={global.title} description={global.title}>
<slot /> <slot />
</Layout> </Layout>
<script>
document.addEventListener('DOMContentLoaded', () => {
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', () => {
document.documentElement.classList.add('theme-switching');
const rippleElements = document.querySelectorAll('.theme-ripple');
rippleElements.forEach((el) => {
el.classList.add('ripple-active');
setTimeout(() => {
el.classList.remove('ripple-active');
}, 600);
});
const event = new CustomEvent('themeChange', {
detail: {
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
},
});
document.dispatchEvent(event);
setTimeout(() => {
document.documentElement.classList.remove('theme-switching');
}, 600);
});
}
const socialLinks = document.querySelectorAll('.social-link');
socialLinks.forEach((link) => {
link.addEventListener('mouseenter', () => {
link.classList.add('hover-active');
});
link.addEventListener('mouseleave', () => {
link.classList.remove('hover-active');
});
});
});
</script>

View File

@@ -18,7 +18,6 @@ export async function getStaticPaths() {
} }
const post = Astro.props; const post = Astro.props;
const published_date: string = post.published_date.toLocaleString();
let canonicalURL; let canonicalURL;
try { try {
@@ -30,19 +29,34 @@ try {
--- ---
<Layout title={post.title} description={post.description}> <Layout title={post.title} description={post.description}>
<article class="prose prose-zinc mx-auto max-w-4xl dark:prose-invert lg:prose-lg"> <article
<div class="mb-12"> class="prose prose-zinc dark:prose-invert lg:prose-lg mx-auto max-w-4xl"
transition:animate="slide"
>
<div class="hero-text mb-12">
<h1 <h1
class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl" 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="mb-6 flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400"> <p
<FormattedDate date={published_date} /> class="mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400"
>
{post.description}
</p>
<div
class="hero-text mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"
>
<FormattedDate date={post.published_date} />
</div> </div>
<TagList tags={post.tags} class="mt-2" /> <div
class="hero-text mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"
>
<TagList tags={post.tags} />
</div>
</div> </div>
<!-- Hero image --> <!-- Hero image -->
@@ -70,13 +84,12 @@ try {
<div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800"> <div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row"> <div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
<ShareButtons url={canonicalURL.toString()} title={post.title} /> <ShareButtons url={canonicalURL.toString()} title={post.title} />
<!-- Convert URL to string -->
</div> </div>
</div> </div>
{ {
post.updated_date && ( post.updated_date && (
<div class="mt-8 text-sm italic text-zinc-500 dark:text-zinc-400"> <div class="mt-8 text-sm text-zinc-500 italic dark:text-zinc-400">
Last updated on <FormattedDate date={post.updated_date} /> Last updated on <FormattedDate date={post.updated_date} />
</div> </div>
) )
@@ -87,285 +100,60 @@ try {
</Layout> </Layout>
<script> <script>
// Blog post SPA transitions document.addEventListener('astro:page-load', () => {
function setupBlogPostTransitions() { // Add smooth reveal animations for content after loading
// Animate article entrance const animateContent = () => {
const article = document.querySelector('article'); // Animate hero section
if (article) { const heroElements = document.querySelectorAll(
article.classList.add('article-entering'); '.hero-text div, .hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p, .hero-text + a'
// Remove class after animation completes
setTimeout(() => {
article.classList.remove('article-entering');
}, 1000);
}
// Ensure consistent code block styling
function updateCodeBlockStyles() {
document.querySelectorAll('pre').forEach((pre) => {
// Force the background color with !important for both light and dark mode
pre.setAttribute('style', 'background-color: #1e293b !important');
// Also apply to any nested code elements
const codeElements = pre.querySelectorAll('code');
codeElements.forEach((code) => {
code.setAttribute(
'style',
'background-color: transparent !important; color: #e5e7eb !important;'
);
});
});
}
// Initial application
updateCodeBlockStyles();
// Watch for theme changes
const observer = new MutationObserver(() => {
updateCodeBlockStyles();
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
// Also run on any content changes that might add new code blocks
const contentObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length) {
updateCodeBlockStyles();
break;
}
}
});
contentObserver.observe(document.body, { childList: true, subtree: true });
// Clean up observers when navigating away
document.addEventListener('spa-navigation-start', () => {
observer.disconnect();
contentObserver.disconnect();
});
// Remove the parallax effect for hero image
// Handle prev/next navigation links
const navLinks = document.querySelectorAll('.blog-nav-link');
navLinks.forEach((link) => {
if (!link.hasAttribute('data-spa-handled')) {
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('mouseenter', () => {
link.classList.add('nav-link-hover');
});
link.addEventListener('mouseleave', () => {
link.classList.remove('nav-link-hover');
});
}
});
// Animate headings when they enter the viewport
const animateHeadings = () => {
const headings = document.querySelectorAll('article h2, article h3');
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('heading-visible');
observer.unobserve(entry.target);
}
});
},
{
threshold: 0.2,
rootMargin: '0px 0px -100px 0px',
}
); );
heroElements.forEach((el, index) => {
headings.forEach((heading) => { setTimeout(
heading.classList.add('heading-animated'); () => {
observer.observe(heading); el.classList.add('animate-reveal');
},
100 + index * 150
);
}); });
return observer; // Animate posts with staggered delay
const articles = document.querySelectorAll('article.group');
articles.forEach((article, index) => {
setTimeout(
() => {
article.classList.add('animate-reveal');
},
500 + index * 150
);
});
}; };
// Initialize heading animations animateContent();
const headingObserver = animateHeadings(); });
// Enhance code blocks with syntax highlighting and copy button
function enhanceCodeBlocks() {
const codeBlocks = document.querySelectorAll('pre code');
codeBlocks.forEach((codeBlock) => {
// Skip if already processed
if (codeBlock.parentElement.classList.contains('enhanced')) return;
// Mark as enhanced
codeBlock.parentElement.classList.add('enhanced');
// Create copy button
const copyButton = document.createElement('button');
copyButton.className = 'copy-code-button';
copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
</svg>
`;
// Add copy functionality
copyButton.addEventListener('click', () => {
const code = codeBlock.textContent;
navigator.clipboard.writeText(code);
// Show copied feedback
copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
`;
setTimeout(() => {
copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
</svg>
`;
}, 2000);
});
// Add copy button to pre element
codeBlock.parentElement.appendChild(copyButton);
// Fix line numbers implementation
const codeText = codeBlock.textContent;
const lines = codeText.split('\n');
const lineNumbers = document.createElement('div');
lineNumbers.className = 'line-numbers';
// Always include all lines, including empty ones
for (let i = 0; i < lines.length; i++) {
const lineNumber = document.createElement('span');
lineNumber.textContent = i + 1;
lineNumbers.appendChild(lineNumber);
}
codeBlock.parentElement.classList.add('with-line-numbers');
codeBlock.parentElement.insertBefore(lineNumbers, codeBlock);
// Fix language label detection and display
const className = codeBlock.className;
const languageMatch = className.match(/language-(\w+)/);
if (languageMatch && languageMatch[1]) {
const language = languageMatch[1];
// Add language label at top right
const languageLabel = document.createElement('div');
languageLabel.className = 'language-label';
languageLabel.textContent = language;
codeBlock.parentElement.appendChild(languageLabel);
// Add language badge at bottom right with markdown syntax
const languageBadge = document.createElement('div');
languageBadge.className = 'language-badge';
languageBadge.textContent = `\`\`\`${language}`;
languageBadge.style.position = 'absolute';
languageBadge.style.bottom = '0.5rem';
languageBadge.style.right = '0.5rem';
languageBadge.style.fontSize = '0.7rem';
languageBadge.style.padding = '0.1rem 0.3rem';
languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)';
languageBadge.style.color = '#e5e7eb';
languageBadge.style.borderRadius = '0.25rem';
languageBadge.style.fontFamily =
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
languageBadge.style.zIndex = '10';
codeBlock.parentElement.appendChild(languageBadge);
}
});
}
// Enhance tables with better styling
function enhanceTables() {
const tables = document.querySelectorAll('.markdown-content table');
tables.forEach((table) => {
if (table.classList.contains('enhanced-table')) return;
table.classList.add('enhanced-table');
// Wrap table in responsive container
const wrapper = document.createElement('div');
wrapper.className = 'table-container';
table.parentNode.insertBefore(wrapper, table);
wrapper.appendChild(table);
// Add zebra striping to rows
const rows = table.querySelectorAll('tbody tr');
rows.forEach((row, index) => {
if (index % 2 === 0) {
row.classList.add('even-row');
} else {
row.classList.add('odd-row');
}
});
});
}
// Enhance blockquotes with icons
function enhanceBlockquotes() {
const blockquotes = document.querySelectorAll('.markdown-content blockquote');
blockquotes.forEach((blockquote) => {
if (blockquote.classList.contains('enhanced-quote')) return;
blockquote.classList.add('enhanced-quote');
// Add quote icon
const icon = document.createElement('div');
icon.className = 'quote-icon';
icon.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
</svg>
`;
blockquote.insertBefore(icon, blockquote.firstChild);
});
}
// Run all enhancements
enhanceCodeBlocks();
enhanceTables();
enhanceBlockquotes();
// Clean up observers when navigating away
document.addEventListener('spa-navigation-start', () => {
if (headingObserver) {
headingObserver.disconnect();
}
});
}
// Initialize on first load
document.addEventListener('DOMContentLoaded', setupBlogPostTransitions);
// Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupBlogPostTransitions);
// For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupBlogPostTransitions);
// Also initialize when SPA navigation completes
document.addEventListener('spa-navigation-complete', setupBlogPostTransitions);
</script> </script>
<style> <style>
/* Enhanced hero image styling */ /* Content reveal animations */
.hero-text h1,
.hero-text div,
.hero-text ~ div,
.hero-text span,
.hero-text p,
.hero-text + a,
article.group {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
/* Hero image styling */
article img:first-of-type { article img:first-of-type {
border-radius: 1rem; border-radius: 1rem;
box-shadow: box-shadow:
@@ -377,22 +165,4 @@ try {
article img:first-of-type:hover { article img:first-of-type:hover {
transform: scale(1.01); transform: scale(1.01);
} }
/* Article entrance animation */
.article-entering {
animation: article-fade-in 0.8s ease-out forwards;
}
@keyframes article-fade-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Rest of the styles remain unchanged... */
</style> </style>

View File

@@ -1,7 +1,10 @@
--- ---
import { ClientRouter } from 'astro:transitions';
import Navigation from '../components/Navigation.astro'; import Navigation from '../components/Navigation.astro';
import Footer from '../components/Footer.astro'; import Footer from '../components/Footer.astro';
import Background from '../components/Background.astro'; import Background from '../components/Background.astro';
import '../styles/global.css'; import '../styles/global.css';
interface Props { interface Props {
@@ -17,7 +20,7 @@ const { title, description } = Astro.props;
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" /> <meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" /> <link rel="icon" type="image/svg+xml" href="/favicon.png" />
<meta name="generator" content={Astro.generator} /> <meta name="generator" content={Astro.generator} />
<meta name="description" content={description} /> <meta name="description" content={description} />
<title>{title}</title> <title>{title}</title>
@@ -27,284 +30,43 @@ const { title, description } = Astro.props;
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
<!-- Load theme early to prevent flashes between light and dark modes -->
<script is:inline>
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
window.localStorage.setItem('theme', theme);
</script>
<ClientRouter />
</head> </head>
<body <body
class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100" class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100"
> >
<!-- Page transition overlay - for smooth transitions between pages -->
<div
id="page-transition"
class="pointer-events-none fixed inset-0 z-40 flex items-center justify-center bg-white opacity-0 transition-opacity duration-300 dark:bg-zinc-900"
>
<div class="transition-spinner"></div>
</div>
<!-- Background component with dot pattern and ambient glow -->
<Background /> <Background />
<div class="mx-auto w-full max-w-3xl flex-grow px-4 sm:px-6"> <div class="mx-auto w-full max-w-3xl grow px-4 sm:px-6">
<Navigation /> <Navigation />
<main class="py-12"> <main class="py-12">
<slot /> <slot />
</main> </main>
</div> </div>
<Footer /> <Footer />
<script>
// SPA transition system with history API
document.addEventListener('DOMContentLoaded', () => {
const pageTransition = document.getElementById('page-transition');
const mainContent = document.querySelector('main');
// Initialize content with entrance animation
if (mainContent) {
mainContent.classList.add('content-entering');
setTimeout(() => {
mainContent.classList.remove('content-entering');
}, 800);
}
// Function to load content via fetch
async function loadContent(url) {
try {
// Show transition overlay
if (pageTransition) {
pageTransition.classList.remove('opacity-0', 'pointer-events-none');
pageTransition.classList.add('opacity-100');
}
// Fade out current content
if (mainContent) {
mainContent.style.opacity = '0';
mainContent.style.transform = 'translateY(10px)';
}
// Fetch the new page content
const response = await fetch(url);
if (!response.ok) throw new Error(`Failed to fetch ${url}`);
const html = await response.text();
// Create a temporary element to parse the HTML
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// Extract the main content
const newContent = doc.querySelector('main');
if (!newContent) throw new Error('Could not find main content in the fetched page');
// Extract the title
const newTitle = doc.querySelector('title');
if (newTitle) {
document.title = newTitle.textContent;
}
// Extract meta description
const newDescription = doc.querySelector('meta[name="description"]');
if (newDescription) {
const currentDescription = document.querySelector('meta[name="description"]');
if (currentDescription) {
currentDescription.setAttribute(
'content',
newDescription.getAttribute('content') || ''
);
}
}
// Wait a bit for transition effect
await new Promise((resolve) => setTimeout(resolve, 300));
// Replace the content
if (mainContent && newContent) {
mainContent.innerHTML = newContent.innerHTML;
// Run scripts in the new content
Array.from(newContent.querySelectorAll('script')).forEach((oldScript) => {
const newScript = document.createElement('script');
Array.from(oldScript.attributes).forEach((attr) => {
newScript.setAttribute(attr.name, attr.value);
});
newScript.textContent = oldScript.textContent;
if (oldScript.parentNode) {
mainContent.appendChild(newScript);
}
});
}
// Fade in new content with animation
if (mainContent) {
mainContent.style.opacity = '0';
mainContent.style.transform = 'translateY(10px)';
setTimeout(() => {
mainContent.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
mainContent.style.opacity = '1';
mainContent.style.transform = 'translateY(0)';
// Add entrance animation class
mainContent.classList.add('content-entering');
setTimeout(() => {
mainContent.classList.remove('content-entering');
}, 800);
}, 50);
}
// Hide transition overlay
if (pageTransition) {
setTimeout(() => {
pageTransition.classList.add('opacity-0', 'pointer-events-none');
pageTransition.classList.remove('opacity-100');
}, 200);
}
// Dispatch custom event for content loaded
document.dispatchEvent(
new CustomEvent('spa-content-loaded', {
detail: { url },
})
);
// Scroll to top or to saved position
window.scrollTo(0, 0);
// Re-attach event listeners to new content
attachLinkListeners();
} catch (error) {
console.error('Error loading content:', error);
// Fallback to traditional navigation on error
window.location.href = url;
}
}
// Function to attach event listeners to all links
function attachLinkListeners() {
document.querySelectorAll('a').forEach((link) => {
// Skip links that are already handled, anchor links, external links, or have special attributes
if (
link.hasAttribute('data-spa-handled') ||
!link.href.startsWith(window.location.origin) ||
link.href.includes('#') ||
link.hasAttribute('target') ||
link.hasAttribute('download') ||
link.getAttribute('rel') === 'external' ||
link.getAttribute('rel') === 'nofollow'
) {
return;
}
// Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
e.preventDefault();
const targetHref = link.href;
// Don't transition if clicking the current page
if (targetHref === window.location.href) {
return;
}
// Update browser history
window.history.pushState({ path: targetHref }, '', targetHref);
// Load the new content
loadContent(targetHref);
});
});
}
// Initial attachment of link listeners
attachLinkListeners();
// Handle browser back/forward navigation
window.addEventListener('popstate', (e) => {
if (e.state && e.state.path) {
loadContent(e.state.path);
} else {
loadContent(window.location.href);
}
});
// Check RSS feed availability
const checkAndGenerateRSS = async () => {
try {
const response = await fetch('/rss.xml');
if (!response.ok) {
console.warn('RSS feed not found. Please generate it using an RSS plugin for Astro.');
}
} catch (error) {
console.warn('Could not check RSS feed status.');
}
};
// Check RSS feed availability
checkAndGenerateRSS();
});
// Theme handling with transition effects
function setupThemeHandling() {
// Apply theme from localStorage or system preference
const theme = localStorage.getItem('theme');
if (
theme === 'dark' ||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Listen for theme changes
document.addEventListener('themeChanged', () => {
// Add transition class to body
document.body.classList.add('theme-transitioning');
// Remove class after transition completes
setTimeout(() => {
document.body.classList.remove('theme-transitioning');
}, 500);
});
}
// Initialize theme handling
document.addEventListener('DOMContentLoaded', setupThemeHandling);
</script>
</body> </body>
</html> </html>
<style> <style>
/* Page transition effects */
#page-transition {
transition: opacity 0.3s ease;
backdrop-filter: blur(4px);
}
/* Transition spinner animation */
.transition-spinner {
width: 30px;
height: 30px;
border: 2px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top-color: #3b82f6;
animation: spin 0.7s linear infinite;
}
:global(.dark) .transition-spinner {
border-color: rgba(255, 255, 255, 0.1);
border-top-color: #60a5fa;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Content entrance animation */ /* Content entrance animation */
main { main {
opacity: 1; opacity: 1;

View File

@@ -1,25 +0,0 @@
---
import { ViewTransitions } from 'astro:transitions';
import BaseLayout from './BaseLayout.astro';
const { title, description } = Astro.props;
---
<BaseLayout title={title} description={description}>
<ViewTransitions fallback="swap" />
<div transition:animate="slide">
<slot />
</div>
</BaseLayout>
<style>
/* Custom transition styles */
::view-transition-old(root) {
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-to-left;
}
::view-transition-new(root) {
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-from-right;
}
</style>

View File

@@ -5,35 +5,20 @@ import Layout from '../layouts/Layout.astro';
<Layout title="404 - Page Not Found"> <Layout title="404 - Page Not Found">
<div <div
class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center" class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
transition:animate="slide"
> >
<!-- Animated background elements -->
<div class="absolute inset-0 overflow-hidden">
<div
class="animate-blob absolute -left-20 -top-20 h-64 w-64 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50"
>
</div>
<div
class="animate-blob animation-delay-2000 absolute right-1/4 top-1/2 h-96 w-96 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30"
>
</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 --> <!-- Main content with animation -->
<div class="relative z-10 mx-auto max-w-xl"> <div class="relative z-10 mx-auto max-w-xl">
<div class="glitch-wrapper"> <div class="glitch-wrapper">
<h1 <h1
class="glitch text-9xl font-bold leading-none text-zinc-900 dark:text-zinc-100 sm:text-[12rem]" class="glitch text-9xl leading-none font-bold text-zinc-900 sm:text-[12rem] dark:text-zinc-100"
data-text="404" data-text="404"
> >
404 404
</h1> </h1>
</div> </div>
<h2 class="mt-6 text-2xl font-bold text-zinc-800 dark:text-zinc-200 sm:text-3xl"> <h2 class="mt-6 text-2xl font-bold text-zinc-800 sm:text-3xl dark:text-zinc-200">
Page Not Found Page Not Found
</h2> </h2>
@@ -48,7 +33,8 @@ import Layout from '../layouts/Layout.astro';
> >
<span <span
class="absolute inset-0 z-0 bg-gradient-to-r from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100" class="absolute inset-0 z-0 bg-gradient-to-r from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
></span> >
</span>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
@@ -61,14 +47,15 @@ import Layout from '../layouts/Layout.astro';
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25" d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
></path> >
</path>
</svg> </svg>
<span class="relative z-10 font-medium">Return Home</span> <span class="relative z-10 font-medium">Return Home</span>
</a> </a>
<button <button
id="back-button" id="back-button"
class="group inline-flex items-center gap-2 rounded-lg border border-zinc-300 px-6 py-3 text-zinc-700 shadow-sm transition-all duration-300 hover:bg-zinc-100 hover:shadow-md dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800" class="group inline-flex items-center gap-2 rounded-lg border border-zinc-300 px-6 py-3 text-zinc-700 shadow-xs transition-all duration-300 hover:bg-zinc-100 hover:shadow-md dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -81,7 +68,9 @@ import Layout from '../layouts/Layout.astro';
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path> d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
>
</path>
</svg> </svg>
<span class="font-medium">Go Back</span> <span class="font-medium">Go Back</span>
</button> </button>
@@ -89,9 +78,9 @@ import Layout from '../layouts/Layout.astro';
<!-- Random fun fact --> <!-- Random fun fact -->
<div <div
class="mx-auto mt-16 max-w-md rounded-xl border border-zinc-100 bg-zinc-50 p-6 shadow-sm backdrop-blur-sm dark:border-zinc-700/50 dark:bg-zinc-800/50" class="mx-auto mt-16 max-w-md rounded-xl border border-zinc-100 bg-zinc-50 p-6 shadow-xs backdrop-blur-xs dark:border-zinc-700/50 dark:bg-zinc-800/50"
> >
<h3 class="text-sm font-medium uppercase tracking-wider text-zinc-500 dark:text-zinc-400"> <h3 class="text-sm font-medium tracking-wider text-zinc-500 uppercase dark:text-zinc-400">
Did you know? Did you know?
</h3> </h3>
<p class="mt-2 text-sm text-zinc-700 dark:text-zinc-300" id="fun-fact"> <p class="mt-2 text-sm text-zinc-700 dark:text-zinc-300" id="fun-fact">
@@ -127,97 +116,9 @@ import Layout from '../layouts/Layout.astro';
const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)]; const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)];
funFactElement.textContent = randomFact; funFactElement.textContent = randomFact;
} }
// Handle SPA transitions for 404 page
function setupSPATransitions() {
// Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed
if (
link.getAttribute('href').includes('#') ||
link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')
) {
return;
}
// Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
e.preventDefault();
const targetHref = link.getAttribute('href');
// Trigger page transition animation
const pageTransition = document.getElementById('page-transition');
if (pageTransition) {
pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100');
// Navigate after transition effect
setTimeout(() => {
window.location.href = targetHref;
}, 300);
} else {
// Fallback if transition element doesn't exist
window.location.href = targetHref;
}
});
});
// Re-initialize back button after SPA navigation
const backButton = document.getElementById('back-button');
if (backButton) {
backButton.addEventListener('click', () => {
window.history.back();
});
}
}
// Initialize on first load
document.addEventListener('DOMContentLoaded', setupSPATransitions);
// Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupSPATransitions);
// For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupSPATransitions);
</script> </script>
<style> <style>
/* Animation for floating blobs */
@keyframes blob {
0% {
transform: translate(0px, 0px) scale(1);
}
33% {
transform: translate(30px, -50px) scale(1.1);
}
66% {
transform: translate(-20px, 20px) scale(0.9);
}
100% {
transform: translate(0px, 0px) scale(1);
}
}
.animate-blob {
animation: blob 7s infinite;
}
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}
/* Glitch effect for 404 text */ /* Glitch effect for 404 text */
.glitch-wrapper { .glitch-wrapper {
position: relative; position: relative;

View File

@@ -1,7 +1,6 @@
--- ---
import BaseLayout from '../layouts/BaseLayout.astro'; import BaseLayout from '../layouts/BaseLayout.astro';
import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa'; import DynamicIcon from '../utils/DynamicIcon.tsx';
import { SiTypescript, SiAstro } from 'react-icons/si';
import directus from '../../lib/directus'; import directus from '../../lib/directus';
import { readSingleton, readItems } from '@directus/sdk'; import { readSingleton, readItems } from '@directus/sdk';
@@ -17,23 +16,16 @@ const skills = await directus.request(
--- ---
<BaseLayout title="About Me" description={global.description}> <BaseLayout title="About Me" description={global.description}>
<div class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16"> <div
class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16"
transition:animate="slide"
>
<!-- Hero Section --> <!-- Hero Section -->
<div class="relative mb-12 sm:mb-16 md:mb-20"> <div class="relative mb-12 sm:mb-16 md:mb-20">
<!-- Decorative elements -->
<div
class="animate-blob theme-transition-bg absolute -left-10 -top-10 h-36 w-36 rounded-full bg-zinc-100 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-left-20 sm:-top-20 sm:h-48 sm:w-48 md:h-72 md:w-72"
>
</div>
<div
class="animate-blob animation-delay-2000 theme-transition-bg absolute -bottom-10 -right-10 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-48 sm:w-48 md:h-72 md:w-72"
>
</div>
<div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12"> <div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12">
<div class="order-2 text-center md:order-1 md:text-left"> <div class="hero-text order-2 text-center md:order-1 md:text-left">
<h1 <h1
class="theme-transition-color mb-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:mb-6 sm:text-4xl md:text-5xl" class="theme-transition-color hero-text 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 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" 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"
@@ -42,7 +34,7 @@ const skills = await directus.request(
</h1> </h1>
<p <p
class="theme-transition-color mb-6 text-lg leading-relaxed text-zinc-600 dark:text-zinc-400 sm:mb-8 sm:text-xl" class="theme-transition-color hero-text 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>
@@ -56,7 +48,7 @@ const skills = await directus.request(
<div class="relative order-1 md:order-2"> <div class="relative order-1 md:order-2">
<div <div
class="theme-transition-all mx-auto aspect-square w-full max-w-[280px] overflow-hidden rounded-3xl border-4 border-white shadow-xl dark:border-zinc-800 sm:max-w-[320px] sm:border-8 sm:shadow-2xl md:max-w-md" 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}`
@@ -65,13 +57,6 @@ const skills = await directus.request(
loading="eager" loading="eager"
/> />
</div> </div>
<!-- Decorative elements -->
<div
class="theme-transition-all absolute -bottom-4 -right-4 flex h-16 w-16 items-center justify-center rounded-full border-2 border-white bg-zinc-100 shadow-lg dark:border-zinc-900 dark:bg-zinc-800 sm:-bottom-6 sm:-right-6 sm:h-20 sm:w-20 sm:border-4 md:h-24 md:w-24"
>
<span class="text-2xl sm:text-3xl">👋</span>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -80,27 +65,33 @@ const skills = await directus.request(
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24"> <div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
<div class="mx-auto max-w-3xl"> <div class="mx-auto max-w-3xl">
<h2 <h2
class="theme-transition-color mb-6 flex items-center justify-center text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-8 sm:text-3xl md:justify-start" 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 <span
class="theme-transition-bg mr-4 hidden h-1 w-8 bg-zinc-300 dark:bg-zinc-700 sm:inline-block sm:w-12" 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> ></span>
About Me About Me
<span <span
class="theme-transition-bg ml-4 hidden h-1 w-8 bg-zinc-300 dark:bg-zinc-700 sm:inline-block sm:w-12" 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> ></span>
</h2> </h2>
<div class="theme-transition-all prose prose-zinc max-w-none dark:prose-invert"> <div class="theme-transition-all hero-text prose prose-zinc dark:prose-invert max-w-none">
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"> <p
class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
>
{about.experience} {about.experience}
</p> </p>
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"> <p
class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
>
{about.education} {about.education}
</p> </p>
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"> <p
class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
>
{about.certifications} {about.certifications}
</p> </p>
</div> </div>
@@ -110,7 +101,7 @@ const skills = await directus.request(
<!-- Skills Section --> <!-- Skills Section -->
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24"> <div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
<h2 <h2
class="theme-transition-color mb-8 text-center text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-12 sm:text-3xl" 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 Tech Stack
</h2> </h2>
@@ -119,37 +110,34 @@ const skills = await directus.request(
<!-- Main slider container --> <!-- Main slider container -->
<div class="slider-track animate-slide flex"> <div class="slider-track animate-slide flex">
{ {
skills.map((skill, index) => ( [...skills, ...skills, ...skills].map((skill, index) => (
<div <div
key={`${skill.title}-${index}`} key={`${skill.title}-${index}`}
class="skill-card theme-transition-element mx-2 min-w-[220px] transform rounded-xl border border-zinc-200 bg-white transition-all duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-300 hover:shadow-xl dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600 sm:mx-4 sm:min-w-[280px]" 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="p-4 sm:p-6"> <div class="p-4 sm:p-6">
<div class="mb-4 flex items-center justify-between sm:mb-6"> <div class="mb-4 flex items-center justify-between sm:mb-6">
<div class="flex items-center gap-2 sm:gap-4"> <div class="flex items-center gap-2 sm:gap-4">
<div class="theme-transition-bg theme-transition-color flex h-8 w-8 transform items-center justify-center rounded-lg bg-zinc-100 text-zinc-800 transition-transform group-hover:rotate-12 dark:bg-zinc-800 dark:text-zinc-200 sm:h-12 sm:w-12"> <div class="theme-transition-bg theme-transition-color flex h-8 w-8 transform items-center justify-center rounded-lg bg-zinc-100 text-zinc-800 transition-transform group-hover:rotate-12 sm:h-12 sm:w-12 dark:bg-zinc-800 dark:text-zinc-200">
<skill.icon <DynamicIcon name={skill.icon} />
size={20}
className="sm:text-2xl transform transition-all hover:scale-125"
/>
</div> </div>
<h3 class="theme-transition-color text-base font-semibold text-zinc-900 dark:text-zinc-100 sm:text-xl"> <h3 class="theme-transition-color text-base font-semibold text-zinc-900 sm:text-xl dark:text-zinc-100">
{skill.title} {skill.title}
</h3> </h3>
</div> </div>
<span class="theme-transition-all rounded-full bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 sm:px-2.5 sm:py-1 sm:text-sm"> <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}% {skill.level}%
</span> </span>
</div> </div>
<div class="theme-transition-bg relative h-1.5 w-full overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-700 sm:h-2"> <div class="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="progress-bar-animate theme-transition-bg absolute left-0 top-0 h-full rounded-full bg-gradient-to-r from-zinc-700 via-zinc-600 to-zinc-800 transition-all duration-1000 dark:from-zinc-300 dark:via-zinc-400 dark:to-zinc-200" 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 class="theme-transition-color mt-1 flex justify-between font-mono text-[10px] text-zinc-400 dark:text-zinc-500 sm:mt-2 sm:text-xs"> <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>
@@ -161,228 +149,84 @@ const skills = await directus.request(
<!-- Gradient overlays for smooth fade effect --> <!-- Gradient overlays for smooth fade effect -->
<div <div
class="theme-transition-bg absolute bottom-0 left-0 top-0 z-10 w-12 bg-gradient-to-r from-white to-transparent dark:from-zinc-900 sm:w-24" 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>
<div <div
class="theme-transition-bg absolute bottom-0 right-0 top-0 z-10 w-12 bg-gradient-to-l from-white to-transparent dark:from-zinc-900 sm:w-24" 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> </div>
<!-- Contact Section --> <!-- Contact Section -->
<div class="theme-transition-all mx-auto max-w-3xl text-center"> <div class="theme-transition-all mx-auto max-w-3xl text-center">
<h2 <h2
class="theme-transition-color mb-4 text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-6 sm:text-3xl" class="theme-transition-color mb-4 text-2xl font-bold text-zinc-900 sm:mb-6 sm:text-3xl dark:text-zinc-100"
> >
Get in Touch Get in Touch
</h2> </h2>
<p <p
class="theme-transition-color mb-6 text-base text-zinc-600 dark:text-zinc-400 sm:mb-8 sm:text-lg" 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 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. just say hello, feel free to reach out.
</p> </p>
<div class="group">
<a <a
href=`mailto:${global.email}` href=`mailto:${global.email}`
class="theme-transition-all inline-flex items-center justify-center rounded-lg bg-zinc-900 px-6 py-3 text-base font-medium text-zinc-100 transition-colors hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300 sm:px-8 sm:py-4 sm:text-lg" class="theme-transition-all inline-flex items-center justify-center rounded-lg bg-zinc-900 px-6 py-3 text-base font-medium text-zinc-100 transition-colors group-hover:bg-blue-600 group-hover:text-zinc-100 sm:px-8 sm:py-4 sm:text-lg dark:bg-zinc-100 dark:text-zinc-900 dark:group-hover:bg-blue-600 dark:group-hover:text-zinc-100"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
> >
<path <svg
stroke-linecap="round" xmlns="http://www.w3.org/2000/svg"
stroke-linejoin="round" class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
stroke-width="2" fill="none"
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" viewBox="0 0 24 24"
></path> stroke="currentColor"
</svg> >
Say Hello <path
</a> 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>
<span class="relative inline-block overflow-hidden">
<span class="relative z-10">Say Hello</span>
<span
class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-100 transition-all duration-300 group-hover:w-full"
></span>
</span>
</a>
</div>
</div> </div>
</div> </div>
</BaseLayout> </BaseLayout>
<style>
/* Blob animation */
.animate-blob {
animation: blob-bounce 8s infinite ease;
}
.animation-delay-2000 {
animation-delay: 2s;
}
@keyframes blob-bounce {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(5%, 5%) scale(1.05);
}
50% {
transform: translate(0, 10%) scale(1);
}
75% {
transform: translate(-5%, 5%) scale(0.95);
}
}
/* Tech Stack Slider */
.slider-track {
width: fit-content;
animation: scroll 40s linear infinite;
}
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
}
}
@media (min-width: 640px) {
.slider-track {
animation: scroll 60s linear infinite;
}
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
}
}
}
.tech-stack-slider:hover .slider-track {
animation-play-state: paused;
}
.skill-card {
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
}
.skill-card:hover {
z-index: 10;
}
/* Reduce animation complexity on mobile for better performance */
@media (max-width: 640px) {
.skill-card {
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
}
.skill-card:hover {
transform: translateY(-5px) !important;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
}
}
.skill-card:before {
content: '';
position: absolute;
top: -10%;
left: -10%;
width: 120%;
height: 120%;
background: radial-gradient(
circle at center,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0) 70%
);
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
}
.skill-card:hover:before {
opacity: 1;
}
.progress-bar-animate {
position: relative;
overflow: hidden;
}
.progress-bar-animate:after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: progress-shine 2s infinite;
}
@keyframes progress-shine {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
/* Improved touch targets for mobile */
@media (max-width: 640px) {
a,
button {
min-height: 44px;
display: flex;
align-items: center;
}
.social-link {
min-width: 44px;
min-height: 44px;
}
}
/* Theme transition effect */
:global(.theme-switching) .theme-transition-element {
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Smooth card transition during theme switch */
.skill-card.theme-transition-element {
transition:
background-color var(--theme-transition),
border-color var(--theme-transition),
color var(--theme-transition),
box-shadow var(--theme-transition),
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
</style>
<script> <script>
// Wait for the DOM to be fully loaded document.addEventListener('astro:page-load', () => {
document.addEventListener('DOMContentLoaded', () => { // Add smooth reveal animations for content after loading
const sliderTrack = document.querySelector('.slider-track'); const animateContent = () => {
const heroElements = document.querySelectorAll(
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
);
heroElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
};
animateContent();
// Create seamless infinite scrolling effect // Create seamless infinite scrolling effect
const sliderTrack = document.querySelector('.slider-track');
function setupInfiniteScroll() { function setupInfiniteScroll() {
const cards = document.querySelectorAll('.skill-card'); const cards = document.querySelectorAll('.skill-card');
if (!cards.length) return; if (!cards.length) return;
// Clone the first set of cards and append to create seamless loop
const firstSetCount = cards.length / 3; // We have 3 sets in the markup
// Set proper animation based on screen size // Set proper animation based on screen size
function updateScrollAnimation() { function updateScrollAnimation() {
if (window.innerWidth >= 640) { if (window.innerWidth >= 640) {
@@ -467,9 +311,7 @@ const skills = await directus.request(
// Handle theme transition // Handle theme transition
document.addEventListener('themeChange', () => { document.addEventListener('themeChange', () => {
// Add special effects during theme transition
cards.forEach((card, index) => { cards.forEach((card, index) => {
// Add staggered animation delay
setTimeout(() => { setTimeout(() => {
card.classList.add('theme-changing'); card.classList.add('theme-changing');
setTimeout(() => { setTimeout(() => {
@@ -481,103 +323,155 @@ const skills = await directus.request(
}); });
</script> </script>
<script> <style>
// Handle SPA transitions for about page /* Tech Stack Slider */
function setupSPATransitions() { .slider-track {
// Handle all internal links for SPA transitions width: fit-content;
document.querySelectorAll('a[href^="/"]').forEach((link) => { animation: scroll 40s linear infinite;
// Skip links that are anchor links, external links, or already processed
if (
link.getAttribute('href').includes('#') ||
link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')
) {
return;
}
// Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
e.preventDefault();
const targetHref = link.getAttribute('href');
// Trigger page transition animation
const pageTransition = document.getElementById('page-transition');
if (pageTransition) {
pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100');
// Navigate after transition effect
setTimeout(() => {
window.location.href = targetHref;
}, 300);
} else {
// Fallback if transition element doesn't exist
window.location.href = targetHref;
}
});
});
// Initialize animations for about page
function animateAboutContent() {
// Animate hero section elements
const heroElements = document.querySelectorAll('h1, .order-2 p, .social-links-container');
heroElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
// Animate profile image
const profileImage = document.querySelector('.aspect-square');
if (profileImage) {
setTimeout(() => {
profileImage.classList.add('animate-reveal');
}, 200);
}
// Animate skill bars with staggered delay
const skillBars = document.querySelectorAll('.skill-bar');
skillBars.forEach((bar, index) => {
setTimeout(
() => {
bar.classList.add('animate-skill');
},
500 + index * 100
);
});
// Animate sections with staggered delay
const sections = document.querySelectorAll('section');
sections.forEach((section, index) => {
setTimeout(
() => {
section.classList.add('animate-reveal');
},
300 + index * 200
);
});
}
// Run animations
animateAboutContent();
} }
// Initialize on first load @keyframes scroll {
document.addEventListener('DOMContentLoaded', setupSPATransitions); 0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
}
}
// Re-initialize when content changes via Astro's view transitions @media (min-width: 640px) {
document.addEventListener('astro:page-load', setupSPATransitions); .slider-track {
animation: scroll 60s linear infinite;
}
// For compatibility with custom transition system @keyframes scroll {
document.addEventListener('page-transition-complete', setupSPATransitions); 0% {
</script> transform: translateX(0);
}
100% {
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
}
}
}
.tech-stack-slider:hover .slider-track {
animation-play-state: paused;
}
.skill-card {
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
}
.skill-card:hover {
z-index: 10;
}
/* Reduce animation complexity on mobile */
@media (max-width: 640px) {
.skill-card {
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
}
.skill-card:hover {
transform: translateY(-5px) !important;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
}
}
.skill-card:before {
content: '';
position: absolute;
top: -10%;
left: -10%;
width: 120%;
height: 120%;
background: radial-gradient(
circle at center,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0) 70%
);
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
}
.skill-card:hover:before {
opacity: 1;
}
.progress-bar-animate {
position: relative;
overflow: hidden;
}
.progress-bar-animate:after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: progress-shine 2s infinite;
}
@keyframes progress-shine {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
/* Touch targets for mobile */
@media (max-width: 640px) {
a,
button {
min-height: 44px;
display: flex;
align-items: center;
}
.social-link {
min-width: 44px;
min-height: 44px;
}
}
/* Content reveal animations */
.hero-text h1,
.hero-text span,
.hero-text p,
.hero-text ~ div {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
/* Theme transition effect */
:global(.theme-switching) .theme-transition-element {
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
/* Smooth card transition during theme switch */
.skill-card.theme-transition-element {
transition:
background-color var(--theme-transition),
border-color var(--theme-transition),
color var(--theme-transition),
box-shadow var(--theme-transition),
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
</style>

View File

@@ -41,25 +41,25 @@ const { post, nextPost, prevPost } = Astro.props;
updated_date={post.updated_date} updated_date={post.updated_date}
tags={post.tags} tags={post.tags}
> >
<!-- Main Content - Enhanced with better typography and spacing --> <!-- Main Content -->
<div <div
class="prose prose-sm prose-zinc max-w-none dark:prose-invert sm:prose-base prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:font-medium prose-a:text-zinc-800 prose-a:underline-offset-4 hover:prose-a:text-zinc-600 prose-img:rounded-xl dark:prose-a:text-zinc-300 dark:hover:prose-a:text-zinc-100" class="hero-text 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 set:html={post.content} />
</div> </div>
<!-- Next/Previous Navigation - Improved responsive design --> <!-- Next/Previous Navigation -->
<div <div
class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 dark:border-zinc-800 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2" 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"
> >
{ {
prevPost && ( prevPost && (
<a <a
href={`/blog/${prevPost.slug}`} 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 dark:border-zinc-800 dark:hover:bg-zinc-800/50 sm:p-6" 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 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" /> <div class="absolute inset-0 bg-gradient-to-r from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
<span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm"> <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 <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="20" width="20"
@@ -76,7 +76,7 @@ const { post, nextPost, prevPost } = Astro.props;
</svg> </svg>
Previous Article Previous Article
</span> </span>
<h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg"> <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>
@@ -86,10 +86,10 @@ const { post, nextPost, prevPost } = Astro.props;
nextPost && ( nextPost && (
<a <a
href={`/blog/${nextPost.slug}`} 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 dark:border-zinc-800 dark:hover:bg-zinc-800/50 sm:p-6 md:text-right" 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 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" /> <div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
<span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm md:justify-end"> <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 <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -106,7 +106,7 @@ const { post, nextPost, prevPost } = Astro.props;
<path d="m9 18 6-6-6-6" /> <path d="m9 18 6-6-6-6" />
</svg> </svg>
</span> </span>
<h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg"> <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>
@@ -116,7 +116,62 @@ const { post, nextPost, prevPost } = Astro.props;
</BlogPost> </BlogPost>
<script> <script>
// Removing TOC-related functions document.addEventListener('astro:page-load', () => {
// Show button when scrolled down
const backToTopButton = document.getElementById('back-to-top');
if (backToTopButton) {
const toggleBackToTopButton = () => {
if (window.scrollY > 300) {
backToTopButton.classList.remove('opacity-0', 'invisible');
backToTopButton.classList.add('opacity-100', 'visible');
} else {
backToTopButton.classList.remove('opacity-100', 'visible');
backToTopButton.classList.add('opacity-0', 'invisible');
}
};
// Scroll to top when clicked
backToTopButton.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
// Check scroll position
window.addEventListener('scroll', toggleBackToTopButton);
toggleBackToTopButton();
}
// Add smooth reveal animations for content after loading
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
);
heroElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
// Animate posts with staggered delay
const articles = document.querySelectorAll('article.group');
articles.forEach((article, index) => {
setTimeout(
() => {
article.classList.add('animate-reveal');
},
500 + index * 150
);
});
};
animateContent();
});
// Add copy buttons to code blocks // Add copy buttons to code blocks
function initializeCodeCopyButtons() { function initializeCodeCopyButtons() {
@@ -183,50 +238,9 @@ const { post, nextPost, prevPost } = Astro.props;
}); });
} }
// Handle SPA transitions for blog post navigation
function setupSPATransitions() {
// Handle prev/next navigation links
document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
// Skip links that are anchor links or already processed
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
return;
}
// Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
e.preventDefault();
const targetHref = link.getAttribute('href');
// Trigger page transition animation
const pageTransition = document.getElementById('page-transition');
if (pageTransition) {
pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100');
// Navigate after transition effect
setTimeout(() => {
window.location.href = targetHref;
}, 300);
} else {
// Fallback if transition element doesn't exist
window.location.href = targetHref;
}
});
});
}
// Main initialization function // Main initialization function
function initializeBlogPost() { function initializeBlogPost() {
// Initialize remaining components
initializeCodeCopyButtons(); initializeCodeCopyButtons();
setupSPATransitions();
// Scroll to hash if present in URL // Scroll to hash if present in URL
if (window.location.hash) { if (window.location.hash) {
@@ -239,18 +253,28 @@ const { post, nextPost, prevPost } = Astro.props;
} }
} }
// Initialize on first load
document.addEventListener('DOMContentLoaded', initializeBlogPost);
// Re-initialize when content changes via Astro's view transitions // Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', initializeBlogPost); document.addEventListener('astro:page-load', initializeBlogPost);
// For compatibility with custom transition system
document.addEventListener('page-transition-complete', initializeBlogPost);
</script> </script>
<style> <style>
/* Removing TOC-related styles */ /* Content reveal animations */
.hero-text h1,
.hero-text span,
.hero-text p,
.hero-text ~ div,
article.group {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
/* Language badge styling */ /* Language badge styling */
.language-badge { .language-badge {
@@ -309,7 +333,7 @@ const { post, nextPost, prevPost } = Astro.props;
} }
.prose code { .prose code {
@reference rounded bg-zinc-100 px-1.5 py-0.5 text-sm font-medium text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200; @reference rounded-sm bg-zinc-100 px-1.5 py-0.5 text-sm font-medium text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200;
} }
.prose pre { .prose pre {

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,7 @@
--- ---
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 TagList from '../components/TagList.astro';
import directus from '../../lib/directus'; import directus from '../../lib/directus';
import { readItems, readSingleton } from '@directus/sdk'; import { readItems, readSingleton } from '@directus/sdk';
@@ -22,22 +23,14 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
--- ---
<Layout title=`Home | ${global.name}`> <Layout title=`Home | ${global.name}`>
<!-- Hero Section with improved mobile responsiveness --> <section
<section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20"> class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20"
transition:animate="slide"
>
<div class="relative mx-auto max-w-2xl"> <div class="relative mx-auto max-w-2xl">
<!-- Adjusted blob positions and sizes for better mobile appearance -->
<div
class="animate-blob theme-transition-bg absolute -left-10 -top-10 h-40 w-40 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50 sm:-left-20 sm:-top-20 sm:h-64 sm:w-64"
>
</div>
<div
class="animate-blob animation-delay-2000 theme-transition-bg absolute -bottom-10 -right-10 h-40 w-40 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-64 sm:w-64"
>
</div>
<div class="relative text-center sm:text-left"> <div class="relative text-center sm:text-left">
<h1 <h1
class="theme-transition-color hero-text text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-4xl md:text-5xl lg:text-6xl" 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="mt-1 block">development, and</span> <span class="mt-1 block">development, and</span>
@@ -51,7 +44,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
</span> </span>
</h1> </h1>
<p <p
class="theme-transition-color mx-auto mt-4 max-w-lg text-base leading-relaxed text-zinc-600 dark:text-zinc-400 sm:mx-0 sm:mt-6 sm:text-lg md:mt-8" 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>
@@ -60,7 +53,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
> >
<a <a
href="/about" 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" 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-400 dark:hover:text-zinc-100"
> >
<span>More about me</span> <span>More about me</span>
<svg <svg
@@ -76,31 +69,28 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
stroke-linejoin="round" stroke-linejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path> d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
</svg> </svg>
<span
class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
></span>
</a> </a>
</div> </div>
</div> </div>
</div> </div>
</section> </section>
<!-- Featured Post Section - Improved for mobile --> <!-- Featured post section -->
<section <section
class="theme-transition-all border-t border-zinc-100 px-4 py-10 dark:border-zinc-800 sm:px-6 sm:py-12 md:py-16" 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="mx-auto max-w-3xl"> <div class="mx-auto max-w-3xl">
<div <div
class="mb-6 flex flex-col justify-between gap-4 sm:mb-8 sm:flex-row sm:items-center md:mb-12" class="mb-6 flex flex-col justify-between gap-4 sm:mb-8 sm:flex-row sm:items-center md:mb-12"
> >
<h2 <h2
class="theme-transition-color text-center text-xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-left sm:text-2xl md:text-3xl" 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 Recent Posts
</h2> </h2>
<a <a
href="/blog" href="/blog"
class="theme-transition-color group relative flex min-h-[44px] items-center justify-center self-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300 sm:self-auto" 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-400 dark:hover:text-zinc-100"
> >
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
View all posts View all posts
@@ -118,25 +108,22 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path> d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
</svg> </svg>
</span> </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 --> <!-- Grid for mobile layout -->
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3"> <div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3">
{ {
recentPosts.map((post, index) => ( recentPosts.map((post, index) => (
<article class="hover-3d theme-transition-element group relative mx-auto flex w-full max-w-sm flex-col items-start sm:mx-0"> <article class="hover-3d theme-transition-element group relative mx-auto flex w-full max-w-sm flex-col items-start sm:mx-0">
<div class="theme-transition-bg absolute -inset-x-4 -inset-y-6 z-0 scale-95 bg-zinc-50 opacity-0 transition group-hover:scale-100 group-hover:opacity-100 dark:bg-zinc-800/50 sm:-inset-x-6 sm:rounded-2xl" /> <div class="theme-transition-all absolute -inset-x-4 -inset-y-6 z-0 border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 sm:-inset-x-6 sm:rounded-2xl dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70" />
{post.image && ( {post.image && (
<div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg"> <div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
<img <img
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`} src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
alt={post.title} alt={post.title}
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" class="h-full w-full object-cover"
loading={index === 0 ? 'eager' : 'lazy'} loading={index === 0 ? 'eager' : 'lazy'}
width="400" width="400"
height="225" height="225"
@@ -144,13 +131,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
</div> </div>
)} )}
<div class="theme-transition-color relative z-10 flex w-full flex-wrap items-center justify-center gap-x-3 gap-y-2 text-xs text-zinc-500 dark:text-zinc-400 sm:justify-start sm:gap-x-4"> <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">
<time datetime={post.published_date.toLocaleString()} class="font-medium">
<FormattedDate date={post.published_date} />
</time>
</div>
<h3 class="theme-transition-color relative z-10 mt-3 w-full text-center text-lg font-semibold tracking-tight text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:text-left sm:text-xl">
<a <a
href={`/blog/${post.slug}`} href={`/blog/${post.slug}`}
class="flex min-h-[44px] items-center justify-center sm:justify-start" class="flex min-h-[44px] items-center justify-center sm:justify-start"
@@ -160,45 +141,29 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
</a> </a>
</h3> </h3>
<p class="theme-transition-color relative z-10 mt-2 line-clamp-3 w-full text-center text-sm text-zinc-600 dark:text-zinc-400 sm:mt-3 sm:text-left"> <p class="z-10 mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400">
{post.description} {post.description}
</p> </p>
{post.tags && post.tags.length > 0 && ( <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">
<div class="relative z-10 mt-3 flex w-full flex-wrap justify-center gap-2 sm:mt-4 sm:justify-start"> <FormattedDate date={post.published_date} />
{post.tags.slice(0, 3).map((tag) => ( </div>
<a
href={`/topics/${tag}`} <TagList tags={post.tags} class="z-10" />
class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-800 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 sm:px-3"
>
#{tag}
</a>
))}
{post.tags.length > 3 && (
<span class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
+{post.tags.length - 3} more
</span>
)}
</div>
)}
<a <a
href={`/blog/${post.slug}`} href={`/blog/${post.slug}`}
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 dark:text-zinc-300 dark:group-hover:text-zinc-100 sm:mx-0 sm:mt-4" 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="relative inline-block overflow-hidden">
<span class="block transition-transform duration-300 group-hover:-translate-y-full"> <span class="relative z-10">Read article</span>
Read article <span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
</span>
<span class="absolute left-0 top-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
Explore now
</span>
</span> </span>
<svg <svg
viewBox="0 0 16 16" viewBox="0 0 16 16"
fill="none" fill="none"
aria-hidden="true" aria-hidden="true"
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 group-hover:translate-x-1" class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
> >
<path <path
d="M6.75 5.75 9.25 8l-2.5 2.25" d="M6.75 5.75 9.25 8l-2.5 2.25"
@@ -215,28 +180,28 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
</div> </div>
</section> </section>
<!-- Topics/Tags Section - Improved for mobile --> <!-- Topics section -->
{ {
allTags.length > 0 && ( allTags.length > 0 && (
<section class="theme-transition-all border-t border-zinc-100 px-4 py-10 dark:border-zinc-800 sm:px-6 sm:py-12 md:py-16"> <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">
<div class="mx-auto max-w-3xl"> <div class="mx-auto max-w-3xl">
<h2 class="theme-transition-color mb-6 text-center text-xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:mb-8 sm:text-left sm:text-2xl md:text-3xl"> <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">
Explore Topics Popular Tags
</h2> </h2>
<div class="mx-auto grid max-w-xs grid-cols-1 gap-3 sm:max-w-none sm:grid-cols-2 sm:gap-4 md:grid-cols-3"> <div class="hover-3d mx-auto grid max-w-xs grid-cols-1 gap-3 sm:max-w-none sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
{allTags.map((tag) => { {allTags.map((tag) => {
const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length; const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
return ( return (
<a <a
href={`/topics/${tag}`} href={`/tags/${tag}`}
class="theme-transition-all group flex min-h-[80px] flex-col rounded-xl border border-zinc-200 p-3 transition-all duration-300 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/70 sm:min-h-[90px] sm:p-4 md:p-6" class="theme-transition-all flex min-h-[80px] flex-col rounded-xl border border-zinc-300 bg-white/50 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:bg-zinc-900/50 dark:hover:bg-zinc-800/70"
> >
<div class="mb-2 flex items-start justify-between"> <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"> <span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
#{tag} #{tag}
</span> </span>
<span class="theme-transition-all flex-shrink-0 rounded-full bg-zinc-100 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400"> <span class="theme-transition-all shrink-0 rounded-full bg-zinc-100 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
{tagCount} {tagCount === 1 ? 'post' : 'posts'} {tagCount} {tagCount === 1 ? 'post' : 'posts'}
</span> </span>
</div> </div>
@@ -247,29 +212,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
); );
})} })}
</div> </div>
<div class="mt-6 text-center sm:mt-8">
<a
href="/tags"
class="theme-transition-color inline-flex min-h-[44px] items-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
>
<span>View all topics</span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
/>
</svg>
</a>
</div>
</div> </div>
</section> </section>
) )
@@ -278,8 +220,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
<script> <script>
// Add hover effect for cards on touch devices // Add hover effect for cards on touch devices
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('astro:page-load', () => {
// Check if it's a touch device
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0; const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) { if (isTouchDevice) {
@@ -297,11 +238,11 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
}); });
}); });
// Disable hover animations on touch devices for better performance // Disable hover animations on touch devices
document.documentElement.classList.add('touch-device'); document.documentElement.classList.add('touch-device');
} }
// Improved viewport height fix for mobile browsers // Viewport height fix for mobile browsers
const setVh = () => { const setVh = () => {
const vh = window.innerHeight * 0.01; const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`); document.documentElement.style.setProperty('--vh', `${vh}px`);
@@ -339,7 +280,7 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
}); });
} }
// Improved theme change handler that preserves scroll position and provides smoother transitions // Theme change handler that preserves scroll position and provides smoother transitions
document.addEventListener('themeChanged', () => { document.addEventListener('themeChanged', () => {
// Store current scroll position // Store current scroll position
const scrollPosition = window.scrollY; const scrollPosition = window.scrollY;
@@ -445,90 +386,8 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
}); });
}; };
// Run animations after the loading screen is hidden animateContent();
const loadingScreen = document.getElementById('loading-screen');
if (loadingScreen) {
// Check if loading screen is already hidden (page refresh)
if (loadingScreen.style.display === 'none') {
animateContent();
} else {
// Wait for loading screen to hide
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (
mutation.target === loadingScreen &&
mutation.type === 'attributes' &&
mutation.attributeName === 'style' &&
loadingScreen.style.display === 'none'
) {
animateContent();
observer.disconnect();
}
});
});
observer.observe(loadingScreen, { attributes: true });
// Fallback
setTimeout(animateContent, 3500);
}
} else {
// If loading screen doesn't exist for some reason
animateContent();
}
}); });
// SPA transition handling for homepage
function setupSPATransitions() {
// Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed
if (
link.getAttribute('href').includes('#') ||
link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')
) {
return;
}
// Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
e.preventDefault();
const targetHref = link.getAttribute('href');
// Trigger page transition animation
const pageTransition = document.getElementById('page-transition');
if (pageTransition) {
pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100');
// Navigate after transition effect
setTimeout(() => {
window.location.href = targetHref;
}, 300);
} else {
// Fallback if transition element doesn't exist
window.location.href = targetHref;
}
});
});
}
// Initialize on first load
document.addEventListener('DOMContentLoaded', setupSPATransitions);
// Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupSPATransitions);
// For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupSPATransitions);
</script> </script>
<style> <style>
@@ -555,13 +414,6 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
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 */
:global(*) {
transition-property: background-color, border-color, color, fill, stroke, opacity;
transition-duration: var(--theme-transition-duration);
transition-timing-function: var(--theme-transition-timing);
}
/* Remove the forced transition disabling which causes flickering */ /* Remove the forced transition disabling which causes flickering */
:global(.theme-switching), :global(.theme-switching),
:global(.theme-switching *) { :global(.theme-switching *) {
@@ -586,6 +438,4 @@ const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0,
opacity: 1 !important; opacity: 1 !important;
transform: translateY(0) !important; transform: translateY(0) !important;
} }
/* Rest of your existing styles... */
</style> </style>

View File

@@ -14,7 +14,6 @@ export async function getStaticPaths() {
}) })
); );
// Get all unique tags
const uniqueTags = [...new Set(posts.flatMap((post) => post.tags || []))]; const uniqueTags = [...new Set(posts.flatMap((post) => post.tags || []))];
// Create a path for each tag // Create a path for each tag
@@ -41,7 +40,6 @@ const sortedPosts =
: []; : [];
console.log(`Sorted posts length: ${sortedPosts.length}`); console.log(`Sorted posts length: ${sortedPosts.length}`);
const tagHue = Math.abs(tag.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % 360);
const relatedTags = [ const relatedTags = [
...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)), ...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)),
].slice(0, 5); ].slice(0, 5);
@@ -49,20 +47,10 @@ const relatedTags = [
<BaseLayout title={`Posts tagged with "${tag}"`}> <BaseLayout title={`Posts tagged with "${tag}"`}>
<div class="mx-auto max-w-5xl px-4 py-10 sm:py-16"> <div class="mx-auto max-w-5xl px-4 py-10 sm:py-16">
<!-- Header section -->
<div class="relative mb-10 sm:mb-16"> <div class="relative mb-10 sm:mb-16">
<div
class="animate-blob absolute -left-20 -top-20 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl dark:bg-zinc-900/30 sm:h-64 sm:w-64"
>
</div>
<div
class="animate-blob animation-delay-2000 absolute -bottom-10 -right-10 h-36 w-36 rounded-full bg-zinc-200 opacity-20 blur-2xl dark:bg-zinc-900/20 sm:h-48 sm:w-48"
>
</div>
<div class="relative text-center sm:text-left"> <div class="relative text-center sm:text-left">
<a <a
href="/tags" href="/blog#topics"
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" 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 <svg
@@ -76,9 +64,11 @@ const relatedTags = [
<path <path
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path> d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
>
</path>
</svg> </svg>
<span>Back to all topics</span> <span>Back to blog</span>
<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" 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> ></span>
@@ -88,7 +78,7 @@ const relatedTags = [
class="mb-2 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:justify-start" class="mb-2 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:justify-start"
> >
<div <div
class="tag-icon mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-zinc-100 shadow-sm dark:bg-zinc-800 sm:mx-0" class="tag-icon mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-zinc-100 shadow-xs sm:mx-0 dark:bg-zinc-800"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@@ -102,27 +92,28 @@ const relatedTags = [
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="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" 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> </path>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z"> </path>
</svg> </svg>
</div> </div>
<h1 <h1
class="text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-4xl" 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 h-1 w-full bg-zinc-200 dark:bg-zinc-700" <span class="absolute -bottom-1 left-0 h-1 w-full bg-zinc-200 dark:bg-zinc-700"
></span> ></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" class="animate-expand absolute -bottom-1 left-0 h-1 w-full bg-zinc-900 opacity-70 dark:bg-zinc-100"
></span> ></span>
</span> </span>
</h1> </h1>
</div> </div>
<p <p
class="mx-auto mt-4 max-w-2xl text-base text-zinc-600 dark:text-zinc-400 sm:mx-0 sm:text-lg" 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" Exploring <span class="font-medium text-zinc-900 dark:text-zinc-100"
>{sortedPosts.length}</span >{sortedPosts.length}</span
@@ -136,15 +127,15 @@ const relatedTags = [
<!-- Related tags section --> <!-- Related tags section -->
{ {
relatedTags.length > 0 && ( relatedTags.length > 0 && (
<div class="hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12"> <div class="hero-text hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12">
<h2 class="mb-3 text-center text-lg font-medium text-zinc-900 dark:text-zinc-100 sm:text-left"> <h2 class="mb-3 text-center text-lg font-medium text-zinc-900 sm:text-left dark:text-zinc-100">
Related topics Related topics
</h2> </h2>
<div class="flex flex-nowrap justify-center gap-2 sm:justify-start"> <div class="flex flex-nowrap justify-center gap-2 sm:justify-start">
{relatedTags.map((relatedTag) => ( {relatedTags.map((relatedTag) => (
<a <a
href={`/topics/${relatedTag}`} href={`/tags/${relatedTag}`}
class="inline-flex flex-shrink-0 items-center rounded-full bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-900 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700" class="inline-flex shrink-0 items-center rounded-full bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-900 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
> >
#{relatedTag} #{relatedTag}
</a> </a>
@@ -156,72 +147,52 @@ const relatedTags = [
<!-- Posts list --> <!-- Posts list -->
<div class="relative"> <div class="relative">
<div class="bg-grid-pattern pointer-events-none absolute inset-0 opacity-5 dark:opacity-10"> <div
class="hero-text bg-grid-pattern pointer-events-none absolute inset-0 opacity-5 dark:opacity-10"
>
</div> </div>
<div class="relative space-y-6 sm:space-y-8"> <div class="relative space-y-6 sm:space-y-8">
{ {
sortedPosts.map((post) => ( sortedPosts.map((post) => (
<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 dark:border-zinc-800 dark:hover:bg-zinc-900/50 sm:mx-0 sm:p-8"> <article class="hover-3d theme-transition-element group relative mx-auto flex max-w-2xl flex-col p-5 sm:mx-0 sm:p-8">
<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="absolute inset-0 rounded-2xl border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 hover:bg-zinc-50/80 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70 dark:hover:bg-zinc-900/50" />
<div class="flex flex-col gap-5 sm:flex-row sm:gap-6"> <div class="flex flex-col gap-5 sm:flex-row sm:gap-6">
{post.image && ( {post.image && (
<div class="mx-auto h-40 w-full flex-shrink-0 overflow-hidden rounded-xl shadow-sm transition-all duration-300 group-hover:shadow-md sm:mx-0 sm:w-56"> <div class="z-10 mx-auto h-40 w-full shrink-0 overflow-hidden rounded-xl 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="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" class="h-full w-full object-cover"
loading="lazy" loading="lazy"
/> />
</div> </div>
)} )}
<div class="flex-1"> <div class="z-10 flex-1">
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 dark:text-zinc-400 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm"> <h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 sm:mb-3 sm:text-left sm:text-2xl dark:text-zinc-100">
{post.published_date && (
<time
datetime={post.published_date.toLocaleString()}
class="flex items-center gap-1.5"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-3.5 w-3.5 sm:h-4 sm:w-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0
A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5"
/>
</svg>
<FormattedDate date={post.published_date} />
</time>
)}
</div>
<h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:mb-3 sm:text-left sm:text-2xl">
<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> </a>
</h2> </h2>
<p class="mb-4 line-clamp-2 text-center text-sm text-zinc-600 dark:text-zinc-400 sm:line-clamp-3 sm:text-left sm:text-base"> <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">
{post.description} {post.description}
</p> </p>
<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">
<FormattedDate date={post.published_date} />
</div>
</div> </div>
</div> </div>
<div class="mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 dark:border-zinc-800 sm:justify-between"> <div class="z-10 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 && ( {post.tags && post.tags.length > 0 && (
<div class="mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start"> <div class="mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start">
{post.tags.slice(0, 3).map((postTag) => ( {post.tags.slice(0, 3).map((postTag) => (
<a <a
href={`/topics/${postTag}`} href={`/blog/${postTag}`}
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${ class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${
postTag === tag postTag === tag
? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100' ? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100'
@@ -239,33 +210,26 @@ const relatedTags = [
</div> </div>
)} )}
<div class="mx-auto sm:ml-auto sm:mr-0"> <div class="mx-auto sm:mr-0 sm:ml-auto">
<a <a
href={`/blog/${post.slug}/`} 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" 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"
aria-hidden="true"
tabindex="-1"
> >
<span class="relative inline-block overflow-hidden"> <span class="relative inline-block overflow-hidden">
<span class="block transition-transform duration-300 group-hover:-translate-y-full"> <span class="relative z-10">Read article</span>
Read article <span class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200" />
</span>
<span class="absolute left-0 top-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
Explore now
</span>
</span> </span>
<svg <svg
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"
fill="none" fill="none"
viewBox="0 0 24 24" aria-hidden="true"
stroke-width="1.5" class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
stroke="currentColor"
class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
> >
<path <path
d="M6.75 5.75 9.25 8l-2.5 2.25"
stroke-width="1.5"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
/> />
</svg> </svg>
</a> </a>
@@ -277,18 +241,18 @@ const relatedTags = [
</div> </div>
</div> </div>
<!-- Empty state với màu zinc --> <!-- Empty state -->
{ {
sortedPosts.length === 0 && ( sortedPosts.length === 0 && (
<div class="py-12 text-center sm:py-20"> <div class="py-12 text-center sm:py-20">
<div class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 dark:bg-zinc-800 sm:mb-6 sm:h-20 sm:w-20"> <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">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
stroke="currentColor" stroke="currentColor"
class="h-8 w-8 text-zinc-500 dark:text-zinc-400 sm:h-10 sm:w-10" class="h-8 w-8 text-zinc-500 sm:h-10 sm:w-10 dark:text-zinc-400"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@@ -297,7 +261,7 @@ const relatedTags = [
/> />
</svg> </svg>
</div> </div>
<h2 class="mb-2 text-xl font-semibold text-zinc-900 dark:text-zinc-100 sm:text-2xl"> <h2 class="mb-2 text-xl font-semibold text-zinc-900 sm:text-2xl dark:text-zinc-100">
No posts found No posts found
</h2> </h2>
<p class="text-zinc-600 dark:text-zinc-400">There are no posts with this tag yet.</p> <p class="text-zinc-600 dark:text-zinc-400">There are no posts with this tag yet.</p>
@@ -327,6 +291,61 @@ const relatedTags = [
</div> </div>
</BaseLayout> </BaseLayout>
<script>
document.addEventListener('astro:page-load', () => {
// Add smooth reveal animations for content after loading
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
);
heroElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
// Animate posts with staggered delay
const articles = document.querySelectorAll('article.group');
articles.forEach((article, index) => {
setTimeout(
() => {
article.classList.add('animate-reveal');
},
500 + index * 150
);
});
};
animateContent();
// Add hover effect for cards on touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) {
const cards = document.querySelectorAll('.hover-3d');
cards.forEach((card) => {
card.addEventListener('touchstart', () => {
card.classList.add('is-touched');
});
card.addEventListener('touchend', () => {
setTimeout(() => {
card.classList.remove('is-touched');
}, 300);
});
});
// Disable hover animations on touch devices
document.documentElement.classList.add('touch-device');
}
});
</script>
<style> <style>
/* Grid pattern background */ /* Grid pattern background */
.bg-grid-pattern { .bg-grid-pattern {
@@ -354,7 +373,7 @@ const relatedTags = [
width: 0; width: 0;
} }
to { to {
width: 50%; width: 100%;
} }
} }
@@ -362,43 +381,22 @@ const relatedTags = [
animation: expand 1s ease-out forwards; animation: expand 1s ease-out forwards;
} }
/* Blob animation */ /* Content reveal animations */
.animate-blob { .hero-text h1,
animation: blob 7s infinite; .hero-text span,
} .hero-text p,
.hero-text ~ div,
.animation-delay-2000 { article.group {
animation-delay: 2s; opacity: 0;
} transform: translateY(20px);
@keyframes blob {
0% {
transform: translate(0px, 0px) scale(1);
}
33% {
transform: translate(20px, -20px) scale(1.1);
}
66% {
transform: translate(-20px, 20px) scale(0.9);
}
100% {
transform: translate(0px, 0px) scale(1);
}
}
/* Hover card effect */
.hover-card {
transform: translateY(0);
transition: transition:
transform 0.3s ease, opacity 0.8s ease,
box-shadow 0.3s ease, transform 0.8s ease;
background-color 0.3s ease;
} }
@media (hover: hover) { .animate-reveal {
.hover-card:hover { opacity: 1 !important;
transform: translateY(-2px); transform: translateY(0) !important;
}
} }
/* Line clamp for descriptions */ /* Line clamp for descriptions */
@@ -423,98 +421,3 @@ const relatedTags = [
} }
} }
</style> </style>
<script>
// Handle SPA transitions for tag pages
function setupSPATransitions() {
// Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed
if (
link.getAttribute('href').includes('#') ||
link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')
) {
return;
}
// Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
e.preventDefault();
const targetHref = link.getAttribute('href');
// Trigger page transition animation
const pageTransition = document.getElementById('page-transition');
if (pageTransition) {
pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100');
// Navigate after transition effect
setTimeout(() => {
window.location.href = targetHref;
}, 300);
} else {
// Fallback if transition element doesn't exist
window.location.href = targetHref;
}
});
});
// Initialize animations for tag page
function animateTagContent() {
// Animate header elements
const headerElements = document.querySelectorAll('h1, .tag-icon, .tag-description');
headerElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
// Animate posts with staggered delay
const articles = document.querySelectorAll('article');
articles.forEach((article, index) => {
setTimeout(
() => {
article.classList.add('animate-reveal');
},
400 + index * 100
);
});
// Animate related tags
const relatedTags = document.querySelectorAll('.related-tags a');
relatedTags.forEach((tag, index) => {
setTimeout(
() => {
tag.classList.add('animate-reveal');
},
600 + index * 50
);
});
}
// Run animations
animateTagContent();
}
// Initialize on first load
document.addEventListener('DOMContentLoaded', setupSPATransitions);
// Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupSPATransitions);
// For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupSPATransitions);
</script>
<!-- Add this at the end of your page -->

View File

@@ -1,714 +0,0 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import directus from '../../../lib/directus';
import { readItems } from '@directus/sdk';
const posts = await directus.request(
readItems('posts', {
fields: ['*'],
sort: ['-published_date'],
})
);
const tags = [...new Set(posts.flatMap((post) => post.tags || []))].sort();
// Count posts for each tag and create tag objects with additional data
const tagObjects = tags.map((tag) => {
const count = posts.filter((post) => post.tags?.includes(tag)).length;
// 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);
return {
name: tag,
count,
size: Math.max(1, Math.min(3, Math.floor(count / 2) + 1)), // Size 1-3 based on count
hue,
};
});
const sortedTags = [...tagObjects].sort((a, b) => b.count - a.count);
---
<BaseLayout title="Explore Tags">
<div class="theme-transition-all mx-auto w-full px-3 py-6 sm:px-6 sm:py-12 md:py-16">
<!-- Enhanced header section with animated elements - improved for mobile -->
<div class="theme-transition-element relative mb-8 text-center sm:mb-12 md:mb-16">
<div
class="animate-blob theme-transition-bg absolute -left-16 -top-16 h-36 w-36 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50 sm:h-48 sm:w-48 md:h-72 md:w-72"
>
</div>
<div
class="animate-blob animation-delay-2000 theme-transition-bg absolute -bottom-16 -right-16 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:h-48 sm:w-48 md:h-72 md:w-72"
>
</div>
<div
class="animate-blob animation-delay-4000 theme-transition-bg absolute right-8 top-8 h-24 w-24 rounded-full bg-zinc-100/30 opacity-40 blur-2xl dark:bg-zinc-700/20 sm:h-32 sm:w-32 md:h-40 md:w-40"
>
</div>
<h1
class="theme-transition-color relative mb-3 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:mb-4 sm:text-4xl md:mb-6 md:text-5xl lg:text-6xl"
>
<span class="relative inline-block">
<span class="relative inline-block">
<span
class="theme-transition-bg absolute -inset-1 rounded-lg bg-gradient-to-r from-zinc-200/50 to-zinc-300/50 blur-sm dark:from-zinc-800/50 dark:to-zinc-700/50"
></span>
<span class="relative">Explore</span>
</span>
{' '}
<span class="relative inline-block">
Topics
<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 dark:from-zinc-600 dark:to-zinc-400 sm:-bottom-2 sm:h-1"
></span>
</span>
</span>
</h1>
<p
class="theme-transition-color relative mx-auto max-w-2xl text-sm text-zinc-600 dark:text-zinc-400 sm:text-base md:text-lg lg:text-xl"
>
Discover content organized by your interests
</p>
</div>
{
tags.length === 0 ? (
<div class="theme-transition-element py-8 text-center sm:py-12 md:py-16">
<div class="theme-transition-bg mb-3 inline-flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 shadow-inner dark:bg-zinc-800 sm:mb-4 sm:h-20 sm:w-20 md:mb-6 md:h-24 md:w-24">
<svg
xmlns="http://www.w3.org/2000/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 dark:text-zinc-400 sm:h-10 sm:w-10 md:h-12 md:w-12"
>
<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 dark:text-zinc-200 sm:text-xl md:text-2xl">
No tags found yet.
</p>
<p class="theme-transition-color mt-2 text-xs text-zinc-500 dark:text-zinc-500 sm:text-sm md:text-base">
Check back later for categorized content.
</p>
</div>
) : (
<div class="flex w-full justify-center">
<div class="tag-cloud hover-3d glass theme-transition-all relative w-full rounded-lg border border-zinc-100 bg-white/50 p-3 backdrop-blur-sm dark:border-zinc-800 dark:bg-zinc-900/50 sm:rounded-xl sm:p-4 md:rounded-2xl md:p-6 lg:rounded-3xl lg:p-8">
<div class="bg-grid-pattern theme-transition-bg absolute inset-0 opacity-5 dark:opacity-10" />
<div class="theme-transition-bg absolute -right-8 -top-8 h-20 w-20 rounded-full bg-gradient-to-br from-zinc-200/30 to-zinc-300/20 blur-xl dark:from-zinc-700/20 dark:to-zinc-800/10 sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40" />
<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 dark:from-zinc-700/20 dark:to-zinc-800/10 sm:h-24 sm:w-24 md:h-32 md:w-32 lg:h-40 lg:w-40" />
<h2 class="theme-transition-color mb-3 text-center text-lg font-bold text-zinc-900 dark:text-zinc-100 sm:mb-4 sm:text-xl md:mb-6 md:text-2xl lg:mb-8 lg:text-3xl">
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) => (
<a
href={`/topics/${tag.name}`}
class="theme-transition-element theme-ripple group relative min-w-0 flex-grow overflow-hidden rounded-md border border-zinc-200 transition-all duration-300 hover:scale-[1.03] hover:border-zinc-300 hover:shadow-md active:scale-95 dark:border-zinc-800 dark:hover:border-zinc-700 sm:rounded-lg sm:hover:shadow-lg md:rounded-xl"
style={`--tag-hue: ${tag.hue};`}
>
<div class="theme-transition-bg absolute inset-0 bg-gradient-to-br from-zinc-50/90 to-zinc-100/90 opacity-100 transition-opacity group-hover:opacity-95 dark:from-zinc-800/90 dark:to-zinc-900/90" />
<div class="xxxs:px-2 xxs:px-2 xs:px-2 xxxs:py-2 xxs:py-2 xs:py-2 xxs:gap-2 relative flex w-full items-center gap-1.5 px-1.5 py-1.5 sm:px-3 sm:py-3 md:px-4 md:py-4">
<div class="xxxs:w-6 xxxs:h-6 xxs:w-6 xxs:h-6 xs:w-7 xs:h-7 group-hover:bg-accent/20 dark:group-hover:bg-accent/20 group-hover:text-accent-dark dark:group-hover:text-accent-light theme-transition-all flex h-5 w-5 flex-shrink-0 items-center justify-center rounded-full bg-zinc-100 text-zinc-700 shadow-sm transition-all duration-300 dark:bg-zinc-800 dark:text-zinc-300 sm:h-8 sm:w-8 md:h-10 md:w-10">
<span class="xxxs:text-xs xxs:text-xs xs:text-sm text-xs font-semibold sm:text-base md:text-lg">
#
</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 hyphens-auto break-words text-[10px] font-bold text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:text-sm md:text-base">
{tag.name}
</h3>
<p class="xxxs:text-[9px] xxs:text-[9px] xs:text-[10px] theme-transition-color truncate text-[8px] text-zinc-500 dark:text-zinc-400 sm:text-xs md:text-xs">
{tag.count} article{tag.count !== 1 ? 's' : ''}
</p>
</div>
</div>
</a>
))}
</div>
</div>
</div>
)
}
</div>
</BaseLayout>
<script>
// Ultra-reliable responsiveness handling
document.addEventListener('DOMContentLoaded', () => {
// Fix viewport width issues on mobile
const fixViewportWidth = () => {
// Force the viewport to be exactly the width of the device
const viewport = document.querySelector('meta[name="viewport"]');
if (!viewport) {
const meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
document.getElementsByTagName('head')[0].appendChild(meta);
} else {
viewport.setAttribute(
'content',
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
);
}
// Fix for horizontal overflow
document.body.style.overflowX = 'hidden';
document.documentElement.style.overflowX = 'hidden';
document.documentElement.style.width = '100%';
document.body.style.width = '100%';
};
fixViewportWidth();
// Adjust tag items based on screen size with extreme precision
const adjustTagItems = () => {
const tagItems = document.querySelectorAll('.theme-ripple');
const width =
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
const isVerySmall = width < 360;
const isExtremelySmall = width < 280;
const isMicroScreen = width < 240;
// Fix container width to match viewport exactly
const container = document.querySelector('.tag-cloud');
if (container) {
container.style.maxWidth = '100vw';
container.style.width = '100%';
container.style.boxSizing = 'border-box';
// Remove any margins that might cause overflow
container.style.marginLeft = '0';
container.style.marginRight = '0';
}
// Fix grid width
const grid = document.querySelector('.grid');
if (grid) {
grid.style.width = '100%';
grid.style.maxWidth = '100%';
}
tagItems.forEach((item) => {
// Set appropriate classes based on screen size
if (isMicroScreen) {
item.classList.add('micro-screen');
item.classList.remove('extremely-small-screen', 'very-small-screen');
} else if (isExtremelySmall) {
item.classList.add('extremely-small-screen');
item.classList.remove('very-small-screen', 'micro-screen');
} else if (isVerySmall) {
item.classList.add('very-small-screen');
item.classList.remove('extremely-small-screen', 'micro-screen');
} else {
item.classList.remove('very-small-screen', 'extremely-small-screen', 'micro-screen');
}
// Ensure text doesn't overflow on small screens
const tagName = item.querySelector('h3');
const tagCount = item.querySelector('p');
if (tagName) {
// Set title for accessibility
tagName.title = tagName.textContent.trim();
// Adjust text length based on screen size
if (isMicroScreen && tagName.textContent.length > 6) {
tagName.dataset.fullText = tagName.textContent;
tagName.textContent = tagName.textContent.substring(0, 6) + '...';
} else if (isExtremelySmall && tagName.textContent.length > 8) {
tagName.dataset.fullText = tagName.textContent;
tagName.textContent = tagName.textContent.substring(0, 8) + '...';
} else if (isVerySmall && tagName.textContent.length > 12) {
tagName.dataset.fullText = tagName.textContent;
tagName.textContent = tagName.textContent.substring(0, 12) + '...';
} else if (tagName.dataset.fullText) {
tagName.textContent = tagName.dataset.fullText;
delete tagName.dataset.fullText;
}
}
// Set the tag hue for hover effects
const hue = item.style.getPropertyValue('--tag-hue');
item.style.setProperty('--hover-hue', hue);
});
};
// Run on load
adjustTagItems();
// Run on resize with optimized debounce
let resizeTimer;
const handleResize = () => {
if (resizeTimer) {
window.cancelAnimationFrame(resizeTimer);
}
resizeTimer = window.requestAnimationFrame(() => {
fixViewportWidth();
adjustTagItems();
});
};
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleResize);
// Ensure layout is recalculated after page is fully loaded
window.addEventListener('load', () => {
fixViewportWidth();
adjustTagItems();
// Force recalculation after images and fonts are loaded
setTimeout(() => {
fixViewportWidth();
adjustTagItems();
}, 500);
});
// Fix for iOS Safari and other mobile browsers
if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) {
document.documentElement.style.setProperty(
'--safe-area-inset-bottom',
'env(safe-area-inset-bottom)'
);
// Fix for mobile viewport height issues
const setVh = () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
};
setVh();
window.addEventListener('resize', setVh);
window.addEventListener('orientationchange', () => {
// Wait for orientation change to complete
setTimeout(() => {
setVh();
fixViewportWidth();
}, 100);
});
}
// Add touch support for mobile devices
const addTouchSupport = () => {
const tagItems = document.querySelectorAll('.theme-ripple');
tagItems.forEach((item) => {
item.addEventListener(
'touchstart',
() => {
item.classList.add('touch-active');
},
{ passive: true }
);
item.addEventListener(
'touchend',
() => {
setTimeout(() => {
item.classList.remove('touch-active');
}, 150);
},
{ passive: true }
);
// Cancel active state if touch moves away
item.addEventListener(
'touchmove',
(e) => {
const touch = e.touches[0];
const rect = item.getBoundingClientRect();
if (
touch.clientX < rect.left ||
touch.clientX > rect.right ||
touch.clientY < rect.top ||
touch.clientY > rect.bottom
) {
item.classList.remove('touch-active');
}
},
{ passive: true }
);
});
};
addTouchSupport();
});
</script>
<style>
/* Base styles */
.tag-cloud {
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.03),
0 2px 4px rgba(0, 0, 0, 0.03),
0 4px 8px rgba(0, 0, 0, 0.05);
transform-style: preserve-3d;
perspective: 1000px;
transition: all var(--theme-transition);
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
margin-left: 0 !important;
margin-right: 0 !important;
}
/* Fix for horizontal overflow */
:global(html),
:global(body) {
overflow-x: hidden;
width: 100%;
max-width: 100%;
}
:global(.max-w-6xl) {
max-width: 100% !important;
width: 100% !important;
}
/* Ultra-responsive breakpoints for extreme reliability */
/* Micro screens (below 240px) */
@media (max-width: 239px) {
.tag-cloud {
padding: 0.5rem !important;
margin: 0 !important;
border-radius: 0.25rem !important;
width: 100% !important;
}
.tag-cloud h2 {
font-size: 0.875rem !important;
margin-bottom: 0.375rem !important;
}
.grid {
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
gap: 0.375rem !important;
width: 100% !important;
}
.micro-screen .flex {
padding: 0.25rem !important;
}
.micro-screen h3 {
font-size: 0.625rem !important;
}
.micro-screen p {
font-size: 0.5rem !important;
}
}
/* Extra extra extra small screens (240px-279px) */
@media (min-width: 240px) and (max-width: 279px) {
.xxxs\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.xxxs\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.xxxs\:py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.xxxs\:w-6 {
width: 1.5rem;
}
.xxxs\:h-6 {
height: 1.5rem;
}
.xxxs\:text-xs {
font-size: 0.75rem;
}
.xxxs\:gap-2 {
gap: 0.5rem;
}
.xxxs\:text-\[9px\] {
font-size: 9px;
}
}
/* Extra extra small screens (280px-359px) */
@media (min-width: 280px) {
.xxs\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.xxs\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.xxs\:py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.xxs\:w-6 {
width: 1.5rem;
}
.xxs\:h-6 {
height: 1.5rem;
}
.xxs\:text-xs {
font-size: 0.75rem;
}
.xxs\:gap-2 {
gap: 0.5rem;
}
.xxs\:text-\[9px\] {
font-size: 9px;
}
}
/* Extra small screens (360px-639px) */
@media (min-width: 360px) {
.xs\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.xs\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.xs\:py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.xs\:w-7 {
width: 1.75rem;
}
.xs\:h-7 {
height: 1.75rem;
}
.xs\:text-xs {
font-size: 0.75rem;
}
.xs\:text-sm {
font-size: 0.875rem;
}
.xs\:gap-2 {
gap: 0.5rem;
}
.xs\:text-\[10px\] {
font-size: 10px;
}
}
/* Ensure text doesn't overflow on small screens */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
/* Ensure proper word breaking for long tag names */
.break-words {
word-break: break-word;
overflow-wrap: break-word;
}
.hyphens-auto {
hyphens: auto;
}
/* Improved shadow for dark mode */
:global(.dark) .tag-cloud {
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.05),
0 2px 4px rgba(0, 0, 0, 0.1),
0 4px 8px rgba(0, 0, 0, 0.15);
}
/* Prevent layout shifts */
.flex-grow {
flex-grow: 1;
}
.min-w-0 {
min-width: 0;
}
/* Ensure container doesn't overflow */
.overflow-hidden {
overflow: hidden;
}
/* Touch support for mobile */
.touch-active {
transform: scale(0.97) !important;
opacity: 0.9;
transition:
transform 0.15s ease-in-out,
opacity 0.15s ease-in-out !important;
}
/* Animation for blob */
@keyframes blob {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(10px, -10px) scale(1.05);
}
50% {
transform: translate(0, 20px) scale(0.95);
}
75% {
transform: translate(-10px, -10px) scale(1.05);
}
}
.animate-blob {
animation: blob 20s infinite ease-in-out;
}
.animation-delay-2000 {
animation-delay: 2s;
}
.animation-delay-4000 {
animation-delay: 4s;
}
/* Animation for underline */
@keyframes underline {
0% {
transform: scaleX(0);
}
100% {
transform: scaleX(1);
}
}
.animate-underline {
animation: underline 1.5s ease-out forwards;
}
/* Fix for iOS Safari notch */
@supports (padding: max(0px)) {
.tag-cloud {
padding-left: max(0.75rem, env(safe-area-inset-left));
padding-right: max(0.75rem, env(safe-area-inset-right));
padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
}
}
</style>
<script>
// Handle SPA transitions for tags index page
function setupSPATransitions() {
// Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed
if (
link.getAttribute('href').includes('#') ||
link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')
) {
return;
}
// Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
e.preventDefault();
const targetHref = link.getAttribute('href');
// Trigger page transition animation
const pageTransition = document.getElementById('page-transition');
if (pageTransition) {
pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100');
// Navigate after transition effect
setTimeout(() => {
window.location.href = targetHref;
}, 300);
} else {
// Fallback if transition element doesn't exist
window.location.href = targetHref;
}
});
});
// Add hover effect for tag cards on touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) {
const tagCards = document.querySelectorAll('.tag-cloud a');
tagCards.forEach((card) => {
card.addEventListener('touchstart', () => {
card.classList.add('is-touched');
});
card.addEventListener('touchend', () => {
setTimeout(() => {
card.classList.remove('is-touched');
}, 300);
});
});
}
// Animate tag cards with staggered delay
const tagCards = document.querySelectorAll('.tag-cloud a');
tagCards.forEach((card, index) => {
setTimeout(
() => {
card.classList.add('animate-reveal');
},
100 + index * 50
);
});
}
// Initialize on first load
document.addEventListener('DOMContentLoaded', setupSPATransitions);
// Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupSPATransitions);
// For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupSPATransitions);
</script>

View File

@@ -1,5 +1,8 @@
/* Remove all the complex mobile menu styles and keep only what's necessary */ @import 'tailwindcss';
@import "tailwindcss";
/* Dark mode support for Tailwind CSS v4 */
/* https://tailwindcss.com/docs/dark-mode */
@custom-variant dark (&:where(.dark, .dark *));
@layer base { @layer base {
:root { :root {
@@ -12,10 +15,11 @@
html { html {
scroll-behavior: smooth; scroll-behavior: smooth;
scroll-padding-top: 5rem; scroll-padding-top: 5rem;
overflow-y: scroll;
} }
body { body {
@reference min-h-screen bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100; @apply min-h-screen bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100;
margin: 0; margin: 0;
padding: 0; padding: 0;
overflow-x: hidden; overflow-x: hidden;
@@ -38,10 +42,10 @@
scroll-padding-top: 4rem; scroll-padding-top: 4rem;
} }
/* Better touch targets on mobile */ /* Touch targets on mobile */
button, button,
a { a {
@reference min-h-[44px]; @apply min-h-[44px];
} }
} }
@@ -125,27 +129,10 @@
/* Smooth hover transitions */ /* Smooth hover transitions */
a, a,
button { button {
transition: all 0.2s ease; transition: all 0.5s ease;
} }
a:hover, a.hover:hover,
button:hover { button:hover {
transform: translateY(-1px); transform: translateY(-2px);
}
/* Smooth page transitions */
.page-transition {
transition:
opacity 0.3s ease,
transform 0.3s ease;
}
.page-entering {
opacity: 0;
transform: translateY(10px);
}
.page-entered {
opacity: 1;
transform: translateY(0);
} }

40
src/utils/DynamicIcon.tsx Normal file
View File

@@ -0,0 +1,40 @@
import React from 'react';
import * as FaIcons from 'react-icons/fa';
import * as MdIcons from 'react-icons/md';
import * as AiIcons from 'react-icons/ai';
import * as GiIcons from 'react-icons/gi';
import * as IoIcons from 'react-icons/io';
import * as CiIcons from 'react-icons/ci';
import * as FiIcons from 'react-icons/fi';
import * as LuIcons from 'react-icons/lu';
import * as SiIcons from 'react-icons/si';
// Load React Icon library dynamically from attributes in Directus
const iconSets = {
fa: FaIcons,
md: MdIcons,
ai: AiIcons,
gi: GiIcons,
io: IoIcons,
ci: CiIcons,
fi: FiIcons,
lu: LuIcons,
si: SiIcons,
};
const DynamicIcon = ({ name, set = 'fa' }: { name: string; set: 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 />;
};
export default DynamicIcon;

View File

@@ -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,