Compare commits

..

214 Commits
0.8.5 ... 2.0.4

Author SHA1 Message Date
f92f911360 release 2.0.4
Some checks failed
renovate / renovate (push) Successful in 46s
test-build / build (push) Failing after 47s
release-image / release (push) Failing after 1m26s
2025-08-11 19:34:59 -05:00
1cdbbd4a11 use double quotes
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Has been cancelled
2025-08-11 19:34:43 -05:00
da7c5c4a58 release 2.0.3
Some checks failed
renovate / renovate (push) Successful in 26s
test-build / build (push) Failing after 33s
release-image / release (push) Failing after 1m18s
2025-08-11 19:29:43 -05:00
931d1009ed support published value
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
2025-08-11 19:29:26 -05:00
43ff986963 release 2.0.2
Some checks failed
renovate / renovate (push) Successful in 34s
test-build / build (push) Failing after 35s
release-image / release (push) Failing after 1m11s
2025-08-11 19:11:27 -05:00
b9d85a5520 fix layout
Some checks failed
renovate / renovate (push) Successful in 31s
test-build / build (push) Failing after 39s
2025-08-11 19:10:46 -05:00
9836b40531 fix height 2025-08-11 18:48:08 -05:00
ea1c3d9f1a 2.0.1 release
All checks were successful
test-build / build (push) Successful in 1m18s
release-image / release (push) Successful in 2m26s
renovate / renovate (push) Successful in 45s
2025-08-11 18:14:00 -05:00
28f73be784 update content 2025-08-11 18:13:22 -05:00
284f30c392 downgrade actions
All checks were successful
renovate / renovate (push) Successful in 33s
test-build / build (push) Successful in 57s
release-image / release (push) Successful in 2m53s
2025-08-11 17:25:44 -05:00
9e4a2d681b update checkout
Some checks failed
test-build / build (push) Failing after 3s
renovate / renovate (push) Failing after 3s
2025-08-11 17:18:51 -05:00
c8e250c5b2 Merge pull request 'Update dependency astro-compressor to v1' (#68) from renovate/astro-compressor-1.x into main
All checks were successful
renovate / renovate (push) Successful in 42s
test-build / build (push) Successful in 1m1s
Reviewed-on: #68
2025-08-11 22:15:42 +00:00
58f05178a4 Update dependency astro-compressor to v1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 1m3s
2025-08-11 22:10:18 +00:00
b8966e2b88 Merge pull request 'Update dependency typescript to v5.9.2' (#66) from renovate/typescript-5.x into main
All checks were successful
renovate / renovate (push) Successful in 50s
test-build / build (push) Successful in 55s
Reviewed-on: #66
2025-08-11 22:09:32 +00:00
3f6563a0d3 Update dependency typescript to v5.9.2
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 47s
2025-08-11 22:07:18 +00:00
4840d15101 Merge pull request 'Update dependency @playform/compress to ^0.2.0' (#65) from renovate/playform-compress-0.x into main
All checks were successful
test-build / build (push) Successful in 57s
renovate / renovate (push) Successful in 1m17s
Reviewed-on: #65
2025-08-11 22:06:26 +00:00
f2cb98888a Update dependency @playform/compress to ^0.2.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 51s
2025-08-11 22:03:38 +00:00
9d1402ee82 Merge pull request 'Update dependency eslint to v9.33.0' (#62) from renovate/eslint-monorepo into main
All checks were successful
test-build / build (push) Successful in 1m13s
renovate / renovate (push) Successful in 1m59s
Reviewed-on: #62
2025-08-11 22:02:33 +00:00
741338ae9f Update dependency eslint to v9.33.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 1m39s
2025-08-11 21:34:24 +00:00
89d8b025d3 Merge pull request 'Update dependency typescript-eslint to v8.39.1' (#69) from renovate/typescript-eslint-monorepo into main
All checks were successful
test-build / build (push) Successful in 1m29s
renovate / renovate (push) Successful in 1m51s
2025-08-11 21:33:26 +00:00
5e74f8b01e Update dependency typescript-eslint to v8.39.1
Some checks are pending
renovate/stability-days Updates have not met minimum release age requirement
test-build / build (pull_request) Successful in 1m30s
2025-08-11 21:33:20 +00:00
bf4835e797 Merge pull request 'Update astro monorepo' (#61) from renovate/astro-monorepo into main
Some checks failed
test-build / build (push) Successful in 1m1s
renovate / renovate (push) Has been cancelled
Reviewed-on: #61
2025-08-11 21:32:27 +00:00
fc766599e1 Merge pull request 'Update dependency typescript-eslint to v8.39.0' (#60) from renovate/typescript-eslint-monorepo into main
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
Reviewed-on: #60
2025-08-11 21:32:16 +00:00
465bda1859 Update dependency typescript-eslint to v8.39.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 1m6s
2025-08-11 21:29:30 +00:00
19d2558436 Update astro monorepo
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 2m8s
2025-08-11 21:28:10 +00:00
2f797ca614 Update dependency shiki to v3.9.2
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 1m14s
test-build / build (push) Successful in 1m59s
renovate / renovate (push) Successful in 2m44s
2025-08-11 21:27:13 +00:00
99e451a934 Update dependency preline to v3.2.3
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
test-build / build (pull_request) Successful in 1m50s
2025-08-11 21:26:19 +00:00
1dc4ccfbc6 merge in new changes
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
2025-08-11 16:25:03 -05:00
a484feb7cd init 2025-08-11 16:16:05 -05:00
93d11dca17 update dependencies
All checks were successful
test-build / build (push) Successful in 26s
renovate / renovate (push) Successful in 52s
2025-08-01 20:22:38 -05:00
3eacf17f61 switch to static build 2025-08-01 20:19:53 -05:00
12ffcc4d72 Merge pull request 'Update astro monorepo' (#58) from renovate/astro-monorepo into main
All checks were successful
renovate / renovate (push) Successful in 35s
test-build / build (push) Successful in 1m21s
2025-08-02 00:01:59 +00:00
060400183f Update astro monorepo
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 1m10s
2025-08-02 00:01:44 +00:00
31ec9908e6 Update dependency framer-motion to v12.23.12
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (push) Successful in 24s
renovate / renovate (push) Successful in 31s
test-build / build (pull_request) Successful in 36s
2025-07-31 21:42:36 +00:00
4180a2eceb Merge pull request 'Update dependency astro to v5.12.6' (#56) from renovate/astro-monorepo into main
All checks were successful
test-build / build (push) Successful in 37s
renovate / renovate (push) Successful in 51s
2025-07-31 21:42:01 +00:00
fdef90e636 Update dependency astro to v5.12.6
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 26s
2025-07-31 21:41:47 +00:00
c369651a70 Update dependency @directus/sdk to v20.0.1
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 26s
test-build / build (push) Successful in 39s
renovate / renovate (push) Has been cancelled
2025-07-31 21:40:49 +00:00
75fd981f10 remove workflow
Some checks failed
test-build / build (push) Successful in 43s
renovate / renovate (push) Has been cancelled
2025-07-31 16:40:06 -05:00
80a4aee41c release 1.1.1
All checks were successful
test-build / build (push) Successful in 43s
renovate / renovate (push) Successful in 45s
release-image / release (push) Successful in 1m40s
2025-07-30 21:23:55 -05:00
9e84de0a5a update rendering 2025-07-30 21:23:28 -05:00
64140cce6b release 1.1.0
Some checks failed
test-build / build (push) Successful in 41s
release-image / release (push) Successful in 1m50s
process-repository / process-repository (push) Failing after 10s
renovate / renovate (push) Successful in 59s
2025-07-30 18:06:07 -05:00
0733fe6a06 switch to ssr
All checks were successful
renovate / renovate (push) Successful in 18s
test-build / build (push) Successful in 33s
2025-07-30 18:05:29 -05:00
0f5c015932 Update astro monorepo
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
renovate / renovate (push) Successful in 32s
test-build / build (pull_request) Successful in 51s
test-build / build (push) Successful in 1m3s
2025-07-30 21:00:16 +00:00
dc17aeb3d5 Merge pull request 'Update react monorepo to v19.1.1' (#53) from renovate/react-monorepo into main
Some checks failed
test-build / build (push) Successful in 37s
renovate / renovate (push) Has been cancelled
Reviewed-on: #53
2025-07-30 20:59:11 +00:00
a852f22409 Update react monorepo to v19.1.1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 31s
2025-07-30 00:03:21 +00:00
130a3866bc Merge pull request 'Update dependency framer-motion to v12.23.11' (#52) from renovate/framer-motion-12.x-lockfile into main
All checks were successful
test-build / build (push) Successful in 33s
renovate / renovate (push) Successful in 50s
2025-07-30 00:02:18 +00:00
2fb0542d36 Update dependency framer-motion to v12.23.11
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 1m40s
2025-07-30 00:02:09 +00:00
8a2be36f17 Merge pull request 'Update dependency astro to v5.12.4' (#51) from renovate/astro-monorepo into main
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Has been cancelled
2025-07-30 00:01:56 +00:00
266d25e0f2 Update dependency astro to v5.12.4
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 1m45s
2025-07-30 00:01:35 +00:00
34dbe6d809 Merge pull request 'Update dependency eslint to v9.32.0' (#50) from renovate/eslint-monorepo into main
Some checks failed
test-build / build (push) Successful in 1m28s
process-repository / process-repository (push) Failing after 35s
renovate / renovate (push) Successful in 2m46s
Reviewed-on: #50
2025-07-27 14:33:52 +00:00
3c654e19e1 Update dependency eslint to v9.32.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 34s
2025-07-27 00:01:17 +00:00
2a0142ee83 Update dependency framer-motion to v12.23.9
Some checks failed
process-repository / process-repository (push) Failing after 16s
renovate / renovate (push) Successful in 1m31s
renovate/stability-days Updates have met minimum release age requirement
test-build / build (push) Successful in 34s
test-build / build (pull_request) Successful in 41s
2025-07-26 00:01:11 +00:00
7836f49828 1.0.1 release
Some checks failed
test-build / build (push) Successful in 32s
release-image / release (push) Successful in 1m27s
process-repository / process-repository (push) Failing after 18s
renovate / renovate (push) Successful in 1m23s
2025-07-25 00:05:00 -05:00
25280a239c fix math
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Has been cancelled
2025-07-25 00:04:28 -05:00
c56dc99e72 disable descriptions using comments 2025-07-25 00:04:20 -05:00
48b7a13729 update colo
All checks were successful
renovate / renovate (push) Successful in 19s
test-build / build (push) Successful in 34s
2025-07-24 23:48:32 -05:00
ac026b0264 update workflow
All checks were successful
test-build / build (push) Successful in 31s
renovate / renovate (push) Successful in 34s
2025-07-24 21:24:50 -05:00
5332854856 1.0.0 release
All checks were successful
renovate / renovate (push) Successful in 24s
test-build / build (push) Successful in 38s
2025-07-24 20:51:35 -05:00
2e0c2f3de5 rewrite a few sections 2025-07-24 20:51:35 -05:00
88d510b06f update favicon 2025-07-24 20:51:35 -05:00
7843378503 Merge pull request 'Update typescript-eslint monorepo to v8.38.0' (#45) from renovate/typescript-eslint-monorepo into main
All checks were successful
renovate / renovate (push) Successful in 28s
test-build / build (push) Successful in 35s
Reviewed-on: #45
2025-07-25 00:47:24 +00:00
75016fdb4f Update typescript-eslint monorepo to v8.38.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 45s
2025-07-25 00:02:19 +00:00
4d74f74ab2 Merge pull request 'Update dependency framer-motion to v12.23.7' (#48) from renovate/framer-motion-12.x-lockfile into main
All checks were successful
renovate / renovate (push) Successful in 40s
test-build / build (push) Successful in 42s
2025-07-25 00:01:38 +00:00
2c1b7f577d Update dependency framer-motion to v12.23.7
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 45s
2025-07-25 00:01:27 +00:00
0e79b32012 Merge pull request 'Update dependency astro to v5.12.3' (#47) from renovate/astro-monorepo into main
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
2025-07-25 00:01:22 +00:00
c1ef2d2ba2 Update dependency astro to v5.12.3
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 43s
2025-07-25 00:01:07 +00:00
020c709b43 0.12.0 release
Some checks failed
test-build / build (push) Successful in 48s
release-image / release (push) Successful in 1m37s
process-repository / process-repository (push) Failing after 26s
renovate / renovate (push) Successful in 1m43s
2025-07-23 20:32:04 -05:00
9f346ee156 add colors and logo 2025-07-23 20:30:55 -05:00
e820e4f163 add theme 2025-07-23 20:30:55 -05:00
796926316e Merge pull request 'Update dependency astro to v5.12.2' (#46) from renovate/astro-monorepo into main
All checks were successful
test-build / build (push) Successful in 34s
renovate / renovate (push) Successful in 41s
2025-07-24 00:01:08 +00:00
bf8578045e Update dependency astro to v5.12.2
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 30s
2025-07-24 00:00:57 +00:00
f16af9a98d Update dependency astro to v5.12.1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 29s
test-build / build (push) Successful in 31s
renovate / renovate (push) Successful in 1m0s
2025-07-23 00:01:09 +00:00
ec45ad29ed 0.11.3 release
Some checks failed
test-build / build (push) Successful in 29s
release-image / release (push) Successful in 3m27s
process-repository / process-repository (push) Failing after 13s
renovate / renovate (push) Successful in 1m9s
2025-07-21 21:00:55 -05:00
17afce6710 minor tweaks and polish
All checks were successful
renovate / renovate (push) Successful in 1m11s
test-build / build (push) Successful in 1m36s
2025-07-21 20:58:34 -05:00
f83fe98b38 0.11.2 release
Some checks failed
test-build / build (push) Successful in 44s
release-image / release (push) Successful in 3m31s
process-repository / process-repository (push) Failing after 16s
renovate / renovate (push) Successful in 38s
2025-07-20 22:22:20 -05:00
2f244761ed add transition 2025-07-20 22:21:54 -05:00
649bf4482b 0.11.1 release
All checks were successful
renovate / renovate (push) Successful in 30s
test-build / build (push) Successful in 41s
release-image / release (push) Successful in 3m38s
2025-07-20 22:20:42 -05:00
2028e2247e fix page transition
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
2025-07-20 22:20:16 -05:00
fcae7676c6 0.11.0 release
Some checks failed
test-build / build (push) Successful in 25s
release-image / release (push) Successful in 1m20s
process-repository / process-repository (push) Failing after 16s
renovate / renovate (push) Successful in 36s
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
test-build / build (push) Successful in 34s
renovate / renovate (push) Successful in 47s
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
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 27s
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
test-build / build (push) Successful in 24s
renovate / renovate (push) Successful in 41s
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
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 30s
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
renovate / renovate (push) Successful in 21s
test-build / build (push) Successful in 30s
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
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 38s
2025-07-13 00:01:03 +00:00
073911c1b9 use tag ids
Some checks failed
process-pull-requests / process-pull-requests (push) Failing after 11s
process-issues / process-issues (push) Failing after 10s
renovate / renovate (push) Successful in 58s
test-build / build (push) Successful in 31s
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
renovate / renovate (push) Successful in 39s
test-build / build (push) Successful in 1m22s
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
renovate / renovate (push) Successful in 19s
test-build / build (push) Successful in 38s
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
renovate / renovate (push) Successful in 19s
test-build / build (push) Has been cancelled
2025-06-10 14:03:40 -05:00
6d112b52df remove step
Some checks failed
renovate / renovate (push) Successful in 17s
test-build / build (push) Has been cancelled
2025-06-10 14:02:58 -05:00
ff17af604f convert to python script
Some checks failed
renovate / renovate (push) Successful in 27s
test-build / build (push) Has been cancelled
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
renovate / renovate (push) Successful in 18s
test-build / build (push) Successful in 45s
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
renovate / renovate (push) Successful in 19s
test-build / build (push) Successful in 1m1s
2025-06-10 12:15:47 -05:00
3017668cd2 fix lint
All checks were successful
renovate / renovate (push) Successful in 18s
test-build / build (push) Successful in 2m31s
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
renovate / renovate (push) Successful in 17s
test-build / build (push) Successful in 2m14s
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
cedcae02ce update astro
All checks were successful
renovate / renovate (push) Successful in 1m41s
release-image / release (push) Successful in 2m13s
2025-06-09 12:38:55 -05:00
4ef6e85ed9 change url
Some checks failed
renovate / renovate (push) Has been cancelled
2025-06-09 12:37:04 -05:00
1ad039e9ff add workflow to tag old issues
All checks were successful
renovate / renovate (push) Successful in 1m14s
2025-06-09 12:32:34 -05:00
034d6d1120 downgrade priority
All checks were successful
renovate / renovate (push) Successful in 1m40s
2025-06-08 23:28:42 -05:00
2c436100c5 fix topic
All checks were successful
renovate / renovate (push) Successful in 1m14s
2025-06-08 23:23:57 -05:00
6ea1467653 change ntfy workflwo
All checks were successful
renovate / renovate (push) Successful in 2m30s
2025-06-08 23:02:40 -05:00
1ba76ab5cf change log level
All checks were successful
renovate / renovate (push) Successful in 1m22s
2025-06-08 22:29:17 -05:00
478482ab01 remove test
Some checks failed
renovate / renovate (push) Successful in 10m9s
release-image-gitea / release (push) Failing after 21m20s
2025-06-08 22:19:00 -05:00
f1e3e4ecaa change workflow
All checks were successful
renovate / renovate (push) Successful in 1m36s
2025-06-08 22:12:43 -05:00
05eb8a092c change command
Some checks failed
renovate / renovate (push) Successful in 5m33s
release-image-gitea / release (push) Failing after 11m33s
2025-06-08 21:54:10 -05:00
633e374a17 change tags
Some checks failed
renovate / renovate (push) Has been cancelled
2025-06-08 21:47:30 -05:00
cd75440a6d remove command
Some checks failed
renovate / renovate (push) Successful in 1m7s
release-image-gitea / release (push) Failing after 14m4s
2025-06-08 20:50:39 -05:00
3354975e2e remove command
Some checks failed
renovate / renovate (push) Has been cancelled
2025-06-08 20:49:53 -05:00
1ffe933d6e change build arg
Some checks failed
renovate / renovate (push) Successful in 1m26s
release-image-gitea / release (push) Failing after 1m24s
2025-06-08 20:45:47 -05:00
111 changed files with 13506 additions and 7810 deletions

View File

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

View File

@@ -1,9 +1,9 @@
name: release-image-gitea
name: release-image
on:
push:
tags:
- 0.*
- 2.*
workflow_dispatch:
@@ -42,7 +42,7 @@ jobs:
namespace=gitea
qemu.install=true
buildkitd-config-inline: |
[registry."hub.docker"]
[registry."docker.io"]
mirrors = ["harbor.alexlebens.net/proxy-hub.docker/"]
- name: Available Platforms
@@ -53,32 +53,12 @@ jobs:
uses: docker/metadata-action@v5
with:
tags: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha
images: |
${{ vars.REPOSITORY_HOST }}/${{ gitea.repository }}
${{ vars.REGISTRY_HOST }}/images/site-profile
- name: Build and Export to Docker
uses: docker/build-push-action@v6
with:
context: .
load: true
tags: ${{ env.TEST_TAG }}
file: ./Dockerfile
build-args: |
REGISTRY=hub.docker
- name: Test
run: |
docker run --rm ${{ env.TEST_TAG }}
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
@@ -88,14 +68,31 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./Dockerfile
build-args: |
REGISTRY=hub.docker
- name: Actions Ntfy
run: |
curl \
-H "Authorization: Bearer ${{ secrets.NTFY_CRED }}" \
-H "Title: Site-Profile Image Released to Gitea: ${{ steps.meta.outputs.tags }}" \
-H "Content-Type: text/plain" \
-d 'Repo: ${{ gitea.repository }}\nCommit: ${{ gitea.sha }}\nRef: ${{ gitea.ref }}\nStatus: ${{ job.status}}' \
${{ secrets.NTFY_URL }}
- name: ntfy Success
uses: niniyas/ntfy-action@master
if: success()
with:
url: '${{ secrets.NTFY_URL }}'
topic: '${{ secrets.NTFY_TOPIC }}'
title: 'Gitea Action'
priority: 3
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
tags: action,successfully,completed
details: 'Site Profile build workflow has successfully completed!'
icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png'
- name: ntfy Failed
uses: niniyas/ntfy-action@master
if: failure()
with:
url: '${{ secrets.NTFY_URL }}'
topic: '${{ secrets.NTFY_TOPIC }}'
title: 'Gitea Action'
priority: 4
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
tags: action,failed
details: 'Site Profile build workflow has failed!'
icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png'
actions: '[{"action": "view", "label": "Open Gitea", "url": "https://gitea.alexlebens.dev/alexlebens/site-profile/actions?workflow=release-image.yml", "clear": true}]'
image: true

View File

@@ -13,18 +13,20 @@ on:
jobs:
renovate:
runs-on: ubuntu-latest
container: ghcr.io/renovatebot/renovate:40
container: ghcr.io/renovatebot/renovate:41
steps:
- uses: actions/checkout@v4
- run: renovate
- name: Checkout
uses: actions/checkout@v4
- name: Renovate
run: renovate
env:
RENOVATE_PLATFORM: gitea
RENOVATE_AUTODISCOVER: true
RENOVATE_ONBOARDING: true
RENOVATE_ENDPOINT: http://gitea-http.gitea:3000
RENOVATE_ENDPOINT: ${{ vars.INSTANCE_URL }}
RENOVATE_REPOSITORIES: alexlebens/site-profile
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net>
LOG_LEVEL: debug
LOG_LEVEL: info
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
RENOVATE_GIT_PRIVATE_KEY: ${{ secrets.RENOVATE_GIT_PRIVATE_KEY }}
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
RENOVATE_REDIS_URL: redis://gitea-renovate-valkey-primary.gitea:6379
RENOVATE_REDIS_URL: ${{ vars.RENOVATE_REDIS_URL }}

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

4
.gitignore vendored
View File

@@ -12,10 +12,9 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.local
.env.development
.env.production
# macOS-specific files
@@ -24,4 +23,3 @@ pnpm-debug.log*
# ide
.vscode/
site-profile.code-workspace
.pre-commit-config.yaml

2
.npmrc
View File

@@ -1,3 +1,3 @@
registry=https://registry.npmjs.org/
engine-strict=true
save-exact=true

1
.prettierignore Normal file
View File

@@ -0,0 +1 @@
/src/components/ui/sections/Experience.astro

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
FROM ${REGISTRY}/node:22.16.0-alpine3.22 AS base
ARG REGISTRY=docker.io
FROM ${REGISTRY}/node:22.18.0-alpine3.22 AS base
LABEL version="0.8.5"
LABEL version="2.0.4"
LABEL description="Astro based personal website"
ENV PNPM_HOME="/pnpm"
@@ -13,15 +13,15 @@ WORKDIR /app
COPY package.json pnpm-lock.yaml ./
FROM base AS prod-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --save form-data
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
FROM prod-deps AS build-deps
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --save form-data
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
FROM build-deps AS build
COPY . .
RUN pnpm run build
RUN pnpm prune --omit=dev
RUN pnpm prune --prod
FROM base AS runtime
COPY --from=prod-deps /app/node_modules /app/node_modules
@@ -33,4 +33,4 @@ ENV DIRECTUS_URL=https://directus.alexlebens.dev
ENV PORT=4321
EXPOSE $PORT
CMD node ./dist/server/entry.mjs
CMD ["node", "./dist/server/entry.mjs"]

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Alex Lebens
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -2,6 +2,8 @@
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
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights

View File

@@ -1,78 +1,31 @@
# Alex Lebens Personal Site
# This is an open-source and simple blog built with Astro.
Personal site used for information about myself and blog.
## Features
- 🚀 **Maximum Performance** - Built with Astro.js for lightning-fast static sites
- 🎨 **Minimalist Design** - Clean UI that focuses on content
- 🌓 **Light/Dark Mode** - Smooth theme switching
- 📱 **Responsive** - Perfect experience on all devices
- **SPA Transitions** - Smooth page navigation with transition effects
- 📝 **Markdown & MDX** - Write posts with Markdown and extend with MDX
- 🔍 **SEO Optimized** - Meta tags, Open Graph, and Twitter Cards
- 📊 **Analytics** - Reading time, views, and statistics
- 🔖 **Categorization** - Tags and categories system
- 🔄 **RSS Feed** - Automatically generated RSS feed
- 🌐 **Internationalization Ready** - Prepared for multiple languages
- 🔒 **Secure** - No unnecessary client-side JavaScript
- 🐈 Simple And Beautiful
- 🖥️️ Responsive And Light/Dark mode
- 🐛 SiteMap & RSS Feed
- 🐝 Category Support
- 🐜 SEO and Responsiveness
- 🪲 Markdown And MDX
- 🏂🏾 Page Compression & Image Optimization
## Getting Started
### Development Commands
### Requirements
With dependencies installed, you can utilize the following npm scripts to manage your project's development lifecycle:
- Node.js 16+ and pnpm/yarn
- `pnpm run dev`: Starts a local development server with hot reloading enabled.
- `pnpm run preview`: Serves your build output locally for preview before deployment.
- `pnpm run build`: Bundles your site into static files for production.
### Installation
For detailed help with Astro CLI commands, visit [Astro's documentation](https://docs.astro.build/en/reference/cli-reference/).
```bash
# Clone repository
git clone https://gitea.alexlebens.dev/alexlebens/site-profile
## Thanks
# Navigate to project directory
cd astro-blog
Thanks https://github.com/mearashadowfax/ScrewFast, https://github.com/godruoyi/gblog/tree/gblog-template
# Install dependencies
pnpm install
## License
# Create .env file from template
cp .env.example .env
# Edit .env with your information
```
### Development
```bash
# Start development server
pnpm run dev
# Open browser at http://localhost:4321
```
### Build
```bash
# Create production build
pnpm run build
# Preview production build
pnpm run preview
```
## Project Structure
```
/
├── public/ # Static assets
├── src/
│ ├── components/ # Reusable UI components
│ ├── content/ # Blog content (Markdown/MDX)
│ ├── layouts/ # Page layouts
│ ├── pages/ # Pages and routes
│ ├── styles/ # CSS and Tailwind
│ └── utils/ # Utilities and helpers
├── astro.config.mjs # Astro configuration
├── tailwind.config.js # Tailwind configuration
└── tsconfig.json # TypeScript configuration
```
This project is released under the MIT License. Please read the [LICENSE](https://gitea.alexlebens.dev/alexlebens/site-profile/src/LICENSE.md) file for more details.

View File

@@ -1,8 +1,16 @@
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import react from '@astrojs/react';
import { defineConfig, passthroughImageService, sharpImageService } from 'astro/config';
import mdx from '@astrojs/mdx';
import node from '@astrojs/node';
import partytown from '@astrojs/partytown';
import react from '@astrojs/react';
import sitemap from '@astrojs/sitemap';
import tailwindcss from '@tailwindcss/vite';
import icon from 'astro-icon';
import swup from '@swup/astro';
import rehypePrettyCode from 'rehype-pretty-code';
import { transformerCopyButton } from '@rehype-pretty/transformers';
const getSiteURL = () => {
if (process.env.SITE_URL) {
@@ -13,17 +21,92 @@ const getSiteURL = () => {
export default defineConfig({
site: getSiteURL(),
integrations: [tailwindcss(), react()],
image: {
service: {
entrypoint: 'astro/assets/services/sharp',
}
},
prefetch: true,
integrations: [
mdx(),
partytown(),
react(),
sitemap(),
icon({
include: {
mdi: ['*'],
},
}),
swup({
theme: 'fade',
native: true,
cache: true,
preload: true,
accessibility: true,
smoothScrolling: true,
morph: ['#nav'],
}),
(await import('@playform/compress')).default({
CSS: true,
JavaScript: true,
HTML: {
'html-minifier-terser': {
collapseWhitespace: true,
minifyCSS: false,
minifyJS: true,
},
},
Image: false,
SVG: true,
Logger: 2,
}),
],
markdown: {
syntaxHighlight: false,
rehypePlugins: [
[
rehypePrettyCode,
{
theme: {
light: 'github-light',
dark: 'github-dark-dimmed',
},
keepBackground: false,
transformers: [
transformerCopyButton({
visibility: 'always',
feedbackDuration: 2500,
}),
],
},
],
],
},
plugins: {
"@tailwindcss/postcss": {},
'@tailwindcss/postcss': {},
},
vite: {
plugins: [tailwindcss()]
plugins: [tailwindcss()],
},
output: 'static',
adapter: node({
mode: 'standalone'
})
mode: 'standalone',
}),
build: {
// Specifies the directory in the build output where Astro-generated assets (bundled JS and CSS for example) should live.
// see https://docs.astro.build/en/reference/configuration-reference/#buildassets
assets: 'assets',
// see https://docs.astro.build/en/reference/configuration-reference/#buildassetsprefix
assetsPrefix:
!!import.meta.env.S3_ENABLE || !!process.env.S3_ENABLE ? 'https://digitalocean.com' : '',
},
});

11
eslint.config.mjs Normal file
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

@@ -1,58 +0,0 @@
import { createDirectus, rest } from '@directus/sdk';
type Global = {
title: string;
description: string;
name: string;
initals: string;
tagline: string;
email: string;
portrait: string;
portrait_alt: string;
about: string;
};
type About = {
background: string;
experience: string;
education: string;
certifications: string;
};
type Links = {
github: string;
linkedin: string;
};
type Skill = {
title: string;
description: string;
icon: string;
level: string;
};
export type Post = {
slug: string;
title: string;
description: string;
content: string;
image: string;
image_alt: string;
published_date: Date;
updated_date: Date;
tags: string[];
};
type Schema = {
global: Global;
about: About;
links: Links;
skills: Skill[];
posts: Post[];
};
const directus = createDirectus<Schema>(
process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'
).with(rest());
export default directus;

View File

@@ -1,39 +1,84 @@
{
"name": "site-profile",
"type": "module",
"version": "0.8.5",
"private": true,
"version": "2.0.4",
"homepage": "https://www.alexlebens.dev",
"bugs": {
"url": "https://gitea.alexlebens.dev/alexlebens/site-profile/issues",
"email": "alexander.lebens@gmail.com"
},
"repository": {
"type": "git",
"url": "https://gitea.alexlebens.dev/alexlebens/site-profile"
},
"license": "MIT",
"author": {
"name": "Alex Lebens",
"email": "alexander.lebens@gmail.com",
"url": "https://www.alexlebens.dev"
},
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"format": "prettier . --write",
"astro": "astro"
"astro": "astro",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,md,mdx,astro}\"",
"lint": "eslint \"src/**/*.{js,ts,jsx,tsx,astro}\"",
"lint:fix": "eslint --fix \"src/**/*.{js,ts,jsx,tsx,astro}\""
},
"dependencies": {
"@astrojs/mdx": "^4.3.0",
"@astrojs/node": "^9.2.2",
"@astrojs/check": "^0.9.4",
"@astrojs/mdx": "^4.3.3",
"@astrojs/node": "^9.3.3",
"@astrojs/partytown": "^2.1.4",
"@astrojs/react": "^4.3.0",
"@astrojs/rss": "^4.0.12",
"@astrojs/sitemap": "^3.4.1",
"@directus/sdk": "^19.1.0",
"@astrojs/sitemap": "^3.4.2",
"@giscus/react": "^3.1.0",
"@iconify-json/mdi": "^1.1.63",
"@iconify-json/pajamas": "^1.2.13",
"@iconify-json/simple-icons": "^1.2.47",
"@playform/compress": "^0.2.0",
"@rehype-pretty/transformers": "^0.13.2",
"@swup/astro": "1.7.0",
"@tailwindcss/postcss": "^4.1.8",
"@tailwindcss/vite": "^4.1.8",
"astro": "^5.9.1",
"form-data": "4.0.3",
"@directus/sdk": "^20.0.0",
"@types/react": "^19.0.0",
"@types/unist": "^3.0.2",
"astro": "^5.12.8",
"astro-compressor": "^1.0.0",
"astro-icon": "^1.1.5",
"framer-motion": "^12.16.0",
"postcss-preset-env": "^10.2.1",
"mdast-util-to-string": "^4.0.0",
"preline": "^3.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hotkeys-hook": "^5.1.0",
"react-icons": "^5.5.0",
"sanitize-html": "^2.17.0",
"tailwindcss": "^4.1.8"
"reading-time": "^1.5.0",
"rehype-pretty-code": "^0.14.1",
"sharp": "^0.34.3",
"sharp-ico": "^0.1.5",
"shiki": "^3.2.2",
"tailwindcss": "^4.1.11",
"ultrahtml": "^1.5.3"
},
"devDependencies": {
"@eslint-react/eslint-plugin": "^1.52.3",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"astro-icon": "^1.1.5",
"eslint": "^9.32.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-astro": "^1.3.1",
"eslint-plugin-format": "^1.0.1",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.0",
"prettier-plugin-tailwindcss": "^0.6.0"
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.14",
"timeago.js": "^4.0.2",
"typescript": "5.9.2",
"typescript-eslint": "8.39.1"
}
}

10251
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

2
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,2 @@
onlyBuiltDependencies:
- swup

View File

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

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;

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 713 B

After

Width:  |  Height:  |  Size: 9.9 KiB

View File

@@ -0,0 +1,352 @@
/**
* Skipped minification because the original files appears to be already minified.
* Original file: /npm/@preline/collapse@2.1.0/index.js
*
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
*/
!(function (t, e) {
if ('object' == typeof exports && 'object' == typeof module) module.exports = e();
else if ('function' == typeof define && define.amd) define([], e);
else {
var n = e();
for (var o in n) ('object' == typeof exports ? exports : t)[o] = n[o];
}
})(self, () =>
(() => {
'use strict';
var t = {
737: (t, e) => {
/*
* HSBasePlugin
* @version: 2.1.0
* @author: HTMLStream
* @license: Licensed under MIT (https://preline.co/docs/license.html)
* Copyright 2023 HTMLStream
*/
Object.defineProperty(e, '__esModule', { value: !0 });
var n = (function () {
function t(t, e, n) {
((this.el = t),
(this.options = e),
(this.events = n),
(this.el = t),
(this.options = e),
(this.events = {}));
}
return (
(t.prototype.createCollection = function (t, e) {
var n;
t.push({
id:
(null === (n = null == e ? void 0 : e.el) || void 0 === n ? void 0 : n.id) ||
t.length + 1,
element: e,
});
}),
(t.prototype.fireEvent = function (t, e) {
if ((void 0 === e && (e = null), this.events.hasOwnProperty(t)))
return this.events[t](e);
}),
(t.prototype.on = function (t, e) {
this.events[t] = e;
}),
t
);
})();
e.default = n;
},
652: function (t, e, n) {
/*
* HSCollapse
* @version: 2.1.0
* @author: HTMLStream
* @license: Licensed under MIT (https://preline.co/docs/license.html)
* Copyright 2023 HTMLStream
*/
var o,
i =
(this && this.__extends) ||
((o = function (t, e) {
return (
(o =
Object.setPrototypeOf ||
({ __proto__: [] } instanceof Array &&
function (t, e) {
t.__proto__ = e;
}) ||
function (t, e) {
for (var n in e) Object.prototype.hasOwnProperty.call(e, n) && (t[n] = e[n]);
}),
o(t, e)
);
}),
function (t, e) {
if ('function' != typeof e && null !== e)
throw new TypeError(
'Class extends value ' + String(e) + ' is not a constructor or null'
);
function n() {
this.constructor = t;
}
(o(t, e),
(t.prototype =
null === e ? Object.create(e) : ((n.prototype = e.prototype), new n())));
});
Object.defineProperty(e, '__esModule', { value: !0 });
var s = n(969),
r = (function (t) {
function e(e, n, o) {
var i = t.call(this, e, n, o) || this;
return (
(i.contentId = i.el.dataset.hsCollapse),
(i.content = document.querySelector(i.contentId)),
(i.animationInProcess = !1),
i.content && i.init(),
i
);
}
return (
i(e, t),
(e.prototype.init = function () {
var t = this;
(this.createCollection(window.$hsCollapseCollection, this),
this.el.addEventListener('click', function () {
t.content.classList.contains('open') ? t.hide() : t.show();
}));
}),
(e.prototype.hideAllMegaMenuItems = function () {
this.content
.querySelectorAll('.hs-mega-menu-content.block')
.forEach(function (t) {
(t.classList.remove('block'), t.classList.add('hidden'));
});
}),
(e.prototype.show = function () {
var t = this;
if (this.animationInProcess || this.el.classList.contains('open')) return !1;
((this.animationInProcess = !0),
this.el.classList.add('open'),
this.content.classList.add('open'),
this.content.classList.remove('hidden'),
(this.content.style.height = '0'),
setTimeout(function () {
((t.content.style.height = ''.concat(t.content.scrollHeight, 'px')),
t.fireEvent('beforeOpen', t.el),
(0, s.dispatch)('beforeOpen.hs.collapse', t.el, t.el));
}),
(0, s.afterTransition)(this.content, function () {
((t.content.style.height = ''),
t.fireEvent('open', t.el),
(0, s.dispatch)('open.hs.collapse', t.el, t.el),
(t.animationInProcess = !1));
}));
}),
(e.prototype.hide = function () {
var t = this;
if (this.animationInProcess || !this.el.classList.contains('open')) return !1;
((this.animationInProcess = !0),
this.el.classList.remove('open'),
(this.content.style.height = ''.concat(this.content.scrollHeight, 'px')),
setTimeout(function () {
t.content.style.height = '0';
}),
this.content.classList.remove('open'),
(0, s.afterTransition)(this.content, function () {
(t.content.classList.add('hidden'),
(t.content.style.height = ''),
t.fireEvent('hide', t.el),
(0, s.dispatch)('hide.hs.collapse', t.el, t.el),
(t.animationInProcess = !1));
}),
this.content.querySelectorAll('.hs-mega-menu-content.block').length &&
this.hideAllMegaMenuItems());
}),
(e.getInstance = function (t, e) {
void 0 === e && (e = !1);
var n = window.$hsCollapseCollection.find(function (e) {
return e.element.el === ('string' == typeof t ? document.querySelector(t) : t);
});
return n ? (e ? n : n.element.el) : null;
}),
(e.autoInit = function () {
(window.$hsCollapseCollection || (window.$hsCollapseCollection = []),
document
.querySelectorAll('.hs-collapse-toggle:not(.--prevent-on-load-init)')
.forEach(function (t) {
window.$hsCollapseCollection.find(function (e) {
var n;
return (
(null === (n = null == e ? void 0 : e.element) || void 0 === n
? void 0
: n.el) === t
);
}) || new e(t);
}));
}),
(e.show = function (t) {
var e = window.$hsCollapseCollection.find(function (e) {
return e.element.el === ('string' == typeof t ? document.querySelector(t) : t);
});
e && e.element.content.classList.contains('hidden') && e.element.show();
}),
(e.hide = function (t) {
var e = window.$hsCollapseCollection.find(function (e) {
return e.element.el === ('string' == typeof t ? document.querySelector(t) : t);
});
e && !e.element.content.classList.contains('hidden') && e.element.hide();
}),
(e.on = function (t, e, n) {
var o = window.$hsCollapseCollection.find(function (t) {
return t.element.el === ('string' == typeof e ? document.querySelector(e) : e);
});
o && (o.element.events[t] = n);
}),
e
);
})(n(737).default);
(window.addEventListener('load', function () {
r.autoInit();
}),
'undefined' != typeof window && (window.HSCollapse = r),
(e.default = r));
},
969: function (t, e) {
var n = this;
(Object.defineProperty(e, '__esModule', { value: !0 }),
(e.menuSearchHistory =
e.classToClassList =
e.htmlToElement =
e.afterTransition =
e.dispatch =
e.debounce =
e.isFormElement =
e.isParentOrElementHidden =
e.isEnoughSpace =
e.isIpadOS =
e.isIOS =
e.getClassPropertyAlt =
e.getClassProperty =
e.stringToBoolean =
void 0));
e.stringToBoolean = function (t) {
return 'true' === t;
};
e.getClassProperty = function (t, e, n) {
return (
void 0 === n && (n = ''),
(window.getComputedStyle(t).getPropertyValue(e) || n).replace(' ', '')
);
};
e.getClassPropertyAlt = function (t, e, n) {
void 0 === n && (n = '');
var o = '';
return (
t.classList.forEach(function (t) {
t.includes(e) && (o = t);
}),
o.match(/:(.*)]/) ? o.match(/:(.*)]/)[1] : n
);
};
e.isIOS = function () {
return (
!!/iPad|iPhone|iPod/.test(navigator.platform) ||
(navigator.maxTouchPoints &&
navigator.maxTouchPoints > 2 &&
/MacIntel/.test(navigator.platform))
);
};
e.isIpadOS = function () {
return (
navigator.maxTouchPoints &&
navigator.maxTouchPoints > 2 &&
/MacIntel/.test(navigator.platform)
);
};
e.isEnoughSpace = function (t, e, n, o, i) {
(void 0 === n && (n = 'auto'), void 0 === o && (o = 10), void 0 === i && (i = null));
var s = e.getBoundingClientRect(),
r = i ? i.getBoundingClientRect() : null,
l = window.innerHeight,
c = r ? s.top - r.top : s.top,
a = (i ? r.bottom : l) - s.bottom,
u = t.clientHeight + o;
return 'bottom' === n ? a >= u : 'top' === n ? c >= u : c >= u || a >= u;
};
e.isFormElement = function (t) {
return (
t instanceof HTMLInputElement ||
t instanceof HTMLTextAreaElement ||
t instanceof HTMLSelectElement
);
};
var o = function (t) {
return !!t && ('none' === window.getComputedStyle(t).display || o(t.parentElement));
};
e.isParentOrElementHidden = o;
e.debounce = function (t, e) {
var o;
return (
void 0 === e && (e = 200),
function () {
for (var i = [], s = 0; s < arguments.length; s++) i[s] = arguments[s];
(clearTimeout(o),
(o = setTimeout(function () {
t.apply(n, i);
}, e)));
}
);
};
e.dispatch = function (t, e, n) {
void 0 === n && (n = null);
var o = new CustomEvent(t, {
detail: { payload: n },
bubbles: !0,
cancelable: !0,
composed: !1,
});
e.dispatchEvent(o);
};
e.afterTransition = function (t, e) {
var n = function () {
(e(), t.removeEventListener('transitionend', n, !0));
};
window.getComputedStyle(t, null).getPropertyValue('transition') !==
(navigator.userAgent.includes('Firefox') ? 'all' : 'all 0s ease 0s')
? t.addEventListener('transitionend', n, !0)
: e();
};
e.htmlToElement = function (t) {
var e = document.createElement('template');
return ((t = t.trim()), (e.innerHTML = t), e.content.firstChild);
};
e.classToClassList = function (t, e, n, o) {
(void 0 === n && (n = ' '),
void 0 === o && (o = 'add'),
t.split(n).forEach(function (t) {
return 'add' === o ? e.classList.add(t) : e.classList.remove(t);
}));
};
e.menuSearchHistory = {
historyIndex: -1,
addHistory: function (t) {
this.historyIndex = t;
},
existsInHistory: function (t) {
return t > this.historyIndex;
},
clearHistory: function () {
this.historyIndex = -1;
},
};
},
},
e = {};
var n = (function n(o) {
var i = e[o];
if (void 0 !== i) return i.exports;
var s = (e[o] = { exports: {} });
return (t[o].call(s.exports, s, s.exports, n), s.exports);
})(652);
return n;
})()
);

View File

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

View File

@@ -0,0 +1,10 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"typescript.tsdk": "node_modules/typescript/lib"
}
}

View File

@@ -1,109 +0,0 @@
---
// Background.astro - Dot pattern and ambient glow background with smooth theme transitions
---
<div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden">
<!-- Dot pattern background -->
<div
class="bg-grid-pattern theme-transition-bg absolute inset-0 bg-[center_top_-1px] [mask-image:radial-gradient(white,transparent_85%)]"
>
</div>
<!-- Ambient glow effects -->
<div
class="animate-glow theme-transition-bg absolute left-1/4 top-1/4 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-zinc-400/20 opacity-50 blur-3xl dark:bg-zinc-500/20"
>
</div>
<div
class="animate-glow animation-delay-1000 theme-transition-bg absolute bottom-1/3 right-1/4 h-64 w-64 translate-x-1/2 translate-y-1/2 rounded-full bg-zinc-300/20 opacity-40 blur-3xl dark:bg-zinc-600/20"
>
</div>
<!-- Theme transition overlay -->
<div
id="theme-transition-overlay"
class="pointer-events-none absolute inset-0 bg-white opacity-0 dark:bg-zinc-900"
>
</div>
</div>
<script>
// Theme transition script
document.addEventListener('DOMContentLoaded', () => {
const themeToggle = document.querySelector('[data-theme-toggle]');
const overlay = document.getElementById('theme-transition-overlay');
if (themeToggle && overlay) {
themeToggle.addEventListener('click', () => {
// Add transitioning class to optimize performance
document.documentElement.classList.add('theme-transitioning');
// Fade in overlay
overlay.style.opacity = '0.15';
overlay.style.transition = 'opacity 0.3s ease';
setTimeout(() => {
// Fade out overlay
overlay.style.opacity = '0';
// Remove transitioning class after animation completes
setTimeout(() => {
document.documentElement.classList.remove('theme-transitioning');
}, 700);
}, 300);
});
}
});
</script>
<style>
/* Grid pattern for dots */
.bg-grid-pattern {
background-size: 24px 24px;
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px);
transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1);
}
/* Dark mode version */
:global(.dark) .bg-grid-pattern {
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
}
/* Ambient glow animations */
.animate-glow {
animation: glow 12s cubic-bezier(0.4, 0, 0.6, 1) infinite;
transition:
background-color 0.7s cubic-bezier(0.65, 0, 0.35, 1),
opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1);
}
.animation-delay-1000 {
animation-delay: 1s;
}
@keyframes glow {
0%,
100% {
opacity: 0.4;
transform: translate(0, 0) scale(1);
}
25% {
opacity: 0.5;
transform: translate(5%, 5%) scale(1.1);
}
50% {
opacity: 0.3;
transform: translate(0, 10%) scale(0.95);
}
75% {
opacity: 0.5;
transform: translate(-5%, 5%) scale(1.05);
}
}
/* Theme transition overlay */
#theme-transition-overlay {
transition: opacity 0.3s ease;
z-index: 10;
}
</style>

View File

@@ -0,0 +1,95 @@
---
import { getImage } from 'astro:assets';
import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import brandSrc from '@images/brand_logo.png';
import faviconSvgSrc from '@images/favicon_icon.svg';
import faviconSrc from '@images/favicon_icon.png';
import { SEO } from '@/config';
interface Props {
title: string;
description: string;
ogImage?: any;
ogTitle?: string;
ogDescription?: string;
structuredData?: object;
}
const canonicalURL = Astro.url.href;
let {
title,
description,
ogImage,
ogTitle = title,
ogDescription = description,
structuredData = SEO.structuredData,
} = Astro.props;
let card = 'summary_large_image';
if (!ogImage) {
ogImage = brandSrc;
card = 'summary';
}
const global = await directus.request(readSingleton('site_global'));
const faviconSvg = await getImage({ src: faviconSvgSrc, format: 'svg' });
const appleTouchIcon = await getImage({ src: faviconSrc, width: 180, height: 180, format: 'png' });
const socialImageRes = await getImage({ src: ogImage, width: 1200, height: 600 });
let socialImage = socialImageRes.src;
if (!socialImage.startsWith('http')) {
socialImage = Astro.url.origin + socialImageRes.src;
}
---
<!-- Inject structured data https://developers.google.com/search/docs/advanced/structured-data/intro-structured-data -->{
structuredData && <script type="application/ld+json" set:html={JSON.stringify(structuredData)} />
}
<!-- Global Metadata -->
<meta name="title" content={title} />
<meta name="description" content={description} />
<meta charset="utf-8" />
<meta name="web_author" content={global.name} />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0"
/>
<meta name="generator" content={Astro.generator} />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="theme-color" content="#facc15" />
<!-- Open Graph -->
<meta property="og:type" content="website" />
<meta property="og:locale" content="en_US" />
<meta property="og:url" content={Astro.url} />
<meta property="og:type" content="website" />
<meta property="og:title" content={ogTitle} />
<meta property="og:site_name" content={global.name} />
<meta property="og:description" content={ogDescription} />
<meta property="og:image" content={socialImage} />
<meta content="1200" property="og:image:width" />
<meta content="600" property="og:image:height" />
<meta content="image/png" property="og:image:type" />
<!-- Twitter -->
<meta property="twitter:card" content={card} />
<meta property="twitter:url" content={Astro.url} />
<meta property="twitter:domain" content={Astro.url} />
<meta property="twitter:title" content={ogTitle} />
<meta property="twitter:description" content={ogDescription} />
<meta property="twitter:image" content={socialImage} />
<!-- Links -->
<link href={canonicalURL} rel="canonical" />
<link rel="sitemap" href="/sitemap-index.xml" />
<!--<link href="/manifest.json" rel="manifest" />-->
<link href="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" />
<link href={faviconSvg.src} rel="icon" type="image/svg+xml" sizes="any" />
<link href={appleTouchIcon.src} rel="apple-touch-icon" />
<link href={appleTouchIcon.src} rel="shortcut icon" />
<link rel="preconnect" href="https://461ZQ3AX3S-dsn.algolia.net" crossorigin />

View File

@@ -1,128 +1,58 @@
---
import directus from '../../lib/directus';
import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton('global'));
const links = await directus.request(readSingleton('links'));
import directus from '@lib/directus';
import BrandLogo from '@components/ui/logos/BrandLogo.astro';
import Image from '@components/ui/images/Image.astro';
import { NavigationLinks, FooterLinks } from '@/config';
import footerImg from '@images/flowers.png';
const global = await directus.request(readSingleton('site_global'));
const currentYear = new Date().getFullYear();
const navLinks = [
{ text: 'About', href: '/about' },
{ text: 'Blog', href: '/blog' },
{ text: 'Topics', href: '/topics' },
{ text: 'RSS', href: '/rss.xml' },
];
const socialLinks = [
{
name: 'GitHub',
href: links.github,
icon: `<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path>`,
},
{
name: 'LinkedIn',
href: links.linkedin,
icon: `<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path>`,
},
];
---
<footer
class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
class="w-full overflow-hidden bg-stone-300/40 dark:bg-stone-800/20"
transition:animate="none"
>
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<div
class="theme-transition-all animate-float-slow absolute -right-40 -top-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
>
</div>
<div
class="theme-transition-all animate-float-slow animation-delay-2000 absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
>
</div>
<div
class="theme-transition-all animate-float-slow animation-delay-1000 absolute left-1/4 top-20 h-40 w-40 rounded-full bg-zinc-200/50 opacity-30 blur-2xl dark:bg-zinc-700/20"
>
</div>
</div>
<div class="relative px-4 pb-12 pt-16 sm:px-6">
<div class="mx-auto max-w-4xl">
<!-- Main footer content -->
<div class="relative px-4 pt-16 pb-12 sm:px-6">
<div class="mx-auto max-w-[85rem]">
<div class="grid grid-cols-1 gap-10 md:grid-cols-12">
<!-- Brand section -->
<div class="col-span-1 md:col-span-3">
<a href="/" class="group inline-block">
<div class="flex items-center">
<div
class="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"
>
<span
class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900"
>{global.initals}</span
>
<div
class="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
>
</div>
<div class="mx-auto aspect-square overflow-hidden rounded-lg">
<BrandLogo class="max-h-[40px] max-w-[40px] rounded-full" />
</div>
<span
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
>Blog</span
>
<span class="ml-3 text-xl font-bold text-neutral-800 dark:text-neutral-200">
{global.name}
</span>
</div>
</a>
<p
class="theme-transition-color mt-4 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400"
>
{global.description}
<p class="mt-4 text-sm leading-relaxed text-neutral-600 dark:text-neutral-400">
{global.about}
</p>
<!-- Social links -->
<div class="mt-6 flex items-center space-x-4">
{
socialLinks.map((social) => (
<a
href={social.href}
target="_blank"
rel="noopener noreferrer"
class="group relative flex h-10 w-10 transform items-center justify-center rounded-full bg-zinc-100 text-zinc-500 transition-all duration-300 hover:-translate-y-1 hover:text-zinc-900 hover:ring-2 hover:ring-zinc-300 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100 dark:hover:ring-zinc-700"
aria-label={social.name}
>
<span class="absolute inset-0 rounded-full bg-gradient-to-br from-zinc-200 to-zinc-300 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-700 dark:to-zinc-600" />
<svg
class="relative z-10 h-5 w-5 transition-transform duration-300 group-hover:scale-110"
fill="currentColor"
viewBox="0 0 24 24"
aria-hidden="true"
>
<Fragment set:html={social.icon} />
</svg>
</a>
))
}
</div>
</div>
<!-- Quick links -->
<div class="col-span-1 md:col-span-3">
<!-- Left links -->
<div class="col-span-1 md:col-span-2">
<h3
class="theme-transition-color relative inline-block pb-2 text-sm font-semibold uppercase tracking-wider text-zinc-900 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:bg-zinc-300 after:content-[''] dark:text-zinc-100 dark:after:bg-zinc-700"
class="after:bg-steel dark:after:bg-bermuda relative inline-block pb-2 text-sm font-semibold tracking-wider text-neutral-800 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-[''] dark:text-neutral-100"
>
Navigation
Blog
</h3>
<ul class="mt-4 space-y-3">
{
navLinks.map((link) => (
NavigationLinks.map((link) => (
<li>
<a
href={link.href}
class="group flex items-center text-base text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
href={link.url}
class="group flex items-center text-base text-neutral-600 transition-colors hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200"
>
<span class="relative inline-block overflow-hidden">
<span class="relative z-10">{link.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 class="relative z-10">{link.name}</span>
</span>
</a>
</li>
@@ -130,115 +60,85 @@ const socialLinks = [
}
</ul>
</div>
<!-- Right links -->
<div class="col-span-1 md:col-span-3">
<h3
class="after:bg-steel dark:after:bg-bermuda relative inline-block pb-2 text-sm font-semibold tracking-wider text-neutral-800 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-[''] dark:text-neutral-100"
>
Other
</h3>
<ul class="mt-4 space-y-3">
{
FooterLinks.map((link) => (
<li>
<a
href={link.url}
class="group flex items-center text-base text-neutral-600 transition-colors hover:text-neutral-800 dark:text-neutral-400 dark:hover:text-neutral-200"
>
<span class="relative inline-block overflow-hidden">
<span class="relative z-10">{link.name}</span>
</span>
</a>
</li>
))
}
</ul>
</div>
<!-- Right image -->
<div class="col-span-3 mt-10 flex justify-center md:mt-0">
<div class="-mt-10 hidden max-h-[460px] max-w-[220px] scale-80 md:block">
<Image
src={footerImg}
alt={global.footer_image_alt}
class="h-full w-full object-cover object-center"
draggable="false"
loading="eager"
format="webp"
quality="low"
widths={[440]}
disableBlur={true}
/>
</div>
</div>
</div>
<!-- Bottom section -->
<div class="mt-12 border-t border-neutral-400/30 pt-8 dark:border-neutral-600/50">
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
<p class="text-sm text-neutral-600 dark:text-neutral-400">
&copy; {currentYear} All rights reserved.
</p>
<!-- Bottom section -->
<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">
<p class="theme-transition-color text-sm text-zinc-600 dark:text-zinc-400">
&copy; {currentYear} All rights reserved.
</p>
<div class="flex items-center space-x-2">
<span class="theme-transition-color text-xs text-zinc-500 dark:text-zinc-400"
>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"
<div class="flex items-center space-x-2">
<span class="text-xs text-neutral-500 dark:text-neutral-400">Built with </span>
<a
href="https://astro.build"
target="_blank"
rel="noopener noreferrer"
class="group inline-flex items-center text-xs text-neutral-600 transition-colors hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100"
>
<svg class="mr-1 h-4 w-4 text-[#FF5D01]" viewBox="0 0 36 36" fill="none">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.833 22.958c.622-1.185 1.832-1.918 3.18-1.918 2.292 0 4.145 1.86 4.145 4.153 0 1.34-.626 2.54-1.601 3.303 1.223-1.299 1.97-3.048 1.97-4.971 0-3.994-3.243-7.233-7.242-7.233-2.818 0-5.26 1.6-6.469 3.933.78-2.912 3.428-5.06 6.577-5.06 3.75 0 6.79 3.035 6.79 6.78 0 2.606-1.468 4.868-3.616 6.002a4.163 4.163 0 0 0 2.285-3.724c0-2.293-1.853-4.153-4.145-4.153-1.348 0-2.558.733-3.18 1.918l1.306-3.03Z"
fill="currentColor"></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M22.155 12.056c-.622 1.185-1.832 1.918-3.18 1.918-2.292 0-4.145-1.86-4.145-4.153 0-1.34.626-2.54 1.601-3.303-1.223 1.299-1.97 3.048-1.97 4.971 0 3.994 3.243 7.233 7.242 7.233 2.818 0 5.26-1.6 6.469-3.933-.78 2.912-3.428 5.06-6.577 5.06-3.75 0-6.79-3.035-6.79-6.78 0-2.606 1.468-4.868 3.616-6.002a4.163 4.163 0 0 0-2.285 3.724c0 2.293 1.853 4.153 4.145 4.153 1.348 0 2.558-.733 3.18-1.918l-1.306 3.03Z"
fill="currentColor"></path>
</svg>
<span class="relative">
Astro
<span
class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M8.833 22.958c.622-1.185 1.832-1.918 3.18-1.918 2.292 0 4.145 1.86 4.145 4.153 0 1.34-.626 2.54-1.601 3.303 1.223-1.299 1.97-3.048 1.97-4.971 0-3.994-3.243-7.233-7.242-7.233-2.818 0-5.26 1.6-6.469 3.933.78-2.912 3.428-5.06 6.577-5.06 3.75 0 6.79 3.035 6.79 6.78 0 2.606-1.468 4.868-3.616 6.002a4.163 4.163 0 0 0 2.285-3.724c0-2.293-1.853-4.153-4.145-4.153-1.348 0-2.558.733-3.18 1.918l1.306-3.03Z"
fill="currentColor"></path>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M22.155 12.056c-.622 1.185-1.832 1.918-3.18 1.918-2.292 0-4.145-1.86-4.145-4.153 0-1.34.626-2.54 1.601-3.303-1.223 1.299-1.97 3.048-1.97 4.971 0 3.994 3.243 7.233 7.242 7.233 2.818 0 5.26-1.6 6.469-3.933-.78 2.912-3.428 5.06-6.577 5.06-3.75 0-6.79-3.035-6.79-6.78 0-2.606 1.468-4.868 3.616-6.002a4.163 4.163 0 0 0-2.285 3.724c0 2.293 1.853 4.153 4.145 4.153 1.348 0 2.558-.733 3.18-1.918l-1.306 3.03Z"
fill="currentColor"></path>
</svg>
<span class="relative">
Astro
<span
class="absolute bottom-0 left-0 h-0.5 w-0 bg-[#FF5D01] transition-all duration-300 group-hover:w-full"
></span>
</span>
</a>
</div>
</span>
</a>
</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>

View File

@@ -1,21 +0,0 @@
---
export interface Props {
date?: Date | string;
}
const { date } = Astro.props;
const parsedDate = typeof date === 'string' ? new Date(date) : date;
---
{
parsedDate && (
<time datetime={parsedDate.toISOString()}>
{parsedDate.toLocaleDateString('en-us', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
)
}

100
src/components/Header.astro Normal file
View File

@@ -0,0 +1,100 @@
---
import BrandLogo from '@components/ui/logos/BrandLogo.astro';
import ThemeToggle from '@components/ui/buttons/ThemeToggle.astro';
import { NavigationLinks } from '@/config';
const pathname = new URL(Astro.request.url).pathname;
const currentPath = pathname.slice(1);
---
<header
id="nav"
class="sticky inset-x-0 top-4 z-50 flex w-full flex-wrap text-sm transition-none md:flex-nowrap md:justify-start"
>
<nav
class="relative mx-2 w-full rounded-[36px] border border-neutral-100 bg-neutral-100 px-4 py-3 md:flex md:items-center md:justify-between md:px-6 lg:px-8 dark:border-neutral-700/40 dark:bg-neutral-800/80"
aria-label="Global"
>
<div class="flex items-center justify-between">
<a
class="h-[42px] flex-none rounded-lg text-xl font-bold ring-neutral-500 outline-none focus-visible:ring dark:ring-neutral-200 dark:focus:outline-none"
href="/"
aria-label="Brand"
>
<BrandLogo class="h-full w-auto rounded-full object-cover" />
</a>
<div class="ml-auto md:hidden">
<button
type="button"
class="hs-collapse-toggle flex h-8 w-8 items-center justify-center rounded-full text-sm font-bold text-neutral-600 transition duration-300 hover:bg-neutral-200 disabled:pointer-events-none disabled:opacity-50 dark:text-neutral-400 dark:hover:bg-neutral-700 dark:focus:outline-none"
data-hs-collapse="#navbar-collapse-with-animation"
aria-controls="navbar-collapse-with-animation"
aria-label="Toggle navigation"
>
<svg
class="hs-collapse-open:hidden h-[1.25rem] w-[1.25rem] flex-shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="3" x2="21" y1="6" y2="6"></line>
<line x1="3" x2="21" y1="12" y2="12"></line>
<line x1="3" x2="21" y1="18" y2="18"></line>
</svg>
<svg
class="hs-collapse-open:block hidden h-[1.25rem] w-[1.25rem] flex-shrink-0"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18"></path>
<path d="m6 6 12 12"></path>
</svg>
</button>
</div>
</div>
<div
id="navbar-collapse-with-animation"
class="hs-collapse hidden grow basis-full overflow-hidden transition-all duration-300 md:block"
>
<div
class="mt-5 flex flex-col gap-x-0 gap-y-4 md:mt-0 md:flex-row md:items-center md:justify-end md:gap-x-4 md:gap-y-0 md:ps-7 lg:gap-x-7"
>
{
NavigationLinks.map((item) => {
const isActive = currentPath === (item.url === '/' ? '' : item.url.slice(1));
return (
<a
href={item.url}
class={`text-sm font-medium ${
isActive
? 'text-orange-500 dark:text-orange-300'
: 'text-neutral-600 hover:text-neutral-900 dark:text-neutral-400 dark:hover:text-neutral-100'
}`}
>
{item.name}
</a>
);
})
}
<span class="md:inline-block">
<ThemeToggle />
</span>
</div>
</div>
</nav>
</header>
<script is:inline src="/vendor/preline/collapse2.1.0.min.js"></script>

View File

@@ -1,245 +0,0 @@
---
import ThemeToggle from './ThemeToggle.astro';
import directus from '../../lib/directus';
import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton('global'));
const navItems = [
{ text: 'Home', href: '/' },
{ text: 'Blog', href: '/blog' },
{ text: 'Topics', href: '/topics' },
{ text: 'About', href: '/about' },
{ text: 'RSS', href: 'rss.xml' },
];
const pathname = new URL(Astro.request.url).pathname;
const currentPath = pathname.slice(1);
---
<header
class="fixed left-0 right-0 top-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
>
<div class="mx-auto flex max-w-3xl items-center justify-between px-4">
<!-- Logo -->
<a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">{global.initals}</a>
<!-- Desktop navigation -->
<nav class="hidden items-center space-x-6 sm:flex">
{
navItems.map((item) => {
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
return (
<a
href={item.href}
class={`text-sm font-medium ${
isActive
? 'text-zinc-900 dark:text-white'
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
}`}
>
{item.text}
</a>
);
})
}
<ThemeToggle />
</nav>
<!-- Mobile menu button -->
<button id="mobile-menu-button" class="flex items-center sm:hidden" aria-label="Menu">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6 text-zinc-900 dark:text-white"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
</svg>
</button>
</div>
</header>
<!-- Mobile menu overlay -->
<div
id="mobile-menu"
class="pointer-events-none fixed inset-0 z-50 flex flex-col bg-white opacity-0 transition-all duration-300 ease-in-out dark:bg-zinc-900"
>
<div class="flex items-center justify-between border-b border-zinc-100 p-4 dark:border-zinc-800">
<a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">JD</a>
<button
id="close-menu-button"
class="rounded-md p-2 text-zinc-900 transition-colors hover:bg-zinc-100 dark:text-white dark:hover:bg-zinc-800"
aria-label="Close menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<nav class="flex flex-1 flex-col items-center justify-center space-y-6 text-center">
{
navItems.map((item, index) => {
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
return (
<a
href={item.href}
class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
isActive
? 'text-zinc-900 dark:text-white'
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
}`}
style={`transition-delay: ${index * 0.05}s;`}
>
{item.text}
</a>
);
})
}
<div class="mobile-nav-item translate-y-4 pt-4 opacity-0" style="transition-delay: 0.25s;">
<ThemeToggle />
</div>
</nav>
</div>
<!-- Spacer to prevent content from hiding behind fixed header -->
<div class="h-16"></div>
<script>
// Mobile menu toggle with animations
document.addEventListener('DOMContentLoaded', () => {
const mobileMenuButton = document.getElementById('mobile-menu-button');
const closeMenuButton = document.getElementById('close-menu-button');
const mobileMenu = document.getElementById('mobile-menu');
const navItems = document.querySelectorAll('.mobile-nav-item');
// Open menu with animations
mobileMenuButton?.addEventListener('click', () => {
if (!mobileMenu) return;
// Prevent body scrolling
document.body.style.overflow = 'hidden';
// Show menu with fade in
mobileMenu.classList.remove('pointer-events-none');
mobileMenu.classList.add('pointer-events-auto');
// Animate opacity
setTimeout(() => {
mobileMenu.style.opacity = '1';
// Animate each nav item with staggered delay
navItems.forEach((item) => {
setTimeout(() => {
item.classList.remove('opacity-0', 'translate-y-4');
}, 150);
});
}, 50);
});
// Close menu with animations
const closeMenu = () => {
if (!mobileMenu) return;
// Fade out nav items first
navItems.forEach((item) => {
item.classList.add('opacity-0', 'translate-y-4');
});
// Then fade out the menu
setTimeout(() => {
mobileMenu.style.opacity = '0';
// After animation completes, hide menu and restore scrolling
setTimeout(() => {
mobileMenu.classList.remove('pointer-events-auto');
mobileMenu.classList.add('pointer-events-none');
document.body.style.overflow = '';
}, 300);
}, 100);
};
// Close button event
closeMenuButton?.addEventListener('click', closeMenu);
// Close menu when clicking a link
const mobileLinks = mobileMenu?.querySelectorAll('a');
mobileLinks?.forEach((link) => {
link.addEventListener('click', closeMenu);
});
// Close menu on escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && mobileMenu?.classList.contains('pointer-events-auto')) {
closeMenu();
}
});
// Add smooth animation to header on scroll
const header = document.querySelector('header');
let lastScrollY = window.scrollY;
window.addEventListener('scroll', () => {
if (!header) return;
const currentScrollY = window.scrollY;
// Add shadow on scroll
if (currentScrollY > 10) {
header.classList.add('shadow-sm');
} else {
header.classList.remove('shadow-sm');
}
// Update last scroll position
lastScrollY = currentScrollY;
});
});
</script>
<style>
/* Smooth animations for mobile navigation */
.mobile-nav-item {
transition:
opacity 0.5s ease,
transform 0.5s ease,
color 0.3s ease;
}
/* Header transition */
header {
transition:
box-shadow 0.3s ease,
transform 0.3s ease,
background-color 0.3s ease;
}
/* Mobile menu button hover effect */
#mobile-menu-button {
transition: transform 0.2s ease;
}
#mobile-menu-button:hover {
transform: scale(1.05);
}
/* Mobile menu transition */
#mobile-menu {
transition: opacity 0.3s ease;
backdrop-filter: blur(4px);
}
</style>

View File

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

View File

@@ -1,23 +0,0 @@
---
export interface Props {
tags: string[];
class?: string;
}
const { tags = [], class: className = '' } = Astro.props;
---
{
tags.length > 0 && (
<div class={`mt-3 flex flex-wrap gap-2 ${className}`}>
{tags.map((tag) => (
<a
href={`/tag/${tag}`}
class="inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-800 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
>
{tag}
</a>
))}
</div>
)
}

View File

@@ -0,0 +1,61 @@
---
import { Icon } from 'astro-icon/components';
import type { Post } from '@lib/directusTypes';
import { getDirectusImageURL } from '@lib/directusFunctions';
import Image from '@components/ui/images/Image.astro';
import { formatDate } from '@support/time';
interface Props {
post: Post;
}
const { post } = Astro.props;
const baseClasses = 'group group-hover smooth-reveal-cards rounded-xl flex flex-col';
const borderClasses = 'border border-stone-200/50 dark:border-stone-700/50';
const bgColorClasses =
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
---
<div class={`${baseClasses}`}>
<a
class={`rounded-xl duration-300 transition-all ${borderClasses} ${shadowClasses} ${bgColorClasses}`}
href={`/blog/${post.slug}/`}
data-astro-prefetch
>
<div
class="relative w-full flex-shrink-0 overflow-hidden rounded-t-xl before:absolute before:inset-x-0 before:z-[1] before:size-full"
>
<Image
class="h-auto w-full rounded-t-xl"
src={getDirectusImageURL(post.image)}
alt={post.image_alt}
draggable="false"
loading="eager"
format="webp"
width="800"
height="460"
/>
</div>
<div class="rounded-xl p-4 md:p-5">
<h3 class="text-xl font-bold text-neutral-600 dark:text-neutral-200">
{post.title}
</h3>
<div
class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-[44px] items-center font-medium text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-400"
>
<span class="relative inline-block overflow-hidden"> Read more </span>
<Icon
name="mdi:keyboard-arrow-right"
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
/>
<p class="ml-auto text-sm text-neutral-600 dark:text-neutral-400">
{formatDate(post.published_date)}
</p>
</div>
</div>
</a>
</div>

View File

@@ -0,0 +1,70 @@
---
interface Props {
slug: string;
title: string;
description: string;
count: number;
publishDate: string;
}
const { slug, title, description, count, publishDate } = Astro.props;
const baseClasses =
'group group-hover rounded-xl flex h-full min-h-[220px] cursor-pointer flex-col overflow-hidden';
const bgColorClasses =
'bg-neutral-100/60 dark:bg-neutral-800/60 hover:bg-neutral-100 dark:hover:bg-neutral-800/90 ';
---
<a class={`rounded-xl`} href={`/categories/${slug}/`} data-astro-prefetch="false">
<div class={`${baseClasses}`}>
<div
class={`relative min-h-0 flex-grow overflow-hidden transition-all duration-300 ${bgColorClasses}`}
>
<div class="absolute inset-1 flex flex-col p-3 md:p-4 lg:p-5">
<div class="overflow-hidden">
<h2
class="group-hover:text-steel dark:group-hover:text-bermuda transition-text mb-4 text-4xl font-extrabold tracking-tight text-balance whitespace-nowrap text-neutral-800 duration-300 dark:text-neutral-200"
>
{title}
</h2>
<p class="mb-4 font-light text-neutral-600 sm:text-lg dark:text-neutral-400">
{description}
</p>
</div>
<div
class="mt-auto flex items-center justify-between pt-1 text-xs text-neutral-600 md:pt-2 dark:text-neutral-300"
>
<span class="inline-flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
class="mr-1"
>
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"></path>
</svg>
{count}
</span>
<span class="inline-flex items-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
class="mr-1"
>
<circle cx="12" cy="12" r="10"></circle>
<polyline points="12 6 12 12 16 14"></polyline>
</svg>
{publishDate}
</span>
</div>
</div>
</div>
</div>
</a>

View File

@@ -0,0 +1,29 @@
---
import type { Post } from '@lib/directusTypes';
import BlogCard from '@components/blog/BlogCard.astro';
interface Props {
posts: Post[];
}
const { posts } = Astro.props;
---
<section class="mx-auto mb-10 max-w-[85rem] px-4 py-8 sm:px-6 lg:px-8 2xl:max-w-full">
<div class="text-left">
<h2
id="selected-articel"
class="smooth-reveal-2 mb-4 text-5xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
>
Older Articles
</h2>
</div>
<div class="flex flex-col md:flex-row md:space-x-12 lg:space-x-16">
<div class="w-full">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{posts.map((b) => <BlogCard post={b} />)}
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,44 @@
---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import Image from '@components/ui/images/Image.astro';
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
img: any;
imgAlt: any;
}
const { title, subTitle, btnExists, btnTitle, btnURL, img, imgAlt } = Astro.props;
---
<section
class="mx-auto max-w-[85rem] items-center gap-8 px-4 py-10 sm:px-6 sm:py-16 md:grid md:grid-cols-2 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 xl:gap-16 2xl:max-w-full"
>
<Image
class="h-full w-full rounded-xl object-cover sm:max-h-[320px] md:max-h-[360px]"
src={img}
alt={imgAlt}
draggable="false"
loading="lazy"
width="850"
height="420"
/>
<div class="mt-4 md:mt-0">
<h2
class="mb-4 text-4xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
>
{title}
</h2>
<p
class="mb-4 max-w-prose font-light text-pretty text-neutral-600 sm:text-lg dark:text-neutral-300"
>
{subTitle}
</p>
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
</div>
</section>

View File

@@ -0,0 +1,45 @@
---
import type { Post } from '@lib/directusTypes';
import { getDirectusImageURL } from '@lib/directusFunctions';
import BlogLeftSection from '@components/blog/BlogLeftSection.astro';
import BlogRightSection from '@components/blog/BlogRightSection.astro';
interface Props {
posts: Post[];
}
const { posts } = Astro.props;
const blogPosts = posts.slice(0, 5);
---
<section class="smooth-reveal">
{
blogPosts.map((b, index) =>
index % 2 === 0 ? (
<BlogLeftSection
title={b.title}
subTitle={b.description}
btnExists={true}
btnTitle="Read More"
btnURL={`/blog/${b.slug}`}
img={getDirectusImageURL(b.image)}
imgAlt={b.image_alt}
/>
) : (
<BlogRightSection
title={b.title}
subTitle={b.description}
btnExists={true}
btnTitle="Read More"
btnURL={`/blog/${b.slug}`}
single={!b.image_second}
imgOne={getDirectusImageURL(b.image)}
imgOneAlt={b.image_alt}
imgTwo={getDirectusImageURL(b?.image_second)}
imgTwoAlt={b?.image_second_alt}
/>
)
)
}
</section>

View File

@@ -0,0 +1,87 @@
---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import Image from '@components/ui/images/Image.astro';
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
single?: boolean;
imgOne?: any;
imgOneAlt?: any;
imgTwo?: any;
imgTwoAlt?: any;
}
const {
title,
subTitle,
btnExists,
btnTitle,
btnURL,
single,
imgOne,
imgOneAlt,
imgTwo,
imgTwoAlt,
} = Astro.props;
---
<section
class="mx-auto max-w-[85rem] items-center gap-16 px-4 py-10 sm:px-6 lg:grid lg:grid-cols-2 lg:px-8 lg:py-14 2xl:max-w-full"
>
<div>
<h2
class="mb-4 text-4xl font-extrabold tracking-tight text-balance text-neutral-800 dark:text-neutral-200"
>
{title}
</h2>
<p
class="mb-4 max-w-prose font-light text-pretty text-neutral-600 sm:text-lg dark:text-neutral-400"
>
{subTitle}
</p>
{btnExists ? <PrimaryCTA title={btnTitle} url={btnURL} /> : null}
</div>
{
single ? (
<div class="mt-8">
<Image
class="w-full rounded-lg"
src={imgOne}
alt={imgOneAlt}
format="webp"
loading="lazy"
width="850"
height="420"
/>
</div>
) : (
<div class="mt-8 grid grid-cols-2 gap-4">
<Image
class="w-full rounded-xl"
src={imgOne}
alt={imgOneAlt}
draggable="false"
format="webp"
loading="lazy"
width="400"
height="230"
/>
<Image
class="mt-4 w-full rounded-xl lg:mt-10"
src={imgTwo}
alt={imgTwoAlt}
draggable="false"
format="webp"
loading="lazy"
width="400"
height="230"
/>
</div>
)
}
</section>

View File

@@ -0,0 +1,85 @@
---
import Icon from '@components/ui/icons/icon.astro';
---
<button
type="button"
class="focus-visible:ring-secondary group inline-flex items-center rounded-lg p-2.5 text-neutral-600 ring-neutral-500 transition duration-300 outline-none hover:bg-neutral-100 focus:outline-none focus-visible:ring-1 focus-visible:outline-none dark:text-neutral-400 dark:ring-neutral-200 dark:hover:bg-neutral-700"
data-bookmark-button="bookmark-button"
>
<Icon name="bookmark" />
</button>
<script>
class Bookmark {
private static readonly BOOKMARKS_KEY = 'bookmarks';
private bookmarkButton: Element | null;
constructor(private dataAttrValue: string) {
this.bookmarkButton = document.querySelector(`[data-bookmark-button="${dataAttrValue}"]`);
}
private getStoredBookmarks(): string[] {
const item = localStorage.getItem(Bookmark.BOOKMARKS_KEY);
return item ? JSON.parse(item) : [];
}
init(): void {
if (this.bookmarkButton && this.isStored()) {
this.markAsStored();
}
this.bookmarkButton?.addEventListener('click', () => this.toggleBookmark());
}
isStored(): boolean {
return this.getStoredBookmarks().includes(window.location.pathname);
}
markAsStored(): void {
if (this.bookmarkButton) {
this.bookmarkButton.classList.add('bookmarked');
const svgElement = this.bookmarkButton.querySelector('svg');
if (svgElement) {
svgElement.setAttribute('class', 'h-6 w-6 fill-red-500 dark:fill-red-500');
}
const pathElement = svgElement?.querySelector('path');
if (pathElement) {
pathElement.setAttribute('class', 'fill-current text-red-500 dark:text-red-500');
}
}
}
unmarkAsStored(): void {
if (this.bookmarkButton) {
this.bookmarkButton.classList.remove('bookmarked');
const svgElement = this.bookmarkButton.querySelector('svg');
if (svgElement) {
svgElement.setAttribute('class', 'h-6 w-6 fill-none');
}
const pathElement = svgElement?.querySelector('path');
if (pathElement) {
pathElement.setAttribute(
'class',
'fill-current text-neutral-500 group-hover:text-red-400 dark:text-neutral-500 group-hover:dark:text-red-400'
);
}
}
}
toggleBookmark(): void {
const storedBookmarks = this.getStoredBookmarks();
const index = storedBookmarks.indexOf(window.location.pathname);
if (index !== -1) {
storedBookmarks.splice(index, 1);
this.unmarkAsStored();
} else {
storedBookmarks.push(window.location.pathname);
this.markAsStored();
}
localStorage.setItem(Bookmark.BOOKMARKS_KEY, JSON.stringify(storedBookmarks));
}
}
new Bookmark('bookmark-button').init();
</script>

View File

@@ -0,0 +1,32 @@
---
import { Icon } from 'astro-icon/components';
const { title, url } = Astro.props;
interface Props {
title?: string;
url?: string;
}
const baseClasses =
'group group-hover inline-flex items-center justify-center gap-x-3 rounded-full px-4 py-3 text-center text-sm font-medium text-neutral-200';
const borderClasses = 'border border-transparent';
const bgColorClasses =
'bg-gitea-primary hover:bg-gitea-secondary dark:bg-gitea-secondary dark:hover:bg-gitea-primary';
const shadowClasses = 'shadow-sm';
const fontSizeClasses = '2xl:text-base';
---
<a
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${shadowClasses} ${fontSizeClasses} `}
href={url}
target="_blank"
rel="noopener noreferrer"
>
<Icon name="pajamas:gitea" class="h-4 w-4 md:h-6 md:w-6" />
{title}
<Icon
name="mdi:keyboard-arrow-right"
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
/>
</a>

View File

@@ -0,0 +1,35 @@
---
import Icon from '@components/ui/icons/icon.astro';
const { title, noArrow } = Astro.props;
interface Props {
title?: string;
url?: string;
noArrow?: boolean;
addHome?: boolean;
}
const baseClasses =
'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-50 ring-neutral-500 transition duration-300 focus-visible:ring outline-none';
const borderClasses = 'border border-transparent';
const bgColorClasses = 'bg-steel hover:bg-sky-800 active:bg-orange-500 dark:focus:outline-none';
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
const fontSizeClasses = '2xl:text-base';
const ringClasses = 'dark:ring-neutral-200';
---
<button
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses}`}
id="back-button"
data-astro-prefetch
>
{noArrow ? null : <Icon name="arrowLeft" />}
{title}
</button>
<script>
document.getElementById('back-button')?.addEventListener('click', () => {
window.history.back();
});
</script>

View File

@@ -0,0 +1,45 @@
---
import { Icon } from 'astro-icon/components';
const { title, url, noArrow, addHome, addClass } = Astro.props;
interface Props {
title?: string;
url?: string;
noArrow?: boolean;
addHome?: boolean;
addClass?: string;
}
const baseClasses =
'group inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-sm font-bold text-neutral-100 transition duration-300 ';
const borderClasses = 'border border-transparent';
const bgColorClasses = 'bg-bermuda hover:bg-turquoise dark:bg-turquoise dark:hover:bg-bermuda';
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
const fontSizeClasses = '2xl:text-base';
const ringClasses = 'dark:ring-neutral-200';
---
<a
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${addClass}`}
href={url}
data-astro-prefetch
>
{
addHome ? (
<Icon
name="mdi:home-variant-outline"
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
/>
) : null
}
{title}
{
noArrow ? null : (
<Icon
name="mdi:keyboard-arrow-right"
class="h-3 w-3 translate-y-0.25 transition duration-300 group-hover:translate-x-1 md:h-5 md:w-5"
/>
)
}
</a>

View File

@@ -0,0 +1,26 @@
---
const { title, url } = Astro.props;
interface Props {
title?: string;
url?: string;
}
const baseClasses =
'inline-flex items-center justify-center gap-x-2 rounded-lg px-4 py-3 text-center text-sm font-medium text-neutral-600 shadow-sm outline-none ring-neutral-500 focus-visible:ring transition duration-300';
const borderClasses = 'border border-neutral-200';
const bgColorClasses = 'bg-neutral-300';
const hoverClasses = 'hover:bg-neutral-400/50 hover:text-neutral-600 active:text-neutral-700';
const disableClasses = 'disabled:pointer-events-none disabled:opacity-50';
const fontSizeClasses = '2xl:text-base';
const ringClasses = 'ring-neutral-500';
const darkClasses =
'dark:border-neutral-700 dark:bg-neutral-700 dark:text-neutral-300 dark:ring-neutral-200 dark:hover:bg-neutral-600 dark:focus:outline-none';
---
<a
class={`${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${disableClasses} ${fontSizeClasses} ${ringClasses} ${darkClasses}`}
href={url}
>
{title}
</a>

View File

@@ -0,0 +1,150 @@
---
import Icon from '@components/ui/icons/icon.astro';
const { pageTitle, title = 'Share' } = Astro.props;
interface Props {
pageTitle: string;
title?: string;
}
type SocialPlatform = {
name: string;
url: string;
svg: string;
};
const socialPlatforms: SocialPlatform[] = [
{
name: 'Facebook',
url: `https://www.facebook.com/share.php?u=${Astro.url}&title=${pageTitle}`,
svg: 'facebook',
},
{
name: 'X',
url: `https://twitter.com/home/?status=${pageTitle}${Astro.url}`,
svg: 'x',
},
{
name: 'LinkedIn',
url: `https://www.linkedin.com/shareArticle?mini=true&url=${Astro.url}&title=${pageTitle}`,
svg: 'linkedIn',
},
];
---
<div class="hs-dropdown relative inline-flex [--auto-close:inside] [--placement:top-left]">
<button
id="hs-dropup"
type="button"
class="hs-dropdown-toggle inline-flex items-center gap-x-2 rounded-lg px-4 py-3 text-sm font-medium text-neutral-600 ring-neutral-500 transition duration-300 outline-none hover:bg-neutral-100 hover:text-neutral-700 focus-visible:ring dark:text-neutral-400 dark:ring-neutral-200 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:outline-none"
>
<Icon name="share" />
{title}
</button>
<div
class="hs-dropdown-menu duration hs-dropdown-open:opacity-100 z-10 hidden w-72 divide-y divide-neutral-200 rounded-lg bg-neutral-50 p-2 opacity-0 shadow-md transition-[opacity,margin] dark:divide-neutral-700 dark:border dark:border-neutral-700 dark:bg-neutral-800"
aria-labelledby="hs-dropup"
>
<div class="py-2 first:pt-0 last:pb-0">
{
socialPlatforms.map((platform) => (
<a
class="flex items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700"
href={platform.url}
>
<Icon name={platform.svg} />
Share on {platform.name}
</a>
))
}
</div>
<div class="py-2 first:pt-0 last:pb-0">
<button
type="button"
class="js-clipboard hover:text-dark focus-visible:ring-secondary group inline-flex w-full items-center gap-x-3.5 rounded-lg px-3 py-2 text-sm text-neutral-700 hover:bg-neutral-200 focus:bg-neutral-100 focus:outline-none focus-visible:ring-1 focus-visible:outline-none dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-neutral-300 dark:focus:bg-neutral-700"
data-clipboard-success-text="Copied"
>
<svg
class="js-clipboard-default h-4 w-4 transition group-hover:rotate-6"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect width="8" height="4" x="8" y="2" rx="1" ry="1"></rect>
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
</svg>
<svg
class="js-clipboard-success hidden h-4 w-4 text-neutral-700 dark:text-neutral-300"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
<span class="js-clipboard-success-text">Copy link</span>
</button>
</div>
</div>
</div>
<!--Import the necessary Dropdown and Clipboard plugins-->
<!--https://preline.co/plugins/html/dropdown.html-->
<!--<script is:inline src="/scripts/vendor/preline/dropdown/index.js"></script>-->
<!-- https://clipboardjs.com/ -->
<!--<script is:inline src="/scripts/vendor/clipboard.min.js"></script>-->
<script is:inline>
(function () {
window.addEventListener('load', () => {
const $clipboards = document.querySelectorAll('.js-clipboard');
$clipboards.forEach((el) => {
const clipboard = new ClipboardJS(el, {
text: () => {
return window.location.href;
},
});
clipboard.on('success', () => {
const $default = el.querySelector('.js-clipboard-default');
const $success = el.querySelector('.js-clipboard-success');
const $successText = el.querySelector('.js-clipboard-success-text');
const successText = el.dataset.clipboardSuccessText || '';
let oldSuccessText;
if ($successText) {
oldSuccessText = $successText.textContent;
$successText.textContent = successText;
}
if ($default && $success) {
$default.style.display = 'none';
$success.style.display = 'block';
}
setTimeout(() => {
if ($successText && oldSuccessText) {
$successText.textContent = oldSuccessText;
}
if ($default && $success) {
$success.style.display = '';
$default.style.display = '';
}
}, 800);
});
});
});
})();
</script>

View File

@@ -5,14 +5,14 @@
<button
id="theme-toggle"
data-theme-toggle
class="group relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-zinc-300 dark:hover:bg-zinc-800 dark:focus:ring-zinc-700 sm:p-2"
class="group dark:hover:bg-steel/30 relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-yellow-300/20 focus:outline-hidden sm:p-2"
aria-label="Toggle dark mode"
>
<div class="relative z-10 flex h-5 w-5 items-center justify-center">
<!-- Sun icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-light absolute h-5 w-5 rotate-0 scale-100 text-zinc-800 transition-all duration-500 dark:-rotate-90 dark:scale-0 dark:text-zinc-200"
class="icon-light absolute h-5 w-5 scale-100 rotate-0 text-neutral-600 transition-all duration-500 dark:scale-0 dark:-rotate-90 dark:text-neutral-400"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -29,7 +29,7 @@
<!-- Moon icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-dark absolute h-5 w-5 rotate-90 scale-0 text-zinc-800 transition-all duration-500 dark:rotate-0 dark:scale-100 dark:text-zinc-200"
class="icon-dark absolute h-5 w-5 scale-0 rotate-90 text-neutral-600 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-neutral-400"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
@@ -40,31 +40,27 @@
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</div>
<!-- Ripple effect -->
<span
class="absolute inset-0 h-full w-full bg-zinc-200 opacity-0 transition-opacity duration-300 group-active:opacity-20 dark:bg-zinc-700"
></span>
</button>
<script is:inline>
// Use a function to persist theme when using SPA transitions
// https://docs.astro.build/en/guides/view-transitions/#script-re-execution
function applyTheme() {
localStorage.theme === 'dark'
? document.documentElement.classList.add('dark')
: document.documentElement.classList.remove('dark');
}
document.addEventListener('astro:after-swap', applyTheme);
applyTheme();
</script>
<script>
// Use a function to handle theme toggle to ensure it can be called from anywhere
function setupThemeToggle() {
const themeToggles = document.querySelectorAll('[data-theme-toggle]');
// 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
if (!document.querySelector('.theme-switch-overlay')) {
const overlay = document.createElement('div');
@@ -117,11 +113,6 @@
// Add transition class
document.documentElement.classList.add('theme-switching');
// Add ripple effect
const ripple = document.createElement('span');
ripple.className = 'theme-toggle-ripple';
toggle.appendChild(ripple);
// Force a reflow to ensure all elements update
document.body.offsetHeight;
@@ -154,7 +145,6 @@
// Remove transition class after animation completes
document.documentElement.classList.remove('theme-switching');
ripple.remove();
}, 300);
}, 50);
},
@@ -184,7 +174,7 @@
}
// 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
document.addEventListener('visibilitychange', () => {
@@ -233,30 +223,6 @@
transition-property: background-color, border-color, color, fill, stroke !important;
}
/* Ripple animation */
.theme-toggle-ripple {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
width: 100%;
height: 100%;
border-radius: 50%;
background-color: rgba(161, 161, 170, 0.3);
animation: ripple 0.8s ease-out;
}
@keyframes ripple {
0% {
transform: translate(-50%, -50%) scale(0);
opacity: 0.5;
}
100% {
transform: translate(-50%, -50%) scale(2.5);
opacity: 0;
}
}
/* Subtle hover animation */
#theme-toggle {
transform: translateY(0);
@@ -274,12 +240,12 @@
}
#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);
}
#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);
}
}
@@ -302,10 +268,6 @@
transform: none;
transition: none;
}
.theme-toggle-ripple {
animation-duration: 0.4s;
}
}
/* Adjust size for very small screens */

View File

@@ -0,0 +1,45 @@
---
import { Icon } from 'astro-icon/components';
interface Props {
title?: string;
description?: string;
url?: string;
icon?: string;
}
const { title, description, url, icon } = Astro.props;
const baseClasses = 'smooth-reveal-2 group group-hover flex flex-col ';
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
const bgColorClasses =
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
---
<div class={`${baseClasses}`}>
<a
class={`rounded-xl duration-300 transition-all h-30 ${borderClasses} ${bgColorClasses} ${shadowClasses}`}
href={url}
data-astro-prefetch
>
<div class="p-4 md:p-5">
<div class="flex">
<Icon
name={icon}
class="group-hover:text-steel dark:group-hover:text-bermuda h-6 w-6 text-neutral-600 transition-all duration-300 md:h-8 md:w-8 dark:text-neutral-200"
/>
<div class="ms-5 grow">
<span
class="group-hover:text-steel dark:group-hover:text-bermuda block text-lg font-bold text-neutral-600 transition-all duration-300 dark:text-neutral-300"
>
{title}
</span>
<span class="mt-1 block text-neutral-500 dark:text-neutral-400">
{description}
</span>
</div>
</div>
</div>
</a>
</div>

View File

@@ -0,0 +1,39 @@
---
import { Icons } from './icons.ts';
interface Path {
d: string;
class?: string;
}
const { name } = Astro.props;
const icon = (Icons as any)[name] || {};
const paths: Path[] = icon.paths || [];
---
{
icon ? (
<svg
class={icon.class}
height={icon.height}
viewBox={icon.viewBox}
width={icon.width}
fill={icon.fill}
clip-rule={icon.clipRule}
fill-rule={icon.fillRule}
stroke={icon.stroke}
stroke-width={icon.strokeWidth}
stroke-linecap={icon.strokeLinecap}
stroke-linejoin={icon.strokeLinejoin}
>
<title>{icon.title}</title>
<circle cx={icon.circleCx} cy={icon.circleCy} r={icon.circleR} />
{paths.map((path) => (
<path d={path.d} class={path.class || ''} />
))}
</svg>
) : (
'Icon not found'
)
}

View File

@@ -0,0 +1,573 @@
export const Icons = {
groups: {
paths: [
{
d: 'm150-400 82-80-82-82-80 82 80 80Zm573-10 87-140 88 140H723Zm-243-70q-50 0-85-35t-35-85q0-51 35-85.5t85-34.5q51 0 85.5 34.5T600-600q0 50-34.5 85T480-480Zm.351-180Q455-660 437.5-642.851t-17.5 42.5Q420-575 437.351-557.5t43 17.5Q506-540 523-557.351t17-43Q540-626 522.851-643t-42.5-17ZM480-600ZM0-240v-53q0-39.464 42-63.232T150.398-380q12.158 0 23.38.5T196-377.273q-8 17.273-12 34.842-4 17.57-4 37.431v65H0Zm240 0v-65q0-65 66.5-105T480-450q108 0 174 40t66 105v65H240Zm570-140q67.5 0 108.75 23.768T960-293v53H780v-65q0-19.861-3.5-37.431Q773-360 765-377.273q11-1.727 22.171-2.227 11.172-.5 22.829-.5Zm-330.2-10Q400-390 350-366q-50 24-50 61v5h360v-6q0-36-49.5-60t-130.7-24Zm.2 90Z',
},
],
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
width: 48,
height: 48,
viewBox: '0 -960 960 960',
},
books: {
paths: [
{
d: 'M343-420h225v-60H343v60Zm0-90h395v-60H343v60Zm0-90h395v-60H343v60Zm-83 400q-24 0-42-18t-18-42v-560q0-24 18-42t42-18h560q24 0 42 18t18 42v560q0 24-18 42t-42 18H260Zm0-60h560v-560H260v560ZM140-80q-24 0-42-18t-18-42v-620h60v620h620v60H140Zm120-740v560-560Z',
},
],
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
width: 48,
height: 48,
viewBox: '0 -960 960 960',
},
verified: {
paths: [
{
d: 'm346-60-76-130-151-31 17-147-96-112 96-111-17-147 151-31 76-131 134 62 134-62 77 131 150 31-17 147 96 111-96 112 17 147-150 31-77 130-134-62-134 62Zm27-79 107-45 110 45 67-100 117-30-12-119 81-92-81-94 12-119-117-28-69-100-108 45-110-45-67 100-117 28 12 119-81 94 81 92-12 121 117 28 70 100Zm107-341Zm-43 133 227-225-45-41-182 180-95-99-46 45 141 140Z',
},
],
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
width: 48,
height: 48,
viewBox: '0 -960 960 960',
},
frame: {
paths: [
{
d: 'M480-480q-51 0-85.5-34.5T360-600q0-50 34.5-85t85.5-35q50 0 85 35t35 85q0 51-35 85.5T480-480Zm-.351-60Q505-540 522.5-557.149t17.5-42.5Q540-625 522.649-642.5t-43-17.5Q454-660 437-642.649t-17 43Q420-574 437.149-557t42.5 17ZM240-240v-76q0-27 17.5-47.5T300-397q42-22 86.943-32.5 44.942-10.5 93-10.5Q528-440 573-429.5t87 32.5q25 13 42.5 33.5T720-316v76H240Zm240-140q-47.546 0-92.773 13T300-328v28h360v-28q-42-26-87.227-39-45.227-13-92.773-13Zm0-220Zm0 300h180-360 180ZM140-80q-24 0-42-18t-18-42v-172h60v172h172v60H140ZM80-648v-172q0-24 18-42t42-18h172v60H140v172H80ZM648-80v-60h172v-172h60v172q0 24-18 42t-42 18H648Zm172-568v-172H648v-60h172q24 0 42 18t18 42v172h-60Z',
},
],
class: 'mt-1 h-8 w-8 flex-shrink-0 fill-orange-400 dark:fill-orange-300',
width: 48,
height: 48,
viewBox: '0 -960 960 960',
},
tools: {
paths: [
{
d: 'M764-80q-6 0-11-2t-10-7L501-331q-5-5-7-10t-2-11q0-6 2-11t7-10l85-85q5-5 10-7t11-2q6 0 11 2t10 7l242 242q5 5 7 10t2 11q0 6-2 11t-7 10l-85 85q-5 5-10 7t-11 2Zm0-72 43-43-200-200-43 43 200 200ZM195-80q-6 0-11.5-2T173-89l-84-84q-5-5-7-10.5T80-195q0-6 2-11t7-10l225-225h85l38-38-175-175h-57L80-779l99-99 125 125v57l175 175 130-130-67-67 56-56H485l-18-18 128-128 18 18v113l56-56 169 169q15 15 23.5 34.5T870-600q0 20-6.5 38.5T845-528l-85-85-56 56-52-52-211 211v84L216-89q-5 5-10 7t-11 2Zm0-72 200-200v-43h-43L152-195l43 43Zm0 0-43-43 22 21 21 22Zm569 0 43-43-43 43Z',
},
],
class:
'mt-2 h-6 w-6 flex-shrink-0 fill-neutral-700 hs-tab-active:fill-orange-400 dark:fill-neutral-300 dark:hs-tab-active:fill-orange-300 md:h-7 md:w-7',
width: 48,
height: 48,
viewBox: '0 -960 960 960',
},
dashboard: {
paths: [
{
d: 'M510-570v-270h330v270H510ZM120-450v-390h330v390H120Zm390 330v-390h330v390H510Zm-390 0v-270h330v270H120Zm60-390h210v-270H180v270Zm390 330h210v-270H570v270Zm0-450h210v-150H570v150ZM180-180h210v-150H180v150Zm210-330Zm180-120Zm0 180ZM390-330Z',
},
],
class:
'mt-2 h-6 w-6 flex-shrink-0 fill-neutral-700 hs-tab-active:fill-orange-400 dark:fill-neutral-300 dark:hs-tab-active:fill-orange-300 md:h-7 md:w-7',
width: 48,
height: 48,
viewBox: '0 -960 960 960',
},
house: {
paths: [
{
d: 'M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25',
},
],
class: 'h-4 w-4 flex-shrink-0 md:h-5 md:w-5',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '2',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
home: {
paths: [
{
d: 'M8.25 21v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21m0 0h4.5V3.545M12.75 21h7.5V10.75M2.25 21h1.5m18 0h-18M2.25 9l4.5-1.636M18.75 3l-1.5.545m0 6.205 3 1m1.5.5-1.5-.5M6.75 7.364V3h-3v18m3-13.636 10.5-3.819',
},
],
class:
'h-6 w-6 flex-shrink-0 group-hover:text-steel dark:group-hover:text-steel transition-all duration-200 text-neutral-600 dark:text-neutral-300 md:h-7 md:w-7',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '1.5',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
arrowUp: {
paths: [
{
d: 'm5 12 7-7 7 7',
},
{
d: 'M12 19V5',
},
],
class: 'h-5 w-5 flex-shrink-0 text-orange-400 dark:text-orange-300',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '2',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
checkCircle: {
paths: [
{
d: 'M10 18a8 8 0 100-16 8 8 0 000 16zM13.707 8.293a1 1 0 00-1.414-1.414L9 10.586l-1.293-1.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z',
},
],
class: 'h-5 w-5 shrink-0',
viewBox: '0 0 20 20',
fill: 'currentColor',
fillRule: 'evenodd',
clipRule: 'evenodd',
},
bookmark: {
paths: [
{
d: 'M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z',
class:
'fill-current text-neutral-500 transition duration-300 group-hover:text-red-400 group-hover:dark:text-red-400',
},
],
class: 'h-6 w-6 fill-none transition duration-300',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '1.5',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
arrowRight: {
paths: [
{
d: 'm9 18 6-6-6-6',
},
],
class: 'h-4 w-4 flex-shrink-0 transition duration-300 group-hover:translate-x-1',
width: 20,
height: 20,
viewBox: '0 0 22 22',
fill: 'none',
strokeWidth: '2',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
arrowLeft: {
paths: [
{
d: 'm15 18-6-6 6-6',
},
],
class: 'h-4 w-4 flex-shrink-0 transition duration-300 group-hover:-translate-x-1',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '2',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
facebook: {
paths: [
{
d: 'M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z',
},
],
class: 'size-4 flex-shrink-0 fill-current',
viewBox: '0 0 24 24',
stroke: 'currentColor',
},
x: {
paths: [
{
d: 'M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z',
},
],
class: 'size-4 flex-shrink-0 fill-current',
viewBox: '0 0 24 24',
stroke: 'currentColor',
},
linkedIn: {
paths: [
{
d: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
},
],
class: 'size-4 flex-shrink-0 fill-current',
viewBox: '0 0 24 24',
stroke: 'currentColor',
},
share: {
paths: [
{
d: 'M7.217 10.907a2.25 2.25 0 1 0 0 2.186m0-2.186c.18.324.283.696.283 1.093s-.103.77-.283 1.093m0-2.186 9.566-5.314m-9.566 7.5 9.566 5.314m0 0a2.25 2.25 0 1 0 3.935 2.186 2.25 2.25 0 0 0-3.935-2.186Zm0-12.814a2.25 2.25 0 1 0 3.933-2.185 2.25 2.25 0 0 0-3.933 2.185Z',
},
],
class: 'h-4 w-4 group-hover:text-neutral-700',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '1.5',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
github: {
paths: [
{
d: 'M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.012 8.012 0 0 0 16 8c0-4.42-3.58-8-8-8z',
},
],
class: 'w-4.5 h-4.5 transition flex-shrink-0 text-neutral-700 duration-300',
width: 16,
height: 16,
viewBox: '0 0 16 16',
fill: 'currentColor',
},
gitea: {
paths: [
{
d: 'M4.209 4.603c-.247 0-.525.02-.84.088-.333.07-1.28.283-2.054 1.027C-.403 7.25.035 9.685.089 10.052c.065.446.263 1.687 1.21 2.768 1.749 2.141 5.513 2.092 5.513 2.092s.462 1.103 1.168 2.119c.955 1.263 1.936 2.248 2.89 2.367 2.406 0 7.212-.004 7.212-.004s.458.004 1.08-.394c.535-.324 1.013-.893 1.013-.893s.492-.527 1.18-1.73c.21-.37.385-.729.538-1.068 0 0 2.107-4.471 2.107-8.823-.042-1.318-.367-1.55-.443-1.627-.156-.156-.366-.153-.366-.153s-4.475.252-6.792.306c-.508.011-1.012.023-1.512.027v4.474l-.634-.301c0-1.39-.004-4.17-.004-4.17-1.107.016-3.405-.084-3.405-.084s-5.399-.27-5.987-.324c-.187-.011-.401-.032-.648-.032zm.354 1.832h.111s.271 2.269.6 3.597C5.549 11.147 6.22 13 6.22 13s-.996-.119-1.641-.348c-.99-.324-1.409-.714-1.409-.714s-.73-.511-1.096-1.52C1.444 8.73 2.021 7.7 2.021 7.7s.32-.859 1.47-1.145c.395-.106.863-.12 1.072-.12zm8.33 2.554c.26.003.509.127.509.127l.868.422-.529 1.075a.686.686 0 0 0-.614.359.685.685 0 0 0 .072.756l-.939 1.924a.69.69 0 0 0-.66.527.687.687 0 0 0 .347.763.686.686 0 0 0 .867-.206.688.688 0 0 0-.069-.882l.916-1.874a.667.667 0 0 0 .237-.02.657.657 0 0 0 .271-.137 8.826 8.826 0 0 1 1.016.512.761.761 0 0 1 .286.282c.073.21-.073.569-.073.569-.087.29-.702 1.55-.702 1.55a.692.692 0 0 0-.676.477.681.681 0 1 0 1.157-.252c.073-.141.141-.282.214-.431.19-.397.515-1.16.515-1.16.035-.066.218-.394.103-.814-.095-.435-.48-.638-.48-.638-.467-.301-1.116-.58-1.116-.58s0-.156-.042-.27a.688.688 0 0 0-.148-.241l.516-1.062 2.89 1.401s.48.218.583.619c.073.282-.019.534-.069.657-.24.587-2.1 4.317-2.1 4.317s-.232.554-.748.588a1.065 1.065 0 0 1-.393-.045l-.202-.08-4.31-2.1s-.417-.218-.49-.596c-.083-.31.104-.691.104-.691l2.073-4.272s.183-.37.466-.497a.855.855 0 0 1 .35-.077z',
},
],
class: 'w-6 h-6 transition flex-shrink-0 duration-300',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'currentColor',
},
arrowRightStatic: {
paths: [
{
d: 'm9 18 6-6-6-6',
},
],
class: 'size-4 flex-shrink-0',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '2',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
openInNew: {
paths: [
{
d: 'm4.5 19.5 15-15m0 0H8.25m11.25 0v11.25',
},
],
class: 'ml-0.5 w-3 h-3 md:w-4 md:h-4 inline pb-0.5',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '3',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
accordionNotActive: {
paths: [
{
d: 'm6 9 6 6 6-6',
},
],
class:
'block h-5 w-5 flex-shrink-0 text-neutral-600 group-hover:text-neutral-500 hs-accordion-active:hidden dark:text-neutral-400',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '2',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
accordionActive: {
paths: [
{
d: 'm18 15-6-6-6 6',
},
],
class:
'hidden h-5 w-5 flex-shrink-0 text-neutral-600 group-hover:text-neutral-500 hs-accordion-active:block dark:text-neutral-400',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '2',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
xFooter: {
paths: [
{
d: 'M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z',
},
],
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
viewBox: '0 0 24 24',
fill: 'currentColor',
title: 'Twitter',
},
facebookFooter: {
paths: [
{
d: 'M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z',
},
],
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
viewBox: '0 0 24 24',
fill: 'currentColor',
title: 'Facebook',
},
githubFooter: {
paths: [
{
d: 'M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12',
},
],
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
viewBox: '0 0 24 24',
fill: 'currentColor',
title: 'GitHub',
},
googleFooter: {
paths: [
{
d: 'M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z',
},
],
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
viewBox: '0 0 24 24',
fill: 'currentColor',
title: 'Google',
},
slackFooter: {
paths: [
{
d: 'M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z',
},
],
class: 'h-4 w-4 flex-shrink-0 fill-current text-neutral-700 dark:text-neutral-400',
viewBox: '0 0 24 24',
fill: 'currentColor',
title: 'Slack',
},
quotation: {
paths: [
{
d: 'M7.39762 10.3C7.39762 11.0733 7.14888 11.7 6.6514 12.18C6.15392 12.6333 5.52552 12.86 4.76621 12.86C3.84979 12.86 3.09047 12.5533 2.48825 11.94C1.91222 11.3266 1.62421 10.4467 1.62421 9.29999C1.62421 8.07332 1.96459 6.87332 2.64535 5.69999C3.35231 4.49999 4.33418 3.55332 5.59098 2.85999L6.4943 4.25999C5.81354 4.73999 5.26369 5.27332 4.84476 5.85999C4.45201 6.44666 4.19017 7.12666 4.05926 7.89999C4.29491 7.79332 4.56983 7.73999 4.88403 7.73999C5.61716 7.73999 6.21938 7.97999 6.69067 8.45999C7.16197 8.93999 7.39762 9.55333 7.39762 10.3ZM14.6242 10.3C14.6242 11.0733 14.3755 11.7 13.878 12.18C13.3805 12.6333 12.7521 12.86 11.9928 12.86C11.0764 12.86 10.3171 12.5533 9.71484 11.94C9.13881 11.3266 8.85079 10.4467 8.85079 9.29999C8.85079 8.07332 9.19117 6.87332 9.87194 5.69999C10.5789 4.49999 11.5608 3.55332 12.8176 2.85999L13.7209 4.25999C13.0401 4.73999 12.4903 5.27332 12.0713 5.85999C11.6786 6.44666 11.4168 7.12666 11.2858 7.89999C11.5215 7.79332 11.7964 7.73999 12.1106 7.73999C12.8437 7.73999 13.446 7.97999 13.9173 8.45999C14.3886 8.93999 14.6242 9.55333 14.6242 10.3Z',
},
],
class:
'absolute start-0 top-0 h-16 w-16 -translate-x-6 -translate-y-8 transform text-neutral-300 dark:text-neutral-700',
width: 16,
height: 16,
viewBox: '0 0 16 16',
fill: 'currentColor',
},
question: {
paths: [
{
d: 'M9.879 7.519c1.171-1.025 3.071-1.025 4.242 0 1.172 1.025 1.172 2.687 0 3.712-.203.179-.43.326-.67.442-.745.361-1.45.999-1.45 1.827v.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9 5.25h.008v.008H12v-.008Z',
},
],
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '1.5',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
chatBubble: {
paths: [
{
d: 'M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155',
},
],
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '1.5',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
mapPin: {
paths: [
{
d: 'M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z',
},
{
d: 'M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z',
},
],
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '1.5',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
envelopeOpen: {
paths: [
{
d: 'M21.75 9v.906a2.25 2.25 0 0 1-1.183 1.981l-6.478 3.488M2.25 9v.906a2.25 2.25 0 0 0 1.183 1.981l6.478 3.488m8.839 2.51-4.66-2.51m0 0-1.023-.55a2.25 2.25 0 0 0-2.134 0l-1.022.55m0 0-4.661 2.51m16.5 1.615a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V8.844a2.25 2.25 0 0 1 1.183-1.981l7.5-4.039a2.25 2.25 0 0 1 2.134 0l7.5 4.039a2.25 2.25 0 0 1 1.183 1.98V19.5Z',
},
],
class: 'mt-1.5 h-6 w-6 flex-shrink-0 text-neutral-600 dark:text-neutral-400',
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '1.5',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
earth: {
paths: [
{
d: 'm20.893 13.393-1.135-1.135a2.252 2.252 0 0 1-.421-.585l-1.08-2.16a.414.414 0 0 0-.663-.107.827.827 0 0 1-.812.21l-1.273-.363a.89.89 0 0 0-.738 1.595l.587.39c.59.395.674 1.23.172 1.732l-.2.2c-.212.212-.33.498-.33.796v.41c0 .409-.11.809-.32 1.158l-1.315 2.191a2.11 2.11 0 0 1-1.81 1.025 1.055 1.055 0 0 1-1.055-1.055v-1.172c0-.92-.56-1.747-1.414-2.089l-.655-.261a2.25 2.25 0 0 1-1.383-2.46l.007-.042a2.25 2.25 0 0 1 .29-.787l.09-.15a2.25 2.25 0 0 1 2.37-1.048l1.178.236a1.125 1.125 0 0 0 1.302-.795l.208-.73a1.125 1.125 0 0 0-.578-1.315l-.665-.332-.091.091a2.25 2.25 0 0 1-1.591.659h-.18c-.249 0-.487.1-.662.274a.931.931 0 0 1-1.458-1.137l1.411-2.353a2.25 2.25 0 0 0 .286-.76m11.928 9.869A9 9 0 0 0 8.965 3.525m11.928 9.868A9 9 0 1 1 8.965 3.525',
},
],
class: 'w-4 h-4 flex-shrink-0',
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '1.5',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
party: {
paths: [
{
d: 'M5.8 11.3 2 22l10.7-3.79',
},
{
d: 'M4 3h.01',
},
{
d: 'M22 8h.01',
},
{
d: 'M15 2h.01',
},
{
d: 'M22 20h.01',
},
{
d: 'm22 2-2.24.75a2.9 2.9 0 0 0-1.96 3.12v0c.1.86-.57 1.63-1.45 1.63h-.38c-.86 0-1.6.6-1.76 1.44L14 10',
},
{
d: 'm22 13-.82-.33c-.86-.34-1.82.2-1.98 1.11v0c-.11.7-.72 1.22-1.43 1.22H17',
},
{
d: 'm11 2 .33.82c.34.86-.2 1.82-1.11 1.98v0C9.52 4.9 9 5.52 9 6.23V7',
},
{
d: 'M11 13c1.93 1.93 2.83 4.17 2 5-.83.83-3.07-.07-5-2-1.93-1.93-2.83-4.17-2-5 .83-.83 3.07.07 5 2Z',
},
],
class:
'w-6 h-6 group-hover:text-steel dark:group-hover:text-steel transition-all duration-200 text-neutral-600 dark:text-neutral-300',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '2',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
email: {
paths: [
{
d: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
},
],
class:
'w-8 h-8 group-hover:text-steel dark:group-hover:text-steel transition-all duration-200 text-neutral-600 dark:text-neutral-300',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '2',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
sun: {
paths: [
{
d: 'M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4',
},
],
circleCx: '12',
circleCy: '12',
circleR: '5',
class:
'icon-light absolute h-5 w-5 scale-100 rotate-0 text-neutral-800 transition-all duration-500 dark:scale-0 dark:-rotate-90 dark:text-neutral-200',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '2',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
moon: {
paths: [
{
d: 'M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z',
},
],
class:
'icon-dark absolute h-5 w-5 scale-0 rotate-90 text-neutral-800 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-neutral-200',
width: 24,
height: 24,
viewBox: '0 0 24 24',
fill: 'none',
strokeWidth: '2',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
arrow: {
paths: [
{
d: 'M5.22 14.78a.75.75 0 001.06 0l7.22-7.22v5.69a.75.75 0 001.5 0v-7.5a.75.75 0 00-.75-.75h-7.5a.75.75 0 000 1.5h5.69l-7.22 7.22a.75.75 0 000 1.06z',
},
],
class:
'icon-dark absolute h-5 w-5 scale-0 rotate-90 text-neutral-800 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-neutral-200',
width: 16,
height: 16,
viewBox: '0 0 20 20',
fill: 'none',
strokeWidth: '2',
strokeLinecap: 'round',
strokeLinejoin: 'round',
stroke: 'currentColor',
},
};

View File

@@ -0,0 +1,17 @@
---
import { Image } from 'astro:assets';
import { ImageMetadata } from 'astro';
import { blurStyle } from '@support/image';
interface FsPathImage extends ImageMetadata {
fsPath?: string;
}
const props = Astro.props;
const image = props.src as FsPathImage;
const showBlur = !props.disableBlur;
const blurCSS = image.fsPath && showBlur ? await blurStyle(image.fsPath) : {};
---
<Image {...props} style={blurCSS} inferSize={true} />

View File

@@ -0,0 +1,11 @@
---
import { readSingleton } from '@directus/sdk';
import Image from '@components/ui/images/Image.astro';
import logo from '@images/brand_logo.png';
import directus from '@lib/directus';
const global = await directus.request(readSingleton('site_global'));
---
<Image src={logo} alt={global.name} {...Astro.props} draggable="false" loading="eager" />

View File

@@ -0,0 +1,129 @@
---
import { Icon } from 'astro-icon/components';
import { readItems } from '@directus/sdk';
import type { Education } from '@lib/directusTypes';
import directus from '@lib/directus';
const education = await directus.request(
readItems('site_education', {
fields: ['*'],
sort: ['-graduationDate'],
})
);
const certificate = await directus.request(
readItems('site_certificate', {
fields: ['*'],
sort: ['-issuerDate'],
})
);
const baseClasses = ' rounded-xl flex flex-col';
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
const bgColorClasses =
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
---
<section class:list={['order-first flex flex-col gap-4', Astro.props.className]}>
<h3
class="smooth-reveal-1 relative flex w-full items-center gap-3 pb-5 text-5xl text-neutral-800 dark:text-neutral-200"
>
Education
</h3>
<div class="ml-8">
<h4 class="smooth-reveal-1 pt-5 text-2xl font-semibold text-neutral-800 dark:text-neutral-200">
University
</h4>
<ul class="space-y-4 py-3">
{
education.map(({ institution, area, url }) => {
return (
<div class="smooth-reveal-cards mt-4 grid grid-cols-3 gap-4 rounded-xl">
<div>
<div
class={`p-4 transition-all duration-300 md:p-5 ${shadowClasses} ${bgColorClasses} ${baseClasses} ${borderClasses}`}
>
<h3 class="flex flex-row text-lg font-bold text-neutral-800 dark:text-neutral-200">
<Icon
name="mdi:university-outline"
class="mr-2 h-3 w-3 translate-y-1 md:h-5 md:w-5"
/>
{institution}
</h3>
<p class="mt-2 ml-7 text-xs font-medium text-neutral-600 uppercase dark:text-neutral-400">
{area}
</p>
<div class="ml-6 flex">
<a
class="group group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
href={url}
>
<div class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-[44px] items-center text-sm font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
<span class="relative inline-block overflow-hidden"> Visit Page </span>
<Icon
name="mdi:keyboard-arrow-right"
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
/>
</div>
</a>
</div>
</div>
</div>
</div>
);
})
}
</ul>
</div>
{
certificate.length > 0 && (
<div class="ml-8">
<h4 class="smooth-reveal-1 pt-8 text-2xl font-semibold text-neutral-800 dark:text-neutral-200">
Certificates
</h4>
<ul class="space-y-4 py-3">
{certificate.map(({ name, issuer, url }) => {
return (
<div class="smooth-reveal-cards mt-4 grid grid-cols-3 gap-4 rounded-xl">
<div>
<div
class={`p-4 transition-all duration-300 md:p-5 ${shadowClasses} ${bgColorClasses} ${baseClasses} ${borderClasses}`}
>
<h3 class="flex flex-row text-lg font-bold text-neutral-800 dark:text-neutral-200">
<Icon
name="mdi:script-text-outline"
class="mr-2 h-3 w-3 translate-y-1 md:h-5 md:w-5"
/>
{name}
</h3>
<p class="mt-2 ml-7 text-xs font-medium text-neutral-600 uppercase dark:text-neutral-400">
{issuer}
</p>
<div class="ml-6 flex">
<a
class="group group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
href={url}
>
<div class="group-hover:text-steel dark:group-hover:text-bermuda transition-text relative z-10 mx-auto flex min-h-[44px] items-center text-sm font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
<span class="relative inline-block overflow-hidden"> Visit Page </span>
<Icon
name="mdi:keyboard-arrow-right"
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
/>
</div>
</a>
</div>
</div>
</div>
</div>
);
})}
</ul>
</div>
)
}
</section>

View File

@@ -0,0 +1,152 @@
---
import { Icon } from 'astro-icon/components';
import { readItems } from '@directus/sdk';
import type { Experience } from '@lib/directusTypes';
import directus from '@lib/directus';
const experiences = await directus.request(
readItems('site_experience', {
fields: ['*'],
sort: ['-endDate'],
})
);
---
<section
class:list={['flex flex-col gap-4', Astro.props.className]}
>
<h3 class="relative smooth-reveal-1 flex w-full items-center gap-3 pb-10 text-5xl text-neutral-800 dark:text-neutral-200">Experience</h3>
<ul class="ml-8 w-full flex flex-col">
{
experiences.map(
(experience: Experience) => {
const startYear = new Date(experience.startDate).getFullYear();
const endYear = experience.endDate != null ? new Date(experience.endDate).getFullYear() : 'Present';
return (
<li class="relative">
<div class="group smooth-reveal-2 relative grid pb-1 transition-all sm:grid-cols-18 sm:gap-8 md:gap-6 lg:hover:!opacity-100">
<header class="relative mt-1 text-lg font-semibold sm:col-span-3 text-neutral-800 dark:text-neutral-200">
<time datetime={experience.startDate} data-title={experience.startDate}>
{startYear}
</time>{' '}
-{' '}
<time datetime={experience.endDate} data-title={experience.endDate}>
{endYear}
</time>
</header>
<div class="relative flex flex-col pb-6 before:absolute before:mt-8 before:-ml-6 before:h-full before:w-px before:bg-stone-400 sm:col-span-12">
<div class="absolute mt-4 h-2 w-2 -translate-x-[1.71rem] rounded-full bg-stone-400" />
<h3>
<div
class="inline-flex items-center text-2xl leading-tight font-semibold"
aria-label="{position} - {company}"
>
<span class="text-neutral-800 dark:text-neutral-200">
{experience.position} <span>@</span>
{experience.url ? (
<a
class="hover:text-steel dark:hover:text-bermuda"
href={experience.url}
title={`Ver ${experience.name}`}
target="_blank"
>
{experience.name}
</a>
) : (
<span>{experience.name}</span>
)}
</span>
</div>
</h3>
{(experience.location || experience.location_type) && (
<div class="text-sm text-neutral-600 dark:text-neutral-400">
{experience.location} {experience.location && experience.location_type && '-'} {experience.location_type}
</div>
)}
<div class="text-md mt-4 flex flex-col gap-4" x-data="{ expanded: false }">
{experience.summary && (
<div class="flex flex-col gap-1">
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Summary:</h4>
<ul class="flex list-disc flex-col gap-2 text-neutral-700 dark:text-neutral-400 [&>li]:ml-4">
{Array.isArray(experience.summary) ? (
experience.summary.map((item) => ({ item }))
) : (
<li class="marker:text-steel dark:marker:text-bermuda">{experience.summary}</li>
)}
</ul>
</div>
)}
{(experience.responsibilities || experience.achievements) && (
<div class="relative flex flex-col gap-4 max-sm:!h-auto md:after:absolute md:after:bottom-0 md:after:h-12 md:after:w-full md:after:bg-gradient-to-t md:after:from-neutral-200 dark:md:after:from-stone-700 md:after:content-[''] " :class="expanded ? 'after:hidden' : ''" x-show="expanded" x-collapse.min.50px>
{experience.responsibilities && (
<div class="flex flex-col gap-1">
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Responsibilities:</h4>
<ul class="text-neutral-700 dark:text-neutral-400 [&>li]:ml-4 flex list-disc flex-col gap-2">
{experience.responsibilities.map(responsibility => (
<li class="marker:text-steel dark:marker:text-bermuda">{responsibility}</li>
))}
</ul>
</div>
)}
{experience.achievements && (
<div class="flex flex-col gap-1">
<h4 class="font-semibold text-neutral-800 dark:text-neutral-200">Achievements:</h4>
<ul class="text-neutral-700 dark:text-neutral-400 [&>li]:ml-4 flex list-disc flex-col gap-2">
{experience.achievements.map(achievement => (
<li class="marker:text-steel dark:marker:text-bermuda">{achievement}</li>
))}
</ul>
</div>
)}
</div>
<button @click="expanded = ! expanded" class="group/more w-fit cursor-pointer items-center justify-center gap-1.5 text-xs underline text-neutral-700 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-neutral-400 transition-all flex">
<span x-text="expanded ? 'Show less' : 'Show more'">Show more</span>
<svg
class="h-4 w-4 duration-200 ease-out group-hover/more:translate-y-0.5"
:class="{ 'rotate-180': expanded }"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</button>
<ul class="flex print:hidden flex-wrap gap-2" aria-label="Technologies used">
{experience.skills && experience.skills.map(skill => {
const iconName = skill.toLowerCase();
return (
<li class="bg-steel/20 border-steel/20 text-neutral-800 dark:bg-bermuda/20 dark:border-bermuda/20 dark:text-neutral-200 flex gap-1 items-center border-solid border rounded-md px-2 py-0.5 text-xs">
<Icon name={`mdi:${iconName}`} /> <span>{skill}</span>
</li>
)
})}
</ul>
)}
</div>
</div>
</div>
</li>
);
}
)
}
</ul>
</section>
<!-- Alpine Plugins -->
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
<!-- Alpine Core -->
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>

View File

@@ -0,0 +1,37 @@
---
import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import FeaturesCard from '@components/ui/cards/FeaturesCard.astro';
const global = await directus.request(readSingleton('site_global'));
---
<section class="mx-auto mb-20 max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full">
<div
class="flex flex-col items-center justify-center gap-y-2 sm:flex-row sm:gap-x-12 sm:gap-y-0 lg:gap-x-24"
>
<div class="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
<div class="grid gap-3 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3">
<FeaturesCard
title="Cloud Engineer"
description="Full stack and cloud engineer."
url="/about"
icon="mdi:cloud-outline"
/>
<FeaturesCard
title="Homelab"
description="Tinkering, testing, deploying, etc, etc ..."
url="/categories/homelab/"
icon="mdi:home-variant-outline"
/>
<FeaturesCard
title="Email"
description={`Send me a message.`}
url=`mailto:${global.email}`
icon="mdi:email-fast"
/>
</div>
</div>
</div>
</section>

View File

@@ -0,0 +1,35 @@
---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
interface Props {
title: string;
subTitle: string;
btnExists?: boolean;
btnTitle?: string;
btnURL?: string;
}
const { title, subTitle, btnExists, btnTitle, btnURL } = Astro.props;
---
<section class="mx-auto mt-10 px-4 sm:px-6 lg:px-8 lg:pt-10 2xl:max-w-full">
<div class="flex-wrap md:flex md:items-center md:justify-between">
<div class="w-full md:w-auto">
<h1
class="smooth-reveal block text-4xl font-bold tracking-tight text-balance text-neutral-800 md:text-5xl lg:text-6xl dark:text-neutral-200"
>
{title}
</h1>
<p class="smooth-reveal mt-4 text-lg text-pretty text-neutral-600 dark:text-neutral-400">
{subTitle}
</p>
{
btnExists ? (
<div class="smooth-reveal mt-4 md:mt-8">
<PrimaryCTA title={btnTitle} url={btnURL} />
</div>
) : null
}
</div>
</div>
</section>

View File

@@ -0,0 +1,63 @@
---
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import SecondaryCTA from '@components/ui/buttons/SecondaryCTA.astro';
import Image from '@components/ui/images/Image.astro';
const { title, subTitle, primaryBtn, primaryBtnURL, secondaryBtn, secondaryBtnURL, src, alt } =
Astro.props;
interface Props {
title: string;
subTitle?: string;
primaryBtn?: string;
primaryBtnURL?: string;
secondaryBtn?: string;
secondaryBtnURL?: string;
src?: any;
alt?: string;
}
---
<section
class="mx-auto grid max-w-[85rem] gap-4 px-4 py-14 sm:px-6 md:grid-cols-2 md:items-center md:gap-8 lg:px-8 2xl:max-w-full"
>
<div>
<h1
class="smooth-reveal block text-3xl font-bold tracking-tight text-balance text-neutral-800 sm:text-4xl lg:text-7xl lg:leading-tight dark:text-neutral-200"
>
<Fragment set:html={title} />
</h1>
{
subTitle && (
<p class="smooth-reveal mt-6 text-lg leading-relaxed text-pretty text-neutral-700 lg:w-4/5 dark:text-neutral-300">
{subTitle}
</p>
)
}
<div class="smooth-reveal mt-7 grid w-full gap-3 sm:inline-flex">
{primaryBtn && <PrimaryCTA title={primaryBtn} url={primaryBtnURL} />}
{secondaryBtn && <SecondaryCTA title={secondaryBtn} url={secondaryBtnURL} />}
</div>
</div>
<div class="smooth-reveal-fade hidden w-full md:block">
<div class="top-12 flex w-full justify-center overflow-hidden md:ml-4">
{
src && alt && (
<Image
src={src}
alt={alt}
class="h-full w-[420px] scale-100 object-cover object-center"
draggable="false"
loading="eager"
format="webp"
quality="low"
widths={[840]}
disableBlur={true}
/>
)
}
</div>
</div>
</section>

View File

@@ -0,0 +1,164 @@
---
import GiteaBtn from '@components/ui/buttons/GiteaBtn.astro';
const { title, subTitle, url } = Astro.props;
const btnTitle = 'Continue to Gitea';
interface Props {
title: string;
subTitle?: string;
url?: string;
}
---
<section class="lg:px- relative mx-auto mb-20 max-w-[85rem] px-4 pt-30 pb-30 sm:px-6">
<div
class="smooth-reveal absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]"
>
<svg
class="animate-hover animate-hover-1"
width="64"
height="64"
fill="none"
stroke-width="1.5"
color="#ea580c"
viewBox="0 0 24 24"
>
<path
fill="#ea580c"
stroke="#ea580c"
stroke-linecap="round"
stroke-linejoin="round"
d="M12 23a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM3 8a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM3 18a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
></path>
<path
stroke="#ea580c"
stroke-linecap="round"
stroke-linejoin="round"
d="M21 7.353v9.294a.6.6 0 0 1-.309.525l-8.4 4.666a.6.6 0 0 1-.582 0l-8.4-4.666A.6.6 0 0 1 3 16.647V7.353a.6.6 0 0 1 .309-.524l8.4-4.667a.6.6 0 0 1 .582 0l8.4 4.667a.6.6 0 0 1 .309.524Z"
></path>
<path
stroke="#ea580c"
stroke-linecap="round"
stroke-linejoin="round"
d="m3.528 7.294 8.18 4.544a.6.6 0 0 0 .583 0l8.209-4.56M12 21v-9"></path>
</svg>
</div>
<div class="smooth-reveal absolute top-0 left-[85%] scale-75">
<svg
class="animate-hover animate-hover-2"
width="64"
height="64"
fill="none"
stroke-width="1.5"
color="#fbbf24"
viewBox="0 0 24 24"
>
<path
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"></path>
<path
fill="#fbbf24"
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M5 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"></path>
<path stroke="#fbbf24" stroke-linecap="round" stroke-linejoin="round" d="M5 10.5V9M5 15v-1.5"
></path>
<path
fill="#fbbf24"
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M5 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM19 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"></path>
<path
stroke="#fbbf24"
stroke-linecap="round"
stroke-linejoin="round"
d="M10.5 19H9M15 19h-1.5"></path>
</svg>
</div>
<div
class="smooth-reveal absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]"
>
<svg
class="animate-hover animate-hover-3"
width="64"
height="64"
fill="none"
stroke-width="1.5"
color="#a3a3a3"
viewBox="0 0 24 24"
>
<path
stroke="#a3a3a3"
stroke-linecap="round"
stroke-linejoin="round"
d="M5.164 17c.29-1.049.67-2.052 1.132-3M11.5 7.794A16.838 16.838 0 0 1 14 6.296M4.5 22a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"
></path>
<path
stroke="#a3a3a3"
stroke-linecap="round"
stroke-linejoin="round"
d="M9.5 12a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5ZM19.5 7a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"
></path>
</svg>
</div>
<!-- Hero Section Heading -->
<div class="smooth-reveal-2 mx-auto mt-5 max-w-xl text-center">
<h2
class="block text-4xl leading-tight font-bold tracking-tight text-balance text-neutral-800 md:text-5xl lg:text-5xl dark:text-neutral-200"
>
{title}
</h2>
</div>
<!-- Hero Section Sub-heading -->
<div class="smooth-reveal-2 mx-auto mt-5 max-w-3xl text-center">
{
subTitle && (
<p class="text-lg text-pretty text-neutral-600 dark:text-neutral-400">{subTitle}</p>
)
}
</div>
<!-- Github Button -->
{
url && (
<div class="smooth-reveal-2 mt-8 flex justify-center gap-3">
<GiteaBtn url={url} title={btnTitle} />
</div>
)
}
</section>
<style>
@keyframes animate-hover {
from {
transform: translateY(15px);
}
to {
transform: translateY(-15px);
}
}
.animate-hover {
animation: animate-hover ease-in-out;
animation-iteration-count: infinite;
animation-direction: alternate;
}
.animate-hover-1 {
animation-duration: 5s;
}
.animate-hover-2 {
animation-duration: 5.5s;
}
.animate-hover-3 {
animation-duration: 6s;
}
</style>

View File

@@ -0,0 +1,35 @@
---
import { readItems } from '@directus/sdk';
import directus from '@lib/directus';
import type { Post } from '@lib/directusTypes';
import BlogCard from '@components/blog/BlogCard.astro';
const posts = await directus.request(
readItems('posts', {
filter: { published: { _eq: true } },
fields: ['*'],
sort: ['-published_date'],
})
);
const recentPosts = posts
.sort((a: Post, b: Post) => b.published_date.getTime() - a.published_date.getTime())
.slice(0, 3);
---
<section class="mx-auto mb-20 max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14 2xl:max-w-full">
<div class="mx-auto mb-10 max-w-2xl text-center lg:mb-14">
<h1
class="smooth-reveal block text-4xl font-bold text-neutral-800 md:text-5xl md:leading-tight lg:text-5xl dark:text-neutral-200"
>
Latest Posts
</h1>
<p class="smooth-reveal mt-1 text-pretty text-neutral-600 dark:text-neutral-300">
More recent posts.
</p>
</div>
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{recentPosts.map((b) => <BlogCard post={b} />)}
</div>
</section>

View File

@@ -0,0 +1,75 @@
---
import { Icon } from 'astro-icon/components';
import { readItems } from '@directus/sdk';
import type { Project } from '@lib/directusTypes';
import directus from '@lib/directus';
const projects = await directus.request(
readItems('site_projects', {
fields: ['*'],
sort: ['-isActive'],
})
);
const baseClasses = 'smooth-reveal-cards rounded-xl flex flex-col';
const borderClasses = 'border border-neutral-100 dark:border-stone-500/20';
const bgColorClasses =
'bg-neutral-100/80 hover:bg-neutral-100 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
const shadowClasses = 'shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg';
---
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
<h3
class="relative flex w-full items-center gap-3 pb-10 text-5xl text-neutral-800 dark:text-neutral-200"
>
Projects
</h3>
<div class="ml-8 grid grid-cols-1 gap-3 md:grid-cols-2 print:flex print:flex-col">
{
projects.map((project: Project) => {
return (
<div class={`${baseClasses}`}>
<div
class={`rounded-xl transition-all duration-300 ${borderClasses} ${bgColorClasses} ${shadowClasses}`}
>
<div class="p-4 md:p-10">
<h3 class="text-lg font-bold text-gray-800 dark:text-white">{project.name}</h3>
<p class="mt-2 text-gray-500 dark:text-neutral-400">{project.description}</p>
<ul class="mt-1 flex list-disc flex-col gap-2 text-sm text-gray-500 dark:text-neutral-400 [&>li]:ml-4">
{project.highlights.map((highlight) => {
return <li class="marker:text-yellow-500">{highlight}</li>;
})}
</ul>
<div class="flex">
<a
class="group group-hover relative inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
href={project.url}
>
<div class="group-hover:text-steel dark:group-hover:text-bermuda transition-text text-md relative z-10 mx-auto flex min-h-[44px] items-center font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
<span class="relative inline-block overflow-hidden"> Visit Page </span>
<Icon
name="mdi:keyboard-arrow-right"
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
/>
</div>
</a>
<a
class="group group-hover relative ml-auto inline-block gap-x-1 rounded-lg border border-transparent disabled:pointer-events-none disabled:opacity-50"
href={project.source}
>
<div class="group-hover:text-gitea-primary dark:group-hover:text-gitea-primary transition-text text-md relative z-10 mx-auto flex min-h-[44px] items-center font-semibold text-neutral-600 decoration-2 duration-300 sm:mx-0 sm:mt-4 dark:text-neutral-300">
<span class="relative inline-block overflow-hidden"> Source </span>
<Icon name="pajamas:gitea" class="ml-2 translate-y-0.5" />
</div>
</a>
</div>
</div>
</div>
</div>
);
})
}
</div>
</section>

View File

@@ -0,0 +1,250 @@
---
import { Icon } from 'astro-icon/components';
import { readItems } from '@directus/sdk';
import type { Skill } from '@lib/directusTypes';
import directus from '@lib/directus';
const skills = await directus.request(
readItems('site_skills', {
fields: ['*'],
sort: ['-date_created'],
})
);
const baseClasses = 'mx-2 min-w-[220px] sm:mx-4 sm:min-w-[280px]';
const borderClasses =
'border border-neutral-100 hover:border-neutral-200 dark:border-stone-500/20 dark:hover:border-neutral-800';
const bgColorClasses = 'bg-neutral-100/80 dark:bg-neutral-800/60 dark:hover:bg-neutral-800/90';
const hoverClasses = 'hover:-translate-y-2 hover:scale-105 ';
const shadowClasses = 'shadow-xs hover:shadow-lg';
---
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
<h3
class="relative flex w-full items-center gap-3 pb-4 text-5xl text-neutral-800 dark:text-neutral-200"
>
Skills
</h3>
<div class="">
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
<!-- Main slider container -->
<div class="slider-track animate-slide flex">
{
[...skills, ...skills, ...skills].map((skill: Skill) => {
return (
<div
class={`skill-card transform rounded-xl transition-all duration-300 ${baseClasses} ${borderClasses} ${bgColorClasses} ${hoverClasses} ${shadowClasses}`}
>
<div class="p-4 sm:p-6">
<div class="mb-4 flex items-center justify-between sm:mb-6">
<div class="flex items-center gap-2 sm:gap-4">
<div class="flex transform items-center justify-center rounded-lg text-neutral-800 transition-transform group-hover:rotate-12 dark:text-neutral-200">
<Icon name={skill.icon} class="h-8 w-8 sm:h-12 sm:w-12" />
</div>
<h3 class="text-base font-semibold text-neutral-900 sm:text-xl dark:text-neutral-100">
{skill.title}
</h3>
</div>
<span class="rounded-full bg-neutral-200 px-2 py-0.5 font-mono text-xs text-neutral-700 sm:px-2.5 sm:py-1 sm:text-sm dark:bg-neutral-800 dark:text-neutral-300">
{skill.level}%
</span>
</div>
<div class="relative h-1.5 w-full overflow-hidden rounded-full bg-stone-500/20 sm:h-2 dark:bg-stone-500/20">
<div
class="progress-bar-animate from-steel via-bermuda to-steel absolute top-0 left-0 h-full rounded-full bg-gradient-to-r transition-all duration-1000"
style={`width: ${skill.level}%`}
/>
</div>
<div class="mt-1 flex justify-between font-mono text-[10px] text-neutral-600 sm:mt-2 sm:text-xs dark:text-neutral-400">
<span>Beginner</span>
<span>Advanced</span>
</div>
</div>
</div>
);
})
}
</div>
<!-- Gradient overlays for smooth fade effect -->
<div
class="absolute top-0 bottom-0 left-0 z-10 w-12 bg-gradient-to-r from-neutral-200 to-transparent sm:w-24 dark:from-stone-700"
>
</div>
<div
class="absolute top-0 right-0 bottom-0 z-10 w-12 bg-gradient-to-l from-neutral-200 to-transparent sm:w-24 dark:from-stone-700"
>
</div>
</div>
</div>
</section>
<script>
document.addEventListener('astro:page-load', () => {
// Create seamless infinite scrolling effect
function setupInfiniteScroll() {
const cards = document.querySelectorAll('.skill-card');
if (!cards.length) return;
}
setupInfiniteScroll();
// Add hover effects to cards - only on non-touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const cards = document.querySelectorAll('.skill-card');
if (!isTouchDevice) {
cards.forEach((card) => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const angleX = (y - centerY) / 15;
const angleY = (centerX - x) / 15;
card.style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) scale(1.08) translateZ(20px)`;
// Dynamic shadow based on tilt
const shadowX = (x - centerX) / 25;
const shadowY = (y - centerY) / 25;
card.style.boxShadow = `
${shadowX}px ${shadowY}px 20px rgba(0, 0, 0, 0.1),
0 10px 20px rgba(0, 0, 0, 0.05)
`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = '';
card.style.boxShadow = '';
});
});
} else {
// Simpler effects for touch devices
cards.forEach((card) => {
card.addEventListener('touchstart', () => {
card.classList.add('is-touched');
});
card.addEventListener('touchend', () => {
setTimeout(() => {
card.classList.remove('is-touched');
}, 300);
});
});
}
});
</script>
<style>
/* Tech Stack Slider */
.slider-track {
width: fit-content;
animation: scroll 40s linear infinite;
}
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
}
}
@media (min-width: 640px) {
.slider-track {
animation: scroll 80s linear infinite;
}
@keyframes scroll {
0% {
transform: translateX(0);
}
100% {
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
}
}
}
.tech-stack-slider:hover .slider-track {
animation-play-state: paused;
}
.skill-card {
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
position: relative;
overflow: hidden;
}
.skill-card:hover {
z-index: 10;
}
/* Reduce animation complexity on mobile */
@media (max-width: 640px) {
.skill-card {
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
}
.skill-card:hover {
transform: translateY(-5px) !important;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
}
}
.skill-card:before {
content: '';
position: absolute;
top: -10%;
left: -10%;
width: 120%;
height: 120%;
background: radial-gradient(
circle at center,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0) 70%
);
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
}
.skill-card:hover:before {
opacity: 1;
}
.progress-bar-animate {
position: relative;
overflow: hidden;
}
.progress-bar-animate:after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: progress-shine 2s infinite;
}
@keyframes progress-shine {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
</style>

66
src/config.ts Normal file
View File

@@ -0,0 +1,66 @@
import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
export interface NavigationLink {
name: string;
url: string;
}
const global = await directus.request(readSingleton('site_global'));
export const WorkInformation = [
{
name: 'Tech Startup',
position: 'Junior Web Developer',
location_type: 'On site',
location: 'Auckland, New Zealand',
url: 'https://techstartup.com',
startDate: '2024-01-01',
endDate: null,
summary:
'Developing and maintaining web applications using JavaScript, HTML, and CSS. Collaborating with the team to implement new features and fix bugs.',
highlights: ['Improved website performance by optimizing code'],
responsibilities: [
'Collaborated with senior developers to design and implement web applications using modern JavaScript frameworks.',
'Assisted in debugging and optimizing front-end code to ensure smooth user experiences.',
'Participated in code reviews and contributed to improving coding standards within the team.',
],
achievements: [
'Developing and maintaining web applications using JavaScript, HTML, and CSS. Collaborating with the team to implement new features and fix bugs.',
],
skills: ['React', 'Tailwind', 'GitHub'],
},
];
export const NavigationLinks: NavigationLink[] = [
{ name: 'Home', url: '/' },
{ name: 'Blog', url: '/blog/' },
{ name: 'Categories', url: '/categories/' },
{ name: 'About Me', url: '/about/' },
];
export const FooterLinks: NavigationLink[] = [
{ name: 'RSS', url: '/rss.xml' },
{ name: 'Gitea', url: '/https://gitea.alexlebens.dev' },
];
export const SEO = {
title: global.name,
description: global.about,
structuredData: {
'@context': 'https://schema.org',
'@type': 'WebPage',
inLanguage: 'en-US',
'@id': global.site_url,
url: global.site_url,
name: global.name,
description: global.about,
isPartOf: {
'@type': 'WebSite',
url: global.site_url,
name: global.name,
description: global.about,
},
},
};

View File

@@ -0,0 +1,4 @@
---
title: 'Books 📖'
description: 'Books I have read or listened to'
---

View File

@@ -0,0 +1,4 @@
---
title: 'Cloud ☁️'
description: "Its just someone else's server"
---

View File

@@ -0,0 +1,4 @@
---
title: 'Homelab 🏠'
description: 'What happens when rack servers find a home'
---

View File

@@ -0,0 +1,4 @@
---
title: 'Kubernetes ☸️'
description: 'The container orchestration system'
---

View File

@@ -0,0 +1,4 @@
---
title: 'Life 🏃🏻'
description: 'Just random musings on everyday stuff'
---

View File

@@ -0,0 +1,4 @@
---
title: 'Minnesota 🌳'
description: 'Land of 10,000 Lakes'
---

View File

@@ -0,0 +1,4 @@
---
title: 'PostgreSQL'
description: 'PostgreSQL is an open-source relational database management system (RDBMS)'
---

View File

@@ -0,0 +1,4 @@
---
title: 'Python 🐍'
description: 'Generally my go to language'
---

View File

@@ -0,0 +1,4 @@
---
title: 'Tool 🪜'
description: 'Usually just the software kind'
---

View File

@@ -0,0 +1,4 @@
---
title: 'What Is?'
description: 'A series on discovery'
---

12
src/content/config.ts Normal file
View File

@@ -0,0 +1,12 @@
import { defineCollection, z } from 'astro:content';
const categoryCollection = defineCollection({
type: 'content',
schema: () =>
z.object({
title: z.string(),
description: z.string(),
}),
});
export const collections = { categories: categoryCollection };

BIN
src/images/autumn_bench.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 MiB

BIN
src/images/autumn_tree.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 MiB

BIN
src/images/brand_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

1
src/images/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
src/images/favicon_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.9 KiB

BIN
src/images/flowers.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.8 MiB

BIN
src/images/portrait.avif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

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

@@ -1,59 +1,93 @@
---
import Layout from './Layout.astro';
import directus from '../../lib/directus';
import { ClientRouter } from 'astro:transitions';
import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton('global'));
import directus from '@lib/directus';
import BaseHead from '@components/BaseHead.astro';
import Footer from '@components/Footer.astro';
import Header from '@components/Header.astro';
export interface Props {
title: string;
import '@styles/global.css';
interface Props {
title?: string;
description?: string;
ogImage?: any;
lang?: string;
structuredData?: object;
}
const { title, description = 'Alex Lebens', ogImage, lang = 'en', structuredData } = Astro.props;
const global = await directus.request(readSingleton('site_global'));
const normalizeTitle = !title ? global.name : `${title} | ${global.name}`;
---
<Layout title={global.title} description={global.description}>
<slot />
</Layout>
<html lang={lang}>
<head>
<title>{normalizeTitle}</title>
<BaseHead
title={normalizeTitle}
description={description}
ogImage={ogImage}
ogTitle={title === '' ? global.name : title}
ogDescription={description}
structuredData={structuredData}
/>
<ClientRouter fallback="swap" />
<script is:inline>
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
<script>
document.addEventListener('DOMContentLoaded', () => {
const themeToggle = document.getElementById('theme-toggle');
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
window.localStorage.setItem('theme', theme);
</script>
</head>
<body class="bg-stone-200 selection:bg-yellow-400 selection:text-neutral-700 dark:bg-stone-700">
<!-- <div class="fixed inset-0 -z-10">
<div
class="bg-grid-pattern absolute inset-0 [mask-image:radial-gradient(white,transparent_85%)] bg-[center_top_-1px]"
>
</div>
</div> -->
<div class="mx-auto w-full max-w-(--breakpoint-2xl) flex-grow px-4 sm:px-6 lg:px-8">
<Header />
<main class="min-h-screen">
<slot />
</main>
</div>
<Footer />
<style>
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>
</body>
</html>
if (themeToggle) {
themeToggle.addEventListener('click', () => {
document.documentElement.classList.add('theme-switching');
<style>
.bg-grid-pattern {
background-size: 24px 24px;
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.2) 1px, transparent 1px);
transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1);
}
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>
:global(.dark) .bg-grid-pattern {
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.25) 1px, transparent 1px);
}
</style>

View File

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

View File

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

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

@@ -1,890 +0,0 @@
/* Article entrance animation */
article {
opacity: 1;
transform: translateY(0);
}
article.article-entering {
animation: article-fade-in 0.8s ease forwards;
}
@keyframes article-fade-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Hero image hover effect */
article img {
transition: transform 0.7s cubic-bezier(0.33, 1, 0.68, 1);
}
/* Heading animations */
article .heading-animated {
opacity: 0;
transform: translateY(10px);
transition:
opacity 0.5s ease,
transform 0.5s ease;
}
article .heading-visible {
opacity: 1;
transform: translateY(0);
}
/* Navigation link hover effect */
.blog-nav-link {
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
}
.blog-nav-link.nav-link-hover {
transform: translateY(-2px);
box-shadow: 0 10px 20px -10px rgba(0, 0, 0, 0.1);
}
/* Ensure dark mode compatibility */
:global(.dark) .blog-nav-link.nav-link-hover {
box-shadow: 0 10px 20px -10px rgba(0, 0, 0, 0.3);
}
/* Enhanced Markdown Content Styling */
.markdown-content {
font-family:
system-ui,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Open Sans',
'Helvetica Neue',
sans-serif;
line-height: 1.7;
color: #374151;
}
.dark .markdown-content {
color: #e5e7eb;
}
/* Headings */
.markdown-content h1 {
font-size: 2.5rem;
font-weight: 800;
margin-top: 2.5rem;
margin-bottom: 1.5rem;
line-height: 1.2;
color: #111827;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 0.5rem;
}
.dark .markdown-content h1 {
color: #f9fafb;
border-bottom-color: #374151;
}
.markdown-content h2 {
font-size: 2rem;
font-weight: 700;
margin-top: 2.5rem;
margin-bottom: 1rem;
line-height: 1.3;
color: #111827;
border-bottom: 1px solid #e5e7eb;
padding-bottom: 0.5rem;
}
.dark .markdown-content h2 {
color: #f9fafb;
border-bottom-color: #374151;
}
.markdown-content h3 {
font-size: 1.5rem;
font-weight: 600;
margin-top: 2rem;
margin-bottom: 1rem;
line-height: 1.4;
color: #111827;
}
.dark .markdown-content h3 {
color: #f9fafb;
}
.markdown-content h4 {
font-size: 1.25rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
line-height: 1.5;
color: #111827;
}
.dark .markdown-content h4 {
color: #f9fafb;
}
.markdown-content h5 {
font-size: 1.125rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
line-height: 1.5;
color: #111827;
}
.dark .markdown-content h5 {
color: #f9fafb;
}
.markdown-content h6 {
font-size: 1rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
line-height: 1.5;
color: #111827;
}
.dark .markdown-content h6 {
color: #f9fafb;
}
/* Paragraphs */
.markdown-content p {
margin-top: 1.25rem;
margin-bottom: 1.25rem;
}
/* Links */
.markdown-content a {
color: #2563eb;
text-decoration: none;
border-bottom: 1px solid transparent;
transition:
border-color 0.2s ease,
color 0.2s ease;
}
.markdown-content a:hover {
color: #1d4ed8;
border-bottom-color: #1d4ed8;
}
.dark .markdown-content a {
color: #3b82f6;
}
.dark .markdown-content a:hover {
color: #60a5fa;
border-bottom-color: #60a5fa;
}
/* Bold text styling - enhanced */
.markdown-content strong {
font-weight: 700;
color: #0f766e;
background: linear-gradient(to bottom, transparent 60%, rgba(20, 184, 166, 0.2) 40%);
padding: 0 0.2em;
border-radius: 0.2em;
}
.dark .markdown-content strong {
color: #14b8a6;
background: linear-gradient(to bottom, transparent 60%, rgba(20, 184, 166, 0.15) 40%);
}
/* Lists */
.markdown-content ul,
.markdown-content ol {
margin-top: 1rem;
margin-bottom: 1rem;
padding-left: 1.5rem;
}
.markdown-content ul {
list-style-type: disc;
}
.markdown-content ol {
list-style-type: decimal;
}
.markdown-content li {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.markdown-content li > ul,
.markdown-content li > ol {
margin-top: 0.25rem;
margin-bottom: 0.25rem;
}
/* Blockquotes */
.markdown-content blockquote {
border-left: 4px solid #3b82f6;
padding: 1rem 1.5rem;
margin: 1.5rem 0;
background-color: #f3f4f6;
border-radius: 0.375rem;
font-style: italic;
position: relative;
overflow: hidden;
}
.dark .markdown-content blockquote {
background-color: #1f2937;
border-left-color: #60a5fa;
}
.markdown-content blockquote p {
margin-top: 0.5rem;
margin-bottom: 0.5rem;
}
.markdown-content blockquote .quote-icon {
position: absolute;
top: 0.5rem;
right: 0.5rem;
opacity: 0.1;
color: #3b82f6;
}
.dark .markdown-content blockquote .quote-icon {
color: #60a5fa;
}
/* Code blocks */
.markdown-content pre {
margin: 1.5rem 0;
padding: 1rem;
background-color: #1e293b !important;
border-radius: 0.5rem;
overflow-x: auto;
position: relative;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
/* Dark mode code blocks - ensure consistency */
.dark .markdown-content pre {
background-color: #1e293b !important;
}
.markdown-content pre code {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
font-size: 0.875rem;
line-height: 1.7;
color: #e5e7eb !important;
background-color: transparent !important;
padding: 0;
border-radius: 0;
display: block;
}
.dark .markdown-content pre code {
color: #e5e7eb !important;
background-color: transparent !important;
}
.markdown-content pre.with-line-numbers {
padding-left: 3.5rem;
}
.markdown-content pre code {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
font-size: 0.875rem;
line-height: 1.7;
color: #e5e7eb;
background-color: transparent;
padding: 0;
border-radius: 0;
display: block;
}
.markdown-content .line-numbers {
position: absolute;
top: 1rem;
left: 0;
width: 2.5rem;
text-align: right;
padding-right: 0.75rem;
color: #6b7280;
user-select: none;
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
font-size: 0.875rem;
line-height: 1.7;
border-right: 1px solid #4b5563;
height: calc(100% - 2rem);
overflow: hidden;
}
.markdown-content .line-numbers span {
display: block;
height: 1.7em;
}
.markdown-content .copy-code-button {
position: absolute;
top: 0.25rem;
right: 0.25rem;
background-color: #4b5563;
color: #e5e7eb;
border: none;
border-radius: 0.25rem;
padding: 0.15rem;
cursor: pointer;
opacity: 0.6;
transition: opacity 0.2s ease;
z-index: 10;
width: 1.25rem;
height: 1.25rem;
display: flex;
align-items: center;
justify-content: center;
}
.markdown-content .copy-code-button:hover {
opacity: 1;
background-color: #6b7280;
}
.markdown-content .copy-code-button svg {
width: 0.875rem;
height: 0.875rem;
}
/* Language label */
.markdown-content .language-label {
position: absolute;
top: 0;
right: 2.5rem;
background-color: #4b5563;
color: #e5e7eb;
font-size: 0.65rem;
padding: 0.125rem 0.375rem;
border-bottom-left-radius: 0.25rem;
border-bottom-right-radius: 0.25rem;
text-transform: uppercase;
font-weight: 600;
letter-spacing: 0.05em;
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
opacity: 0.8;
transition: opacity 0.2s ease;
z-index: 5;
}
.markdown-content pre:hover .language-label {
opacity: 1;
}
/* Language badge at bottom right */
.markdown-content .language-badge {
position: absolute;
bottom: 0.5rem;
right: 0.5rem;
font-size: 0.7rem;
padding: 0.1rem 0.3rem;
background-color: rgba(75, 85, 99, 0.7);
color: #e5e7eb;
border-radius: 0.25rem;
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
opacity: 0.8;
transition: opacity 0.2s ease;
z-index: 10;
}
.markdown-content pre:hover .language-badge {
opacity: 1;
}
/* Inline code */
.markdown-content code:not(pre code) {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
font-size: 0.875em;
color: #ef4444;
background-color: #f3f4f6;
padding: 0.2em 0.4em;
border-radius: 0.25rem;
white-space: nowrap;
}
.dark .markdown-content code:not(pre code) {
color: #f87171;
background-color: #1f2937;
}
/* Tables */
.markdown-content .table-container {
overflow-x: auto;
margin: 1.5rem 0;
border-radius: 0.5rem;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
.markdown-content table {
width: 100%;
border-collapse: collapse;
text-align: left;
font-size: 0.875rem;
}
.markdown-content table th {
background-color: #f3f4f6;
color: #111827;
font-weight: 600;
padding: 0.75rem 1rem;
border-bottom: 2px solid #e5e7eb;
}
.dark .markdown-content table th {
background-color: #1f2937;
color: #f9fafb;
border-bottom-color: #374151;
}
.markdown-content table td {
padding: 0.75rem 1rem;
border-bottom: 1px solid #e5e7eb;
}
.dark .markdown-content table td {
border-bottom-color: #374151;
}
.markdown-content table tr.even-row {
background-color: #f9fafb;
}
.dark .markdown-content table tr.even-row {
background-color: #111827;
}
.markdown-content table tr.odd-row {
background-color: #ffffff;
}
.dark .markdown-content table tr.odd-row {
background-color: #1f2937;
}
.markdown-content table tr:last-child td {
border-bottom: none;
}
/* Images */
.markdown-content img {
max-width: 100%;
height: auto;
border-radius: 0.5rem;
margin: 1.5rem 0;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
/* Horizontal rule */
.markdown-content hr {
border: 0;
height: 1px;
background-color: #e5e7eb;
margin: 2rem 0;
}
.dark .markdown-content hr {
background-color: #374151;
}
/* Task lists */
.markdown-content ul li[data-task-list-item] {
list-style-type: none;
position: relative;
padding-left: 1.5rem;
}
.markdown-content ul li[data-task-list-item]::before {
content: '';
position: absolute;
left: 0;
top: 0.25rem;
width: 1rem;
height: 1rem;
border: 1px solid #9ca3af;
border-radius: 0.25rem;
}
.markdown-content ul li[data-task-list-item][data-checked]::before {
background-color: #3b82f6;
border-color: #3b82f6;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23ffffff'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 13l4 4L19 7'%3E%3C/path%3E%3C/svg%3E");
background-size: 0.75rem;
background-position: center;
background-repeat: no-repeat;
}
/* Footnotes */
.markdown-content .footnotes {
margin-top: 2rem;
padding-top: 1rem;
border-top: 1px solid #e5e7eb;
font-size: 0.875rem;
}
.dark .markdown-content .footnotes {
border-top-color: #374151;
}
.markdown-content .footnotes ol {
padding-left: 1rem;
}
.markdown-content .footnotes li {
margin-bottom: 0.5rem;
}
.markdown-content .footnote-backref {
font-size: 0.75rem;
vertical-align: super;
}
/* Definition lists */
.markdown-content dl {
margin: 1.5rem 0;
}
.markdown-content dt {
font-weight: 600;
color: #111827;
margin-top: 1rem;
}
.dark .markdown-content dt {
color: #f9fafb;
}
.markdown-content dd {
margin-left: 1.5rem;
margin-bottom: 1rem;
}
/* Callouts and admonitions */
.markdown-content .callout {
margin: 1.5rem 0;
padding: 1rem;
border-radius: 0.5rem;
border-left: 4px solid;
background-color: #f3f4f6;
}
.dark .markdown-content .callout {
background-color: #1f2937;
}
.markdown-content .callout.info {
border-left-color: #3b82f6;
}
.markdown-content .callout.warning {
border-left-color: #f59e0b;
background-color: rgba(245, 158, 11, 0.1);
}
.dark .markdown-content .callout.warning {
background-color: rgba(245, 158, 11, 0.05);
}
.markdown-content .callout.danger {
border-left-color: #ef4444;
background-color: rgba(239, 68, 68, 0.1);
}
.dark .markdown-content .callout.danger {
background-color: rgba(239, 68, 68, 0.05);
}
.markdown-content .callout.tip {
border-left-color: #10b981;
background-color: rgba(16, 185, 129, 0.1);
}
.dark .markdown-content .callout.tip {
background-color: rgba(16, 185, 129, 0.05);
}
/* Code syntax highlighting - Light theme */
.markdown-content .token.comment,
.markdown-content .token.prolog,
.markdown-content .token.doctype,
.markdown-content .token.cdata {
color: #6b7280;
}
.markdown-content .token.punctuation {
color: #6b7280;
}
.markdown-content .token.namespace {
opacity: 0.7;
}
.markdown-content .token.property,
.markdown-content .token.tag,
.markdown-content .token.boolean,
.markdown-content .token.number,
.markdown-content .token.constant,
.markdown-content .token.symbol {
color: #ef4444;
}
.markdown-content .token.selector,
.markdown-content .token.attr-name,
.markdown-content .token.string,
.markdown-content .token.char,
.markdown-content .token.builtin {
color: #10b981;
}
.markdown-content .token.operator,
.markdown-content .token.entity,
.markdown-content .token.url,
.markdown-content .language-css .token.string,
.markdown-content .style .token.string {
color: #9333ea;
}
.markdown-content .token.atrule,
.markdown-content .token.attr-value,
.markdown-content .token.keyword {
color: #3b82f6;
}
.markdown-content .token.function,
.markdown-content .token.class-name {
color: #f59e0b;
}
.markdown-content .token.regex,
.markdown-content .token.important,
.markdown-content .token.variable {
color: #ec4899;
}
.markdown-content .token.important,
.markdown-content .token.bold {
font-weight: bold;
}
.markdown-content .token.italic {
font-style: italic;
}
.markdown-content .token.entity {
cursor: help;
}
/* Responsive adjustments */
@media (max-width: 640px) {
.markdown-content h1 {
font-size: 2rem;
}
.markdown-content h2 {
font-size: 1.5rem;
}
.markdown-content h3 {
font-size: 1.25rem;
}
.markdown-content pre {
padding: 0.75rem;
}
.markdown-content pre.with-line-numbers {
padding-left: 3rem;
}
.markdown-content .line-numbers {
width: 2rem;
}
.markdown-content blockquote {
padding: 0.75rem 1rem;
}
}
/* Print styles */
@media print {
.markdown-content {
font-size: 12pt;
}
.markdown-content pre,
.markdown-content code {
font-size: 10pt;
}
.markdown-content a {
color: #000 !important;
text-decoration: underline;
}
.markdown-content blockquote {
border-left: 2pt solid #000;
padding: 0.5cm 1cm;
background: none !important;
}
.markdown-content img {
max-width: 100% !important;
page-break-inside: avoid;
}
.markdown-content h2,
.markdown-content h3,
.markdown-content h4 {
page-break-after: avoid;
}
.markdown-content p,
.markdown-content h2,
.markdown-content h3 {
orphans: 3;
widows: 3;
}
}
/* Additional elements */
.markdown-content details {
margin: 1.5rem 0;
padding: 0.5rem 1rem;
background-color: #f3f4f6;
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
}
.dark .markdown-content details {
background-color: #1f2937;
border-color: #374151;
}
.markdown-content details summary {
font-weight: 600;
cursor: pointer;
padding: 0.5rem 0;
}
.markdown-content details[open] summary {
margin-bottom: 0.5rem;
border-bottom: 1px solid #e5e7eb;
}
.dark .markdown-content details[open] summary {
border-bottom-color: #374151;
}
/* Keyboard shortcuts */
.markdown-content kbd {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
font-size: 0.8em;
padding: 0.2em 0.4em;
margin: 0 0.1em;
background-color: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 0.25rem;
box-shadow: 0 1px 0 #d1d5db;
}
.dark .markdown-content kbd {
background-color: #1f2937;
border-color: #4b5563;
box-shadow: 0 1px 0 #4b5563;
}
/* Abbreviations */
.markdown-content abbr {
cursor: help;
text-decoration: underline dotted;
}
/* Highlight text */
.markdown-content mark {
background-color: #fef3c7;
color: #92400e;
padding: 0.1em 0.2em;
border-radius: 0.25rem;
}
.dark .markdown-content mark {
background-color: rgba(254, 243, 199, 0.2);
color: #fbbf24;
}
/* Subscript and superscript */
.markdown-content sub,
.markdown-content sup {
font-size: 0.75em;
line-height: 0;
position: relative;
vertical-align: baseline;
}
.markdown-content sup {
top: -0.5em;
}
.markdown-content sub {
bottom: -0.25em;
}
/* Diagrams and charts */
.markdown-content .mermaid {
margin: 1.5rem 0;
text-align: center;
}
/* Math equations */
.markdown-content .math {
overflow-x: auto;
margin: 1.5rem 0;
}
/* Embedded content */
.markdown-content iframe {
max-width: 100%;
margin: 1.5rem 0;
border-radius: 0.5rem;
box-shadow:
0 4px 6px -1px rgba(0, 0, 0, 0.1),
0 2px 4px -1px rgba(0, 0, 0, 0.06);
}

27
src/lib/directus.ts Normal file
View File

@@ -0,0 +1,27 @@
import { createDirectus, rest } from '@directus/sdk';
import type {
Global,
Post,
Experience,
Education,
Certificate,
Project,
Skill,
} from '@lib/directusTypes';
import { getDirectusURL } from '@lib/directusFunctions';
type Schema = {
site_global: Global;
posts: Post[];
site_experience: Experience;
site_education: Education;
site_certificate: Certificate;
site_projects: Project;
site_skills: Skill;
};
const directus = createDirectus<Schema>(getDirectusURL()).with(rest());
export default directus;

View File

@@ -0,0 +1,12 @@
const getDirectusURL = () => {
if (process.env.DIRECTUS_URL) {
return `https://${process.env.DIRECTUS_URL}`;
}
return 'https://directus.alexlebens.dev';
};
async function getDirectusImageURL(image: string) {
return `${getDirectusURL()}/assets/${image}`;
}
export { getDirectusURL, getDirectusImageURL };

88
src/lib/directusTypes.ts Normal file
View File

@@ -0,0 +1,88 @@
export type Global = {
name: string;
about: string;
about_description: string;
initials: string;
email: string;
site_url: string;
logo: string;
portrait: string;
portrait_alt: string;
home_image: string;
home_image_alt: string;
categories_image: string;
categories_image_alt: string;
blog_image: string;
blog_image_alt: string;
footer_image: string;
footer_image_alt: string;
};
export type Post = {
slug: string;
title: string;
description: string;
tags: string[];
category: string;
selected: boolean;
published: boolean;
content: string;
image: string;
image_alt: string;
image_second: string;
image_second_alt: string;
published_date: Date;
updated_date: Date;
};
export type Experience = {
id: string;
name: string;
location: string;
location_type: string;
url: string;
startDate: string;
endDate: string;
position: string;
summary: string;
responsibilities: string[];
highlights: string[];
achievements: string[];
skills: string[];
};
export type Education = {
id: string;
institution: string;
url: string;
area: string;
studyType: string;
graduationDate: string;
};
export type Certificate = {
id: string;
name: string;
issuer: string;
issuerDate: string;
url: string;
};
export type Project = {
id: string;
name: string;
isActive: boolean;
description: string;
highlights: string[];
url: string;
source: string;
};
export type Skill = {
id: string;
title: string;
description: string;
icon: string;
level: string;
date_created: string;
};

View File

@@ -1,115 +1,75 @@
---
import Layout from '../layouts/Layout.astro';
import { readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import PrimaryCTA from '@components/ui/buttons/PrimaryCTA.astro';
import GoBack from '@/components/ui/buttons/GoBack.astro';
const global = await directus.request(readSingleton('site_global'));
---
<Layout title="404 - Page Not Found">
<div
class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
>
<!-- 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>
<BaseLayout
title="Page Not Found"
description="Page Not Found"
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
name: `Page Not Found | ${global.name}`,
description: 'Page Not Found',
isPartOf: {
'@type': 'WebSite',
url: global.site_url,
name: global.name,
description: global.about,
},
}}
>
<section class="mt-20 grid place-content-center">
<div class="mx-auto max-w-screen-xl px-4 py-8 lg:px-6 lg:py-16">
<div class="mx-auto max-w-screen-sm text-center">
<div class="glitch-wrapper smooth-reveal">
<h1
class="glitch text-9xl leading-none font-bold text-neutral-900 sm:text-[12rem] dark:text-neutral-100"
data-text="404"
>
Not Found
</h1>
</div>
<!-- Main content with animation -->
<div class="relative z-10 mx-auto max-w-xl">
<div class="glitch-wrapper">
<h1
class="glitch text-9xl font-bold leading-none text-zinc-900 dark:text-zinc-100 sm:text-[12rem]"
data-text="404"
class="text-dark smooth-reveal mb-4 text-7xl font-extrabold text-yellow-500 lg:text-9xl dark:text-yellow-400"
>
404
{`Page Not Found - ${global.name}`}
</h1>
</div>
<h2 class="mt-6 text-2xl font-bold text-zinc-800 dark:text-zinc-200 sm:text-3xl">
Page Not Found
</h2>
<p class="mx-auto mt-6 max-w-md text-lg text-zinc-600 dark:text-zinc-400">
The page you're looking for does not exist.
</p>
<div class="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
<a
href="/"
class="group relative inline-flex items-center gap-2 overflow-hidden rounded-lg bg-zinc-900 px-6 py-3 text-zinc-100 shadow-lg transition-all duration-300 hover:bg-zinc-800 hover:shadow-xl dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
<div
class="smooth-reveal mx-auto mt-16 max-w-md rounded-xl bg-neutral-100 p-6 shadow-xs dark:border-neutral-700/50 dark:bg-stone-800"
>
<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"
></span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="relative z-10 h-5 w-5"
<h3
class="text-sm font-medium tracking-wider text-neutral-500 uppercase dark:text-neutral-400"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 12l8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"
></path>
</svg>
<span class="relative z-10 font-medium">Return Home</span>
</a>
<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"
Did you know?
</h3>
<p class="mt-2 text-sm text-neutral-600 dark:text-neutral-300" id="fun-fact">
The 404 error code originated when CERN's web server displayed room 404 (their server
room) as the error message when a file wasn't found.
</p>
</div>
<div
class="smooth-reveal mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="h-5 w-5 transition-transform duration-300 group-hover:-translate-x-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path>
</svg>
<span class="font-medium">Go Back</span>
</button>
</div>
<!-- Random fun fact -->
<div
class="mx-auto mt-16 max-w-md rounded-xl border border-zinc-100 bg-zinc-50 p-6 shadow-sm backdrop-blur-sm dark:border-zinc-700/50 dark:bg-zinc-800/50"
>
<h3 class="text-sm font-medium uppercase tracking-wider text-zinc-500 dark:text-zinc-400">
Did you know?
</h3>
<p class="mt-2 text-sm text-zinc-700 dark:text-zinc-300" id="fun-fact">
The 404 error code originated when CERN's web server displayed room 404 (their server
room) as the error message when a file wasn't found.
</p>
<GoBack title="Go Back" />
<PrimaryCTA title="Return Home" url={global.site_url} noArrow addHome />
</div>
</div>
</div>
</div>
</Layout>
</section>
</BaseLayout>
<script>
// Go back functionality
document.getElementById('back-button')?.addEventListener('click', () => {
window.history.back();
});
// Array of fun 404 facts
const funFacts = [
"The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found.",
"In internet slang, '404' has become shorthand for something that's missing or someone who's clueless.",
@@ -128,96 +88,26 @@ import Layout from '../layouts/Layout.astro';
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;
}
// Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => {
const animateContent = () => {
// Animate group 1
const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
});
};
// 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);
animateContent();
});
</script>
<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-wrapper {
position: relative;

View File

@@ -1,583 +1,107 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa';
import { SiTypescript, SiAstro } from 'react-icons/si';
import { readSingleton } from '@directus/sdk';
import directus from '../../lib/directus';
import { readSingleton, readItems } from '@directus/sdk';
import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro';
import Experience from '@components/ui/sections/Experience.astro';
import Projects from '@components/ui/sections/Projects.astro';
import Skills from '@components/ui/sections/Skills.astro';
import Education from '@components/ui/sections/Education.astro';
import portraitImg from '@images/portrait.avif';
const global = await directus.request(readSingleton('global'));
const about = await directus.request(readSingleton('about'));
const global = await directus.request(readSingleton('site_global'));
const skills = await directus.request(
readItems('skills', {
fields: ['*'],
})
);
const description = 'About me.';
---
<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">
<!-- Hero Section -->
<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"
>
<BaseLayout
title="About Me"
description={description}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
name: `About | ${global.name}`,
description: description,
isPartOf: {
'@type': 'WebSite',
url: global.site_url,
name: global.name,
description: global.about,
},
}}
>
<HeroSection
title="About Me"
subTitle={global.about}
src={portraitImg}
alt={global.portrait_alt}
/>
<section class="mx-auto max-w-[85rem] px-4 py-10 sm:px-6 lg:px-8 lg:py-14">
<main class="relative grid max-w-7xl gap-12 p-8 max-sm:py-16 md:grid-cols-6 md:p-16 xl:gap-24">
<div class="space-y-12 md:col-span-8">
<Experience className="smooth-reveal-2" />
<Education className="smooth-reveal-2 mt-30" />
<Projects className="smooth-reveal-2 mt-30" />
<Skills className="smooth-reveal-2 mt-30" />
</div>
<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="order-2 text-center md:order-1 md:text-left">
<h1
class="theme-transition-color mb-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:mb-6 sm:text-4xl md:text-5xl"
>
Hello, I'm <span
class="theme-transition-all bg-gradient-to-r from-zinc-500 to-zinc-900 bg-clip-text text-transparent dark:from-zinc-300 dark:to-zinc-100"
>{global.name}</span
>
</h1>
<p
class="theme-transition-color mb-6 text-lg leading-relaxed text-zinc-600 dark:text-zinc-400 sm:mb-8 sm:text-xl"
>
{about.background}
</p>
<div
class="social-links-container theme-transition-children flex flex-wrap justify-center gap-4 md:justify-start"
>
<!-- Social links remain the same -->
</div>
</div>
<div class="relative order-1 md:order-2">
<div
class="theme-transition-all mx-auto aspect-square w-full max-w-[280px] overflow-hidden rounded-3xl border-4 border-white shadow-xl dark:border-zinc-800 sm:max-w-[320px] sm:border-8 sm:shadow-2xl md:max-w-md"
>
<img
src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.portrait}`
alt={global.portrait_alt}
class="h-full w-full object-cover"
loading="eager"
/>
</div>
<!-- Decorative elements -->
<div
class="theme-transition-all absolute -bottom-4 -right-4 flex h-16 w-16 items-center justify-center rounded-full border-2 border-white bg-zinc-100 shadow-lg dark:border-zinc-900 dark:bg-zinc-800 sm:-bottom-6 sm:-right-6 sm:h-20 sm:w-20 sm:border-4 md:h-24 md:w-24"
>
<span class="text-2xl sm:text-3xl">👋</span>
</div>
</div>
</div>
</div>
<!-- About Section -->
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
<div class="mx-auto max-w-3xl">
<h2
class="theme-transition-color mb-6 flex items-center justify-center text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-8 sm:text-3xl md:justify-start"
>
<span
class="theme-transition-bg mr-4 hidden h-1 w-8 bg-zinc-300 dark:bg-zinc-700 sm:inline-block sm:w-12"
></span>
About Me
<span
class="theme-transition-bg ml-4 hidden h-1 w-8 bg-zinc-300 dark:bg-zinc-700 sm:inline-block sm:w-12"
></span>
</h2>
<div class="theme-transition-all prose prose-zinc max-w-none dark:prose-invert">
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
{about.experience}
</p>
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
{about.education}
</p>
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
{about.certifications}
</p>
</div>
</div>
</div>
<!-- Skills Section -->
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
<h2
class="theme-transition-color mb-8 text-center text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-12 sm:text-3xl"
>
Tech Stack
</h2>
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
<!-- Main slider container -->
<div class="slider-track animate-slide flex">
{
skills.map((skill, index) => (
<div
key={`${skill.title}-${index}`}
class="skill-card theme-transition-element mx-2 min-w-[220px] transform rounded-xl border border-zinc-200 bg-white transition-all duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-300 hover:shadow-xl dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600 sm:mx-4 sm:min-w-[280px]"
>
<div class="p-4 sm:p-6">
<div class="mb-4 flex items-center justify-between sm:mb-6">
<div class="flex items-center gap-2 sm:gap-4">
<div class="theme-transition-bg theme-transition-color flex h-8 w-8 transform items-center justify-center rounded-lg bg-zinc-100 text-zinc-800 transition-transform group-hover:rotate-12 dark:bg-zinc-800 dark:text-zinc-200 sm:h-12 sm:w-12">
<skill.icon
size={20}
className="sm:text-2xl transform transition-all hover:scale-125"
/>
</div>
<h3 class="theme-transition-color text-base font-semibold text-zinc-900 dark:text-zinc-100 sm:text-xl">
{skill.title}
</h3>
</div>
<span class="theme-transition-all rounded-full bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 sm:px-2.5 sm:py-1 sm:text-sm">
{skill.level}%
</span>
</div>
<div class="theme-transition-bg relative h-1.5 w-full overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-700 sm:h-2">
<div
class="progress-bar-animate theme-transition-bg absolute left-0 top-0 h-full rounded-full bg-gradient-to-r from-zinc-700 via-zinc-600 to-zinc-800 transition-all duration-1000 dark:from-zinc-300 dark:via-zinc-400 dark:to-zinc-200"
style={`width: ${skill.level}%`}
/>
</div>
<div class="theme-transition-color mt-1 flex justify-between font-mono text-[10px] text-zinc-400 dark:text-zinc-500 sm:mt-2 sm:text-xs">
<span>Beginner</span>
<span>Advanced</span>
</div>
</div>
</div>
))
}
</div>
<!-- Gradient overlays for smooth fade effect -->
<div
class="theme-transition-bg absolute bottom-0 left-0 top-0 z-10 w-12 bg-gradient-to-r from-white to-transparent dark:from-zinc-900 sm:w-24"
>
</div>
<div
class="theme-transition-bg absolute bottom-0 right-0 top-0 z-10 w-12 bg-gradient-to-l from-white to-transparent dark:from-zinc-900 sm:w-24"
>
</div>
</div>
</div>
<!-- Contact Section -->
<div class="theme-transition-all mx-auto max-w-3xl text-center">
<h2
class="theme-transition-color mb-4 text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-6 sm:text-3xl"
>
Get in Touch
</h2>
<p
class="theme-transition-color mb-6 text-base text-zinc-600 dark:text-zinc-400 sm:mb-8 sm:text-lg"
>
I'm always open to new opportunities and collaborations. If you'd like to work together or
just say hello, feel free to reach out.
</p>
<a
href=`mailto:${global.email}`
class="theme-transition-all inline-flex items-center justify-center rounded-lg bg-zinc-900 px-6 py-3 text-base font-medium text-zinc-100 transition-colors hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300 sm:px-8 sm:py-4 sm:text-lg"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg>
Say Hello
</a>
</div>
</div>
</main>
</section>
</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>
// Wait for the DOM to be fully loaded
document.addEventListener('DOMContentLoaded', () => {
const sliderTrack = document.querySelector('.slider-track');
// Create seamless infinite scrolling effect
function setupInfiniteScroll() {
const cards = document.querySelectorAll('.skill-card');
if (!cards.length) return;
// Clone the first set of cards and append to create seamless loop
const firstSetCount = cards.length / 3; // We have 3 sets in the markup
// Set proper animation based on screen size
function updateScrollAnimation() {
if (window.innerWidth >= 640) {
sliderTrack.style.animation = 'scroll 60s linear infinite';
} else {
sliderTrack.style.animation = 'scroll 40s linear infinite';
}
}
updateScrollAnimation();
window.addEventListener('resize', updateScrollAnimation);
}
setupInfiniteScroll();
// Pause animation on hover/touch
sliderTrack?.addEventListener('mouseenter', () => {
sliderTrack.style.animationPlayState = 'paused';
});
sliderTrack?.addEventListener('touchstart', () => {
sliderTrack.style.animationPlayState = 'paused';
});
sliderTrack?.addEventListener('mouseleave', () => {
sliderTrack.style.animationPlayState = 'running';
});
sliderTrack?.addEventListener('touchend', () => {
setTimeout(() => {
sliderTrack.style.animationPlayState = 'running';
}, 1000); // Delay resuming animation after touch
});
// Add hover effects to cards - only on non-touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
const cards = document.querySelectorAll('.skill-card');
if (!isTouchDevice) {
cards.forEach((card) => {
card.addEventListener('mousemove', (e) => {
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const centerX = rect.width / 2;
const centerY = rect.height / 2;
const angleX = (y - centerY) / 15;
const angleY = (centerX - x) / 15;
card.style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) scale(1.08) translateZ(20px)`;
// Dynamic shadow based on tilt
const shadowX = (x - centerX) / 25;
const shadowY = (y - centerY) / 25;
card.style.boxShadow = `
${shadowX}px ${shadowY}px 20px rgba(0, 0, 0, 0.1),
0 10px 20px rgba(0, 0, 0, 0.05)
`;
});
card.addEventListener('mouseleave', () => {
card.style.transform = '';
card.style.boxShadow = '';
});
});
} else {
// Simpler effects for touch devices
cards.forEach((card) => {
card.addEventListener('touchstart', () => {
card.classList.add('is-touched');
});
card.addEventListener('touchend', () => {
setTimeout(() => {
card.classList.remove('is-touched');
}, 300);
});
});
}
// Handle theme transition
document.addEventListener('themeChange', () => {
// Add special effects during theme transition
cards.forEach((card, index) => {
// Add staggered animation delay
setTimeout(() => {
card.classList.add('theme-changing');
setTimeout(() => {
card.classList.remove('theme-changing');
}, 600);
}, index * 50);
});
});
});
</script>
<script>
// Handle SPA transitions for about page
function setupSPATransitions() {
// Handle all internal links for SPA transitions
document.querySelectorAll('a[href^="/"]').forEach((link) => {
// Skip links that are anchor links, external links, or already processed
if (
link.getAttribute('href').includes('#') ||
link.getAttribute('target') === '_blank' ||
link.hasAttribute('data-spa-handled')
) {
return;
}
// Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
e.preventDefault();
const targetHref = link.getAttribute('href');
// Trigger page transition animation
const pageTransition = document.getElementById('page-transition');
if (pageTransition) {
pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100');
// Navigate after transition effect
setTimeout(() => {
window.location.href = targetHref;
}, 300);
} else {
// Fallback if transition element doesn't exist
window.location.href = targetHref;
}
});
});
// Initialize animations for about page
function animateAboutContent() {
// Animate hero section elements
const heroElements = document.querySelectorAll('h1, .order-2 p, .social-links-container');
heroElements.forEach((el, index) => {
// Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => {
const animateContent = () => {
// Animate group 1
const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
50 + index * 100
);
});
// 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) => {
// Animate group 2
const smoothReveal2 = document.querySelectorAll('.smooth-reveal-2');
smoothReveal2.forEach((el, index) => {
setTimeout(
() => {
bar.classList.add('animate-skill');
el.classList.add('animate-reveal');
},
500 + index * 100
200 + index * 250
);
});
// Animate sections with staggered delay
const sections = document.querySelectorAll('section');
sections.forEach((section, index) => {
// Animate topic cards with staggered delay
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
smoothRevealCards.forEach((el, index) => {
setTimeout(
() => {
section.classList.add('animate-reveal');
el.classList.add('animate-reveal');
},
300 + index * 200
400 + index * 250
);
});
}
// Run animations
animateAboutContent();
}
// Animate with just fade in with staggered delay
const smoothRevealFade = document.querySelectorAll('.smooth-reveal-fade');
smoothRevealFade.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal-fade');
},
100 + index * 250
);
});
};
// 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);
animateContent();
});
</script>

View File

@@ -1,347 +1,181 @@
---
import BlogPost from '../../layouts/BlogPost.astro';
import { type CollectionEntry, getCollection } from 'astro:content';
import getReadingTime from 'reading-time';
import { readItems, readSingleton } from '@directus/sdk';
import directus from '../../../lib/directus';
import { readItems } from '@directus/sdk';
import directus from '@lib/directus';
import { getDirectusImageURL } from '@lib/directusFunctions';
import BaseLayout from '@layouts/BaseLayout.astro';
import Image from '@components/ui/images/Image.astro';
import { formatDateTime } from '@support/time';
export async function getStaticPaths() {
const posts = await directus.request(
readItems('posts', {
fields: ['*'],
})
);
const sortedEntries = [...posts].sort(
(a, b) => b.published_date.valueOf() - a.published_date.valueOf()
);
return sortedEntries.map((post, index) => {
return {
params: { slug: post.slug },
props: {
post,
nextPost: index > 0 ? sortedEntries[index - 1] : null,
prevPost: index < sortedEntries.length - 1 ? sortedEntries[index + 1] : null,
},
};
});
const posts = await directus.request(readItems('posts'));
return posts.map((post) => ({
params: { slug: post.slug },
props: post,
}));
}
const post = Astro.props;
const { post, nextPost, prevPost } = Astro.props;
const global = await directus.request(readSingleton('site_global'));
const category: CollectionEntry<'categories'> = (await getCollection('categories'))
.filter((c) => c.slug === post.category)
.pop() as CollectionEntry<'categories'>;
const readingTime = getReadingTime(post.content);
---
<BlogPost
slug={post.slug}
<BaseLayout
title={post.title}
description={post.description}
content={post.content}
image={post.image}
image_alt={post.image_alt}
published_date={post.published_date}
updated_date={post.updated_date}
tags={post.tags}
ogImage={getDirectusImageURL(post.image)}
structuredData={{
'@context': 'https://schema.org',
'@type': 'NewsArticle',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
description: post.description,
isPartOf: {
'@type': 'WebSite',
url: `${global.site_url}/blog`,
name: global.name,
description: global.about,
},
image: [
// post.data.banner,
],
headline: post.title,
datePublished: post.published_date,
dateModified: post.updated_date,
author: [
{
'@type': 'Person',
name: `${global.name}`,
url: `${global.site_url}`,
},
],
}}
>
<!-- Main Content - Enhanced with better typography and spacing -->
<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"
>
<div set:html={post.content} />
</div>
<section class="mx-auto max-w-6xl px-4 pt-8 pb-12 sm:px-6 lg:px-8 lg:pt-12">
<div class="smooth-reveal relative w-full">
<div class="mt-4 rounded-2xl shadow-none sm:mt-0 sm:shadow-sm">
<Image
class="max-h-[600px] w-full rounded-t-2xl object-cover"
src={getDirectusImageURL(post.image)}
alt={post.image_alt}
draggable="false"
format="webp"
loading="lazy"
inferSize={true}
/>
<div
class="rounded-b-2xl px-0 py-6 sm:bg-neutral-100 sm:px-6 md:px-10 lg:px-14 sm:dark:bg-neutral-900/30"
>
<div class="mb-16">
<h2
class="mb-6 block text-3xl font-bold tracking-tight text-balance text-neutral-800 md:text-4xl lg:text-5xl dark:text-neutral-300"
>
{post.title}
</h2>
<ol class="mt-8 flex items-center whitespace-nowrap">
<li class="inline-flex items-center">
<a
class="flex items-center text-sm text-neutral-500 transition-all duration-300 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200"
href=`/categories/${category.slug}`
>
{category?.data?.title}
</a>
<svg
class="mx-2 size-5 flex-shrink-0 text-neutral-500 dark:text-neutral-500"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path d="M6 13L10 3" stroke="currentColor" stroke-linecap="round"></path>
</svg>
</li>
<li
class="inline-flex items-center text-sm text-neutral-500 transition-all duration-300 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200"
>
{formatDateTime(post.published_date)}
<svg
class="mx-2 size-5 flex-shrink-0 text-neutral-500 dark:text-neutral-500"
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path d="M6 13L10 3" stroke="currentColor" stroke-linecap="round"></path>
</svg>
</li>
<li
class="inline-flex items-center text-sm text-neutral-500 transition-all duration-300 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-200"
aria-current="page"
>
{readingTime.minutes.toPrecision(1)} minutes to read
</li>
</ol>
</div>
<!-- Next/Previous Navigation - Improved responsive design -->
<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"
>
{
prevPost && (
<a
href={`/blog/${prevPost.slug}`}
class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/50 sm:p-6"
>
<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">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-3 w-3 transition-transform duration-300 group-hover:-translate-x-1 sm:h-4 sm:w-4"
>
<path d="m15 18-6-6 6-6" />
</svg>
Previous Article
</span>
<h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg">
{prevPost.title}
</h3>
</a>
)
<article
class="prose prose-blog sm:prose-lg dark:prose-invert max-w-none text-justify text-neutral-800 dark:text-neutral-200"
>
<div set:html={post.content} />
</article>
<div
class="mx-auto mt-10 grid max-w-screen-lg gap-y-5 sm:flex sm:items-center sm:justify-between sm:gap-y-0 md:mt-14"
>
<div class="flex flex-wrap gap-x-2 gap-y-1 sm:flex-nowrap sm:items-center sm:gap-y-0">
{
post.tags.map((tag: string) => (
<span class="bg-steel/30 dark:bg-bermuda/60 inline-flex items-center gap-x-1.5 rounded-lg px-3 py-1.5 text-xs font-medium text-neutral-700 outline-none focus:outline-none focus-visible:ring focus-visible:outline-none dark:text-neutral-200">
{tag}
</span>
))
}
</div>
</div>
</div>
</div>
</div>
</section>
<style is:inline>
code[data-theme*=' '],
code[data-theme*=' '] span {
color: var(--shiki-light);
}
{
nextPost && (
<a
href={`/blog/${nextPost.slug}`}
class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/50 sm:p-6 md:text-right"
>
<div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
<span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm md:justify-end">
Next Article
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="h-3 w-3 transition-transform duration-300 group-hover:translate-x-1 sm:h-4 sm:w-4"
>
<path d="m9 18 6-6-6-6" />
</svg>
</span>
<h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg">
{nextPost.title}
</h3>
</a>
)
html.dark {
code[data-theme*=' '],
code[data-theme*=' '] span {
color: var(--shiki-dark);
}
}
</div>
</BlogPost>
</style>
</BaseLayout>
<script>
// Removing TOC-related functions
// Add copy buttons to code blocks
function initializeCodeCopyButtons() {
const codeBlocks = document.querySelectorAll('pre');
codeBlocks.forEach((block) => {
// Skip if already processed by either method
if (
block.classList.contains('code-block-processed') ||
block.classList.contains('enhanced')
) {
return;
}
block.classList.add('code-block-processed');
// Create wrapper if not already wrapped
let wrapper;
if (
block.parentNode.classList.contains('relative') &&
block.parentNode.classList.contains('group')
) {
wrapper = block.parentNode;
} else {
wrapper = document.createElement('div');
wrapper.className = 'relative group';
block.parentNode.insertBefore(wrapper, block);
wrapper.appendChild(block);
}
// Add copy button if not already present
if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-button')) {
const copyButton = document.createElement('button');
copyButton.className =
'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
</svg>
`;
copyButton.addEventListener('click', () => {
const code = block.querySelector('code').innerText;
navigator.clipboard.writeText(code);
// Show copied feedback
copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
</svg>
`;
setTimeout(() => {
copyButton.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
</svg>
`;
}, 2000);
});
wrapper.appendChild(copyButton);
}
});
}
// Handle SPA transitions for blog post navigation
function setupSPATransitions() {
// Handle prev/next navigation links
document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
// Skip links that are anchor links or already processed
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
return;
}
// Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
e.preventDefault();
const targetHref = link.getAttribute('href');
// Trigger page transition animation
const pageTransition = document.getElementById('page-transition');
if (pageTransition) {
pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100');
// Navigate after transition effect
setTimeout(() => {
window.location.href = targetHref;
}, 300);
} else {
// Fallback if transition element doesn't exist
window.location.href = targetHref;
}
// Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => {
const animateContent = () => {
const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 100
);
});
});
}
};
// Main initialization function
function initializeBlogPost() {
// Initialize remaining components
initializeCodeCopyButtons();
setupSPATransitions();
// Scroll to hash if present in URL
if (window.location.hash) {
setTimeout(() => {
const element = document.querySelector(window.location.hash);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}, 100);
}
}
// Initialize on first load
document.addEventListener('DOMContentLoaded', initializeBlogPost);
// Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', initializeBlogPost);
// For compatibility with custom transition system
document.addEventListener('page-transition-complete', initializeBlogPost);
animateContent();
});
</script>
<style>
/* Removing TOC-related styles */
/* Language badge styling */
.language-badge {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
monospace;
text-transform: lowercase;
letter-spacing: 0.05em;
}
/* Extra small screens */
@media (min-width: 480px) {
.xs\:inline {
display: inline;
}
.xs\:hidden {
display: none;
}
}
/* Enhanced typography for blog content - Responsive adjustments */
.prose {
@reference text-zinc-800 dark:text-zinc-200;
}
.prose h1,
.prose h2,
.prose h3,
.prose h4 {
@reference font-semibold text-zinc-900 dark:text-zinc-100;
}
.prose h1 {
@reference text-2xl sm:text-3xl md:text-4xl;
}
.prose h2 {
@reference mb-3 mt-8 border-b border-zinc-200 pb-2 text-xl dark:border-zinc-800 sm:mb-4 sm:mt-12 sm:text-2xl;
}
.prose h3 {
@reference mb-2 mt-6 text-lg sm:mb-3 sm:mt-8 sm:text-xl;
}
.prose p {
@reference mb-4 text-sm leading-relaxed sm:mb-6 sm:text-base;
}
.prose a {
@reference font-medium text-zinc-800 underline decoration-zinc-400 underline-offset-2 transition-colors hover:text-zinc-600 hover:decoration-zinc-600 dark:text-zinc-300 dark:decoration-zinc-600 dark:hover:text-zinc-100 dark:hover:decoration-zinc-400;
}
.prose blockquote {
@reference my-4 border-l-4 border-zinc-300 pl-4 italic text-zinc-700 dark:border-zinc-700 dark:text-zinc-300 sm:my-6;
}
.prose code {
@reference rounded bg-zinc-100 px-1.5 py-0.5 text-sm font-medium text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200;
}
.prose pre {
@reference my-4 overflow-x-auto rounded-lg bg-[#1e293b] p-3 text-xs text-zinc-200 shadow-md dark:bg-[#1e293b] sm:my-6 sm:p-4 sm:text-sm !important;
}
.prose pre code {
@reference bg-transparent p-0 text-zinc-200 dark:text-zinc-200 !important;
}
.prose img {
@reference mx-auto my-6 h-auto max-w-full rounded-lg shadow-md sm:my-8;
}
.prose ul,
.prose ol {
@reference my-4 pl-5 sm:my-6 sm:pl-6;
}
.prose li {
@reference mb-1 text-sm sm:mb-2 sm:text-base;
}
.prose hr {
@reference my-8 border-zinc-200 dark:border-zinc-800 sm:my-10;
}
/* Line clamp for truncating text */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
</style>

View File

@@ -1,438 +1,103 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { readItems, readSingleton } from '@directus/sdk';
import directus from '../../../lib/directus';
import { readItems } from '@directus/sdk';
import type { Post } from '@lib/directusTypes';
import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import BlogRecentCard from '@components/blog/BlogRecentCard.astro';
import BlogFeaturedArticle from '@components/blog/BlogFeaturedArticle.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro';
import blogImg from '@images/autumn_tree.png';
const global = await directus.request(readSingleton('site_global'));
const posts = await directus.request(
readItems('posts', {
filter: { status: { _eq: true } },
fields: ['*'],
sort: ['-published_date'],
})
);
const selectedPosts: Post[] = posts.filter((p) => p.selected);
const sortedPosts = posts.sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf());
// Group posts by year for timeline effect
const postsByYear = sortedPosts.reduce((acc, post) => {
const year = new Date(post.published_date).getFullYear();
if (!acc[year]) acc[year] = [];
acc[year].push(post);
return acc;
}, {});
const years = Object.keys(postsByYear).sort((a, b) => b - a);
// Get total post count
const totalPosts = sortedPosts.length;
// Get unique tags for search suggestions
const allTags = [...new Set(sortedPosts.flatMap((post) => post.tags || []))];
const description =
"Sharing what I've learned, one post at a time. I hope you find something useful.";
---
<BaseLayout title="Blog">
<div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16">
<!-- Header with search -->
<div class="relative mb-12 sm:mb-20">
<!-- Decorative elements -->
<div
class="animate-blob absolute -left-10 -top-10 h-48 w-48 rounded-full bg-zinc-100 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-left-20 sm:-top-20 sm:h-72 sm:w-72"
>
</div>
<div
class="animate-blob animation-delay-2000 absolute -bottom-10 -right-10 h-48 w-48 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-72 sm:w-72"
>
</div>
<BaseLayout
title="Blog"
description={description}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
name: `Blog | ${global.name}`,
description: description,
isPartOf: {
'@type': 'WebSite',
url: global.site_url,
name: global.name,
description: global.about,
},
}}
>
<HeroSection title="Blog" subTitle={description} src={blogImg} alt={global.blog_image_alt} />
<div class="relative text-center">
<h1
class="mb-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-4xl md:text-5xl"
>
Blog
</h1>
<p
class="mx-auto mb-6 max-w-2xl text-sm text-zinc-600 dark:text-zinc-400 sm:mb-10 sm:text-base"
>
Thoughts, ideas, and explorations on technology and selfhosting.
</p>
</div>
</div>
<!-- Grid layout for mobile experience -->
<div class="grid grid-cols-1 gap-6 sm:gap-8 md:grid-cols-12">
<!-- Featured post (if exists) -->
{
sortedPosts.length > 0 && (
<div class="mb-8 sm:mb-12 md:col-span-12">
<article class="group relative overflow-hidden rounded-none border-b border-zinc-200 pb-6 dark:border-zinc-800 sm:pb-8">
<div class="flex h-full flex-col gap-6 sm:gap-8 md:flex-row">
{sortedPosts[0].image && (
<div class="mx-auto h-60 w-full max-w-full overflow-hidden sm:h-80 sm:max-w-md md:mx-0 md:h-96 md:w-1/2">
<img
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${sortedPosts[0].image}`}
alt={sortedPosts[0].title}
class="h-full w-full object-cover grayscale transition-all duration-700 hover:grayscale-0 group-hover:scale-105"
loading="eager"
/>
</div>
)}
<div class="flex flex-1 flex-col justify-center">
<div class="mb-3 flex items-center justify-center gap-2 text-xs text-zinc-500 dark:text-zinc-400 sm:text-sm md:justify-start">
<span class="font-medium uppercase tracking-wider">Featured</span>
<span class="h-px w-6 bg-zinc-300 dark:bg-zinc-700 sm:w-8" />
{sortedPosts[0].published_date && (
<time datetime={sortedPosts[0].published_date.toLocaleString()}>
{sortedPosts[0].published_date.toLocaleString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
)}
</div>
<h2 class="mb-3 text-center text-2xl font-bold text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:mb-4 sm:text-3xl md:text-left">
<a
href={`/blog/${sortedPosts[0].slug}/`}
class="before:absolute before:inset-0"
>
{sortedPosts[0].title}
</a>
</h2>
<p class="mb-4 line-clamp-3 text-center text-sm text-zinc-600 dark:text-zinc-400 sm:mb-6 sm:text-base md:text-left">
{sortedPosts[0].description}
</p>
<div class="flex flex-wrap items-center justify-center gap-3 sm:gap-4 md:justify-start">
{sortedPosts[0].tags && (
<div class="flex flex-wrap justify-center gap-2 md:justify-start">
{sortedPosts[0].tags.slice(0, 2).map((tag) => (
<span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
{tag}
</span>
))}
</div>
)}
</div>
</div>
</div>
</article>
</div>
)
}
<!-- Improved sidebar for mobile -->
<div class="relative md:col-span-3">
<div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0">
<h3
class="mb-4 text-center text-lg font-medium uppercase tracking-wider text-zinc-900 dark:text-zinc-100 md:text-left"
>
Archive
</h3>
<!-- Horizontal scrollable archive on mobile, vertical on desktop -->
<div
class="hide-scrollbar flex overflow-x-auto pb-4 md:flex-col md:overflow-visible md:pb-0"
>
{
years.map((year, index) => (
<a
href={`#year-${year}`}
class={`mr-3 flex items-center whitespace-nowrap rounded-full border-b border-zinc-100 px-4 py-2 transition-colors hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-900 md:mr-0 md:w-full md:whitespace-normal md:rounded-none md:px-0 md:py-3 ${index === 0 ? 'bg-zinc-50 dark:bg-zinc-800/50' : ''}`}
>
<span class="text-base font-medium text-zinc-900 dark:text-zinc-100 md:text-lg">
{year}
</span>
<span class="ml-2 text-xs text-zinc-500 dark:text-zinc-400 md:ml-auto md:text-sm">
{postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''}
</span>
</a>
))
}
</div>
</div>
</div>
<!-- Improved post grid for mobile -->
<div class="md:col-span-9">
{
years.map((year) => (
<div id={`year-${year}`} class="mb-12 scroll-mt-16 sm:mb-20">
<h2 class="mb-6 border-b border-zinc-200 pb-3 text-center text-xl font-bold text-zinc-900 dark:border-zinc-800 dark:text-zinc-100 sm:mb-8 sm:pb-4 sm:text-2xl md:text-left">
{year}
</h2>
<div
class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}
>
{postsByYear[year].map((post, index) => (
<article class="group relative mx-auto flex h-full w-full max-w-sm flex-col sm:max-w-md md:mx-0">
{post.image && (
<div class="mb-4 h-48 overflow-hidden rounded-lg sm:h-56">
<img
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
alt={post.title}
class="h-full w-full object-cover grayscale transition-all duration-700 hover:grayscale-0 group-hover:scale-105"
loading="lazy"
/>
</div>
)}
<div class="flex flex-1 flex-col">
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 dark:text-zinc-400 sm:mb-3 sm:gap-4 sm:text-sm md:justify-start">
{post.pubDate && (
<time
datetime={post.published_date.toLocaleString()}
class="flex items-center"
>
{post.published_date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
})}
</time>
)}
</div>
<h3 class="mb-2 text-center text-lg font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:mb-3 sm:text-xl md:text-left">
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
{post.title}
</a>
</h3>
<p class="mb-4 line-clamp-2 flex-grow text-center text-sm text-zinc-600 dark:text-zinc-400 md:text-left">
{post.description}
</p>
{post.tags && (
<div class="mt-auto flex flex-wrap justify-center gap-2 md:justify-start">
{post.tags.slice(0, 2).map((tag) => (
<span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
{tag}
</span>
))}
{post.tags.length > 2 && (
<span class="border border-zinc-200 px-2 py-1 text-xs uppercase tracking-wider text-zinc-600 dark:border-zinc-800 dark:text-zinc-400 sm:px-3">
+{post.tags.length - 2}
</span>
)}
</div>
)}
</div>
</article>
))}
</div>
</div>
))
}
</div>
</div>
</div>
<BlogRecentCard posts={posts} />
<BlogFeaturedArticle posts={selectedPosts} />
</BaseLayout>
<style>
/* Blob animation */
.animate-blob {
animation: blob-bounce 8s infinite ease;
}
.animation-delay-2000 {
animation-delay: 2s;
}
@keyframes blob-bounce {
0%,
100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(5%, 5%) scale(1.05);
}
50% {
transform: translate(0, 10%) scale(1);
}
75% {
transform: translate(-5%, 5%) scale(0.95);
}
}
/* Search container hover effect */
.search-container:hover .search-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 0;
}
50% {
opacity: 1;
}
}
/* Input focus animation */
input:focus + div .search-pulse {
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Hide scrollbar but keep functionality */
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
/* Line clamp for descriptions */
.line-clamp-2 {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.line-clamp-3 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* Improved touch targets for mobile */
@media (max-width: 640px) {
a,
button {
min-height: 44px;
display: flex;
align-items: center;
}
}
</style>
<script>
// Script không thay đổi - giữ nguyên chức năng
document.addEventListener('DOMContentLoaded', () => {
const backToTopButton = document.getElementById('back-to-top');
if (backToTopButton) {
// Show button when scrolled down
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',
});
// Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => {
const animateContent = () => {
// Animate group 1
const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
200 + index * 300
);
});
// Check scroll position
window.addEventListener('scroll', toggleBackToTopButton);
toggleBackToTopButton(); // Initial check
}
// Add smooth scrolling to year links
document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
anchor.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
const targetElement = document.querySelector(targetId);
if (targetElement) {
window.scrollTo({
top: targetElement.offsetTop - 100,
behavior: 'smooth',
});
// Update URL hash without jumping
history.pushState(null, null, targetId);
}
// Animate group 2
const smoothReveal2 = document.querySelectorAll('.smooth-reveal-2');
smoothReveal2.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
500 + index * 100
);
});
});
// Add touch support for hover effects
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) {
const articles = document.querySelectorAll('article');
articles.forEach((article) => {
article.addEventListener('touchstart', () => {
article.classList.add('is-touched');
});
article.addEventListener('touchend', () => {
setTimeout(() => {
article.classList.remove('is-touched');
}, 300);
});
// Animate topic cards with staggered delay
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
smoothRevealCards.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
1000 + index * 250
);
});
}
// Animate with just fade in with staggered delay
const smoothRevealFade = document.querySelectorAll('.smooth-reveal-fade');
smoothRevealFade.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal-fade');
},
100 + index * 250
);
});
};
animateContent();
});
// SPA transition handling
function setupSPATransitions() {
// Handle all blog post links for SPA transitions
document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
// Skip links that are anchor links or already processed
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
return;
}
// Mark as handled to avoid duplicate listeners
link.setAttribute('data-spa-handled', 'true');
link.addEventListener('click', (e) => {
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return;
}
e.preventDefault();
const targetHref = link.getAttribute('href');
// Trigger page transition animation
const pageTransition = document.getElementById('page-transition');
if (pageTransition) {
pageTransition.classList.remove('opacity-0');
pageTransition.classList.add('opacity-100');
// Navigate after transition effect
setTimeout(() => {
window.location.href = targetHref;
}, 300);
} else {
// Fallback if transition element doesn't exist
window.location.href = targetHref;
}
});
});
// Handle year anchor links specially
document.querySelectorAll('a[href^="#year-"]').forEach((anchor) => {
anchor.setAttribute('data-spa-internal', 'true');
});
}
// Initialize on first load
document.addEventListener('DOMContentLoaded', setupSPATransitions);
// Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', setupSPATransitions);
// For compatibility with custom transition system
document.addEventListener('page-transition-complete', setupSPATransitions);
</script>

View File

@@ -0,0 +1,67 @@
---
import { getCollection } from 'astro:content';
import { readItems, readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import type { Post } from '@lib/directusTypes';
import BaseLayout from '@layouts/BaseLayout.astro';
import BlogCard from '@components/blog/BlogCard.astro';
import HeaderSection from '@components/ui/sections/HeaderSection.astro';
export async function getStaticPaths() {
const categories = await getCollection('categories');
return categories.map((category) => ({
params: { slug: category.slug },
props: { category },
}));
}
const { category } = Astro.props;
const global = await directus.request(readSingleton('site_global'));
const posts = await directus.request(
readItems('posts', {
filter: { published: { _eq: true } },
fields: ['*'],
sort: ['-published_date'],
})
);
const categoriesPosts = posts
.sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf())
.filter((b) => {
return b.category === category.slug;
});
---
<BaseLayout
title={category.data.title}
description={category.data.description}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
name: `${category.data.title} | ${global.name}`,
description: category.data.description,
isPartOf: {
url: `${global.site_url}/categories`,
name: global.name,
description: global.about,
},
}}
>
<HeaderSection
title={category.data.title}
subTitle={category.data.description}
btnExists
btnTitle="Back to Categories"
btnURL="/categories"
/>
<section class="mx-auto mt-10 mb-10 max-w-[85rem] px-4 py-8 sm:px-6 lg:px-8 2xl:max-w-full">
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
{categoriesPosts.map((b) => <BlogCard post={b} />)}
</div>
</section>
</BaseLayout>

View File

@@ -0,0 +1,182 @@
---
import { getCollection } from 'astro:content';
import { readItems, readSingleton } from '@directus/sdk';
import type { Post } from '@lib/directusTypes';
import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import BlogCategoryCard from '@components/blog/BlogCategoryCard.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro';
import { timeago } from '@support/time';
import categoryImg from '@images/autumn_bench.png';
const global = await directus.request(readSingleton('site_global'));
const posts = await directus.request(
readItems('posts', {
filter: { published: { _eq: true } },
fields: ['*'],
sort: ['-published_date'],
})
);
const postMap: Map<string, Post[]> = posts
.sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf())
.reduce((acc, obj) => {
let posts = acc.get(obj.category);
if (!posts) {
posts = [];
}
posts.push(obj);
acc.set(obj.category, posts);
return acc;
}, new Map<string, Post[]>());
const layoutPattern = [
{ col: 2, row: 2 },
{ col: 2, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 2 },
{ col: 2, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 1 },
{ col: 1, row: 1 },
];
const categories = (await getCollection('categories'))
.sort((a, b) => {
const aCount = postMap.get(a.slug)?.length ?? 0;
const bCount = postMap.get(b.slug)?.length ?? 0;
return bCount - aCount;
})
.map((c, index) => {
const posts = postMap.get(c.slug);
const pattern = layoutPattern[index % layoutPattern.length];
const smColSpan = Math.min(pattern.col, 2);
const mdColSpan = Math.min(pattern.col, 4);
const rowSpan = pattern.row;
const rowSpanClass = rowSpan > 1 ? `row-span-${rowSpan}` : 'row-span-1';
const gridItemClass = `col-span-${smColSpan} md:col-span-${mdColSpan} ${rowSpanClass} smooth-reveal-cards rounded-xl transition-all duration-300 shadow-xs hover:shadow-md dark:shadow-md dark:hover:shadow-lg border border-stone-200/50 dark:border-stone-700/50`;
return {
...c,
posts,
gridItemClass,
layoutPattern: {
smCol: smColSpan,
mdCol: mdColSpan,
row: rowSpan,
index,
},
};
});
const description =
'Here are some of the general categories that I am interested in, including homelabs, technology, and Minnesota.';
---
<BaseLayout
title="All Categories"
description={description}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
name: `All Categories | ${global.name}`,
description: description,
isPartOf: {
'@type': 'WebSite',
url: global.site_url,
name: global.name,
description: global.about,
},
}}
>
<HeroSection
title="Categories"
subTitle={description}
src={categoryImg}
alt={global.categories_image_alt}
/>
<section class="mx-auto px-4 py-10 sm:px-6 lg:px-8 lg:py-14 lg:pt-10 2xl:max-w-full">
<div class="grid grid-flow-row-dense grid-cols-2 gap-4 md:grid-cols-4">
{
categories.map((category) => {
return (
<div
class={category.gridItemClass}
style={category.layoutPattern.row > 1 ? 'grid-row: span 2 / span 2;' : ''}
>
<BlogCategoryCard
slug={category.slug}
title={category.data.title}
description={category.data.description}
count={postMap.get(category.slug)?.length ?? 0}
publishDate={timeago(postMap.get(category.slug)?.[0]?.published_date)}
/>
</div>
);
})
}
</div>
</section>
</BaseLayout>
<script>
// Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => {
const animateContent = () => {
// Animate group 1
const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
50 + index * 100
);
});
// Animate group 2
const smoothReveal2 = document.querySelectorAll('.smooth-reveal-2');
smoothReveal2.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
200 + index * 150
);
});
// Animate topic cards with staggered delay
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
smoothRevealCards.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
500 + index * 100
);
});
// Animate with just fade in with staggered delay
const smoothRevealFade = document.querySelectorAll('.smooth-reveal-fade');
smoothRevealFade.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal-fade');
},
100 + index * 250
);
});
};
animateContent();
});
</script>

24
src/pages/favicon.ico.ts Normal file
View File

@@ -0,0 +1,24 @@
import path from 'node:path';
import type { APIRoute } from 'astro';
import sharp from 'sharp';
import ico from 'sharp-ico';
const faviconSrc = path.resolve('src/images/favicon_icon.png');
export const GET: APIRoute = async () => {
// Resize the image to multiple sizes
const sizes = [16, 32];
const buffers = await Promise.all(
sizes.map(async (size) => {
return await sharp(faviconSrc).resize(size).toFormat('png').toBuffer();
})
);
// Convert the image to an ICO file
const icoBuffer = ico.encode(buffers);
return new Response(icoBuffer, {
headers: { 'Content-Type': 'image/x-icon' },
});
};

View File

@@ -1,591 +1,108 @@
---
import Layout from '../layouts/Layout.astro';
import FormattedDate from '../components/FormattedDate.astro';
import { readSingleton } from '@directus/sdk';
import directus from '../../lib/directus';
import { readItems, readSingleton } from '@directus/sdk';
import directus from '@lib/directus';
import BaseLayout from '@layouts/BaseLayout.astro';
import HeroSection from '@components/ui/sections/HeroSection.astro';
import FeaturesSection from '@components/ui/sections/FeaturesSection.astro';
import LatestPosts from '@components/ui/sections/LatestPosts.astro';
import HeroSectionAlt from '@components/ui/sections/HeroSectionAlt.astro';
import homeImg from '@images/autumn_mountain.png';
const global = await directus.request(readSingleton('global'));
const global = await directus.request(readSingleton('site_global'));
const posts = await directus.request(
readItems('posts', {
fields: ['*'],
sort: ['-published_date'],
})
);
const recentPosts = posts
.sort((a, b) => b.published_date.getTime() - a.published_date.getTime())
.slice(0, 3);
const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, 5);
const description =
'Engineering the cloud by day, homelab by night, and exploring Minnesota in between.';
---
<Layout title=`Home | ${global.name}`>
<!-- Hero Section with improved mobile responsiveness -->
<section class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20">
<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>
<BaseLayout
title="Home"
description={description}
structuredData={{
'@context': 'https://schema.org',
'@type': 'WebPage',
inLanguage: 'en-US',
'@id': Astro.url.href,
url: Astro.url.href,
name: `Home | ${global.name}`,
description: description,
isPartOf: {
'@type': 'WebSite',
url: global.site_url,
name: global.name,
description: global.about,
},
}}
>
<HeroSection
title={`Hello, I'm <span class="text-steel dark:text-steel">Alex Lebens</span>`}
subTitle={description}
primaryBtn="About Me"
primaryBtnURL="/about"
src={homeImg}
alt={global.home_image_alt}
/>
<div class="relative text-center sm:text-left">
<h1
class="theme-transition-color hero-text text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-4xl md:text-5xl lg:text-6xl"
>
<span class="block">Writing on technology,</span>
<span class="mt-1 block">development, and</span>
<span class="relative mt-1 block">
<span class="relative inline-block">
selfhosting.
<span
class="theme-transition-bg absolute -bottom-1 left-0 h-1 w-full origin-left transform bg-zinc-800 dark:bg-zinc-200"
></span>
</span>
</span>
</h1>
<p
class="theme-transition-color mx-auto mt-4 max-w-lg text-base leading-relaxed text-zinc-600 dark:text-zinc-400 sm:mx-0 sm:mt-6 sm:text-lg md:mt-8"
>
{global.about}
</p>
<div
class="mt-6 flex flex-wrap justify-center gap-3 sm:mt-8 sm:justify-start sm:gap-4 md:mt-10 md:gap-6"
>
<a
href="/about"
class="theme-transition-color group relative inline-flex min-h-[44px] items-center gap-2 text-sm font-medium text-zinc-900 transition-all duration-300 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
>
<span>More about me</span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
</svg>
<span
class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
></span>
</a>
</div>
</div>
</div>
</section>
<FeaturesSection />
<!-- Featured Post Section - Improved for mobile -->
<section
class="theme-transition-all border-t border-zinc-100 px-4 py-10 dark:border-zinc-800 sm:px-6 sm:py-12 md:py-16"
>
<div class="mx-auto max-w-3xl">
<div
class="mb-6 flex flex-col justify-between gap-4 sm:mb-8 sm:flex-row sm:items-center md:mb-12"
>
<h2
class="theme-transition-color text-center text-xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-left sm:text-2xl md:text-3xl"
>
Recent Posts
</h2>
<a
href="/blog"
class="theme-transition-color group relative flex min-h-[44px] items-center justify-center self-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300 sm:self-auto"
>
<span class="flex items-center gap-1">
View all posts
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"></path>
</svg>
</span>
<span
class="theme-transition-bg absolute -bottom-1 left-0 h-0.5 w-0 bg-zinc-800 transition-all duration-300 group-hover:w-full dark:bg-zinc-200"
></span>
</a>
</div>
<LatestPosts />
<!-- Improved grid for better mobile layout -->
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3">
{
recentPosts.map((post, index) => (
<article class="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" />
{post.image && (
<div class="relative z-10 mb-4 aspect-video w-full overflow-hidden rounded-lg">
<img
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}`}
alt={post.title}
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105"
loading={index === 0 ? 'eager' : 'lazy'}
width="400"
height="225"
/>
</div>
)}
<div class="theme-transition-color relative z-10 flex w-full flex-wrap items-center justify-center gap-x-3 gap-y-2 text-xs text-zinc-500 dark:text-zinc-400 sm:justify-start sm:gap-x-4">
<time datetime={post.published_date.toLocaleString()} class="font-medium">
<FormattedDate date={post.published_date} />
</time>
</div>
<h3 class="theme-transition-color relative z-10 mt-3 w-full text-center text-lg font-semibold tracking-tight text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-zinc-100 dark:group-hover:text-zinc-300 sm:text-left sm:text-xl">
<a
href={`/blog/${post.slug}`}
class="flex min-h-[44px] items-center justify-center sm:justify-start"
>
<span class="absolute -inset-x-4 -inset-y-2.5 sm:-inset-x-6 sm:-inset-y-4" />
{post.title}
</a>
</h3>
<p class="theme-transition-color relative z-10 mt-2 line-clamp-3 w-full text-center text-sm text-zinc-600 dark:text-zinc-400 sm:mt-3 sm:text-left">
{post.description}
</p>
{post.tags && post.tags.length > 0 && (
<div class="relative z-10 mt-3 flex w-full flex-wrap justify-center gap-2 sm:mt-4 sm:justify-start">
{post.tags.slice(0, 3).map((tag) => (
<a
href={`/topics/${tag}`}
class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-100 px-2 py-1 text-xs font-medium text-zinc-800 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 sm:px-3"
>
#{tag}
</a>
))}
{post.tags.length > 3 && (
<span class="theme-transition-all inline-flex min-h-[28px] items-center rounded-full bg-zinc-50 px-2 py-1 text-xs font-medium text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
+{post.tags.length - 3} more
</span>
)}
</div>
)}
<a
href={`/blog/${post.slug}`}
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 dark:text-zinc-300 dark:group-hover:text-zinc-100 sm:mx-0 sm:mt-4"
>
<span class="relative inline-block overflow-hidden">
<span class="block transition-transform duration-300 group-hover:-translate-y-full">
Read article
</span>
<span class="absolute left-0 top-0 translate-y-full whitespace-nowrap transition-transform duration-300 group-hover:translate-y-0">
Explore now
</span>
</span>
<svg
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300 group-hover:translate-x-1"
>
<path
d="M6.75 5.75 9.25 8l-2.5 2.25"
stroke-width="1.5"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</a>
</article>
))
}
</div>
</div>
</section>
<!-- Topics/Tags Section - Improved for mobile -->
{
allTags.length > 0 && (
<section class="theme-transition-all border-t border-zinc-100 px-4 py-10 dark:border-zinc-800 sm:px-6 sm:py-12 md:py-16">
<div class="mx-auto max-w-3xl">
<h2 class="theme-transition-color mb-6 text-center text-xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:mb-8 sm:text-left sm:text-2xl md:text-3xl">
Explore Topics
</h2>
<div class="mx-auto grid max-w-xs grid-cols-1 gap-3 sm:max-w-none sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
{allTags.map((tag) => {
const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
return (
<a
href={`/topics/${tag}`}
class="theme-transition-all group flex min-h-[80px] flex-col rounded-xl border border-zinc-200 p-3 transition-all duration-300 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/70 sm:min-h-[90px] sm:p-4 md:p-6"
>
<div class="mb-2 flex items-start justify-between">
<span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
#{tag}
</span>
<span class="theme-transition-all flex-shrink-0 rounded-full bg-zinc-100 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
{tagCount} {tagCount === 1 ? 'post' : 'posts'}
</span>
</div>
<p class="theme-transition-color mt-1 text-xs text-zinc-600 dark:text-zinc-400">
Explore articles about {tag}
</p>
</a>
);
})}
</div>
<div class="mt-6 text-center sm:mt-8">
<a
href="/tags"
class="theme-transition-color inline-flex min-h-[44px] items-center text-sm font-medium text-zinc-900 hover:text-zinc-700 dark:text-zinc-100 dark:hover:text-zinc-300"
>
<span>View all topics</span>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="ml-1 h-4 w-4 transition-transform duration-300 group-hover:translate-x-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M13.5 4.5L21 12m0 0l-7.5 7.5M21 12H3"
/>
</svg>
</a>
</div>
</div>
</section>
)
}
</Layout>
<HeroSectionAlt
title="Follow me on Gitea"
subTitle="I love open source and have my code availabile on my Gitea server."
url="https://gitea.alexlebens.dev"
/>
</BaseLayout>
<script>
// Add hover effect for cards on touch devices
document.addEventListener('DOMContentLoaded', () => {
// Check if it's a touch device
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) {
const cards = document.querySelectorAll('.hover-3d');
cards.forEach((card) => {
card.addEventListener('touchstart', () => {
card.classList.add('is-touched');
});
card.addEventListener('touchend', () => {
setTimeout(() => {
card.classList.remove('is-touched');
}, 300);
});
});
// Disable hover animations on touch devices for better performance
document.documentElement.classList.add('touch-device');
}
// Improved viewport height fix for mobile browsers
const setVh = () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
};
// Set initial value
setVh();
// Update on resize and scroll to prevent content shifting
window.addEventListener('resize', setVh);
// Use a debounced scroll handler to prevent performance issues
let scrollTimeout;
window.addEventListener('scroll', () => {
if (scrollTimeout) {
window.cancelAnimationFrame(scrollTimeout);
}
scrollTimeout = window.requestAnimationFrame(() => {
// Lock width during scroll
document.body.style.width = '100%';
document.body.style.overflowX = 'hidden';
});
});
// Fix for iOS Safari address bar height changes
if (/iPhone|iPad|iPod/.test(navigator.userAgent)) {
// Force the layout to use the initial viewport size
document.documentElement.style.setProperty('--initial-vh', `${window.innerHeight * 0.01}px`);
// Apply fixed height to sections to prevent resizing
const sections = document.querySelectorAll('section');
sections.forEach((section) => {
section.style.width = '100%';
});
}
// Improved theme change handler that preserves scroll position and provides smoother transitions
document.addEventListener('themeChanged', () => {
// Store current scroll position
const scrollPosition = window.scrollY;
// Create a temporary overlay for smoother transition
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
inset: 0;
background-color: ${document.documentElement.classList.contains('dark') ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'};
z-index: 9999;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
`;
document.body.appendChild(overlay);
// Fade in overlay
requestAnimationFrame(() => {
overlay.style.opacity = '0.5';
// Update theme-transition elements without forcing reflow of entire page
requestAnimationFrame(() => {
document
.querySelectorAll(
'.theme-transition-all, .theme-transition-bg, .theme-transition-color'
)
.forEach((el) => {
// Apply a subtle animation instead of a hard reset
el.style.transition = 'all 0.5s ease';
});
// Fade out overlay after transition completes
setTimeout(() => {
overlay.style.opacity = '0';
setTimeout(() => {
overlay.remove();
}, 300);
}, 300);
});
});
// Restore scroll position (prevents jumping to top)
if (scrollPosition > 0) {
setTimeout(() => {
window.scrollTo({
top: scrollPosition,
behavior: 'auto', // Use 'auto' to prevent animation
});
}, 10);
}
});
// Fix theme inconsistency issues by checking theme on visibility change
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
const storedTheme = localStorage.getItem('theme');
const currentThemeIsDark = document.documentElement.classList.contains('dark');
if (storedTheme === 'dark' && !currentThemeIsDark) {
document.documentElement.classList.add('dark');
} else if (storedTheme === 'light' && currentThemeIsDark) {
document.documentElement.classList.remove('dark');
}
}
});
// Add smooth reveal animations for content after loading
// Add smooth reveal animations for content after loading
document.addEventListener('astro:page-load', () => {
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text span, .hero-text + p, .hero-text ~ div'
);
heroElements.forEach((el, index) => {
// Animate group 1
const smoothReveal = document.querySelectorAll('.smooth-reveal');
smoothReveal.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
50 + index * 100
);
});
// Animate posts with staggered delay
const articles = document.querySelectorAll('article.group');
articles.forEach((article, index) => {
// Animate group 2
const smoothReveal2 = document.querySelectorAll('.smooth-reveal-2');
smoothReveal2.forEach((el, index) => {
setTimeout(
() => {
article.classList.add('animate-reveal');
el.classList.add('animate-reveal');
},
500 + index * 150
200 + index * 250
);
});
// Animate topic cards with staggered delay
const topicCards = document.querySelectorAll('a.group.flex.flex-col');
topicCards.forEach((card, index) => {
const smoothRevealCards = document.querySelectorAll('.smooth-reveal-cards');
smoothRevealCards.forEach((el, index) => {
setTimeout(
() => {
card.classList.add('animate-reveal');
el.classList.add('animate-reveal');
},
800 + index * 100
400 + index * 250
);
});
// Animate with just fade in with staggered delay
const smoothRevealFade = document.querySelectorAll('.smooth-reveal-fade');
smoothRevealFade.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal-fade');
},
100 + index * 250
);
});
};
// Run animations after the loading screen is hidden
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();
}
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>
<style>
/* Fix for theme transition issues */
:global(:root) {
--theme-transition-duration: 0.5s;
--theme-transition-timing: ease;
}
:global(html),
:global(body) {
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
}
:global(.theme-transition-all) {
transition: all var(--theme-transition-duration) var(--theme-transition-timing);
}
:global(.theme-transition-bg) {
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
}
:global(.theme-transition-color) {
transition: color var(--theme-transition-duration) var(--theme-transition-timing);
}
/* Ensure transitions apply to all theme-related properties */
:global(*) {
transition-property: background-color, border-color, color, fill, stroke, opacity;
transition-duration: var(--theme-transition-duration);
transition-timing-function: var(--theme-transition-timing);
}
/* Remove the forced transition disabling which causes flickering */
:global(.theme-switching),
:global(.theme-switching *) {
/* Use a subtle transition instead of none */
transition-duration: 0.3s !important;
}
/* Content reveal animations */
.hero-text span,
.hero-text + p,
.hero-text ~ div,
article.group,
a.group.flex.flex-col {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
/* Rest of your existing styles... */
</style>

31
src/pages/robots.txt.ts Normal file
View File

@@ -0,0 +1,31 @@
// https://docs.astro.build/en/guides/integrations-guide/sitemap/#usage
import type { APIRoute } from 'astro';
const robotsTxt = `
User-agent: Googlebot
Disallow:
Allow: /
Crawl-delay: 10
User-agent: Yandex
Disallow:
Allow: /
Crawl-delay: 2
User-agent: archive.org_bot
Disallow:
Allow: /
Crawl-delay: 2
User-agent: *
Allow: /
Sitemap: ${new URL('sitemap-index.xml', import.meta.env.SITE).href}`.trim();
export const GET: APIRoute = () => {
return new Response(robotsTxt, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
},
});
};

Some files were not shown because too many files have changed in this diff Show More