Compare commits

...

210 Commits

Author SHA1 Message Date
fcae7676c6 0.11.0 release
All checks were successful
renovate / renovate (push) Successful in 16s
test-build / build (push) Successful in 25s
release-image / release (push) Successful in 1m20s
2025-07-19 22:39:43 -05:00
cc16b5435a update node
All checks were successful
renovate / renovate (push) Successful in 1m16s
test-build / build (push) Successful in 1m26s
2025-07-19 22:37:05 -05:00
27b5e6a36b change layout and animations to be more common with each other 2025-07-19 22:37:05 -05:00
bcb91972a1 Merge pull request 'Update astro monorepo' (#42) from renovate/astro-monorepo into main
All checks were successful
renovate / renovate (push) Successful in 23s
test-build / build (push) Successful in 28s
Reviewed-on: #42
2025-07-20 03:34:37 +00:00
b11666decb Update astro monorepo
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 29s
2025-07-20 00:02:37 +00:00
a947a05041 Merge pull request 'Update dependency eslint-config-prettier to v10.1.8' (#43) from renovate/eslint-config-prettier-10.x into main
All checks were successful
renovate / renovate (push) Successful in 47s
test-build / build (push) Successful in 34s
2025-07-20 00:01:54 +00:00
297c573281 Update dependency eslint-config-prettier to v10.1.8
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 25s
2025-07-20 00:01:31 +00:00
9093594973 Update dependency astro to v5.11.2
All checks were successful
test-build / build (push) Successful in 37s
renovate/stability-days Updates have met minimum release age requirement
renovate / renovate (push) Successful in 21s
test-build / build (pull_request) Successful in 36s
2025-07-18 00:01:29 +00:00
77ce0a1182 Update dependency framer-motion to v12.23.6
All checks were successful
renovate / renovate (push) Successful in 28s
test-build / build (pull_request) Successful in 29s
test-build / build (push) Successful in 40s
renovate/stability-days Updates have met minimum release age requirement
2025-07-16 16:14:52 +00:00
799e6b6090 Merge pull request 'Update typescript-eslint monorepo to v8.37.0' (#38) from renovate/typescript-eslint-monorepo into main
Some checks failed
test-build / build (push) Successful in 26s
process-repository / process-repository (push) Failing after 11s
renovate / renovate (push) Successful in 1m21s
Reviewed-on: #38
2025-07-16 16:14:17 +00:00
735e4b4877 Update typescript-eslint monorepo to v8.37.0
All checks were successful
test-build / build (pull_request) Successful in 27s
renovate/stability-days Updates have met minimum release age requirement
2025-07-16 02:59:27 +00:00
3e12a8647d update tag
All checks were successful
test-build / build (push) Successful in 51s
renovate / renovate (push) Successful in 1m9s
release-image / release (push) Successful in 3m32s
2025-07-15 21:58:20 -05:00
e07210638e add astro native SPA transition 2025-07-15 21:58:20 -05:00
22d5b50f73 formatting and layout 2025-07-15 21:58:20 -05:00
40acf8f34a strip theme transition on load to use early script 2025-07-15 21:58:20 -05:00
543516baba remove SPA scripts 2025-07-15 21:58:20 -05:00
e985f905f2 formatting changes 2025-07-15 21:58:20 -05:00
e1f09ca4ec fix slider 2025-07-15 21:58:20 -05:00
0c09eb38e9 Merge pull request 'Update dependency framer-motion to v12.23.5' (#37) from renovate/framer-motion-12.x-lockfile into main
All checks were successful
renovate / renovate (push) Successful in 41s
test-build / build (push) Successful in 24s
2025-07-16 00:03:06 +00:00
95eeb44e4f Update dependency framer-motion to v12.23.5
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 34s
2025-07-16 00:02:26 +00:00
d47d67572e Merge pull request 'Update dependency astro to v5.11.1' (#36) from renovate/astro-monorepo into main
Some checks failed
renovate / renovate (push) Successful in 59s
test-build / build (push) Has been cancelled
2025-07-16 00:01:41 +00:00
fa4841948a Update dependency astro to v5.11.1
All checks were successful
test-build / build (pull_request) Successful in 30s
renovate/stability-days Updates have met minimum release age requirement
2025-07-16 00:00:52 +00:00
71e2b0185b update tag
Some checks failed
test-build / build (push) Successful in 28s
release-image / release (push) Successful in 3m31s
process-repository / process-repository (push) Failing after 1m7s
renovate / renovate (push) Successful in 1m38s
2025-07-15 01:43:31 -05:00
7f9fb4d2b9 fix scrollbar affecting layout
Some checks failed
renovate / renovate (push) Successful in 29s
test-build / build (push) Has been cancelled
2025-07-15 01:39:40 -05:00
8420c8dd58 fix tech stack slider 2025-07-15 01:37:23 -05:00
fa6ed18edb fix dark mode 2025-07-14 23:18:38 -05:00
30860fce1e change paths
All checks were successful
test-build / build (push) Successful in 30s
renovate / renovate (push) Successful in 21s
2025-07-14 22:31:12 -05:00
b479e0e22c use single workflow script
Some checks failed
process-repository / process-repository (push) Failing after 17s
renovate / renovate (push) Successful in 39s
test-build / build (push) Successful in 41s
2025-07-13 23:43:58 -05:00
cf01ebcd3c Merge pull request 'Update dependency eslint to v9.31.0' (#35) from renovate/eslint-monorepo into main
All checks were successful
test-build / build (push) Successful in 35s
renovate / renovate (push) Successful in 34s
process-pull-requests / process-pull-requests (push) Successful in 10s
process-issues / process-issues (push) Successful in 13s
Reviewed-on: #35
2025-07-13 03:33:05 +00:00
df8ccf81c2 Update dependency eslint to v9.31.0
All checks were successful
test-build / build (pull_request) Successful in 38s
renovate/stability-days Updates have met minimum release age requirement
2025-07-13 00:01:03 +00:00
073911c1b9 use tag ids
Some checks failed
test-build / build (push) Successful in 31s
process-pull-requests / process-pull-requests (push) Failing after 11s
process-issues / process-issues (push) Failing after 10s
renovate / renovate (push) Successful in 58s
2025-07-11 21:47:20 -05:00
3eeea3dd8f use tag ids
All checks were successful
renovate / renovate (push) Successful in 20s
test-build / build (push) Successful in 23s
2025-07-11 21:36:40 -05:00
43fea76778 Update dependency framer-motion to v12.23.3
All checks were successful
test-build / build (pull_request) Successful in 36s
renovate / renovate (push) Successful in 32s
test-build / build (push) Successful in 41s
renovate/stability-days Updates have met minimum release age requirement
2025-07-12 00:01:43 +00:00
d64df6473a Update dependency prettier-plugin-tailwindcss to v0.6.14
Some checks failed
test-build / build (push) Successful in 32s
process-pull-requests / process-pull-requests (push) Successful in 15s
process-issues / process-issues (push) Failing after 9s
renovate / renovate (push) Successful in 1m7s
2025-07-10 20:59:07 +00:00
63a6a00817 Update dependency framer-motion to v12.23.1
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 48s
test-build / build (push) Has been cancelled
renovate / renovate (push) Has been cancelled
2025-07-10 20:57:58 +00:00
54759056b3 update version
All checks were successful
renovate / renovate (push) Successful in 1m21s
test-build / build (push) Successful in 33s
release-image / release (push) Successful in 4m15s
2025-07-10 15:57:02 -05:00
3cc9762e0d Merge pull request 'Update typescript-eslint monorepo to v8.36.0' (#31) from renovate/typescript-eslint-monorepo into main
All checks were successful
renovate / renovate (push) Successful in 26s
test-build / build (push) Successful in 45s
Reviewed-on: #31
2025-07-10 03:28:27 +00:00
ef757c4a14 Update typescript-eslint monorepo to v8.36.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 29s
2025-07-09 00:02:57 +00:00
176f92bf67 Merge pull request 'Update dependency framer-motion to v12.23.0' (#27) from renovate/framer-motion-12.x-lockfile into main
Some checks failed
test-build / build (push) Successful in 34s
process-pull-requests / process-pull-requests (push) Successful in 8s
process-issues / process-issues (push) Failing after 13s
renovate / renovate (push) Successful in 42s
Reviewed-on: #27
2025-07-05 04:54:21 +00:00
09d411dd68 Merge pull request 'Update astro monorepo' (#30) from renovate/astro-monorepo into main
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
Reviewed-on: #30
2025-07-05 04:54:14 +00:00
54acfcb24d Update astro monorepo
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 43s
2025-07-05 00:01:04 +00:00
6f3b631862 Update dependency framer-motion to v12.23.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 1m20s
2025-07-04 00:02:50 +00:00
18cd240a9b Update dependency astro to v5.10.2
Some checks failed
test-build / build (push) Successful in 29s
process-issues / process-issues (push) Failing after 14s
process-pull-requests / process-pull-requests (push) Successful in 15s
renovate / renovate (push) Successful in 1m31s
2025-07-03 00:02:59 +00:00
bb4fe8ef37 Update dependency eslint to v9.30.1
Some checks failed
renovate/stability-days Updates have met minimum release age requirement
renovate / renovate (push) Has been cancelled
test-build / build (pull_request) Successful in 30s
test-build / build (push) Has been cancelled
2025-07-03 00:02:05 +00:00
e0e3c1f61a Update typescript-eslint monorepo to v8.35.1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (push) Successful in 48s
test-build / build (pull_request) Successful in 56s
renovate / renovate (push) Successful in 1m7s
2025-07-02 00:01:05 +00:00
0b5c6ae999 update astro
Some checks failed
test-build / build (push) Successful in 38s
process-pull-requests / process-pull-requests (push) Failing after 10s
process-issues / process-issues (push) Failing after 10s
renovate / renovate (push) Successful in 2m14s
2025-06-28 16:41:12 -05:00
a20ba4ab43 Merge pull request 'Update dependency eslint to v9.30.0' (#25) from renovate/eslint-monorepo into main
All checks were successful
renovate / renovate (push) Successful in 22s
test-build / build (push) Successful in 25s
Reviewed-on: #25
2025-06-28 21:37:27 +00:00
550e7dfe52 Merge pull request 'Update dependency @directus/sdk to v20' (#21) from renovate/directus-sdk-20.x into main
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
Reviewed-on: #21
2025-06-28 21:37:21 +00:00
03174cfb9d Update dependency @directus/sdk to v20
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 31s
2025-06-28 21:36:32 +00:00
da50c1928c Update dependency eslint to v9.30.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 31s
2025-06-28 21:36:25 +00:00
f1d1fe979e change confi
All checks were successful
test-build / build (push) Successful in 26s
renovate / renovate (push) Successful in 1m0s
2025-06-28 16:35:35 -05:00
4d6019d0b0 update node
All checks were successful
test-build / build (push) Successful in 42s
renovate / renovate (push) Successful in 1m7s
2025-06-28 16:33:16 -05:00
7dd302b3d4 Update dependency prettier to v3.6.2
All checks were successful
test-build / build (pull_request) Successful in 34s
renovate/stability-days Updates have met minimum release age requirement
test-build / build (push) Successful in 32s
renovate / renovate (push) Successful in 45s
2025-06-28 05:21:40 +00:00
8a8f2a6216 Update dependency framer-motion to v12.19.2
Some checks are pending
test-build / build (pull_request) Successful in 34s
renovate/stability-days Updates have not met minimum release age requirement
test-build / build (push) Successful in 26s
renovate / renovate (push) Successful in 1m0s
2025-06-28 05:20:17 +00:00
97775f1ceb Merge pull request 'Update typescript-eslint monorepo to v8.35.0' (#19) from renovate/typescript-eslint-monorepo into main
All checks were successful
test-build / build (push) Successful in 25s
renovate / renovate (push) Successful in 1m22s
Reviewed-on: #19
2025-06-28 05:19:32 +00:00
0a437a26f1 Merge pull request 'Update dependency prettier to v3.6.1' (#18) from renovate/prettier-3.x-lockfile into main
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Has been cancelled
Reviewed-on: #18
2025-06-28 05:19:19 +00:00
ba67b4d0e4 Merge pull request 'Update dependency framer-motion to v12.19.1' (#17) from renovate/framer-motion-12.x-lockfile into main
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Has been cancelled
Reviewed-on: #17
2025-06-28 05:19:03 +00:00
0bcfa9bed4 Update typescript-eslint monorepo to v8.35.0
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 30s
2025-06-28 00:03:22 +00:00
ada95481f7 Update dependency prettier to v3.6.1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 30s
2025-06-28 00:03:06 +00:00
7c9f4acc00 Update dependency framer-motion to v12.19.1
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 30s
2025-06-28 00:02:54 +00:00
0b7b87580a Update tailwindcss monorepo to v4.1.11
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 42s
test-build / build (push) Successful in 33s
renovate / renovate (push) Successful in 1m43s
2025-06-28 00:01:05 +00:00
08f076e566 Update dependency prettier-plugin-tailwindcss to v0.6.13
All checks were successful
renovate/stability-days Updates have met minimum release age requirement
test-build / build (pull_request) Successful in 1m22s
test-build / build (push) Successful in 42s
renovate / renovate (push) Successful in 21s
2025-06-20 18:18:43 +00:00
26c27b9353 update astro
Some checks failed
test-build / build (push) Successful in 1m20s
process-pull-requests / process-pull-requests (push) Failing after 10s
process-issues / process-issues (push) Failing after 12s
renovate / renovate (push) Successful in 1m37s
2025-06-20 13:17:48 -05:00
ce8b3a2e19 update config
All checks were successful
test-build / build (push) Successful in 1m22s
renovate / renovate (push) Successful in 39s
2025-06-20 00:02:41 -05:00
6d34c0d407 update config
Some checks failed
test-build / build (push) Failing after 2s
renovate / renovate (push) Successful in 12s
2025-06-19 23:59:05 -05:00
63607bbca3 Merge pull request 'Update ghcr.io/renovatebot/renovate Docker tag to v41' (#15) from renovate/ghcr.io-renovatebot-renovate-41.x into main
All checks were successful
renovate / renovate (push) Successful in 35s
test-build / build (push) Successful in 27s
Reviewed-on: #15
2025-06-20 04:40:12 +00:00
745d2553a0 Update ghcr.io/renovatebot/renovate Docker tag to v41
All checks were successful
test-build / build (pull_request) Successful in 27s
2025-06-20 04:32:37 +00:00
8a19559cc7 Merge pull request 'Update typescript-eslint monorepo to v8.34.1' (#13) from renovate/typescript-eslint-monorepo into main
All checks were successful
test-build / build (push) Successful in 1m10s
process-issues / process-issues (push) Successful in 11s
process-pull-requests / process-pull-requests (push) Successful in 11s
renovate / renovate (push) Successful in 39s
Reviewed-on: #13
2025-06-18 05:18:07 +00:00
42854db0fb Update typescript-eslint monorepo to v8.34.1
All checks were successful
test-build / build (pull_request) Successful in 1m2s
2025-06-17 00:01:12 +00:00
7b72e3849b Merge pull request 'Update dependency framer-motion to v12.18.1' (#12) from renovate/framer-motion-12.x-lockfile into main
Some checks failed
test-build / build (push) Successful in 51s
process-pull-requests / process-pull-requests (push) Successful in 6s
process-issues / process-issues (push) Failing after 5s
renovate / renovate (push) Successful in 45s
Reviewed-on: #12
2025-06-14 19:52:45 +00:00
6a8dbb0c7c Merge pull request 'Update dependency eslint to v9.29.0' (#11) from renovate/eslint-monorepo into main
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
Reviewed-on: #11
2025-06-14 19:52:35 +00:00
91fdf5a83f Update dependency framer-motion to v12.18.1
All checks were successful
test-build / build (pull_request) Successful in 1m6s
2025-06-14 00:01:33 +00:00
073f3a7916 Update dependency eslint to v9.29.0
All checks were successful
test-build / build (pull_request) Successful in 1m21s
2025-06-14 00:01:11 +00:00
38202841ca Merge pull request 'Update dependency framer-motion to v12.17.3' (#10) from renovate/framer-motion-12.x-lockfile into main
All checks were successful
test-build / build (push) Successful in 57s
process-issues / process-issues (push) Successful in 8s
process-pull-requests / process-pull-requests (push) Successful in 13s
renovate / renovate (push) Successful in 1m26s
Reviewed-on: #10
2025-06-12 20:12:45 +00:00
0492922cce Update dependency framer-motion to v12.17.3
All checks were successful
test-build / build (pull_request) Successful in 40s
2025-06-12 20:11:41 +00:00
a17500835b Merge pull request 'Update dependency framer-motion to v12.17.0' (#9) from renovate/framer-motion-12.x-lockfile into main
All checks were successful
renovate / renovate (push) Successful in 2m29s
test-build / build (push) Successful in 55s
Reviewed-on: #9
2025-06-12 18:06:43 +00:00
2f8b97208c Merge pull request 'Update tailwindcss monorepo to v4.1.10' (#8) from renovate/tailwindcss-monorepo into main
Some checks failed
renovate / renovate (push) Has been cancelled
test-build / build (push) Has been cancelled
Reviewed-on: #8
2025-06-12 18:06:36 +00:00
d6c30d5e5b Update dependency framer-motion to v12.17.0
All checks were successful
test-build / build (pull_request) Successful in 1m9s
2025-06-12 00:01:41 +00:00
a7ea9db3aa Update tailwindcss monorepo to v4.1.10
All checks were successful
test-build / build (pull_request) Successful in 1m19s
2025-06-12 00:01:31 +00:00
9134e78e8a remove dispatch
All checks were successful
test-build / build (push) Successful in 39s
process-issues / process-issues (push) Successful in 7s
process-pull-requests / process-pull-requests (push) Successful in 7s
renovate / renovate (push) Successful in 1m31s
2025-06-10 16:43:37 -05:00
2ca7d6705d fix
All checks were successful
renovate / renovate (push) Successful in 17s
test-build / build (push) Successful in 46s
2025-06-10 16:42:15 -05:00
5722e8c7a1 fix
All checks were successful
test-build / build (push) Successful in 38s
renovate / renovate (push) Successful in 19s
2025-06-10 16:39:55 -05:00
e39fd2acb8 change token
All checks were successful
renovate / renovate (push) Successful in 21s
test-build / build (push) Successful in 40s
2025-06-10 16:29:55 -05:00
0313fd54bc add ref
All checks were successful
renovate / renovate (push) Successful in 16s
test-build / build (push) Successful in 50s
2025-06-10 16:23:38 -05:00
dbb0f6d7ff update workflows
All checks were successful
renovate / renovate (push) Successful in 16s
test-build / build (push) Successful in 58s
2025-06-10 16:15:10 -05:00
20669d9766 fix
All checks were successful
renovate / renovate (push) Successful in 18s
test-build / build (push) Successful in 1m18s
2025-06-10 14:04:19 -05:00
6b2e6353d1 add dispatch
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Successful in 19s
2025-06-10 14:03:40 -05:00
6d112b52df remove step
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Successful in 17s
2025-06-10 14:02:58 -05:00
ff17af604f convert to python script
Some checks failed
test-build / build (push) Has been cancelled
renovate / renovate (push) Successful in 27s
2025-06-10 14:01:26 -05:00
32ea0989d7 change to pull requests
All checks were successful
renovate / renovate (push) Successful in 22s
test-build / build (push) Successful in 1m27s
2025-06-10 13:11:43 -05:00
e4ab7d134c fix repo name
All checks were successful
renovate / renovate (push) Successful in 20s
test-build / build (push) Successful in 1m44s
2025-06-10 13:06:04 -05:00
5fad13655c fix env
All checks were successful
renovate / renovate (push) Successful in 21s
test-build / build (push) Successful in 1m35s
2025-06-10 12:56:18 -05:00
8614d40a64 debugging
All checks were successful
renovate / renovate (push) Successful in 17s
test-build / build (push) Successful in 48s
2025-06-10 12:40:20 -05:00
8c417b93b3 fix url
All checks were successful
renovate / renovate (push) Successful in 17s
test-build / build (push) Successful in 41s
2025-06-10 12:36:49 -05:00
1d9519831b temp debugging
All checks were successful
renovate / renovate (push) Successful in 20s
test-build / build (push) Successful in 48s
2025-06-10 12:35:13 -05:00
fa57f2e93f temp debugging
All checks were successful
test-build / build (push) Successful in 45s
renovate / renovate (push) Successful in 18s
2025-06-10 12:29:25 -05:00
9e01002d4e add more logging
All checks were successful
renovate / renovate (push) Successful in 21s
test-build / build (push) Successful in 46s
2025-06-10 12:25:45 -05:00
cb52c169a3 add logging
All checks were successful
test-build / build (push) Successful in 1m1s
renovate / renovate (push) Successful in 19s
2025-06-10 12:15:47 -05:00
3017668cd2 fix lint
All checks were successful
test-build / build (push) Successful in 2m31s
renovate / renovate (push) Successful in 18s
2025-06-10 12:05:33 -05:00
1972b3bc19 update lockfile
Some checks failed
renovate / renovate (push) Successful in 19s
test-build / build (push) Failing after 48s
release-image / release (push) Successful in 1m43s
2025-06-10 11:58:31 -05:00
af77f90a49 bump versions to 0.8.11
Some checks failed
test-build / build (push) Failing after 23s
renovate / renovate (push) Successful in 43s
release-image / release (push) Failing after 48s
2025-06-09 23:55:31 -05:00
bdda29f369 fix favicon path 2025-06-09 23:44:22 -05:00
644c5fcd6a fix footer 2025-06-09 23:37:47 -05:00
bafd8158d3 remove 2025-06-09 23:11:38 -05:00
4d9c1a3e8c changes for v4 tailwind 2025-06-09 23:03:04 -05:00
4a4233ac62 change module 2025-06-09 22:33:40 -05:00
c71957348d add pubilc image 2025-06-09 22:21:38 -05:00
400bf16dd9 use global value 2025-06-09 22:21:25 -05:00
85535614a0 remove dependencies 2025-06-09 22:21:09 -05:00
38fcbb635b change description 2025-06-09 21:47:43 -05:00
b1e57c3f17 use apply 2025-06-09 21:47:29 -05:00
e22a1985be remove unused keys
All checks were successful
renovate / renovate (push) Successful in 18s
test-build / build (push) Successful in 2m14s
2025-06-09 21:32:45 -05:00
70b0b86944 change transitions
All checks were successful
renovate / renovate (push) Successful in 1m8s
test-build / build (push) Successful in 1m16s
2025-06-09 21:31:20 -05:00
ba36de8e36 remove translate on hover for links and buttons 2025-06-09 21:21:13 -05:00
d2e44fe046 add gitea link 2025-06-09 21:20:48 -05:00
36ec797d3b change favicon 2025-06-09 19:34:30 -05:00
086d98ba50 remove ignored
Some checks failed
test-build / build (push) Successful in 47s
tag-old-issues / tag-old-issues (push) Failing after 1m11s
renovate / renovate (push) Successful in 24s
2025-06-09 15:59:59 -05:00
8a05fa4d96 update prettier and add eslint
All checks were successful
renovate / renovate (push) Successful in 18s
test-build / build (push) Successful in 2m11s
2025-06-09 15:54:56 -05:00
dbbf886de9 add test build
All checks were successful
test-build / build (push) Successful in 2m14s
renovate / renovate (push) Successful in 17s
2025-06-09 13:14:56 -05:00
ae7e21eb82 change registry
All checks were successful
renovate / renovate (push) Successful in 18s
2025-06-09 13:02:52 -05:00
ce6f476e8f fix repo
All checks were successful
renovate / renovate (push) Successful in 18s
2025-06-09 12:57:28 -05:00
0ca6be1d91 limit repo
All checks were successful
renovate / renovate (push) Successful in 55s
2025-06-09 12:56:12 -05:00
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
release-image-gitea / release (push) Failing after 21m20s
renovate / renovate (push) Successful in 10m9s
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
90318aad14 change build arg
Some checks failed
renovate / renovate (push) Successful in 1m15s
release-image-gitea / release (push) Failing after 1m19s
2025-06-08 20:39:46 -05:00
e454a510c6 change
All checks were successful
renovate / renovate (push) Successful in 1m4s
2025-06-08 20:35:06 -05:00
a6d3ec5052 change
All checks were successful
renovate / renovate (push) Successful in 1m8s
2025-06-08 20:33:24 -05:00
1d134d43da remove oci
All checks were successful
renovate / renovate (push) Successful in 1m11s
2025-06-08 20:30:25 -05:00
54c7c9e259 remove oci
All checks were successful
renovate / renovate (push) Successful in 1m9s
2025-06-08 20:26:29 -05:00
0d8cf28be4 modify daemon
All checks were successful
renovate / renovate (push) Successful in 1m16s
2025-06-08 20:24:03 -05:00
d78a8d8c45 use different registry
All checks were successful
renovate / renovate (push) Successful in 1m7s
2025-06-08 20:19:32 -05:00
5b6abeb9f9 use registry argument
Some checks failed
renovate / renovate (push) Successful in 1m34s
release-image-gitea / release (push) Failing after 23s
2025-06-08 20:14:29 -05:00
a3b0301d23 merge workflows 2025-06-08 20:09:45 -05:00
06f7546212 bump tag
Some checks failed
release-image-gitea / release (push) Failing after 22s
release-image-harbor / release (push) Failing after 26s
renovate / renovate (push) Successful in 1m19s
2025-06-08 19:49:37 -05:00
abd1d43f79 remove command 2025-06-08 19:49:19 -05:00
07f2f5f0e1 add node render
Some checks failed
release-image-gitea / release (push) Failing after 1m5s
renovate / renovate (push) Successful in 1m14s
release-image-harbor / release (push) Failing after 1m15s
2025-06-08 19:46:11 -05:00
91b53a33c2 update tags
Some checks failed
renovate / renovate (push) Successful in 1m43s
release-image-gitea / release (push) Failing after 5m13s
release-image-harbor / release (push) Failing after 5m16s
2025-06-08 19:10:25 -05:00
b3e23f3e6c update tags
Some checks failed
renovate / renovate (push) Has been cancelled
2025-06-08 19:09:57 -05:00
ab68b6248f fix tag 2025-06-08 19:01:01 -05:00
37d1f1d1f2 change workflows
Some checks failed
release-image-harbor / release (push) Failing after 7m20s
release-image-gitea / release (push) Failing after 7m21s
renovate / renovate (push) Successful in 2m6s
2025-06-08 17:15:25 -05:00
89e1c59e37 upgrade to tailwind 4
All checks were successful
renovate / renovate (push) Successful in 2m35s
2025-06-08 17:10:20 -05:00
7153f29022 update docker base
All checks were successful
renovate / renovate (push) Successful in 4m11s
2025-06-08 16:49:57 -05:00
51041f6ae9 apply prettier formatting
Some checks failed
renovate / renovate (push) Has been cancelled
2025-06-08 16:45:36 -05:00
67f12ecf72 add missing config
All checks were successful
renovate / renovate (push) Successful in 4m14s
2025-06-08 16:28:14 -05:00
3e89e6cb1c upgrade to different layout
Some checks failed
renovate / renovate (push) Successful in 5m22s
release-image-gitea / release (push) Failing after 7m9s
release-image-harbor / release (push) Failing after 7m9s
2025-06-08 16:02:39 -05:00
e1632629a9 add renovate
All checks were successful
renovate / renovate (push) Successful in 4m10s
2025-06-04 21:04:00 -05:00
87343e78bb add ntfy action 2025-05-17 20:56:04 -05:00
f243249fb8 update tag
All checks were successful
release-image-gitea / release (push) Successful in 47s
release-image-harbor / release (push) Successful in 50s
2025-05-15 20:43:22 -05:00
6e5458de37 fix lock 2025-05-15 20:33:04 -05:00
b7ea8165d2 update dependencies
Some checks failed
release-image-gitea / release (push) Failing after 2m22s
release-image-harbor / release (push) Failing after 47s
2025-05-15 20:27:06 -05:00
ae4941073c update repo config 2025-03-20 01:15:10 -05:00
de87ffeff2 change tag 2025-03-14 22:55:16 -05:00
369e97af41 bump package image
Some checks failed
release-image-gitea / release (push) Successful in 54s
release-image-harbor / release (push) Failing after 52s
2025-03-14 22:37:55 -05:00
754ff5d9a9 add harbor 2025-03-14 22:37:03 -05:00
351cac00b3 use k8 driver 2025-03-14 22:20:45 -05:00
11c85e324e remove container 2025-03-14 21:52:01 -05:00
5a418428d3 change config 2025-03-14 21:26:34 -05:00
3d4c9c2214 remove verify 2025-03-14 21:16:17 -05:00
10262c4b7a remove certs 2025-03-14 21:15:07 -05:00
57ea8374a5 change workflow 2025-03-14 21:13:27 -05:00
e9e1cabd11 add podman 2025-03-14 21:04:35 -05:00
4c1ec680a9 use buildah 2025-03-14 20:58:33 -05:00
4f826e8964 add build step 2025-03-14 20:43:18 -05:00
3ddce86e64 change config 2025-03-14 20:32:49 -05:00
61aa06310c update 2025-03-14 20:15:23 -05:00
03195017c5 update version 2025-03-14 16:56:25 -05:00
fc3f4fdad4 fix workflow 2025-03-14 16:52:43 -05:00
fc42f31fb0 update images 2025-03-14 16:51:34 -05:00
renovate[bot]
e56b3a001e Update docker/build-push-action digest to 0adf995 (#38)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-19 23:17:00 -06:00
renovate[bot]
13711618b7 Update docker/build-push-action digest to 67a2d40 (#35)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-17 17:17:55 -06:00
04980a38af update tag 2025-01-09 21:31:14 -06:00
385ad20c82 update lock 2025-01-09 21:26:35 -06:00
761652f46d update dependencies 2025-01-09 21:25:00 -06:00
renovate[bot]
8f6b1af8ad Update docker/metadata-action digest to 8e1d546 (#32)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 21:17:30 -06:00
renovate[bot]
90c8d30e3f Update docker/login-action digest to 327cd5a (#31)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 21:17:19 -06:00
renovate[bot]
ad9128acea Update docker/build-push-action digest to 31ca4e5 (#29)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 21:16:45 -06:00
renovate[bot]
5f9235c9dc Migrate config .github/renovate.json (#27)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-21 19:53:06 -06:00
renovate[bot]
3b2702af36 Update renovate/renovate Docker tag to v39 (#24)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-21 19:52:28 -06:00
renovate[bot]
f999b9a92c Update docker/metadata-action digest to 906ecf0 (#21)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-21 19:50:40 -06:00
renovate[bot]
35c940bef7 Update docker/build-push-action digest to 7e09459 (#20)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-21 19:50:36 -06:00
renovate[bot]
7aa6898a93 Update docker/login-action digest to 7ca3450 (#15)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-21 19:50:29 -06:00
9d77c9db2a update dependencies 2024-12-21 19:48:14 -06:00
528eb8fb2e update lockfile 2024-09-30 17:15:52 -05:00
14e73d61ef update dependencies 2024-09-30 17:13:16 -05:00
renovate[bot]
d10fe280a5 Update dependency @astrojs/node to v8.3.4 (#12)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 17:08:27 -05:00
renovate[bot]
5ea5774042 Update docker/build-push-action digest to 4f58ea7 (#13)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 17:08:15 -05:00
renovate[bot]
3c82fb43d8 Update docker/login-action digest to 3b8fed7 (#10)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 17:07:40 -05:00
renovate[bot]
c7071ab583 Update docker/metadata-action digest to 70b2cdc (#11)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 17:07:27 -05:00
renovate[bot]
125d70d62e Update dependency typescript to v5.6.2 (#9)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 17:07:15 -05:00
renovate[bot]
357634d3f0 Update dependency @directus/sdk to v17.0.1 (#8)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 17:06:28 -05:00
renovate[bot]
bd4b85c874 Update dependency astro to v4.14.6 (#6)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-30 16:23:34 -05:00
7efa375427 remove author field 2024-08-24 02:09:34 -05:00
358d6b91c6 upgrade base image 2024-08-24 01:35:22 -05:00
92d4be91df upgrade astro 2024-08-24 01:34:16 -05:00
0f5fc27371 convert to use directus 2024-08-24 01:23:12 -05:00
fd85557d6b replace placeholder info with my own 2024-08-23 22:01:12 -05:00
f6c05b8a0c change work to projects 2024-08-23 21:01:26 -05:00
b8efef1a00 polishing pass 2024-08-23 20:46:46 -05:00
95 changed files with 9537 additions and 4057 deletions

View File

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

View File

@@ -0,0 +1,40 @@
name: process-repository
on:
schedule:
- cron: "@daily"
jobs:
process-repository:
runs-on: ubuntu-latest
steps:
- name: Checkout Python Script
uses: actions/checkout@v4
with:
repository: alexlebens/workflow-scripts
ref: main
token: ${{ secrets.BOT_TOKEN }}
path: workflow-scripts
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.13"
- name: Install dependencies
run: pip install requests immutabledict
- name: Run Script
env:
INSTANCE_URL: ${{ vars.INSTANCE_URL }}
OWNER: ${{ gitea.owner }}
REPOSITORY: ${{ gitea.repository }}
TOKEN: ${{ secrets.BOT_TOKEN }}
LOG_LEVEL: DEBUG
ISSUE_STALE_DAYS: 3
ISSUE_STALE_TAG: 23
ISSUE_EXCLUDE_TAG: 17
PULL_REQUEST_STALE_DAYS: 3
PULL_REQUEST_STALE_TAG: 23
PULL_REQUEST_REQUIRED_TAG: 22
run: python ./workflow-scripts/process-repository.py

View File

@@ -0,0 +1,98 @@
name: release-image
on:
push:
tags:
- 0.*
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REPOSITORY_HOST }}
username: ${{ gitea.actor }}
password: ${{ secrets.REPOSITORY_TOKEN }}
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY_HOST }}
username: ${{ vars.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_SECRET }}
- name: Create Kubeconfig
run: |
mkdir $HOME/.kube
echo "${{ secrets.KUBECONFIG_BUILDX }}" > $HOME/.kube/config
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v3
with:
driver: kubernetes
driver-opts: |
namespace=gitea
qemu.install=true
buildkitd-config-inline: |
[registry."docker.io"]
mirrors = ["harbor.alexlebens.net/proxy-hub.docker/"]
- name: Available Platforms
run: echo ${{ steps.buildx.outputs.platforms }}
- name: Extract Metadata
id: meta
uses: docker/metadata-action@v5
with:
tags: |
type=ref,event=branch
type=ref,event=tag
images: |
${{ vars.REPOSITORY_HOST }}/${{ gitea.repository }}
${{ vars.REGISTRY_HOST }}/images/site-profile
- name: Build and Push Image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./Dockerfile
- name: ntfy Success
uses: niniyas/ntfy-action@master
if: success()
with:
url: '${{ secrets.NTFY_URL }}'
topic: '${{ secrets.NTFY_TOPIC }}'
title: 'Gitea Action'
priority: 3
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
tags: action,successfully,completed
details: 'Site Profile build workflow has successfully completed!'
icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png'
- name: ntfy Failed
uses: niniyas/ntfy-action@master
if: failure()
with:
url: '${{ secrets.NTFY_URL }}'
topic: '${{ secrets.NTFY_TOPIC }}'
title: 'Gitea Action'
priority: 4
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
tags: action,failed
details: 'Site Profile build workflow has failed!'
icon: 'https://cdn.jsdelivr.net/gh/selfhst/icons/png/gitea.png'
actions: '[{"action": "view", "label": "Open Gitea", "url": "https://gitea.alexlebens.dev/alexlebens/site-profile/actions?workflow=release-image.yml", "clear": true}]'
image: true

View File

@@ -0,0 +1,32 @@
name: renovate
on:
schedule:
- cron: '@daily'
push:
branches:
- main
workflow_dispatch:
jobs:
renovate:
runs-on: ubuntu-latest
container: ghcr.io/renovatebot/renovate:41
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Renovate
run: renovate
env:
RENOVATE_PLATFORM: gitea
RENOVATE_ENDPOINT: ${{ vars.INSTANCE_URL }}
RENOVATE_REPOSITORIES: alexlebens/site-profile
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net>
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: ${{ 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.17.1
cache: pnpm
- name: Install Dependencies
run: pnpm install
- name: Lint Code
run: pnpm lint
- name: Build Project
run: pnpm build

View File

@@ -1,2 +0,0 @@
# This file is processed by Renovate bot so that it creates a PR on new major Renovate versions
FROM renovate/renovate:38

44
.github/renovate.json vendored
View File

@@ -1,44 +0,0 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended",
"mergeConfidence:all-badges",
":rebaseStalePrs"
],
"timezone": "US/Central",
"schedule": [
"every weekday"
],
"labels": [],
"prHourlyLimit": 0,
"prConcurrentLimit": 0,
"packageRules": [
{
"description": "Disables for non major Renovate version",
"matchPaths": [
".github/renovate-update-notification/Dockerfile"
],
"matchUpdateTypes": [
"minor",
"patch",
"pin",
"digest",
"rollback"
],
"enabled": false
},
{
"description": "Generate for major Renovate version",
"matchPaths": [
".github/renovate-update-notification/Dockerfile"
],
"matchUpdateTypes": [
"major"
],
"addLabels": [
"upgrade"
],
"automerge": false
}
]
}

View File

@@ -1,45 +0,0 @@
name: release-image
on:
push:
tags:
- 0.*
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
release-image:
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log into the container registry
uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@60a0d343a0d8a18aedee9d34e62251f752153bdb
with:
tags: |
type=ref,event=branch
type=ref,event=tag
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- name: Build and push Docker image
uses: docker/build-push-action@5cd11c3a4ced054e52742c5fd54dca954e0edd85
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
file: ./Dockerfile

8
.gitignore vendored
View File

@@ -12,15 +12,15 @@ yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# environment variables
.env
.env.local
.env.development
.env.production
# macOS-specific files
.DS_Store
# jetbrains setting folder
.idea/
# ide
.vscode/
site-profile.code-workspace

2
.npmrc Normal file
View File

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

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,8 @@
FROM node:20.16.0-alpine3.20 AS base
ARG REGISTRY=docker.io
FROM ${REGISTRY}/node:22.17.1-alpine3.22 AS base
LABEL version="0.1.7"
LABEL description="Astro based website to use as a profile"
LABEL version="0.11.0"
LABEL description="Astro based personal website"
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
@@ -20,12 +21,16 @@ 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 --prod
FROM base AS runtime
COPY --from=prod-deps /app/node_modules /app/node_modules
COPY --from=build /app/dist /app/dist
ENV HOST=0.0.0.0
ENV SITE_URL=https://www.alexlebens.dev
ENV DIRECTUS_URL=https://directus.alexlebens.dev
ENV PORT=4321
EXPOSE 4321
CMD node ./dist/server/entry.mjs
EXPOSE $PORT
CMD ["node", "./dist/server/entry.mjs"]

View File

@@ -1,6 +1,8 @@
MIT License
# MIT License
Copyright (c) 2024 Alex Lebens
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

View File

@@ -1 +1,74 @@
# Profile
# Alex Lebens Personal Site
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
## Getting Started
### Requirements
- Node.js 22+ and pnpm
### Installation
```bash
# Clone repository
git clone https://gitea.alexlebens.dev/alexlebens/site-profile
# Navigate to project directory
cd site-profile
# Install dependencies
pnpm install
```
### Development
```bash
# Start development server
pnpm dev
# Open browser at http://localhost:4321
```
### Build
```bash
# Create production build
pnpm build
# Preview production build
pnpm 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
```

View File

@@ -1,11 +1,29 @@
import { defineConfig } from 'astro/config';
import tailwindcss from '@tailwindcss/vite';
import react from '@astrojs/react';
import node from "@astrojs/node";
import node from '@astrojs/node';
const getSiteURL = () => {
if (process.env.SITE_URL) {
return `https://${process.env.SITE_URL}`;
}
return 'http://localhost:4321';
};
// https://astro.build/config
export default defineConfig({
output: "hybrid",
site: getSiteURL(),
integrations: [tailwindcss(), react()],
plugins: {
'@tailwindcss/postcss': {},
},
vite: {
plugins: [tailwindcss()],
},
adapter: node({
mode: "standalone"
})
mode: 'standalone',
}),
});

11
eslint.config.mjs Normal file
View File

@@ -0,0 +1,11 @@
import eslintPluginAstro from 'eslint-plugin-astro';
import eslintConfigPrettier from "eslint-config-prettier/flat";
export default [
...eslintPluginAstro.configs.recommended,
eslintConfigPrettier,
{
rules: {
}
}
];

59
lib/directus.ts Normal file
View File

@@ -0,0 +1,59 @@
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;
gitea: 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,19 +1,43 @@
{
"name": "site-profile",
"type": "module",
"version": "0.1.7",
"version": "0.11.0",
"private": true,
"scripts": {
"dev": "astro dev",
"start": "astro dev",
"build": "astro check && astro build",
"build": "astro build",
"preview": "astro preview",
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,md,mdx,astro}\"",
"lint": "eslint \"src/**/*.{js,ts,jsx,tsx,astro}\"",
"lint:fix": "eslint --fix \"src/**/*.{js,ts,jsx,tsx,astro}\"",
"astro": "astro"
},
"dependencies": {
"@astrojs/check": "^0.9.3",
"@astrojs/node": "^8.3.3",
"@directus/sdk": "^17.0.0",
"astro": "^4.14.2",
"typescript": "^5.5.4"
"@astrojs/mdx": "^4.3.0",
"@astrojs/node": "^9.2.2",
"@astrojs/react": "^4.3.0",
"@astrojs/rss": "^4.0.12",
"@directus/sdk": "^20.0.0",
"@tailwindcss/postcss": "^4.1.8",
"@tailwindcss/vite": "^4.1.8",
"astro": "^5.10.1",
"framer-motion": "^12.16.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"
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.16",
"@typescript-eslint/parser": "8.37.0",
"eslint": "9.31.0",
"eslint-config-prettier": "10.1.8",
"eslint-plugin-astro": "1.3.1",
"prettier": "^3.5.3",
"prettier-plugin-astro": "^0.14.1",
"prettier-plugin-tailwindcss": "^0.6.12",
"typescript-eslint": "8.37.0"
}
}

5602
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

8
postcss.config.mjs Normal file
View File

@@ -0,0 +1,8 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};
export default config;

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;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1 +0,0 @@
<svg height="640" width="1440" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a"><stop offset=".58" stop-opacity="0"/><stop offset="1"/></linearGradient><linearGradient id="b" gradientUnits="userSpaceOnUse" x1="793.5" x2="759.5" xlink:href="#a" y1="261.5" y2="149.5"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="644.19" x2="645.54" xlink:href="#a" y1="398.02" y2="267.7"/><linearGradient id="d" gradientUnits="userSpaceOnUse" x1="547" x2="522.36" xlink:href="#a" y1="457.27" y2="342.85"/><g clip-rule="evenodd" fill-rule="evenodd" opacity=".15"><path d="m439.57 249.55a2149.47 2149.47 0 0 1 1193.87-182.45l-12.48 93.17a2055.46 2055.46 0 0 0 -1141.66 174.47l-454.24 211.86-39.73-85.2z" fill="url(#b)"/><path d="m272.3 266.93a2393.36 2393.36 0 0 1 1328.96 205.6l-44.42 94.78a2288.7 2288.7 0 0 0 -1270.84-196.61l-553.29 73.05-13.7-103.77z" fill="url(#c)" opacity=".56"/><path d="m195.26 416.13a2149.46 2149.46 0 0 1 1204.86-83.21l-20.13 91.82a2055.46 2055.46 0 0 0 -1152.17 79.56l-470.18 173.62-32.56-88.18 470.18-173.62z" fill="url(#d)"/></g><path d="m-258.15 719.56 1743.12-517.56 182.93 616.12-1743.1 517.56z" fill="#090b11"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1440" height="640"><g opacity=".15"><path fill="url(#a)" d="M439.57 249.55A2149.47 2149.47 0 0 1 1633.44 67.1l-12.48 93.17A2055.46 2055.46 0 0 0 479.3 334.74L25.06 546.6l-39.73-85.2z"/><path fill="url(#b)" d="M272.3 265.93a2393.36 2393.36 0 0 1 1328.96 205.6l-44.42 94.78A2288.7 2288.7 0 0 0 286 369.7l-553.29 73.05-13.7-103.77z" opacity=".56"/><path fill="url(#c)" d="M195.26 416.13a2149.47 2149.47 0 0 1 1204.86-83.21l-20.13 91.82A2055.46 2055.46 0 0 0 227.82 504.3l-470.18 173.62-32.56-88.18 470.18-173.62z"/></g><path fill="#fff" d="M-258 718.56 1485.12 201l182.93 616.12-1743.11 517.56z"/><defs><linearGradient id="d"><stop offset=".58" stop-opacity="0"/><stop offset="1"/></linearGradient><linearGradient xlink:href="#d" id="a" x1="793.5" x2="759.5" y1="261.5" y2="149.5" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#d" id="b" x1="644.19" x2="645.54" y1="397.02" y2="266.7" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#d" id="c" x1="547" x2="522.36" y1="457.27" y2="342.85" gradientUnits="userSpaceOnUse"/></defs></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 188 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@@ -1,9 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
<style>
path { fill: #000; }
@media (prefers-color-scheme: dark) {
path { fill: #FFF; }
}
</style>
</svg>

Before

Width:  |  Height:  |  Size: 749 B

BIN
public/i.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

4
public/robots.txt Normal file
View File

@@ -0,0 +1,4 @@
User-agent: *
Allow: /
Sitemap: https://www.alexlebens.dev/sitemap-index.xml

40
renovate.json Normal file
View File

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

View File

@@ -0,0 +1,104 @@
---
---
<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 [mask-image:radial-gradient(white,transparent_85%)] bg-[center_top_-1px]"
>
</div>
<!-- Ambient glow effects -->
<div
class="animate-glow theme-transition-bg absolute top-1/4 left-1/4 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-zinc-400/20 opacity-50 blur-3xl dark:bg-zinc-500/20"
>
</div>
<div
class="animate-glow animation-delay-1000 theme-transition-bg absolute right-1/4 bottom-1/3 h-64 w-64 translate-x-1/2 translate-y-1/2 rounded-full bg-zinc-300/20 opacity-40 blur-3xl dark:bg-zinc-600/20"
>
</div>
<!-- Theme transition overlay -->
<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('astro:page-load', () => {
const themeToggle = document.querySelector('[data-theme-toggle]');
const overlay = document.getElementById('theme-transition-overlay');
if (themeToggle && overlay) {
themeToggle.addEventListener('click', () => {
document.documentElement.classList.add('theme-transitioning');
overlay.style.opacity = '0.15';
overlay.style.transition = 'opacity 0.3s ease';
setTimeout(() => {
overlay.style.opacity = '0';
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.2) 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.15) 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

@@ -1,56 +0,0 @@
---
interface Props {
href: string;
}
const { href } = Astro.props;
---
<a href={href}><slot /></a>
<style>
a {
position: relative;
display: flex;
place-content: center;
text-align: center;
padding: 0.56em 2em;
gap: 0.8em;
color: var(--accent-text-over);
text-decoration: none;
line-height: 1.1;
border-radius: 999rem;
overflow: hidden;
background: var(--gradient-accent-orange);
box-shadow: var(--shadow-md);
white-space: nowrap;
}
@media (min-width: 20em) {
a {
font-size: var(--text-lg);
}
}
/* Overlay for hover effects. */
a::after {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
transition: background-color var(--theme-transition);
mix-blend-mode: overlay;
}
a:focus::after,
a:hover::after {
background-color: hsla(var(--gray-999-basis), 0.3);
}
@media (min-width: 50em) {
a {
padding: 1.125rem 2.5rem;
font-size: var(--text-xl);
}
}
</style>

View File

@@ -1,46 +0,0 @@
---
import CallToAction from './CallToAction.astro';
import Icon from './Icon.astro';
---
<aside>
<h2>Interested in working together?</h2>
<CallToAction href="mailto:alexander.lebens@gmail.com">
Send Me a Message
<Icon icon="paper-plane-tilt" size="1.2em" />
</CallToAction>
</aside>
<style>
aside {
display: flex;
flex-direction: column;
align-items: center;
gap: 3rem;
border-top: 1px solid var(--gray-800);
border-bottom: 1px solid var(--gray-800);
padding: 5rem 1.5rem;
background-color: var(--gray-999_40);
box-shadow: var(--shadow-sm);
}
h2 {
font-size: var(--text-xl);
text-align: center;
max-width: 15ch;
}
@media (min-width: 50em) {
aside {
padding: 7.5rem;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
h2 {
font-size: var(--text-3xl);
text-align: left;
}
}
</style>

View File

@@ -1,73 +1,248 @@
---
import Icon from './Icon.astro';
import directus from '../../lib/directus';
import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton('global'));
const links = await directus.request(readSingleton('links'));
const currentYear = new Date().getFullYear();
const navLinks = [
{ text: 'Home', href: '/' },
{ text: 'Blog', href: '/blog' },
{ text: 'About', href: '/about' },
{ text: 'RSS', href: '/rss' },
];
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: 'Gitea',
href: links.gitea,
icon: `<path d="M7 5C7 3.89543 7.89543 3 9 3C10.1046 3 11 3.89543 11 5C11 5.34168 10.9143 5.66336 10.7633 5.9447H11.3438C13.5529 5.9447 15.3438 7.73556 15.3438 9.9447V11.2244C15.9301 11.5731 16.323 12.213 16.323 12.9447C16.323 14.0493 15.4276 14.9447 14.323 14.9447C13.2184 14.9447 12.323 14.0493 12.323 12.9447C12.323 12.1959 12.7345 11.5432 13.3438 11.2004V9.9447C13.3438 8.84013 12.4483 7.9447 11.3438 7.9447H10V17.2676C10.5978 17.6134 11 18.2597 11 19C11 20.1046 10.1046 21 9 21C7.89543 21 7 20.1046 7 19C7 18.2597 7.4022 17.6134 8 17.2676V6.73244C7.4022 6.38663 7 5.74028 7 5Z" fill="currentColor"/>`,
},
{
name: 'LinkedIn',
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>
<div class="group">
<p>
Designed & Developed in Minnesota with <a href="https://astro.build/">Astro</a>
<Icon icon="rocket-launch" size="1.2em" />
</p>
<p>&copy; {currentYear} Alex Lebens</p>
<footer
class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
transition:animate="none"
>
<div class="pointer-events-none absolute inset-0 overflow-hidden">
<div
class="theme-transition-all animate-float-slow absolute -top-40 -right-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
>
</div>
<p class="socials">
<a href="https://github.com/alexlebens"> GitHub</a>
<a href="https://www.linkedin.com/in/alexanderlebens"> LinkedIn</a>
<div
class="theme-transition-all animate-float-slow animation-delay-2000 absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
>
</div>
<div
class="theme-transition-all animate-float-slow animation-delay-1000 absolute top-20 left-1/4 h-40 w-40 rounded-full bg-zinc-200/50 opacity-30 blur-2xl dark:bg-zinc-700/20"
>
</div>
</div>
<div class="relative px-4 pt-16 pb-12 sm:px-6">
<div class="mx-auto max-w-4xl">
<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 dark:from-zinc-200 dark:to-zinc-400"
>
<span
class="theme-transition-all text-xl font-bold text-zinc-100 duration-300 dark:text-zinc-900"
>
{global.initals}
</span>
<div class="absolute inset-0"></div>
</div>
<span
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
>
Blog
</span>
</div>
</a>
<p
class="theme-transition-color mt-4 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400"
>
{global.description}
</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="hover group relative flex h-10 w-10 transform items-center justify-center rounded-full bg-zinc-100 text-zinc-500 transition-all duration-300 hover:-translate-y-1 hover:text-zinc-900 hover:ring-2 hover:ring-zinc-300 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100 dark:hover:ring-zinc-700"
aria-label={social.name}
>
<span class="absolute inset-0 rounded-full bg-gradient-to-br from-zinc-200 to-zinc-300 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">
<h3
class="theme-transition-color relative inline-block pb-2 text-sm font-semibold tracking-wider text-zinc-900 uppercase after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:bg-zinc-300 after:content-[''] dark:text-zinc-100 dark:after:bg-zinc-700"
>
Navigation
</h3>
<ul class="mt-4 space-y-3">
{
navLinks.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"
>
<span class="relative inline-block overflow-hidden">
<span class="relative z-10">{link.text}</span>
</span>
</a>
</li>
))
}
</ul>
</div>
</div>
<!-- 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"
>
<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>
</div>
</div>
</div>
</div>
</footer>
<style>
footer {
display: flex;
flex-direction: column;
gap: 3rem;
margin-top: auto;
padding: 3rem 2rem 3rem;
text-align: center;
color: var(--gray-400);
font-size: var(--text-sm);
.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;
}
footer a {
color: var(--gray-400);
text-decoration: 1px solid underline transparent;
text-underline-offset: 0.25em;
transition: text-decoration-color var(--theme-transition);
.theme-transition-color {
transition-property: color, fill, stroke;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
footer a:hover,
footer a:focus {
text-decoration-color: currentColor;
.theme-transition-bg {
transition-property: background-color;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms;
}
.group {
display: flex;
flex-direction: column;
gap: 0.5rem;
@keyframes pulse {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.2);
}
}
.socials {
display: flex;
justify-content: center;
gap: 1rem;
flex-wrap: wrap;
@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);
}
}
@media (min-width: 50em) {
footer {
flex-direction: row;
justify-content: space-between;
padding: 2.5rem 5rem;
.animate-pulse {
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
.group {
flex-direction: row;
gap: 1rem;
flex-wrap: wrap;
.animate-float-slow {
animation: float-slow 20s ease-in-out infinite;
}
.socials {
justify-content: flex-end;
.animation-delay-1000 {
animation-delay: 1s;
}
.animation-delay-2000 {
animation-delay: 2s;
}
</style>

View File

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

View File

@@ -1,65 +0,0 @@
---
interface Props {
variant?: 'offset' | 'small';
}
const { variant } = Astro.props;
---
<ul class:list={['grid', { offset: variant === 'offset', small: variant === 'small' }]}>
<slot />
</ul>
<style>
.grid {
display: grid;
grid-auto-rows: 1fr;
gap: 1rem;
list-style: none;
padding: 0;
}
.grid.small {
grid-template-columns: 1fr 1fr;
gap: 1.5rem;
}
/* If last row contains only one item, make it span both columns. */
.grid.small > :global(:last-child:nth-child(odd)) {
grid-column: 1 / 3;
}
@media (min-width: 50em) {
.grid {
grid-template-columns: 1fr 1fr;
gap: 4rem;
}
.grid.offset {
--row-offset: 7.5rem;
padding-bottom: var(--row-offset);
}
/* Shift first item in each row vertically to create staggered effect. */
.grid.offset > :global(:nth-child(odd)) {
transform: translateY(var(--row-offset));
}
/* If last row contains only one item, display it in the second column. */
.grid.offset > :global(:last-child:nth-child(odd)) {
grid-column: 2 / 3;
transform: none;
}
.grid.small {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 2rem;
}
.grid.small > :global(*) {
flex-basis: 20rem;
}
}
</style>

View File

@@ -1,54 +0,0 @@
---
interface Props {
title: string;
tagline?: string;
align?: 'start' | 'center';
}
const { align = 'center', tagline, title } = Astro.props;
---
<div class:list={['hero stack gap-4', align]}>
<div class="stack gap-2">
<h1 class="title">{title}</h1>
{tagline && <p class="tagline">{tagline}</p>}
</div>
<slot />
</div>
<style>
.hero {
font-size: var(--text-lg);
text-align: center;
}
.title,
.tagline {
max-width: 37ch;
margin-inline: auto;
}
.title {
font-size: var(--text-3xl);
color: var(--gray-0);
}
@media (min-width: 50em) {
.hero {
font-size: var(--text-xl);
}
.start {
text-align: start;
}
.start .title,
.start .tagline {
margin-inline: unset;
}
.title {
font-size: var(--text-5xl);
}
}
</style>

View File

@@ -1,56 +0,0 @@
---
import type { HTMLAttributes } from 'astro/types';
import { iconPaths } from './IconPaths';
interface Props {
icon: keyof typeof iconPaths;
color?: string;
gradient?: boolean;
size?: string;
}
const { color = 'currentcolor', gradient, icon, size } = Astro.props;
const iconPath = iconPaths[icon];
const attrs: HTMLAttributes<'svg'> = {};
if (size) attrs.style = { '--size': size };
const gradientId = 'icon-gradient-' + Math.round(Math.random() * 10e12).toString(36);
---
<svg
xmlns="http://www.w3.org/2000/svg"
width="40"
height="40"
viewBox="0 0 256 256"
aria-hidden="true"
stroke={gradient ? `url(#${gradientId})` : color}
fill={gradient ? `url(#${gradientId})` : color}
{...attrs}
>
<g set:html={iconPath} />
{
gradient && (
<linearGradient
id={gradientId}
x1="23"
x2="235"
y1="43"
y2="202"
gradientUnits="userSpaceOnUse"
>
<stop stop-color="var(--gradient-stop-1)" />
<stop offset=".5" stop-color="var(--gradient-stop-2)" />
<stop offset="1" stop-color="var(--gradient-stop-3)" />
</linearGradient>
)
}
</svg>
<style>
svg {
vertical-align: middle;
width: var(--size, 1em);
height: var(--size, 1em);
}
</style>

View File

@@ -1,38 +0,0 @@
/**
* Icons adapted from https://phosphoricons.com/
*
* Want to add more?
* 1. Find the icon you want on Phosphor Icons.
* 2. Click “Copy SVG”.
* 3. Paste the SVG code in your editor.
* 4. Remove the `<svg>` wrapper so you only have elements like `<path>`, `<circle>`, `<rect>` etc.
* 5. Remove any `stroke="#000000"` attributes
* 6. Replace any `fill="#000000"` attributes with `stroke="none"`
* (or add `stroke="none"` on shapes with no `fill` or `stroke` specified).
*/
export const iconPaths = {
'terminal-window': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m80 96 40 32-40 32m56 0h40"/><rect width="192" height="160" x="32" y="48" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16.97" rx="8.5"/>`,
trophy: `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M56 56v55.1c0 39.7 31.8 72.6 71.5 72.9a72 72 0 0 0 72.5-72V56a8 8 0 0 0-8-8H64a8 8 0 0 0-8 8Zm40 168h64m-32-40v40"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M198.2 128h9.8a32 32 0 0 0 32-32V80a8 8 0 0 0-8-8h-32M58 128H47.9a32 32 0 0 1-32-32V80a8 8 0 0 1 8-8h32"/>`,
strategy: `<circle cx="68" cy="188" r="28" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m40 72 40 40m0-40-40 40m136 56 40 40m0-40-40 40M136 80V40h40"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m136 40 16 16c40 40 8 88-24 96"/>`,
'paper-plane-tilt': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M210.3 35.9 23.9 88.4a8 8 0 0 0-1.2 15l85.6 40.5a7.8 7.8 0 0 1 3.8 3.8l40.5 85.6a8 8 0 0 0 15-1.2l52.5-186.4a7.9 7.9 0 0 0-9.8-9.8Zm-99.4 109.2 45.2-45.2"/>`,
'arrow-right': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M40 128h176m-72-72 72 72-72 72"/>`,
'arrow-left': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M216 128H40m72-72-72 72 72 72"/>`,
code: `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m64 88-48 40 48 40m128-80 48 40-48 40M160 40 96 216"/>`,
'microphone-stage': `<circle cx="168" cy="88" r="64" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m213.3 133.3-90.6-90.6M100 156l-12 12m16.8-70.1L28.1 202.5a7.9 7.9 0 0 0 .8 10.4l14.2 14.2a7.9 7.9 0 0 0 10.4.8l104.6-76.7"/>`,
'pencil-line': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M96 216H48a8 8 0 0 1-8-8v-44.7a7.9 7.9 0 0 1 2.3-5.6l120-120a8 8 0 0 1 11.4 0l44.6 44.6a8 8 0 0 1 0 11.4Zm40-152 56 56"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M216 216H96l-55.5-55.5M164 92l-96 96"/>`,
'rocket-launch': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M94.1 184.6c-11.4 33.9-56.6 33.9-56.6 33.9s0-45.2 33.9-56.6m124.5-56.5L128 173.3 82.7 128l67.9-67.9C176.3 34.4 202 34.7 213 36.3a7.8 7.8 0 0 1 6.7 6.7c1.6 11 1.9 36.7-23.8 62.4Z"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M184.6 116.7v64.6a8 8 0 0 1-2.4 5.6l-32.3 32.4a8 8 0 0 1-13.5-4.1l-8.4-41.9m11.3-101.9H74.7a8 8 0 0 0-5.6 2.4l-32.4 32.3a8 8 0 0 0 4.1 13.5l41.9 8.4"/>`,
list: `<path stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M40 128h176M40 64h176M40 192h176"/>`,
heart: `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M128 216S28 160 28 92a52 52 0 0 1 100-20h0a52 52 0 0 1 100 20c0 68-100 124-100 124Z"/>`,
'moon-stars': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M216 112V64m24 24h-48m-24-64v32m16-16h-32m65 113A92 92 0 0 1 103 39h0a92 92 0 1 0 114 114Z"/>`,
sun: `<circle cx="128" cy="128" r="60" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M128 36V16M63 63 49 49m-13 79H16m47 65-14 14m79 13v20m65-47 14 14m13-79h20m-47-65 14-14"/>`,
'twitter-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M128 88c0-22 18.5-40.3 40.5-40a40 40 0 0 1 36.2 24H240l-32.3 32.3A127.9 127.9 0 0 1 80 224c-32 0-40-12-40-12s32-12 48-36c0 0-64-32-48-120 0 0 40 40 88 48Z"/>`,
'codepen-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m232 101-104 59-104-59 100.1-56.8a8.3 8.3 0 0 1 7.8 0Z"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m232 165-100.1 56.8a8.3 8.3 0 0 1-7.8 0L24 165l104-59Zm0-64v64M24 101v64m104-5v62.8m0-179.6V106"/>`,
'github-logo': `<g stroke-linecap="round" stroke-linejoin="round"><path fill="none" stroke-width="14.7" d="M55.7 167.2c13.9 1 21.3 13.1 22.2 14.6 4.2 7.2 10.4 9.6 18.3 7.1l1.1-3.4a60.3 60.3 0 0 1-25.8-11.9c-12-10.1-18-25.6-18-46.3"/><path fill="none" stroke-width="16" d="M61.4 205.1a24.5 24.5 0 0 1-3-6.1c-3.2-7.9-7.1-10.6-7.8-11.1l-1-.6c-2.4-1.6-9.5-6.5-7.2-13.9 1.4-4.5 6-7.2 12.3-7.2h.8c4 .3 7.6 1.5 10.7 3.2-9.1-10.1-13.6-24.3-13.6-42.3 0-11.3 3.5-21.7 10.1-30.4A46.7 46.7 0 0 1 65 67.3a8.3 8.3 0 0 1 5-4.7c2.8-.9 13.3-2.7 33.2 9.9a105 105 0 0 1 50.5 0c19.9-12.6 30.4-10.8 33.2-9.9 2.3.7 4.1 2.4 5 4.7 5 12.7 4 23.2 2.6 29.4 6.7 8.7 10 18.9 10 30.4 0 42.6-25.8 54.7-43.6 58.7 1.4 4.1 2.2 8.8 2.2 13.7l-.1 23.4v2.3"/><path fill="none" stroke-width="16" d="M160.9 185.7c1.4 4.1 2.2 8.8 2.2 13.7l-.1 23.4v2.3A98.6 98.6 0 1 0 61.4 205c-1.4-2.1-11.3-17.5-11.8-17.8-2.4-1.6-9.5-6.5-7.2-13.9 1.4-4.5 6-7.2 12.3-7.2h.8c4 .3 7.6 1.5 10.7 3.2-9.1-10.1-13.6-24.3-13.6-42.3 0-11.3 3.5-21.7 10.1-30.4A46.4 46.4 0 0 1 65 67.3a8.3 8.3 0 0 1 5-4.7c2.8-.9 13.3-2.7 33.2 9.9a105 105 0 0 1 50.5 0c19.9-12.6 30.4-10.8 33.2-9.9 2.3.7 4.1 2.4 5 4.7 5 12.7 4 23.2 2.6 29.4 6.7 8.7 10 18.9 10 30.4.1 42.6-25.8 54.7-43.6 58.6z"/><path fill="none" stroke-width="18.7" d="m170.1 203.3 17.3-12 17.2-18.7 9.5-26.6v-27.9l-9.5-27.5" /><path fill="none" stroke-width="22.7" d="m92.1 57.3 23.3-4.6 18.7-1.4 29.3 5.4m-110 32.6-8 16-4 21.4.6 20.3 3.4 13" /><path fill="none" stroke-width="13.3" d="M28.8 133a100 100 0 0 0 66.9 94.4v-8.7c-22.4 1.8-33-11.5-35.6-19.8-3.4-8.6-7.8-11.4-8.5-11.8"/></g>`,
'twitch-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M165 200h-42a8 8 0 0 0-5 2l-46 38v-40H48a8 8 0 0 1-8-8V48a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8v108a8 8 0 0 1-3 6l-43 36a8 8 0 0 1-5 2Zm3-112v48m-48-48v48"/>`,
'youtube-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m160 128-48-32v64l48-32z"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M24 128c0 30 3 47 5 56a16 16 0 0 0 10 11c34 13 89 13 89 13s56 0 89-13a16 16 0 0 0 10-11c2-9 5-26 5-56s-3-47-5-56a16 16 0 0 0-10-11c-33-13-89-13-89-13s-55 0-89 13a16 16 0 0 0-10 11c-2 9-5 26-5 56Z"/>`,
'dribbble-logo': `<circle cx="128" cy="128" r="96" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M71 205a160 160 0 0 1 137-77l16 1m-36-76a160 160 0 0 1-124 59 165 165 0 0 1-30-3"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M86 42a161 161 0 0 1 74 177"/>`,
'discord-logo': `<circle stroke="none" cx="96" cy="144" r="12"/><circle stroke="none" cx="160" cy="144" r="12"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M74 80a175 175 0 0 1 54-8 175 175 0 0 1 54 8m0 96a175 175 0 0 1-54 8 175 175 0 0 1-54-8"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="m155 182 12 24a8 8 0 0 0 9 4c25-6 46-16 61-30a8 8 0 0 0 3-8L206 59a8 8 0 0 0-5-5 176 176 0 0 0-30-9 8 8 0 0 0-9 5l-8 24m-53 108-12 24a8 8 0 0 1-9 4c-25-6-46-16-61-30a8 8 0 0 1-3-8L50 59a8 8 0 0 1 5-5 176 176 0 0 1 30-9 8 8 0 0 1 9 5l8 24"/>`,
'linkedin-logo': `<rect width="184" height="184" x="36" y="36" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" rx="8"/><path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M120 112v64m-32-64v64m32-36a28 28 0 0 1 56 0v36"/><circle stroke="none" cx="88" cy="80" r="12"/>`,
'instagram-logo': `<circle cx="128" cy="128" r="40" fill="none" stroke-miterlimit="10" stroke-width="16"/><rect width="184" height="184" x="36" y="36" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" rx="48"/><circle cx="180" cy="76" r="12" stroke="none" />`,
'tiktok-logo': `<path fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="16" d="M168 106a96 96 0 0 0 56 18V84a56 56 0 0 1-56-56h-40v128a28 28 0 1 1-40-25V89a68 68 0 1 0 80 67Z"/>`,
};

View File

@@ -1,47 +0,0 @@
---
import '../styles/global.css';
interface Props {
title?: string | undefined;
description?: string | undefined;
}
const {
title = 'Alex Lebens: Personal Site',
description = 'The personal site of Alex Lebens',
} = Astro.props;
---
<meta charset="UTF-8" />
<meta name="description" property="og:description" content={description} />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<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=Public+Sans:ital,wght@0,400;0,700;1,400&family=Rubik:wght@500;600&display=swap"
rel="stylesheet"
/>
<script is:inline>
// This code is inlined in the head to make dark mode instant & blocking.
const getThemePreference = () => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
};
const isDark = getThemePreference() === 'dark';
document.documentElement.classList[isDark ? 'add' : 'remove']('theme-dark');
if (typeof localStorage !== 'undefined') {
// Watch the document element and persist user preference when it changes.
const observer = new MutationObserver(() => {
const isDark = document.documentElement.classList.contains('theme-dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
});
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
}
</script>

View File

@@ -1,363 +0,0 @@
---
import Icon from './Icon.astro';
import ThemeToggle from './ThemeToggle.astro';
import type { iconPaths } from './IconPaths';
/** Main menu items */
const textLinks: { label: string; href: string }[] = [
{ label: 'Home', href: '/' },
{ label: 'Work', href: '/work/' },
{ label: 'About', href: '/about/' },
];
/** Icon links to social media — edit these with links to your profiles! */
const iconLinks: { label: string; href: string; icon: keyof typeof iconPaths }[] = [
{ label: 'GitHub', href: 'https://github.com/alexlebens', icon: 'github-logo' },
{ label: 'LinkedIn', href: 'https://www.linkedin.com/in/alexanderlebens', icon: 'codepen-logo' },
];
---
<nav>
<div class="menu-header">
<a href="/" class="site-title">
<Icon icon="terminal-window" color="var(--accent-regular)" size="1.6em" gradient />
Alex Lebens
</a>
<menu-button>
<template>
<button class="menu-button" aria-expanded="false">
<span class="sr-only">Menu</span>
<Icon icon="list" />
</button>
</template>
</menu-button>
</div>
<noscript>
<ul class="nav-items">
{
textLinks.map(({ label, href }) => (
<li>
<a
aria-current={Astro.url.pathname === href}
class:list={[
'link',
{
active:
Astro.url.pathname === href ||
(href !== '/' && Astro.url.pathname.startsWith(href)),
},
]}
href={href}
>
{label}
</a>
</li>
))
}
</ul>
</noscript>
<noscript>
<div class="menu-footer">
<div class="socials">
{
iconLinks.map(({ href, icon, label }) => (
<a href={href} class="social">
<span class="sr-only">{label}</span>
<Icon icon={icon} />
</a>
))
}
</div>
</div>
</noscript>
<div id="menu-content" hidden>
<ul class="nav-items">
{
textLinks.map(({ label, href }) => (
<li>
<a
aria-current={Astro.url.pathname === href}
class:list={[
'link',
{
active:
Astro.url.pathname === href ||
(href !== '/' && Astro.url.pathname.startsWith(href)),
},
]}
href={href}
>
{label}
</a>
</li>
))
}
</ul>
<div class="menu-footer">
<div class="socials">
{
iconLinks.map(({ href, icon, label }) => (
<a href={href} class="social">
<span class="sr-only">{label}</span>
<Icon icon={icon} />
</a>
))
}
</div>
<div class="theme-toggle">
<ThemeToggle />
</div>
</div>
</div>
</nav>
<script>
class MenuButton extends HTMLElement {
constructor() {
super();
// Inject menu toggle button when JS runs.
this.appendChild(this.querySelector('template')!.content.cloneNode(true));
const btn = this.querySelector('button')!;
// Hide menu (shown by default to support no-JS browsers).
const menu = document.getElementById('menu-content')!;
menu.hidden = true;
// Add "menu-content" class in JS to avoid covering content in non-JS browsers.
menu.classList.add('menu-content');
/** Set whether the menu is currently expanded or collapsed. */
const setExpanded = (expand: boolean) => {
btn.setAttribute('aria-expanded', expand ? 'true' : 'false');
menu.hidden = !expand;
};
// Toggle menu visibility when the menu button is clicked.
btn.addEventListener('click', () => setExpanded(menu.hidden));
// Hide menu button for large screens.
const handleViewports = (e: MediaQueryList | MediaQueryListEvent) => {
setExpanded(e.matches);
btn.hidden = e.matches;
};
const mediaQueries = window.matchMedia('(min-width: 50em)');
handleViewports(mediaQueries);
mediaQueries.addEventListener('change', handleViewports);
}
}
customElements.define('menu-button', MenuButton);
</script>
<style>
nav {
z-index: 9999;
position: relative;
font-family: var(--font-brand);
font-weight: 500;
margin-bottom: 3.5rem;
}
.menu-header {
display: flex;
justify-content: space-between;
gap: 0.5rem;
padding: 1.5rem;
}
.site-title {
display: flex;
gap: 0.5rem;
align-items: center;
line-height: 1.1;
color: var(--gray-0);
text-decoration: none;
}
.menu-button {
position: relative;
display: flex;
border: 0;
border-radius: 999rem;
padding: 0.5rem;
font-size: 1.5rem;
color: var(--gray-300);
background: radial-gradient(var(--gray-900), var(--gray-800) 150%);
box-shadow: var(--shadow-md);
}
.menu-button[aria-expanded='true'] {
color: var(--gray-0);
background: linear-gradient(180deg, var(--gray-600), transparent),
radial-gradient(var(--gray-900), var(--gray-800) 150%);
}
.menu-button[hidden] {
display: none;
}
.menu-button::before {
position: absolute;
inset: -1px;
content: '';
background: var(--gradient-stroke);
border-radius: 999rem;
z-index: -1;
}
.menu-content {
position: absolute;
left: 0;
right: 0;
}
.nav-items {
margin: 0;
display: flex;
flex-direction: column;
gap: 1rem;
font-size: var(--text-md);
line-height: 1.2;
list-style: none;
padding: 2rem;
background-color: var(--gray-999);
border-bottom: 1px solid var(--gray-800);
}
.link {
display: inline-block;
color: var(--gray-300);
text-decoration: none;
}
.link.active {
color: var(--gray-0);
}
.menu-footer {
--icon-size: var(--text-xl);
--icon-padding: 0.5rem;
display: flex;
justify-content: space-between;
gap: 0.75rem;
padding: 1.5rem 2rem 1.5rem 1.5rem;
background-color: var(--gray-999);
border-radius: 0 0 0.75rem 0.75rem;
box-shadow: var(--shadow-lg);
}
.socials {
display: flex;
flex-wrap: wrap;
gap: 0.625rem;
font-size: var(--icon-size);
}
.social {
display: flex;
padding: var(--icon-padding);
text-decoration: none;
color: var(--accent-dark);
transition: color var(--theme-transition);
}
.social:hover,
.social:focus {
color: var(--accent-text-over);
}
.theme-toggle {
display: flex;
align-items: center;
height: calc(var(--icon-size) + 2 * var(--icon-padding));
}
@media (min-width: 50em) {
nav {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 2.5rem 5rem;
gap: 1rem;
}
.menu-header {
padding: 0;
}
.site-title {
font-size: var(--text-lg);
}
.menu-content {
display: contents;
}
.nav-items {
position: relative;
flex-direction: row;
font-size: var(--text-sm);
border-radius: 999rem;
border: 0;
padding: 0.5rem 0.5625rem;
background: radial-gradient(var(--gray-900), var(--gray-800) 150%);
box-shadow: var(--shadow-md);
}
.nav-items::before {
position: absolute;
inset: -1px;
content: '';
background: var(--gradient-stroke);
border-radius: 999rem;
z-index: -1;
}
.link {
padding: 0.5rem 1rem;
border-radius: 999rem;
transition:
color var(--theme-transition),
background-color var(--theme-transition);
}
.link:hover,
.link:focus {
color: var(--gray-100);
background-color: var(--accent-subtle-overlay);
}
.link.active {
color: var(--accent-text-over);
background-color: var(--accent-regular);
}
.menu-footer {
--icon-padding: 0.375rem;
justify-self: flex-end;
align-items: center;
padding: 0;
background-color: transparent;
box-shadow: none;
}
.socials {
display: none;
}
}
@media (min-width: 60em) {
.socials {
display: flex;
justify-content: flex-end;
gap: 0;
}
}
@media (forced-colors: active) {
.link.active {
color: SelectedItem;
}
}
</style>

View File

@@ -0,0 +1,247 @@
---
import ThemeToggle from './ThemeToggle.astro';
import directus from '../../lib/directus';
import { readSingleton } from '@directus/sdk';
const global = await directus.request(readSingleton('global'));
const links = await directus.request(readSingleton('links'));
const navItems = [
{ text: 'Home', href: '/' },
{ text: 'Blog', href: '/blog' },
{ text: 'About', href: '/about' },
{ text: 'Gitea', href: links.gitea },
{ text: 'RSS', href: 'rss.xml' },
];
const pathname = new URL(Astro.request.url).pathname;
const currentPath = pathname.slice(1);
---
<header
class="fixed top-0 right-0 left-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
transition:animate="none"
>
<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-zinc-100'
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100'
}`}
>
{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">{global.initals}</a>
<button
id="close-menu-button"
class="rounded-md p-2 text-zinc-900 transition-colors hover:bg-zinc-100 dark:text-white dark:hover:bg-zinc-800"
aria-label="Close menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6"
>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</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-zinc-100'
: 'text-zinc-600 group-hover:text-zinc-900 dark:text-zinc-400 dark:group-hover:text-zinc-100'
}`}
style={`transition-delay: ${index * 0.05}s;`}
>
{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('astro:page-load', () => {
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-xs');
} else {
header.classList.remove('shadow-xs');
}
// 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-sm(4px);
}
</style>

View File

@@ -1,16 +0,0 @@
<div class="pill"><slot /></div>
<style>
.pill {
display: flex;
padding: 0.5rem 1rem;
gap: 0.5rem;
color: var(--accent-text-over);
border: 1px solid var(--accent-regular);
background-color: var(--accent-regular);
border-radius: 999rem;
font-size: var(--text-md);
line-height: 1.35;
white-space: nowrap;
}
</style>

View File

@@ -1,64 +0,0 @@
---
import type { CollectionEntry } from 'astro:content';
interface Props {
project: CollectionEntry<'work'>;
}
const { data, slug } = Astro.props.project;
---
<a class="card" href={`/work/${slug}`}>
<span class="title">{data.title}</span>
<img src={data.img} alt={data.img_alt || ''} loading="lazy" decoding="async" />
</a>
<style>
.card {
display: grid;
grid-template: auto 1fr / auto 1fr;
height: 11rem;
background: var(--gradient-subtle);
border: 1px solid var(--gray-800);
border-radius: 0.75rem;
overflow: hidden;
box-shadow: var(--shadow-sm);
text-decoration: none;
font-family: var(--font-brand);
font-size: var(--text-lg);
font-weight: 500;
transition: box-shadow var(--theme-transition);
}
.card:hover {
box-shadow: var(--shadow-md);
}
.title {
grid-area: 1 / 1 / 2 / 2;
z-index: 1;
margin: 0.5rem;
padding: 0.5rem 1rem;
background: var(--gray-999);
color: var(--gray-200);
border-radius: 0.375rem;
}
img {
grid-area: 1 / 1 / 3 / 3;
width: 100%;
height: 100%;
object-fit: cover;
}
@media (min-width: 50em) {
.card {
height: 22rem;
border-radius: 1.5rem;
}
.title {
border-radius: 0.9375rem;
}
}
</style>

View File

@@ -0,0 +1,109 @@
---
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="hover rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on Twitter"
>
<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="hover rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on Facebook"
>
<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="hover rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
aria-label="Share on LinkedIn"
>
<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 rounded-sm bg-zinc-800 px-2 py-1 text-xs whitespace-nowrap text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
>
Copied!
</span>
</button>
</div>
</div>

View File

@@ -1,62 +0,0 @@
---
import Icon from './Icon.astro';
---
<section class="box skills">
<div class="stack gap-2 lg:gap-4">
<Icon icon="terminal-window" color="var(--accent-regular)" size="2.5rem" gradient />
<h2>Full Stack</h2>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod.</p>
</div>
<div class="stack gap-2 lg:gap-4">
<Icon icon="trophy" color="var(--accent-regular)" size="2.5rem" gradient />
<h2>Industry Leader</h2>
<p>Neque viverra justo nec ultrices dui. Est ultricies integer quis auctor elit.</p>
</div>
<div class="stack gap-2 lg:gap-4">
<Icon icon="strategy" color="var(--accent-regular)" size="2.5rem" gradient />
<h2>Strategy-Minded</h2>
<p>Urna porttitor rhoncus dolor purus non enim praesent ornare.</p>
</div>
</section>
<style>
.box {
border: 1px solid var(--gray-800);
border-radius: 0.75rem;
padding: 1.5rem;
background-color: var(--gray-999_40);
box-shadow: var(--shadow-sm);
}
.skills {
display: flex;
flex-direction: column;
gap: 3rem;
}
.skills h2 {
font-size: var(--text-lg);
}
.skills p {
color: var(--gray-400);
}
@media (min-width: 50em) {
.box {
border-radius: 1.5rem;
padding: 2.5rem;
}
.skills {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 5rem;
}
.skills h2 {
font-size: var(--text-2xl);
}
}
</style>

View File

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

View File

@@ -1,95 +1,318 @@
---
import Icon from './Icon.astro';
---
<theme-toggle>
<button>
<span class="sr-only">Dark theme</span>
<span class="icon light"><Icon icon="sun" /></span>
<span class="icon dark"><Icon icon="moon-stars" /></span>
</button>
</theme-toggle>
---
<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:ring-2 focus:ring-zinc-300 focus:outline-hidden sm:p-2 dark:hover:bg-zinc-800 dark:focus:ring-zinc-700"
aria-label="Toggle dark mode"
>
<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 scale-100 rotate-0 text-zinc-800 transition-all duration-500 dark:scale-0 dark:-rotate-90 dark:text-zinc-200"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="5"></circle>
<path
d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"
></path>
</svg>
<!-- Moon icon -->
<svg
xmlns="http://www.w3.org/2000/svg"
class="icon-dark absolute h-5 w-5 scale-0 rotate-90 text-zinc-800 transition-all duration-500 dark:scale-100 dark:rotate-0 dark:text-zinc-200"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</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]');
// Create theme switch overlay element if it doesn't exist
if (!document.querySelector('.theme-switch-overlay')) {
const overlay = document.createElement('div');
overlay.className = 'theme-switch-overlay fixed inset-0 pointer-events-none z-50';
overlay.style.opacity = '0';
overlay.style.transition = 'opacity 0.3s ease-out';
document.body.appendChild(overlay);
}
// Toggle theme when any theme toggle button is clicked
themeToggles.forEach((toggle) => {
// Add event listeners for both click and touch events
['click', 'touchend'].forEach((eventType) => {
toggle.addEventListener(
eventType,
(e) => {
e.preventDefault();
e.stopPropagation();
// Get click/touch position for radial animation
let x, y;
if (e.type === 'touchend' && e.changedTouches && e.changedTouches[0]) {
const rect = toggle.getBoundingClientRect();
x = e.changedTouches[0].clientX - rect.left;
y = e.changedTouches[0].clientY - rect.top;
} else {
const rect = toggle.getBoundingClientRect();
x = e.clientX - rect.left;
y = e.clientY - rect.top;
}
// Set the position variables for the radial gradient
document.documentElement.style.setProperty('--x', `${x}px`);
document.documentElement.style.setProperty('--y', `${y}px`);
// Get the overlay element
const overlay = document.querySelector('.theme-switch-overlay');
// Determine the new theme
const isDark = document.documentElement.classList.contains('dark');
const newTheme = isDark ? 'light' : 'dark';
// Show overlay during transition
if (overlay) {
overlay.style.backgroundColor =
newTheme === 'dark' ? 'rgba(24, 24, 27, 0.3)' : 'rgba(255, 255, 255, 0.3)';
overlay.style.opacity = '1';
}
// Add transition class
document.documentElement.classList.add('theme-switching');
// Add ripple effect
const ripple = document.createElement('span');
ripple.className = 'theme-toggle-ripple';
toggle.appendChild(ripple);
// Force a reflow to ensure all elements update
document.body.offsetHeight;
// Toggle dark mode with a slight delay to allow overlay to appear
setTimeout(() => {
if (isDark) {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
// Store the preference
localStorage.setItem('theme', newTheme);
// Dispatch a custom event for other components to react to
document.dispatchEvent(
new CustomEvent('themeChanged', {
detail: { isDark: newTheme === 'dark' },
})
);
// Force another reflow to ensure all elements update
document.body.offsetHeight;
// Hide overlay after theme has changed
setTimeout(() => {
if (overlay) {
overlay.style.opacity = '0';
}
// Remove transition class after animation completes
document.documentElement.classList.remove('theme-switching');
ripple.remove();
}, 300);
}, 50);
},
{ passive: false }
);
});
// Add touch feedback
toggle.addEventListener(
'touchstart',
() => {
toggle.classList.add('active-touch');
},
{ passive: true }
);
toggle.addEventListener(
'touchend',
() => {
setTimeout(() => {
toggle.classList.remove('active-touch');
}, 150);
},
{ passive: true }
);
});
}
// Run setup on load
document.addEventListener('astro:page-load', setupThemeToggle);
// Also run on page visibility change to ensure theme is consistent
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
const currentTheme = localStorage.getItem('theme');
if (currentTheme === 'dark') {
document.documentElement.classList.add('dark');
} else if (currentTheme === 'light') {
document.documentElement.classList.remove('dark');
}
}
});
// Listen for system preference changes
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', ({ matches }) => {
if (!localStorage.getItem('theme')) {
if (matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
});
</script>
<style>
button {
display: flex;
border: 0;
border-radius: 999rem;
padding: 0;
background-color: var(--gray-999);
box-shadow: inset 0 0 0 1px var(--accent-overlay);
cursor: pointer;
}
.icon {
z-index: 1;
position: relative;
display: flex;
padding: 0.5rem;
width: 2rem;
height: 2rem;
font-size: 1rem;
color: var(--accent-overlay);
}
.icon.light::before {
content: '';
z-index: -1;
position: absolute;
inset: 0;
background-color: var(--accent-regular);
border-radius: 999rem;
}
:global(.theme-dark) .icon.light::before {
transform: translateX(100%);
}
:global(.theme-dark) .icon.dark,
:global(html:not(.theme-dark)) .icon.light,
button[aria-pressed='false'] .icon.light {
color: var(--accent-text-over);
}
@media (prefers-reduced-motion: no-preference) {
.icon,
.icon.light::before {
/* Smooth transition for the entire page when theme changes */
:global(body) {
transition:
transform var(--theme-transition),
color var(--theme-transition);
background-color 0.5s ease,
color 0.5s ease;
}
/* Theme transition overlay */
:global(.theme-switch-overlay) {
position: fixed;
inset: 0;
z-index: 9999;
pointer-events: none;
transition: opacity 0.3s ease-out;
}
/* Ensure theme transitions apply to all elements */
:global(.theme-switching *) {
transition-duration: 0.5s !important;
transition-property: background-color, border-color, color, fill, stroke !important;
}
/* Ripple animation */
.theme-toggle-ripple {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
width: 100%;
height: 100%;
border-radius: 50%;
background-color: rgba(161, 161, 170, 0.3);
animation: ripple 0.8s ease-out;
}
@keyframes ripple {
0% {
transform: translate(-50%, -50%) scale(0);
opacity: 0.5;
}
100% {
transform: translate(-50%, -50%) scale(2.5);
opacity: 0;
}
}
@media (forced-colors: active) {
.icon.light::before {
background-color: SelectedItem;
/* Subtle hover animation */
#theme-toggle {
transform: translateY(0);
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
-webkit-tap-highlight-color: transparent; /* Remove default mobile tap highlight */
min-height: 32px; /* Ensure minimum touch target size */
min-width: 32px; /* Ensure minimum touch target size */
}
/* Only apply hover effects on non-touch devices */
@media (hover: hover) {
#theme-toggle:hover {
transform: translateY(-2px);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
#theme-toggle:hover .icon-light:not(.dark .icon-light) {
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-sm(0 0 2px rgba(129, 140, 248, 0.6));
transform: scale(1.1) rotate(-15deg);
}
}
/* Touch feedback */
#theme-toggle.active-touch {
transform: scale(0.95);
transition: transform 0.15s ease-in-out;
}
/* Optimize animations for mobile */
@media (prefers-reduced-motion: reduce) {
.icon-light,
.icon-dark {
transition: all 0.2s ease-out !important;
}
#theme-toggle,
#theme-toggle:hover {
transform: none;
transition: none;
}
.theme-toggle-ripple {
animation-duration: 0.4s;
}
}
/* Adjust size for very small screens */
@media (max-width: 320px) {
#theme-toggle {
padding: 0.25rem !important;
}
}
</style>
<script>
class ThemeToggle extends HTMLElement {
constructor() {
super();
const button = this.querySelector('button')!;
/** Set the theme to dark/light mode. */
const setTheme = (dark: boolean) => {
document.documentElement.classList[dark ? 'add' : 'remove']('theme-dark');
button.setAttribute('aria-pressed', String(dark));
};
// Toggle the theme when a user clicks the button.
button.addEventListener('click', () => setTheme(!this.isDark()));
// Initialize button state to reflect current theme.
setTheme(this.isDark());
}
isDark() {
return document.documentElement.classList.contains('theme-dark');
}
}
customElements.define('theme-toggle', ThemeToggle);
</script>

View File

@@ -1,15 +0,0 @@
import { defineCollection, z } from 'astro:content';
export const collections = {
work: defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string(),
publishDate: z.coerce.date(),
tags: z.array(z.string()),
img: z.string(),
img_alt: z.string().optional(),
}),
}),
};

View File

@@ -1,23 +0,0 @@
---
title: Bloom Box
publishDate: 2019-12-01 00:00:00
img: /assets/stock-2.jpg
img_alt: A bright pink sheet of paper used to wrap flowers curves in front of rich blue background
description: |
We paired with a cutting-edge music API and a team of horticulturalists
to build AI-generated playlists that maximize houseplant health.
tags:
- Dev
- Branding
- Backend
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur posuere commodo venenatis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam non ligula vel metus efficitur hendrerit. In hac habitasse platea dictumst. Praesent et mauris ut mi dapibus semper. Curabitur tortor justo, efficitur sit amet pretium cursus, porta eget odio. Cras ac venenatis dolor. Donec laoreet posuere malesuada. Curabitur nec mi tempor, placerat leo sit amet, tincidunt est. Quisque pellentesque venenatis magna, eget tristique nibh pulvinar in. Vestibulum vitae volutpat arcu. Aenean ut malesuada odio, sit amet pellentesque odio. Suspendisse nunc elit, blandit nec hendrerit non, aliquet at magna. Donec id leo ut nulla sagittis sodales.
Integer vitae nibh elit. Suspendisse eget urna eu neque bibendum pharetra. Sed interdum lectus sem, in pulvinar magna dignissim vel. Quisque maximus at urna nec laoreet. Suspendisse potenti. Vestibulum rhoncus sem ut mi pellentesque, in vestibulum erat blandit. Aliquam sodales dui ac maximus consectetur. Duis quis est vehicula, imperdiet nisl nec, fermentum erat. Duis tortor diam, pharetra eu euismod in, vehicula non eros. Curabitur facilisis dui at erat ultrices gravida. In at nunc ultricies, pulvinar mi vel, sagittis mauris. Praesent pharetra posuere purus ac imperdiet. Nulla facilisi.
Sed pulvinar porttitor mi in ultricies. Etiam non dolor gravida eros pulvinar pellentesque et dictum ex. Proin eu ornare ligula, sed condimentum dui. Vivamus tincidunt tellus mi, sed semper ipsum pharetra a. Suspendisse sollicitudin at sapien nec volutpat. Etiam justo urna, laoreet ac lacus sed, ultricies facilisis dolor. Integer posuere, metus vel viverra gravida, risus elit ornare magna, id feugiat erat risus ullamcorper libero. Proin vitae diam auctor, laoreet lorem vitae, varius tellus.
Mauris sed eros in ex maximus volutpat. Suspendisse potenti. Donec lacinia justo consectetur sagittis tempor. Proin ullamcorper nisi vitae auctor rhoncus. Sed tristique aliquam augue. Pellentesque vitae fringilla ligula. Nulla arcu elit, efficitur eu nunc malesuada, eleifend tincidunt orci. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer mattis orci in bibendum ultricies. Quisque a dui erat. Phasellus et vulputate ipsum. Proin metus ex, lobortis nec ornare eget, bibendum ut sapien. Aliquam in dolor lobortis, aliquam tellus a, congue augue. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aenean pretium purus augue, ut bibendum erat convallis quis. Cras condimentum quis velit ac mollis. Suspendisse non purus fringilla, venenatis nisl porta, finibus odio. Curabitur aliquet metus faucibus libero interdum euismod. Morbi sed magna nisl. Morbi odio nibh, facilisis vel sapien eu, tempus tincidunt erat. Nullam erat velit, sagittis at purus quis, tristique scelerisque tortor. Pellentesque lacinia tortor id est aliquam viverra. Vestibulum et diam ac ipsum mollis fringilla.

View File

@@ -1,22 +0,0 @@
---
title: h2.0
publishDate: 2019-10-02 00:00:00
img: /assets/stock-4.jpg
img_alt: Soft pink and baby blue water ripples together in a subtle texture.
description: |
We developed brand positioning and design assets for the launch
of a new colored water product.
tags:
- Design
- Branding
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur posuere commodo venenatis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam non ligula vel metus efficitur hendrerit. In hac habitasse platea dictumst. Praesent et mauris ut mi dapibus semper. Curabitur tortor justo, efficitur sit amet pretium cursus, porta eget odio. Cras ac venenatis dolor. Donec laoreet posuere malesuada. Curabitur nec mi tempor, placerat leo sit amet, tincidunt est. Quisque pellentesque venenatis magna, eget tristique nibh pulvinar in. Vestibulum vitae volutpat arcu. Aenean ut malesuada odio, sit amet pellentesque odio. Suspendisse nunc elit, blandit nec hendrerit non, aliquet at magna. Donec id leo ut nulla sagittis sodales.
Integer vitae nibh elit. Suspendisse eget urna eu neque bibendum pharetra. Sed interdum lectus sem, in pulvinar magna dignissim vel. Quisque maximus at urna nec laoreet. Suspendisse potenti. Vestibulum rhoncus sem ut mi pellentesque, in vestibulum erat blandit. Aliquam sodales dui ac maximus consectetur. Duis quis est vehicula, imperdiet nisl nec, fermentum erat. Duis tortor diam, pharetra eu euismod in, vehicula non eros. Curabitur facilisis dui at erat ultrices gravida. In at nunc ultricies, pulvinar mi vel, sagittis mauris. Praesent pharetra posuere purus ac imperdiet. Nulla facilisi.
Sed pulvinar porttitor mi in ultricies. Etiam non dolor gravida eros pulvinar pellentesque et dictum ex. Proin eu ornare ligula, sed condimentum dui. Vivamus tincidunt tellus mi, sed semper ipsum pharetra a. Suspendisse sollicitudin at sapien nec volutpat. Etiam justo urna, laoreet ac lacus sed, ultricies facilisis dolor. Integer posuere, metus vel viverra gravida, risus elit ornare magna, id feugiat erat risus ullamcorper libero. Proin vitae diam auctor, laoreet lorem vitae, varius tellus.
Mauris sed eros in ex maximus volutpat. Suspendisse potenti. Donec lacinia justo consectetur sagittis tempor. Proin ullamcorper nisi vitae auctor rhoncus. Sed tristique aliquam augue. Pellentesque vitae fringilla ligula. Nulla arcu elit, efficitur eu nunc malesuada, eleifend tincidunt orci. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer mattis orci in bibendum ultricies. Quisque a dui erat. Phasellus et vulputate ipsum. Proin metus ex, lobortis nec ornare eget, bibendum ut sapien. Aliquam in dolor lobortis, aliquam tellus a, congue augue. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aenean pretium purus augue, ut bibendum erat convallis quis. Cras condimentum quis velit ac mollis. Suspendisse non purus fringilla, venenatis nisl porta, finibus odio. Curabitur aliquet metus faucibus libero interdum euismod. Morbi sed magna nisl. Morbi odio nibh, facilisis vel sapien eu, tempus tincidunt erat. Nullam erat velit, sagittis at purus quis, tristique scelerisque tortor. Pellentesque lacinia tortor id est aliquam viverra. Vestibulum et diam ac ipsum mollis fringilla.

View File

@@ -1,35 +0,0 @@
---
title: Markdown Mystery Tour
publishDate: 2020-03-02 00:00:00
img: /assets/stock-1.jpg
img_alt: Iridescent ripples of a bright blue and pink liquid
description: |
We designed a whodunnit-style game to introduce Markdown formatting. Suspense — suspicion — syntax!
tags:
- Design
- Dev
- User Testing
---
## Level-two heading
> Tell me and I forget. Teach me and I remember. Involve me and I learn.
Lorem ipsum dolor sit amet, <a href="https://astro.build/">Astro</a> makes people happy. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Proin nibh nisl condimentum id venenatis a condimentum vitae. Dapibus ultrices in iaculis nunc. Arcu odio ut sem nulla pharetra diam sit amet. Diam quis enim lobortis scelerisque fermentum dui faucibus in ornare.
Arcu dui vivamus arcu felis bibendum ut tristique et egestas. Eget gravida cum sociis natoque penatibus. Cras fermentum odio eu feugiat pretium nibh. Proin nibh nisl condimentum id venenatis. Porta nibh venenatis cras sed felis eget velit. Id diam vel quam elementum pulvinar etiam non.
### Level-three heading
Ultrices tincidunt arcu non sodales neque sodales ut. Sed enim ut sem viverra aliquet eget sit amet. Lacus luctus accumsan tortor posuere ac ut consequat semper viverra. Viverra accumsan in nisl nisi scelerisque eu ultrices. In massa tempor nec feugiat nisl pretium fusce.
### Level-three heading
Sed pulvinar porttitor mi in ultricies. Etiam non dolor gravida eros pulvinar pellentesque et dictum ex. Proin eu ornare ligula, sed condimentum dui. Vivamus tincidunt tellus mi, sed semper ipsum pharetra a. Suspendisse sollicitudin at sapien nec volutpat. Etiam justo urna, laoreet ac lacus sed, ultricies facilisis dolor. Integer posuere, metus vel viverra gravida, risus elit ornare magna, id feugiat erat risus ullamcorper libero. Proin vitae diam auctor, laoreet lorem vitae, varius tellus.
Aenean pretium purus augue, ut bibendum erat convallis quis. Cras condimentum quis velit ac mollis. Suspendisse non purus fringilla, venenatis nisl porta, finibus odio. Curabitur aliquet metus faucibus libero interdum euismod. Morbi sed magna nisl. Morbi odio nibh, facilisis vel sapien eu, tempus tincidunt erat. Nullam erat velit, sagittis at purus quis, tristique scelerisque tortor. Pellentesque lacinia tortor id est aliquam viverra. Vestibulum et diam ac ipsum mollis fringilla.
#### Level-four heading
- We noted this
- And also this other point

View File

@@ -1,22 +0,0 @@
---
title: Duvet Genius
publishDate: 2020-03-04 00:00:00
img: /assets/stock-3.jpg
img_alt: Pearls of silky soft white cotton, bubble up under vibrant lighting
description: |
We developed a virtual showcase for the softest bedding imaginable.
tags:
- Design
- Dev
- Branding
---
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur posuere commodo venenatis. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam non ligula vel metus efficitur hendrerit. In hac habitasse platea dictumst. Praesent et mauris ut mi dapibus semper. Curabitur tortor justo, efficitur sit amet pretium cursus, porta eget odio. Cras ac venenatis dolor. Donec laoreet posuere malesuada. Curabitur nec mi tempor, placerat leo sit amet, tincidunt est. Quisque pellentesque venenatis magna, eget tristique nibh pulvinar in. Vestibulum vitae volutpat arcu. Aenean ut malesuada odio, sit amet pellentesque odio. Suspendisse nunc elit, blandit nec hendrerit non, aliquet at magna. Donec id leo ut nulla sagittis sodales.
Integer vitae nibh elit. Suspendisse eget urna eu neque bibendum pharetra. Sed interdum lectus sem, in pulvinar magna dignissim vel. Quisque maximus at urna nec laoreet. Suspendisse potenti. Vestibulum rhoncus sem ut mi pellentesque, in vestibulum erat blandit. Aliquam sodales dui ac maximus consectetur. Duis quis est vehicula, imperdiet nisl nec, fermentum erat. Duis tortor diam, pharetra eu euismod in, vehicula non eros. Curabitur facilisis dui at erat ultrices gravida. In at nunc ultricies, pulvinar mi vel, sagittis mauris. Praesent pharetra posuere purus ac imperdiet. Nulla facilisi.
Sed pulvinar porttitor mi in ultricies. Etiam non dolor gravida eros pulvinar pellentesque et dictum ex. Proin eu ornare ligula, sed condimentum dui. Vivamus tincidunt tellus mi, sed semper ipsum pharetra a. Suspendisse sollicitudin at sapien nec volutpat. Etiam justo urna, laoreet ac lacus sed, ultricies facilisis dolor. Integer posuere, metus vel viverra gravida, risus elit ornare magna, id feugiat erat risus ullamcorper libero. Proin vitae diam auctor, laoreet lorem vitae, varius tellus.
Mauris sed eros in ex maximus volutpat. Suspendisse potenti. Donec lacinia justo consectetur sagittis tempor. Proin ullamcorper nisi vitae auctor rhoncus. Sed tristique aliquam augue. Pellentesque vitae fringilla ligula. Nulla arcu elit, efficitur eu nunc malesuada, eleifend tincidunt orci. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer mattis orci in bibendum ultricies. Quisque a dui erat. Phasellus et vulputate ipsum. Proin metus ex, lobortis nec ornare eget, bibendum ut sapien. Aliquam in dolor lobortis, aliquam tellus a, congue augue. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Aenean pretium purus augue, ut bibendum erat convallis quis. Cras condimentum quis velit ac mollis. Suspendisse non purus fringilla, venenatis nisl porta, finibus odio. Curabitur aliquet metus faucibus libero interdum euismod. Morbi sed magna nisl. Morbi odio nibh, facilisis vel sapien eu, tempus tincidunt erat. Nullam erat velit, sagittis at purus quis, tristique scelerisque tortor. Pellentesque lacinia tortor id est aliquam viverra. Vestibulum et diam ac ipsum mollis fringilla.

2
src/env.d.ts vendored
View File

@@ -1 +1,3 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
/// <reference types="astro/content" />

View File

@@ -1,118 +1,17 @@
---
// Learn about using Astro layouts:
// https://docs.astro.build/en/core-concepts/layouts/
import Layout from './Layout.astro';
// Component Imports
import MainHead from '../components/MainHead.astro';
import Nav from '../components/Nav.astro';
import Footer from '../components/Footer.astro';
import directus from '../../lib/directus';
import { readSingleton } from '@directus/sdk';
interface Props {
title?: string | undefined;
description?: string | undefined;
const global = await directus.request(readSingleton('global'));
export interface Props {
title: string;
description?: string;
}
const { title, description } = Astro.props;
---
<html lang="en">
<head>
<MainHead title={title} description={description} />
</head>
<body>
<div class="stack backgrounds">
<Nav />
<Layout title={global.title} description={global.title}>
<slot />
<Footer />
</div>
<script>
// Add “loaded” class once the document has completely loaded.
addEventListener('load', () => document.documentElement.classList.add('loaded'));
</script>
<style>
:root {
--_placeholder-bg: linear-gradient(transparent, transparent);
--bg-image-main: url('/assets/backgrounds/bg-main-light-800w.jpg');
--bg-image-main-curves: url('/assets/backgrounds/bg-main-light.svg');
--bg-image-subtle-1: var(--_placeholder-bg);
--bg-image-subtle-2: var(--_placeholder-bg);
--bg-image-footer: var(--_placeholder-bg);
--bg-svg-blend-mode: overlay;
--bg-blend-mode: darken;
--bg-image-aspect-ratio: 2.25;
--bg-scale: 1.68;
--bg-aspect-ratio: calc(var(--bg-image-aspect-ratio) / var(--bg-scale));
--bg-gradient-size: calc(var(--bg-scale) * 100%);
}
:root.theme-dark {
--bg-image-main: url('/assets/backgrounds/bg-main-dark-800w.jpg');
--bg-image-main-curves: url('/assets/backgrounds/bg-main-dark.svg');
--bg-svg-blend-mode: darken;
--bg-blend-mode: lighten;
}
/* These backgrounds are displayed below the fold, so we lazy load them
once the `.loaded` class has been set. */
:root.loaded {
--bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-light-800w.jpg');
--bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-light-800w.jpg');
--bg-image-footer: url('/assets/backgrounds/bg-footer-light-800w.jpg');
}
:root.loaded.theme-dark {
--bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-dark-800w.jpg');
--bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-dark-800w.jpg');
--bg-image-footer: url('/assets/backgrounds/bg-footer-dark-800w.jpg');
}
@media (min-width: 50em) {
:root {
--bg-scale: 1;
--bg-image-main: url('/assets/backgrounds/bg-main-light-1440w.jpg');
}
:root.theme-dark {
--bg-image-main: url('/assets/backgrounds/bg-main-dark-1440w.jpg');
}
:root.loaded {
--bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-light-1440w.jpg');
--bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-light-1440w.jpg');
--bg-image-footer: url('/assets/backgrounds/bg-footer-light-1440w.jpg');
}
:root.loaded.theme-dark {
--bg-image-subtle-1: url('/assets/backgrounds/bg-subtle-1-dark-1440w.jpg');
--bg-image-subtle-2: url('/assets/backgrounds/bg-subtle-2-dark-1440w.jpg');
--bg-image-footer: url('/assets/backgrounds/bg-footer-dark-1440w.jpg');
}
}
.backgrounds {
min-height: 100%;
isolation: isolate;
background:
/*noise*/
url('/assets/backgrounds/noise.png') top center/220px repeat,
/*footer*/ var(--bg-image-footer) bottom center/var(--bg-gradient-size) no-repeat,
/*header1*/ var(--bg-image-main-curves) top center/var(--bg-gradient-size) no-repeat,
/*header2*/ var(--bg-image-main) top center/var(--bg-gradient-size) no-repeat,
/*base*/ var(--gray-999);
background-blend-mode: /*noise*/
overlay,
/*footer*/ var(--bg-blend-mode),
/*header1*/ var(--bg-svg-blend-mode),
/*header2*/ normal,
/*base*/ normal;
}
@media (forced-colors: active) {
/* Deactivate custom backgrounds for high contrast users. */
.backgrounds {
background: none;
background-blend-mode: none;
--bg-gradient-size: none;
}
}
</style>
</body>
</html>
</Layout>

168
src/layouts/BlogPost.astro Normal file
View File

@@ -0,0 +1,168 @@
---
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;
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 dark:prose-invert lg:prose-lg mx-auto max-w-4xl"
transition:animate="slide"
>
<div class="hero-text mb-12">
<h1
class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 sm:text-5xl dark:text-zinc-100"
>
{post.title}
</h1>
<p
class="mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400"
>
{post.description}
</p>
<div
class="hero-text mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"
>
<FormattedDate date={post.published_date} />
</div>
<div
class="hero-text mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400"
>
<TagList tags={post.tags} />
</div>
</div>
<!-- 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} />
</div>
</div>
{
post.updated_date && (
<div class="mt-8 text-sm text-zinc-500 italic dark:text-zinc-400">
Last updated on <FormattedDate date={post.updated_date} />
</div>
)
}
</article>
<slot name="after-article" />
</Layout>
<script>
document.addEventListener('astro:page-load', () => {
// Add smooth reveal animations for content after loading
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text div, .hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p, .hero-text + a'
);
heroElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
// Animate posts with staggered delay
const articles = document.querySelectorAll('article.group');
articles.forEach((article, index) => {
setTimeout(
() => {
article.classList.add('animate-reveal');
},
500 + index * 150
);
});
};
animateContent();
});
</script>
<style>
/* Content reveal animations */
.hero-text h1,
.hero-text div,
.hero-text ~ div,
.hero-text span,
.hero-text p,
.hero-text + a,
article.group {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
/* Hero image styling */
article img:first-of-type {
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);
}
</style>

98
src/layouts/Layout.astro Normal file
View File

@@ -0,0 +1,98 @@
---
import { ClientRouter } from 'astro:transitions';
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.png" />
<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"
/>
<!-- Load theme early to prevent flashes between light and dark modes -->
<script is:inline>
const theme = (() => {
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
return localStorage.getItem('theme');
}
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
})();
if (theme === 'light') {
document.documentElement.classList.remove('dark');
} else {
document.documentElement.classList.add('dark');
}
window.localStorage.setItem('theme', theme);
</script>
<ClientRouter />
</head>
<body
class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100"
>
<Background />
<div class="mx-auto w-full max-w-3xl grow px-4 sm:px-6">
<Navigation />
<main class="py-12">
<slot />
</main>
</div>
<Footer />
</body>
</html>
<style>
/* 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

@@ -0,0 +1,890 @@
/* 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);
}

View File

@@ -1,8 +1,368 @@
---
import Hero from '../components/Hero.astro';
import BaseLayout from '../layouts/BaseLayout.astro';
import Layout from '../layouts/Layout.astro';
---
<BaseLayout title="Not Found" description="404 Error — this page was not found">
<Hero title="Page Not Found" tagline="Not found" />
</BaseLayout>
<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"
transition:animate="slide"
>
<!-- Main content with animation -->
<div class="relative z-10 mx-auto max-w-xl">
<div class="glitch-wrapper">
<h1
class="glitch text-9xl leading-none font-bold text-zinc-900 sm:text-[12rem] dark:text-zinc-100"
data-text="404"
>
404
</h1>
</div>
<h2 class="mt-6 text-2xl font-bold text-zinc-800 sm:text-3xl dark:text-zinc-200">
Page Not Found
</h2>
<p class="mx-auto mt-6 max-w-md text-lg text-zinc-600 dark:text-zinc-400">
The page you're looking for does not exist.
</p>
<div class="mt-10 flex flex-col items-center justify-center gap-4 sm:flex-row">
<a
href="/"
class="group relative inline-flex items-center gap-2 overflow-hidden rounded-lg bg-zinc-900 px-6 py-3 text-zinc-100 shadow-lg transition-all duration-300 hover:bg-zinc-800 hover:shadow-xl dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-200"
>
<span
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"
>
<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-xs transition-all duration-300 hover:bg-zinc-100 hover:shadow-md dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
class="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-xs backdrop-blur-xs dark:border-zinc-700/50 dark:bg-zinc-800/50"
>
<h3 class="text-sm font-medium tracking-wider text-zinc-500 uppercase dark:text-zinc-400">
Did you know?
</h3>
<p class="mt-2 text-sm text-zinc-700 dark:text-zinc-300" id="fun-fact">
The 404 error code originated when CERN's web server displayed room 404 (their server
room) as the error message when a file wasn't found.
</p>
</div>
</div>
</div>
</Layout>
<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.",
"Some websites turn their 404 pages into games, like Google's Pac-Man 404 page that once existed.",
'The first web server was a NeXT computer used by Tim Berners-Lee at CERN, where the 404 error was born.',
'Many companies use creative 404 pages as a way to showcase their brand personality and humor.',
"The HTTP 1.0 specification from 1996 officially defined the 404 error as 'Not Found'.",
'Studies show that well-designed 404 pages can reduce bounce rates by up to 30%.',
'The most common cause of 404 errors is mistyped URLs.',
];
// Display a random fun fact
const funFactElement = document.getElementById('fun-fact');
if (funFactElement) {
const randomFact = funFacts[Math.floor(Math.random() * funFacts.length)];
funFactElement.textContent = randomFact;
}
</script>
<style>
/* Glitch effect for 404 text */
.glitch-wrapper {
position: relative;
display: inline-block;
}
.glitch {
position: relative;
display: inline-block;
animation: glitch-skew 1s infinite linear alternate-reverse;
}
.glitch::before,
.glitch::after {
content: attr(data-text);
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.glitch::before {
left: 2px;
text-shadow: -2px 0 #ff00c1;
clip: rect(44px, 450px, 56px, 0);
animation: glitch-anim 5s infinite linear alternate-reverse;
}
.glitch::after {
left: -2px;
text-shadow:
-2px 0 #00fff9,
2px 2px #ff00c1;
animation: glitch-anim2 1s infinite linear alternate-reverse;
}
@keyframes glitch-anim {
0% {
clip: rect(31px, 9999px, 94px, 0);
transform: skew(0.85deg);
}
5% {
clip: rect(70px, 9999px, 71px, 0);
transform: skew(0.17deg);
}
10% {
clip: rect(9px, 9999px, 85px, 0);
transform: skew(0.4deg);
}
15% {
clip: rect(47px, 9999px, 18px, 0);
transform: skew(0.22deg);
}
20% {
clip: rect(7px, 9999px, 78px, 0);
transform: skew(0.96deg);
}
25% {
clip: rect(53px, 9999px, 54px, 0);
transform: skew(0.05deg);
}
30% {
clip: rect(84px, 9999px, 52px, 0);
transform: skew(0.94deg);
}
35% {
clip: rect(46px, 9999px, 7px, 0);
transform: skew(0.01deg);
}
40% {
clip: rect(2px, 9999px, 66px, 0);
transform: skew(0.66deg);
}
45% {
clip: rect(34px, 9999px, 33px, 0);
transform: skew(0.52deg);
}
50% {
clip: rect(80px, 9999px, 73px, 0);
transform: skew(0.9deg);
}
55% {
clip: rect(8px, 9999px, 81px, 0);
transform: skew(0.3deg);
}
60% {
clip: rect(10px, 9999px, 86px, 0);
transform: skew(0.85deg);
}
65% {
clip: rect(36px, 9999px, 25px, 0);
transform: skew(0.28deg);
}
70% {
clip: rect(75px, 9999px, 31px, 0);
transform: skew(0.46deg);
}
75% {
clip: rect(46px, 9999px, 87px, 0);
transform: skew(0.44deg);
}
80% {
clip: rect(19px, 9999px, 40px, 0);
transform: skew(0.07deg);
}
85% {
clip: rect(85px, 9999px, 88px, 0);
transform: skew(0.71deg);
}
90% {
clip: rect(1px, 9999px, 89px, 0);
transform: skew(0.76deg);
}
95% {
clip: rect(44px, 9999px, 25px, 0);
transform: skew(0.58deg);
}
100% {
clip: rect(31px, 9999px, 26px, 0);
transform: skew(0.6deg);
}
}
@keyframes glitch-anim2 {
0% {
clip: rect(65px, 9999px, 65px, 0);
transform: skew(0.16deg);
}
5% {
clip: rect(8px, 9999px, 42px, 0);
transform: skew(0.65deg);
}
10% {
clip: rect(64px, 9999px, 30px, 0);
transform: skew(0.42deg);
}
15% {
clip: rect(29px, 9999px, 49px, 0);
transform: skew(0.05deg);
}
20% {
clip: rect(25px, 9999px, 56px, 0);
transform: skew(0.09deg);
}
25% {
clip: rect(76px, 9999px, 98px, 0);
transform: skew(0.79deg);
}
30% {
clip: rect(72px, 9999px, 3px, 0);
transform: skew(0.12deg);
}
35% {
clip: rect(20px, 9999px, 60px, 0);
transform: skew(0.09deg);
}
40% {
clip: rect(61px, 9999px, 47px, 0);
transform: skew(0.45deg);
}
45% {
clip: rect(29px, 9999px, 69px, 0);
transform: skew(0.09deg);
}
50% {
clip: rect(82px, 9999px, 96px, 0);
transform: skew(0.05deg);
}
55% {
clip: rect(33px, 9999px, 91px, 0);
transform: skew(0.16deg);
}
60% {
clip: rect(56px, 9999px, 23px, 0);
transform: skew(0.01deg);
}
65% {
clip: rect(46px, 9999px, 21px, 0);
transform: skew(0.89deg);
}
70% {
clip: rect(50px, 9999px, 1px, 0);
transform: skew(0.85deg);
}
75% {
clip: rect(82px, 9999px, 33px, 0);
transform: skew(0.87deg);
}
80% {
clip: rect(94px, 9999px, 46px, 0);
transform: skew(0.64deg);
}
85% {
clip: rect(48px, 9999px, 95px, 0);
transform: skew(0.43deg);
}
90% {
clip: rect(60px, 9999px, 10px, 0);
transform: skew(0.29deg);
}
95% {
clip: rect(85px, 9999px, 62px, 0);
transform: skew(0.66deg);
}
100% {
clip: rect(61px, 9999px, 58px, 0);
transform: skew(0.74deg);
}
}
@keyframes glitch-skew {
0% {
transform: skew(-1deg);
}
10% {
transform: skew(0deg);
}
20% {
transform: skew(0.5deg);
}
30% {
transform: skew(-0.5deg);
}
40% {
transform: skew(0.2deg);
}
50% {
transform: skew(0deg);
}
60% {
transform: skew(-0.5deg);
}
70% {
transform: skew(0.8deg);
}
80% {
transform: skew(-0.2deg);
}
90% {
transform: skew(0.5deg);
}
100% {
transform: skew(0deg);
}
}
</style>

View File

@@ -1,120 +1,477 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import DynamicIcon from '../utils/DynamicIcon.tsx';
import ContactCTA from '../components/ContactCTA.astro';
import Hero from '../components/Hero.astro';
import directus from '../../lib/directus';
import { readSingleton, readItems } from '@directus/sdk';
const global = await directus.request(readSingleton('global'));
const about = await directus.request(readSingleton('about'));
const skills = await directus.request(
readItems('skills', {
fields: ['*'],
})
);
---
<BaseLayout title="About | Alex Lebens" description="About Alex Lebens">
<div class="stack gap-20">
<main class="wrapper about">
<Hero
title="About"
tagline="Thanks for stopping by. Read below to learn more about myself and my background."
<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"
transition:animate="slide"
>
<!-- Hero Section -->
<div class="relative mb-12 sm:mb-16 md:mb-20">
<div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12">
<div class="hero-text order-2 text-center md:order-1 md:text-left">
<h1
class="theme-transition-color hero-text mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:mb-6 sm:text-4xl md:text-5xl dark:text-zinc-100"
>
Hello, I'm <span
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 hero-text mb-6 text-lg leading-relaxed text-zinc-600 sm:mb-8 sm:text-xl dark:text-zinc-400"
>
{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 sm:max-w-[320px] sm:border-8 sm:shadow-2xl md:max-w-md dark:border-zinc-800"
>
<img
width="1553"
height="873"
src="/assets/hiking.jpg"
alt="Alex Lebens hiking in Texas"
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"
/>
</Hero>
</div>
</div>
</div>
</div>
<section>
<h2 class="section-title">Background</h2>
<div class="content">
<p>
Lorem ipsum dolor sit amet, <a href="https://astro.build/">Astro</a> makes people happy.
Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Proin nibh nisl condimentum
id venenatis a condimentum vitae. Dapibus ultrices in iaculis nunc. Arcu odio ut sem nulla
pharetra diam sit amet. Diam quis enim lobortis scelerisque fermentum dui faucibus in ornare.
</p>
<p>
Arcu dui vivamus arcu felis bibendum ut tristique et egestas. Eget gravida cum sociis
natoque penatibus. Cras fermentum odio eu feugiat pretium nibh. Proin nibh nisl
condimentum id venenatis. Porta nibh venenatis cras sed felis eget velit. Id diam vel
quam elementum pulvinar etiam non.
</p>
<p>
Ultrices tincidunt arcu non sodales neque sodales ut. Sed enim ut sem viverra aliquet
eget sit amet. Lacus luctus accumsan tortor posuere ac ut consequat semper viverra.
Viverra accumsan in nisl nisi scelerisque eu ultrices. In massa tempor nec feugiat nisl
pretium fusce.
</p>
</div>
</section>
<section>
<h2 class="section-title">Education</h2>
<div class="content">
<p>Corporis voluptates tenetur laudantium.</p>
</div>
</section>
<section>
<h2 class="section-title">Skills</h2>
<div class="content">
<p>officia unde omnis</p>
</div>
</section>
</main>
<!-- 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 sm:mb-8 sm:text-3xl md:justify-start dark:text-zinc-100"
>
<span
class="theme-transition-bg mr-4 hidden h-1 w-8 bg-zinc-300 sm:inline-block sm:w-12 dark:bg-zinc-700"
></span>
About Me
<span
class="theme-transition-bg ml-4 hidden h-1 w-8 bg-zinc-300 sm:inline-block sm:w-12 dark:bg-zinc-700"
></span>
</h2>
<ContactCTA />
<div class="theme-transition-all hero-text prose prose-zinc dark:prose-invert max-w-none">
<p
class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
>
{about.experience}
</p>
<p
class="theme-transition-color hero-text mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg"
>
{about.education}
</p>
<p
class="theme-transition-color hero-text 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 sm:mb-12 sm:text-3xl dark:text-zinc-100"
>
Tech Stack
</h2>
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
<!-- Main slider container -->
<div class="slider-track animate-slide flex">
{
[...skills, ...skills, ...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 sm:mx-4 sm:min-w-[280px] dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600"
>
<div class="p-4 sm:p-6">
<div class="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 sm:h-12 sm:w-12 dark:bg-zinc-800 dark:text-zinc-200">
<DynamicIcon name={skill.icon} />
</div>
<h3 class="theme-transition-color text-base font-semibold text-zinc-900 sm:text-xl dark:text-zinc-100">
{skill.title}
</h3>
</div>
<span class="theme-transition-all rounded-full bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-600 sm:px-2.5 sm:py-1 sm:text-sm dark:bg-zinc-800 dark:text-zinc-400">
{skill.level}%
</span>
</div>
<div class="theme-transition-bg relative h-1.5 w-full overflow-hidden rounded-full bg-zinc-100 sm:h-2 dark:bg-zinc-700">
<div
class="progress-bar-animate theme-transition-bg absolute top-0 left-0 h-full rounded-full bg-gradient-to-r from-zinc-700 via-zinc-600 to-zinc-800 transition-all duration-1000 dark:from-zinc-300 dark:via-zinc-400 dark:to-zinc-200"
style={`width: ${skill.level}%`}
/>
</div>
<div class="theme-transition-color mt-1 flex justify-between font-mono text-[10px] text-zinc-400 sm:mt-2 sm:text-xs dark:text-zinc-500">
<span>Beginner</span>
<span>Advanced</span>
</div>
</div>
</div>
))
}
</div>
<!-- Gradient overlays for smooth fade effect -->
<div
class="theme-transition-bg absolute top-0 bottom-0 left-0 z-10 w-12 bg-gradient-to-r from-white to-transparent sm:w-24 dark:from-zinc-900"
>
</div>
<div
class="theme-transition-bg absolute top-0 right-0 bottom-0 z-10 w-12 bg-gradient-to-l from-white to-transparent sm:w-24 dark:from-zinc-900"
>
</div>
</div>
</div>
<!-- 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 sm:mb-6 sm:text-3xl dark:text-zinc-100"
>
Get in Touch
</h2>
<p
class="theme-transition-color mb-6 text-base text-zinc-600 sm:mb-8 sm:text-lg dark:text-zinc-400"
>
I'm always open to new opportunities and collaborations. If you'd like to work together or
just say hello, feel free to reach out.
</p>
<div class="group">
<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 group-hover:bg-blue-600 group-hover:text-zinc-100 sm:px-8 sm:py-4 sm:text-lg dark:bg-zinc-100 dark:text-zinc-900 dark:group-hover:bg-blue-600 dark:group-hover:text-zinc-100"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
></path>
</svg>
<span class="relative inline-block overflow-hidden">
<span class="relative z-10">Say Hello</span>
<span
class="absolute bottom-0 left-0 h-0.5 w-0 bg-zinc-100 transition-all duration-300 group-hover:w-full"
></span>
</span>
</a>
</div>
</div>
</div>
</BaseLayout>
<script>
document.addEventListener('astro:page-load', () => {
// Add smooth reveal animations for content after loading
const animateContent = () => {
const heroElements = document.querySelectorAll(
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
);
heroElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
};
animateContent();
// Create seamless infinite scrolling effect
const sliderTrack = document.querySelector('.slider-track');
function setupInfiniteScroll() {
const cards = document.querySelectorAll('.skill-card');
if (!cards.length) return;
// 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', () => {
cards.forEach((card, index) => {
setTimeout(() => {
card.classList.add('theme-changing');
setTimeout(() => {
card.classList.remove('theme-changing');
}, 600);
}, index * 50);
});
});
});
</script>
<style>
.about {
/* 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 */
@media (max-width: 640px) {
.skill-card {
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
}
.skill-card:hover {
transform: translateY(-5px) !important;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
}
}
.skill-card:before {
content: '';
position: absolute;
top: -10%;
left: -10%;
width: 120%;
height: 120%;
background: radial-gradient(
circle at center,
rgba(255, 255, 255, 0.1) 0%,
rgba(255, 255, 255, 0) 70%
);
opacity: 0;
transition: opacity 0.5s ease;
pointer-events: none;
}
.skill-card:hover:before {
opacity: 1;
}
.progress-bar-animate {
position: relative;
overflow: hidden;
}
.progress-bar-animate:after {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: progress-shine 2s infinite;
}
@keyframes progress-shine {
0% {
left: -100%;
}
100% {
left: 100%;
}
}
/* Touch targets for mobile */
@media (max-width: 640px) {
a,
button {
min-height: 44px;
display: flex;
flex-direction: column;
gap: 3.5rem;
align-items: center;
}
img {
margin-top: 1.5rem;
border-radius: 1.5rem;
box-shadow: var(--shadow-md);
.social-link {
min-width: 44px;
min-height: 44px;
}
}
section {
display: flex;
flex-direction: column;
gap: 0.5rem;
color: var(--gray-200);
/* Content reveal animations */
.hero-text h1,
.hero-text span,
.hero-text p,
.hero-text ~ div {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.section-title {
grid-column-start: 1;
font-size: var(--text-xl);
color: var(--gray-0);
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
.content {
grid-column: 2 / 4;
/* Theme transition effect */
:global(.theme-switching) .theme-transition-element {
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
}
.content :global(a) {
text-decoration: 1px solid underline transparent;
text-underline-offset: 0.25em;
transition: text-decoration-color var(--theme-transition);
}
.content :global(a:hover),
.content :global(a:focus) {
text-decoration-color: currentColor;
}
@media (min-width: 50em) {
.about {
display: grid;
grid-template-columns: 1fr 60% 1fr;
}
.about > :global(:first-child) {
grid-column-start: 2;
}
section {
display: contents;
font-size: var(--text-lg);
}
/* Smooth card transition during theme switch */
.skill-card.theme-transition-element {
transition:
background-color var(--theme-transition),
border-color var(--theme-transition),
color var(--theme-transition),
box-shadow var(--theme-transition),
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
</style>

View File

@@ -0,0 +1,371 @@
---
import BlogPost from '../../layouts/BlogPost.astro';
import directus from '../../../lib/directus';
import { readItems } from '@directus/sdk';
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 { post, nextPost, prevPost } = Astro.props;
---
<BlogPost
slug={post.slug}
title={post.title}
description={post.description}
content={post.content}
image={post.image}
image_alt={post.image_alt}
published_date={post.published_date}
updated_date={post.updated_date}
tags={post.tags}
>
<!-- Main Content -->
<div
class="hero-text prose prose-sm prose-zinc dark:prose-invert sm:prose-base prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:font-medium prose-a:text-zinc-800 prose-a:underline-offset-4 hover:prose-a:text-zinc-600 prose-img:rounded-xl dark:prose-a:text-zinc-300 dark:hover:prose-a:text-zinc-100 max-w-none"
>
<div set:html={post.content} />
</div>
<!-- Next/Previous Navigation -->
<div
class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2 dark:border-zinc-800"
>
{
prevPost && (
<a
href={`/blog/${prevPost.slug}`}
class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 sm:p-6 dark:border-zinc-800 dark:hover:bg-zinc-800/50"
>
<div class="absolute inset-0 bg-gradient-to-r from-zinc-100 to-transparent 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 sm:mb-2 sm:gap-2 sm:text-sm dark:text-zinc-400">
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="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 sm:text-lg dark:text-white dark:group-hover:text-zinc-300">
{prevPost.title}
</h3>
</a>
)
}
{
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 sm:p-6 md:text-right dark:border-zinc-800 dark:hover:bg-zinc-800/50"
>
<div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
<span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 sm:mb-2 sm:gap-2 sm:text-sm md:justify-end dark:text-zinc-400">
Next Article
<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 sm:text-lg dark:text-white dark:group-hover:text-zinc-300">
{nextPost.title}
</h3>
</a>
)
}
</div>
</BlogPost>
<script>
document.addEventListener('astro:page-load', () => {
// Show button when scrolled down
const backToTopButton = document.getElementById('back-to-top');
if (backToTopButton) {
const toggleBackToTopButton = () => {
if (window.scrollY > 300) {
backToTopButton.classList.remove('opacity-0', 'invisible');
backToTopButton.classList.add('opacity-100', 'visible');
} else {
backToTopButton.classList.remove('opacity-100', 'visible');
backToTopButton.classList.add('opacity-0', 'invisible');
}
};
// Scroll to top when clicked
backToTopButton.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
// Check scroll position
window.addEventListener('scroll', toggleBackToTopButton);
toggleBackToTopButton();
}
// Add smooth reveal animations for content after loading
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
);
heroElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
// Animate posts with staggered delay
const articles = document.querySelectorAll('article.group');
articles.forEach((article, index) => {
setTimeout(
() => {
article.classList.add('animate-reveal');
},
500 + index * 150
);
});
};
animateContent();
});
// Add copy buttons to code blocks
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);
}
});
}
// Main initialization function
function initializeBlogPost() {
initializeCodeCopyButtons();
// 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);
}
}
// Re-initialize when content changes via Astro's view transitions
document.addEventListener('astro:page-load', initializeBlogPost);
</script>
<style>
/* Content reveal animations */
.hero-text h1,
.hero-text span,
.hero-text p,
.hero-text ~ div,
article.group {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
/* Language badge styling */
.language-badge {
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-sm bg-zinc-100 px-1.5 py-0.5 text-sm font-medium text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200;
}
.prose pre {
@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>

845
src/pages/blog/index.astro Normal file
View File

@@ -0,0 +1,845 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import FormattedDate from '../../components/FormattedDate.astro';
import TagList from '../../components/TagList.astro';
import directus from '../../../lib/directus';
import { readItems } from '@directus/sdk';
const posts = await directus.request(
readItems('posts', {
fields: ['*'],
sort: ['-published_date'],
})
);
// Group posts by year for timeline effect
const sortedPosts = posts.sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf());
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);
---
<BaseLayout title="Blog">
<div class="mx-auto w-full max-w-6xl px-4 py-10 sm:px-6 sm:py-16" transition:animate="slide">
<div class="relative mb-12 sm:mb-20">
<div class="hero-text relative text-center">
<h1
class="hero-text mb-4 text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl dark:text-zinc-100"
>
Blog
</h1>
<p
class="hero-text mx-auto mb-6 max-w-2xl text-sm text-zinc-600 sm:mb-10 sm:text-base dark:text-zinc-400"
>
Thoughts, ideas, and explorations on technology and selfhosting.
</p>
</div>
</div>
<!-- Featured post -->
<div class="grid grid-cols-1 gap-6 sm:gap-8 md:grid-cols-12">
{
sortedPosts.length > 0 && (
<div class="mb-8 sm:mb-12 md:col-span-12">
<article class="hover-3d theme-transition-element group relative mx-auto flex max-w-2xl flex-col p-5 sm:mx-0 sm:p-8">
<div class="absolute inset-0 rounded-2xl border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 hover:bg-zinc-50/80 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70 dark:hover:bg-zinc-900/50" />
<div class="flex flex-col gap-5 sm:flex-row sm:gap-6">
{sortedPosts[0].image && (
<div class="z-10 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}?width=500`}
alt={sortedPosts[0].image_alt}
class="h-full w-full object-cover"
loading="eager"
/>
</div>
)}
<div class="z-10 flex-1">
<h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 sm:mb-3 sm:text-left sm:text-2xl dark:text-zinc-100">
<a
href={`/blog/${sortedPosts[0].slug}/`}
class="before:absolute before:inset-0"
>
{sortedPosts[0].title}
</a>
</h2>
<p class="mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400">
{sortedPosts[0].description}
</p>
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400">
<FormattedDate date={sortedPosts[0].published_date} />
</div>
</div>
</div>
<div class="z-10 mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800">
<TagList tags={sortedPosts[0].tags} />
<div class="mx-auto sm:mr-0 sm:ml-auto">
<a
href={`/blog/${sortedPosts[0].slug}`}
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100"
>
<span class="relative inline-block overflow-hidden">
<span class="relative z-10">Read article</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>
<svg
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
>
<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>
</div>
</div>
</article>
</div>
)
}
<!-- Sidebar for mobile -->
<div class="relative md:col-span-3">
<div class="mb-8 space-y-4 md:sticky md:top-24 md:mb-0">
<h3
class="mb-4 text-center text-lg font-medium tracking-wider text-zinc-900 uppercase md:text-left dark:text-zinc-100"
>
History
</h3>
<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 rounded-xl border border-zinc-300 bg-white/50 px-4 py-2 whitespace-nowrap transition-all duration-300 hover:bg-zinc-50 sm:rounded-2xl md:mr-0 md:w-full md:px-0 md:py-3 md:whitespace-normal dark:border-zinc-800 dark:hover:bg-zinc-800/70 ${index === 0 ? 'bg-white/50 dark:bg-zinc-900/50' : ''}`}
>
<span class="ml-3 text-base font-medium text-zinc-900 md:text-lg dark:text-zinc-100">
{year}
</span>
<span class="mr-3 text-xs text-zinc-500 md:ml-auto md:text-sm dark:text-zinc-400">
{postsByYear[year].length} post{postsByYear[year].length !== 1 ? 's' : ''}
</span>
</a>
))
}
</div>
</div>
</div>
<!-- Post grid -->
<div class="md:col-span-9">
{
years.map((year) => (
<div id={`year-${year}`} class="mb-12 scroll-mt-16 sm:mb-20">
<h2 class="mb-6 border-b border-zinc-200 pb-3 text-center text-xl font-bold text-zinc-900 sm:mb-8 sm:pb-4 sm:text-2xl md:text-left dark:border-zinc-800 dark:text-zinc-100">
{year}
</h2>
<div
class={`grid grid-cols-1 ${postsByYear[year].length >= 2 ? 'md:grid-cols-2' : 'md:grid-cols-1'} gap-8 sm:gap-12`}
>
{postsByYear[year].map((post) => (
<article class="hover-3d theme-transition-element group relative mx-auto flex max-w-2xl flex-col p-5 sm:mx-0 sm:p-8">
<div class="absolute inset-0 rounded-2xl border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 hover:bg-zinc-50/80 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70 dark:hover:bg-zinc-900/50" />
{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"
loading="lazy"
/>
</div>
)}
<div class="flex flex-1 flex-col">
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:gap-4 sm:text-sm md:justify-start dark:text-zinc-400">
{post.pubDate && (
<time
datetime={post.published_date.toLocaleString()}
class="flex items-center"
>
{post.published_date.toLocaleString('en-US', {
month: 'short',
day: 'numeric',
})}
</time>
)}
</div>
<h3 class="z-10 mb-2 text-center text-lg font-semibold text-zinc-900 transition-colors group-hover:text-zinc-700 sm:mb-3 sm:text-xl md:text-left dark:text-zinc-100 dark:group-hover:text-zinc-300">
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
{post.title}
</a>
</h3>
<p class="z-10 mb-4 line-clamp-2 grow text-center text-sm text-zinc-600 md:text-left dark:text-zinc-400">
{post.description}
</p>
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400">
<FormattedDate date={post.published_date} />
</div>
<div class="z-10 mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800">
<TagList tags={post.tags} />
<div class="mx-auto sm:mr-0 sm:ml-auto">
<a
href={`/blog/${post.slug}`}
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100"
>
<span class="relative inline-block overflow-hidden">
<span class="relative z-10">Read article</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>
<svg
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
>
<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>
</div>
</div>
</div>
</article>
))}
</div>
</div>
))
}
</div>
</div>
</div>
</BaseLayout>
<script>
document.addEventListener('astro:page-load', () => {
// Force the viewport to be exactly the width of the device
const fixViewportWidth = () => {
const viewport = document.querySelector('meta[name="viewport"]');
if (!viewport) {
const meta = document.createElement('meta');
meta.name = 'viewport';
meta.content = 'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no';
document.getElementsByTagName('head')[0].appendChild(meta);
} else {
viewport.setAttribute(
'content',
'width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no'
);
}
// Fix for horizontal overflow
document.body.style.overflowX = 'hidden';
document.documentElement.style.overflowX = 'hidden';
document.documentElement.style.width = '100%';
document.body.style.width = '100%';
};
fixViewportWidth();
// Show button when scrolled down
const backToTopButton = document.getElementById('back-to-top');
if (backToTopButton) {
const toggleBackToTopButton = () => {
if (window.scrollY > 300) {
backToTopButton.classList.remove('opacity-0', 'invisible');
backToTopButton.classList.add('opacity-100', 'visible');
} else {
backToTopButton.classList.remove('opacity-100', 'visible');
backToTopButton.classList.add('opacity-0', 'invisible');
}
};
// Scroll to top when clicked
backToTopButton.addEventListener('click', () => {
window.scrollTo({
top: 0,
behavior: 'smooth',
});
});
// Check scroll position
window.addEventListener('scroll', toggleBackToTopButton);
toggleBackToTopButton();
}
// Add smooth reveal animations for content after loading
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
);
heroElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
// Animate posts with staggered delay
const articles = document.querySelectorAll('article.group');
articles.forEach((article, index) => {
setTimeout(
() => {
article.classList.add('animate-reveal');
},
500 + index * 150
);
});
};
animateContent();
// Add 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);
}
});
});
// Adjust tag items based on screen size with extreme precision
const adjustTagItems = () => {
const tagItems = document.querySelectorAll('.theme-ripple');
const width =
window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
const isVerySmall = width < 360;
const isExtremelySmall = width < 280;
const isMicroScreen = width < 240;
// Fix container width to match viewport exactly
const container = document.querySelector('.tag-cloud');
if (container) {
container.style.maxWidth = '100vw';
container.style.width = '100%';
container.style.boxSizing = 'border-box';
// Remove any margins that might cause overflow
container.style.marginLeft = '0';
container.style.marginRight = '0';
}
// Fix grid width
const grid = document.querySelector('.grid');
if (grid) {
grid.style.width = '100%';
grid.style.maxWidth = '100%';
}
tagItems.forEach((item) => {
// Set appropriate classes based on screen size
if (isMicroScreen) {
item.classList.add('micro-screen');
item.classList.remove('extremely-small-screen', 'very-small-screen');
} else if (isExtremelySmall) {
item.classList.add('extremely-small-screen');
item.classList.remove('very-small-screen', 'micro-screen');
} else if (isVerySmall) {
item.classList.add('very-small-screen');
item.classList.remove('extremely-small-screen', 'micro-screen');
} else {
item.classList.remove('very-small-screen', 'extremely-small-screen', 'micro-screen');
}
// Ensure text doesn't overflow on small screens
const tagName = item.querySelector('h3');
const tagCount = item.querySelector('p');
if (tagName) {
// Set title for accessibility
tagName.title = tagName.textContent.trim();
// Adjust text length based on screen size
if (isMicroScreen && tagName.textContent.length > 6) {
tagName.dataset.fullText = tagName.textContent;
tagName.textContent = tagName.textContent.substring(0, 6) + '...';
} else if (isExtremelySmall && tagName.textContent.length > 8) {
tagName.dataset.fullText = tagName.textContent;
tagName.textContent = tagName.textContent.substring(0, 8) + '...';
} else if (isVerySmall && tagName.textContent.length > 12) {
tagName.dataset.fullText = tagName.textContent;
tagName.textContent = tagName.textContent.substring(0, 12) + '...';
} else if (tagName.dataset.fullText) {
tagName.textContent = tagName.dataset.fullText;
delete tagName.dataset.fullText;
}
}
// Set the tag hue for hover effects
const hue = item.style.getPropertyValue('--tag-hue');
item.style.setProperty('--hover-hue', hue);
});
};
adjustTagItems();
// Run on resize with optimized debounce
let resizeTimer;
const handleResize = () => {
if (resizeTimer) {
window.cancelAnimationFrame(resizeTimer);
}
resizeTimer = window.requestAnimationFrame(() => {
fixViewportWidth();
adjustTagItems();
});
};
window.addEventListener('resize', handleResize);
window.addEventListener('orientationchange', handleResize);
// Ensure layout is recalculated after page is fully loaded
window.addEventListener('load', () => {
fixViewportWidth();
adjustTagItems();
// Force recalculation after images and fonts are loaded
setTimeout(() => {
fixViewportWidth();
adjustTagItems();
}, 500);
});
// 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);
});
});
}
// Add touch support for mobile devices
const addTouchSupport = () => {
const tagItems = document.querySelectorAll('.theme-ripple');
tagItems.forEach((item) => {
item.addEventListener(
'touchstart',
() => {
item.classList.add('touch-active');
},
{ passive: true }
);
item.addEventListener(
'touchend',
() => {
setTimeout(() => {
item.classList.remove('touch-active');
}, 150);
},
{ passive: true }
);
// Cancel active state if touch moves away
item.addEventListener(
'touchmove',
(e) => {
const touch = e.touches[0];
const rect = item.getBoundingClientRect();
if (
touch.clientX < rect.left ||
touch.clientX > rect.right ||
touch.clientY < rect.top ||
touch.clientY > rect.bottom
) {
item.classList.remove('touch-active');
}
},
{ passive: true }
);
});
};
addTouchSupport();
// Fix for iOS Safari and other mobile browsers
if (/iPhone|iPad|iPod|Android/.test(navigator.userAgent)) {
document.documentElement.style.setProperty(
'--safe-area-inset-bottom',
'env(safe-area-inset-bottom)'
);
// Fix for mobile viewport height issues
const setVh = () => {
const vh = window.innerHeight * 0.01;
document.documentElement.style.setProperty('--vh', `${vh}px`);
};
setVh();
window.addEventListener('resize', setVh);
window.addEventListener('orientationchange', () => {
// Wait for orientation change to complete
setTimeout(() => {
setVh();
fixViewportWidth();
}, 100);
});
}
});
</script>
<style>
/* Content reveal animations */
.hero-text h1,
.hero-text span,
.hero-text p,
.hero-text ~ div,
article.group {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
/* 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;
}
}
/* Shadow for dark mode */
:global(.dark) .tag-cloud {
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.05),
0 2px 4px rgba(0, 0, 0, 0.1),
0 4px 8px rgba(0, 0, 0, 0.15);
}
/* Input focus animation */
input:focus + div .search-pulse {
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}
/* Base styles */
.tag-cloud {
box-shadow:
0 0 0 1px rgba(0, 0, 0, 0.03),
0 2px 4px rgba(0, 0, 0, 0.03),
0 4px 8px rgba(0, 0, 0, 0.05);
transform-style: preserve-3d;
perspective: 1000px;
transition: all var(--theme-transition);
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box;
margin-left: 0 !important;
margin-right: 0 !important;
}
/* Fix for horizontal overflow */
:global(html),
:global(body) {
overflow-x: hidden;
width: 100%;
max-width: 100%;
}
:global(.max-w-6xl) {
max-width: 100% !important;
width: 100% !important;
}
/* Micro screens (below 240px) */
@media (max-width: 239px) {
.tag-cloud {
padding: 0.5rem !important;
margin: 0 !important;
border-radius: 0.25rem !important;
width: 100% !important;
}
.tag-cloud h2 {
font-size: 0.875rem !important;
margin-bottom: 0.375rem !important;
}
.grid {
grid-template-columns: repeat(1, minmax(0, 1fr)) !important;
gap: 0.375rem !important;
width: 100% !important;
}
.micro-screen .flex {
padding: 0.25rem !important;
}
.micro-screen h3 {
font-size: 0.625rem !important;
}
.micro-screen p {
font-size: 0.5rem !important;
}
}
/* Extra extra extra small screens (240px-279px) */
@media (min-width: 240px) and (max-width: 279px) {
.xxxs\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.xxxs\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.xxxs\:py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.xxxs\:w-6 {
width: 1.5rem;
}
.xxxs\:h-6 {
height: 1.5rem;
}
.xxxs\:text-xs {
font-size: 0.75rem;
}
.xxxs\:gap-2 {
gap: 0.5rem;
}
.xxxs\:text-\[9px\] {
font-size: 9px;
}
}
/* Extra extra small screens (280px-359px) */
@media (min-width: 280px) {
.xxs\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.xxs\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.xxs\:py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.xxs\:w-6 {
width: 1.5rem;
}
.xxs\:h-6 {
height: 1.5rem;
}
.xxs\:text-xs {
font-size: 0.75rem;
}
.xxs\:gap-2 {
gap: 0.5rem;
}
.xxs\:text-\[9px\] {
font-size: 9px;
}
}
/* Extra small screens (360px-639px) */
@media (min-width: 360px) {
.xs\:grid-cols-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
.xs\:px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.xs\:py-2 {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
.xs\:w-7 {
width: 1.75rem;
}
.xs\:h-7 {
height: 1.75rem;
}
.xs\:text-xs {
font-size: 0.75rem;
}
.xs\:text-sm {
font-size: 0.875rem;
}
.xs\:gap-2 {
gap: 0.5rem;
}
.xs\:text-\[10px\] {
font-size: 10px;
}
}
/* 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;
}
/* Prevent layout shifts */
.grow {
grow: 1;
}
.min-w-0 {
min-width: 0;
}
/* Ensure container doesn't overflow */
.overflow-hidden {
overflow: hidden;
}
/* Ensure text doesn't overflow on small screens */
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
}
/* Ensure proper word breaking for long tag names */
.break-words {
word-break: break-word;
overflow-wrap: break-word;
}
.hyphens-auto {
hyphens: auto;
}
/* Touch targets for mobile */
@media (max-width: 640px) {
a,
button {
min-height: 44px;
display: flex;
align-items: center;
}
}
.touch-active {
transform: scale(0.97) !important;
opacity: 0.9;
transition:
transform 0.15s ease-in-out,
opacity 0.15s ease-in-out !important;
}
/* Fix for iOS Safari notch */
@supports (padding: max(0px)) {
.tag-cloud {
padding-left: max(0.75rem, env(safe-area-inset-left));
padding-right: max(0.75rem, env(safe-area-inset-right));
padding-bottom: max(0.75rem, env(safe-area-inset-bottom));
}
}
</style>

View File

@@ -1,252 +1,441 @@
---
import { getCollection } from 'astro:content';
import Layout from '../layouts/Layout.astro';
import FormattedDate from '../components/FormattedDate.astro';
import TagList from '../components/TagList.astro';
// Layout import — provides basic page elements: <head>, <nav>, <footer> etc.
import BaseLayout from '../layouts/BaseLayout.astro';
import directus from '../../lib/directus';
import { readItems, readSingleton } from '@directus/sdk';
// Component Imports
import CallToAction from '../components/CallToAction.astro';
import Grid from '../components/Grid.astro';
import Hero from '../components/Hero.astro';
import Icon from '../components/Icon.astro';
import Pill from '../components/Pill.astro';
import PortfolioPreview from '../components/PortfolioPreview.astro';
const global = await directus.request(readSingleton('global'));
// Page section components
import ContactCTA from '../components/ContactCTA.astro';
import Skills from '../components/Skills.astro';
const posts = await directus.request(
readItems('posts', {
fields: ['*'],
sort: ['-published_date'],
})
);
// Content Fetching: List four most recent work projects
const projects = (await getCollection('work'))
.sort((a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf())
.slice(0, 4);
const recentPosts = posts
.sort((a, b) => b.published_date.getTime() - a.published_date.getTime())
.slice(0, 3);
// Full Astro Component Syntax:
// https://docs.astro.build/basics/astro-components/
const allTags = [...new Set(posts.flatMap((post) => post.tags || []))].slice(0, 5);
---
<BaseLayout>
<div class="stack gap-20 lg:gap-48">
<div class="wrapper stack gap-8 lg:gap-20">
<header class="hero">
<Hero
title="Hello, my name is Alex Lebens"
tagline="I am a Cloud Engineer who is currently based in St. Paul, Minnesota."
align="start"
<Layout title=`Home | ${global.name}`>
<section
class="theme-transition-all px-4 py-10 sm:px-6 sm:py-16 md:py-20"
transition:animate="slide"
>
<div class="roles">
<Pill><Icon icon="code" size="1.33em" /> Developer</Pill>
<Pill><Icon icon="microphone-stage" size="1.33em" /> Speaker</Pill>
<Pill><Icon icon="pencil-line" size="1.33em" /> Writer</Pill>
</div>
</Hero>
<img
alt="Alex Lebens in Antarctica"
width="480"
height="620"
src="/assets/portrait.jpg"
/>
</header>
<Skills />
</div>
<main class="wrapper stack gap-20 lg:gap-48">
<section class="section with-background with-cta">
<header class="section-header stack gap-2 lg:gap-4">
<h3>Selected Work</h3>
<p>Take a look below at some of my featured work from the past few years.</p>
</header>
<div class="gallery">
<Grid variant="offset">
{
projects.map((project) => (
<li>
<PortfolioPreview project={project} />
</li>
))
}
</Grid>
</div>
<div class="cta">
<CallToAction href="/work/">
View All
<Icon icon="arrow-right" size="1.2em" />
</CallToAction>
</div>
</section>
<section class="section with-background bg-variant">
<header class="section-header stack gap-2 lg:gap-4">
<h3>Mentions</h3>
<p>
I have been fortunate enough to receive praise for my work in several publications. Take
a look below to learn more.
<div class="relative mx-auto max-w-2xl">
<div class="relative text-center sm:text-left">
<h1
class="theme-transition-color hero-text text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl md:text-5xl lg:text-6xl dark:text-zinc-100"
>
<span class="block">Writing on technology,</span>
<span class="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 sm:mx-0 sm:mt-6 sm:text-lg md:mt-8 dark:text-zinc-400"
>
{global.about}
</p>
</header>
<div class="gallery">
<Grid variant="small">
{
['Medium', 'BuzzFeed', 'The Next Web', 'awwwards.', 'TechCrunch'].map((brand) => (
<li class="mention-card">
<p>{brand}</p>
</li>
))
}
</Grid>
<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-400 dark:hover:text-zinc-100"
>
<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>
</a>
</div>
</div>
</div>
</section>
</main>
<ContactCTA />
<!-- Featured post section -->
<section
class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800"
>
<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 sm:text-left sm:text-2xl md:text-3xl dark:text-zinc-100"
>
Recent Posts
</h2>
<a
href="/blog"
class="theme-transition-color group relative flex min-h-[44px] items-center justify-center self-center text-sm font-medium text-zinc-900 hover:text-zinc-700 sm:self-auto dark:text-zinc-400 dark:hover:text-zinc-100"
>
<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>
</a>
</div>
</BaseLayout>
<!-- Grid for mobile layout -->
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-8 md:gap-12 lg:grid-cols-3">
{
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-all absolute -inset-x-4 -inset-y-6 z-0 border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 sm:-inset-x-6 sm:rounded-2xl dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70" />
{post.image && (
<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"
loading={index === 0 ? 'eager' : 'lazy'}
width="400"
height="225"
/>
</div>
)}
<h3 class="theme-transition-color relative z-10 mt-3 w-full text-center text-lg font-semibold tracking-tight text-zinc-900 transition-colors group-hover:text-zinc-700 sm:text-left sm:text-xl dark:text-zinc-100 dark:group-hover:text-zinc-300">
<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="z-10 mb-2 line-clamp-2 text-center text-sm text-zinc-600 sm:mb-3 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400">
{post.description}
</p>
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400">
<FormattedDate date={post.published_date} />
</div>
<TagList tags={post.tags} class="z-10" />
<a
href={`/blog/${post.slug}`}
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100"
>
<span class="relative inline-block overflow-hidden">
<span class="relative z-10">Read article</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>
<svg
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
>
<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 section -->
{
allTags.length > 0 && (
<section class="theme-transition-all border-t border-zinc-100 px-4 py-10 sm:px-6 sm:py-12 md:py-16 dark:border-zinc-800">
<div class="mx-auto max-w-3xl">
<h2 class="theme-transition-color mb-6 text-center text-xl font-bold tracking-tight text-zinc-900 sm:mb-8 sm:text-left sm:text-2xl md:text-3xl dark:text-zinc-100">
Popular Tags
</h2>
<div class="hover-3d mx-auto grid max-w-xs grid-cols-1 gap-3 sm:max-w-none sm:grid-cols-2 sm:gap-4 md:grid-cols-3">
{allTags.map((tag) => {
const tagCount = posts.filter((post) => post.tags && post.tags.includes(tag)).length;
return (
<a
href={`/tags/${tag}`}
class="theme-transition-all flex min-h-[80px] flex-col rounded-xl border border-zinc-300 bg-white/50 p-3 transition-all duration-300 hover:bg-zinc-50 sm:min-h-[90px] sm:p-4 md:p-6 dark:border-zinc-800 dark:bg-zinc-900/50 dark:hover:bg-zinc-800/70"
>
<div class="mb-2 flex items-start justify-between">
<span class="theme-transition-color mr-2 text-sm font-medium text-zinc-900 dark:text-zinc-100">
#{tag}
</span>
<span class="theme-transition-all shrink-0 rounded-full bg-zinc-100 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800 dark:text-zinc-400">
{tagCount} {tagCount === 1 ? 'post' : 'posts'}
</span>
</div>
<p class="theme-transition-color mt-1 text-xs text-zinc-600 dark:text-zinc-400">
Explore articles about {tag}
</p>
</a>
);
})}
</div>
</div>
</section>
)
}
</Layout>
<script>
// Add hover effect for cards on touch devices
document.addEventListener('astro:page-load', () => {
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) {
const cards = document.querySelectorAll('.hover-3d');
cards.forEach((card) => {
card.addEventListener('touchstart', () => {
card.classList.add('is-touched');
});
card.addEventListener('touchend', () => {
setTimeout(() => {
card.classList.remove('is-touched');
}, 300);
});
});
// Disable hover animations on touch devices
document.documentElement.classList.add('touch-device');
}
// 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%';
});
}
// Theme change handler that preserves scroll position and provides smoother transitions
document.addEventListener('themeChanged', () => {
// Store current scroll position
const scrollPosition = window.scrollY;
// Create a temporary overlay for smoother transition
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
inset: 0;
background-color: ${document.documentElement.classList.contains('dark') ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.05)'};
z-index: 9999;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease;
`;
document.body.appendChild(overlay);
// Fade in overlay
requestAnimationFrame(() => {
overlay.style.opacity = '0.5';
// Update theme-transition elements without forcing reflow of entire page
requestAnimationFrame(() => {
document
.querySelectorAll(
'.theme-transition-all, .theme-transition-bg, .theme-transition-color'
)
.forEach((el) => {
// Apply a subtle animation instead of a hard reset
el.style.transition = 'all 0.5s ease';
});
// Fade out overlay after transition completes
setTimeout(() => {
overlay.style.opacity = '0';
setTimeout(() => {
overlay.remove();
}, 300);
}, 300);
});
});
// Restore scroll position (prevents jumping to top)
if (scrollPosition > 0) {
setTimeout(() => {
window.scrollTo({
top: scrollPosition,
behavior: 'auto', // Use 'auto' to prevent animation
});
}, 10);
}
});
// Fix theme inconsistency issues by checking theme on visibility change
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'visible') {
const storedTheme = localStorage.getItem('theme');
const currentThemeIsDark = document.documentElement.classList.contains('dark');
if (storedTheme === 'dark' && !currentThemeIsDark) {
document.documentElement.classList.add('dark');
} else if (storedTheme === 'light' && currentThemeIsDark) {
document.documentElement.classList.remove('dark');
}
}
});
// Add smooth reveal animations for content after loading
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text span, .hero-text + p, .hero-text ~ div'
);
heroElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
// Animate posts with staggered delay
const articles = document.querySelectorAll('article.group');
articles.forEach((article, index) => {
setTimeout(
() => {
article.classList.add('animate-reveal');
},
500 + index * 150
);
});
// Animate topic cards with staggered delay
const topicCards = document.querySelectorAll('a.group.flex.flex-col');
topicCards.forEach((card, index) => {
setTimeout(
() => {
card.classList.add('animate-reveal');
},
800 + index * 100
);
});
};
animateContent();
});
</script>
<style>
.hero {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
/* Fix for theme transition issues */
:global(:root) {
--theme-transition-duration: 0.5s;
--theme-transition-timing: ease;
}
.roles {
display: none;
:global(html),
:global(body) {
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
}
.hero img {
aspect-ratio: 5 / 4;
object-fit: cover;
object-position: top;
border-radius: 1.5rem;
box-shadow: var(--shadow-md);
:global(.theme-transition-all) {
transition: all var(--theme-transition-duration) var(--theme-transition-timing);
}
@media (min-width: 50em) {
.hero {
display: grid;
grid-template-columns: 6fr 4fr;
padding-inline: 2.5rem;
gap: 3.75rem;
:global(.theme-transition-bg) {
transition: background-color var(--theme-transition-duration) var(--theme-transition-timing);
}
.roles {
margin-top: 0.5rem;
display: flex;
gap: 0.5rem;
:global(.theme-transition-color) {
transition: color var(--theme-transition-duration) var(--theme-transition-timing);
}
.hero img {
aspect-ratio: 3 / 4;
border-radius: 4.5rem;
object-fit: cover;
}
/* 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;
}
/* ====================================================== */
.section {
display: grid;
gap: 2rem;
/* 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;
}
.with-background {
position: relative;
}
.with-background::before {
--hero-bg: var(--bg-image-subtle-2);
content: '';
position: absolute;
pointer-events: none;
left: 50%;
width: 100vw;
aspect-ratio: calc(2.25 / var(--bg-scale));
top: 0;
transform: translateY(-75%) translateX(-50%);
background:
url('/assets/backgrounds/noise.png') top center/220px repeat,
var(--hero-bg) center center / var(--bg-gradient-size) no-repeat,
var(--gray-999);
background-blend-mode: overlay, normal, normal, normal;
mix-blend-mode: var(--bg-blend-mode);
z-index: -1;
}
.with-background.bg-variant::before {
--hero-bg: var(--bg-image-subtle-1);
}
.section-header {
justify-self: center;
text-align: center;
max-width: 50ch;
font-size: var(--text-md);
color: var(--gray-300);
}
.section-header h3 {
font-size: var(--text-2xl);
}
@media (min-width: 50em) {
.section {
grid-template-columns: repeat(4, 1fr);
grid-template-areas: 'header header header header' 'gallery gallery gallery gallery';
gap: 5rem;
}
.section.with-cta {
grid-template-areas: 'header header header cta' 'gallery gallery gallery gallery';
}
.section-header {
grid-area: header;
font-size: var(--text-lg);
}
.section-header h3 {
font-size: var(--text-4xl);
}
.with-cta .section-header {
justify-self: flex-start;
text-align: left;
}
.gallery {
grid-area: gallery;
}
.cta {
grid-area: cta;
}
}
/* ====================================================== */
.mention-card {
display: flex;
height: 7rem;
justify-content: center;
align-items: center;
text-align: center;
border: 1px solid var(--gray-800);
border-radius: 1.5rem;
color: var(--gray-300);
background: var(--gradient-subtle);
box-shadow: var(--shadow-sm);
}
@media (min-width: 50em) {
.mention-card {
border-radius: 1.5rem;
height: 9.5rem;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
</style>

27
src/pages/rss.xml.ts Normal file
View File

@@ -0,0 +1,27 @@
import rss from '@astrojs/rss';
import directus from '../../lib/directus';
import { readItems, readSingleton } from '@directus/sdk';
export async function GET(context: any) {
const global = await directus.request(readSingleton('global'));
const posts = await directus.request(
readItems('posts', {
fields: ['*'],
sort: ['-published_date'],
})
);
return rss({
title: `${global.name}`,
description: `${global.description}`,
site: context.site,
items: posts.map((post) => ({
title: post.title,
pubDate: post.published_date,
description: post.slug,
link: `/blog/${post.slug}/`,
categories: post.tags || [],
})),
});
}

423
src/pages/tags/[tag].astro Normal file
View File

@@ -0,0 +1,423 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import FormattedDate from '../../components/FormattedDate.astro';
import directus from '../../../lib/directus';
import { readItems } from '@directus/sdk';
export const prerender = true;
export async function getStaticPaths() {
const posts = await directus.request(
readItems('posts', {
fields: ['*'],
})
);
const uniqueTags = [...new Set(posts.flatMap((post) => post.tags || []))];
// Create a path for each tag
return uniqueTags.map((tag) => {
// Make tag matching case-insensitive
const filteredPosts = posts.filter(
(post) => post.tags?.some((t) => t.toLowerCase() === (tag as string).toLowerCase()) // Explicitly cast tag to string
);
return {
params: { tag },
props: { posts: filteredPosts },
};
});
}
const { tag } = Astro.params as { tag: string };
const { posts = [] } = Astro.props;
console.log(`Tag: ${tag}, Number of posts: ${posts.length}`);
const sortedPosts =
posts && posts.length > 0
? [...posts].sort((a, b) => b.published_date.valueOf() - a.published_date.valueOf())
: [];
console.log(`Sorted posts length: ${sortedPosts.length}`);
const relatedTags = [
...new Set(sortedPosts.flatMap((post) => post.tags || []).filter((t) => t !== tag)),
].slice(0, 5);
---
<BaseLayout title={`Posts tagged with "${tag}"`}>
<div class="mx-auto max-w-5xl px-4 py-10 sm:py-16">
<div class="relative mb-10 sm:mb-16">
<div class="relative text-center sm:text-left">
<a
href="/blog#topics"
class="group mb-4 inline-flex items-center gap-2 text-sm font-medium text-zinc-600 transition-colors hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-zinc-100"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-4 w-4 transition-transform duration-300 group-hover:-translate-x-1"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"
>
</path>
</svg>
<span>Back to blog</span>
<span
class="block h-0.5 max-w-0 bg-zinc-300 transition-all duration-300 group-hover:max-w-full dark:bg-zinc-700"
></span>
</a>
<div
class="mb-2 flex flex-col justify-center gap-4 sm:flex-row sm:items-center sm:justify-start"
>
<div
class="tag-icon mx-auto flex h-12 w-12 items-center justify-center rounded-xl bg-zinc-100 shadow-xs sm:mx-0 dark:bg-zinc-800"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-6 w-6 text-zinc-700 dark:text-zinc-300"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.568 3H5.25A2.25 2.25 0 003 5.25v4.318c0 .597.237 1.17.659 1.591l9.581 9.581c.699.699 1.78.872 2.607.33a18.095 18.095 0 005.223-5.223c.542-.827.369-1.908-.33-2.607L11.16 3.66A2.25 2.25 0 009.568 3z"
>
</path>
<path stroke-linecap="round" stroke-linejoin="round" d="M6 6h.008v.008H6V6z"> </path>
</svg>
</div>
<h1
class="text-3xl font-bold tracking-tight text-zinc-900 sm:text-4xl dark:text-zinc-100"
>
<span class="relative">
#{tag}
<span class="absolute -bottom-1 left-0 h-1 w-full bg-zinc-200 dark:bg-zinc-700"
></span>
<span
class="animate-expand absolute -bottom-1 left-0 h-1 w-full bg-zinc-900 opacity-70 dark:bg-zinc-100"
></span>
</span>
</h1>
</div>
<p
class="mx-auto mt-4 max-w-2xl text-base text-zinc-600 sm:mx-0 sm:text-lg dark:text-zinc-400"
>
Exploring <span class="font-medium text-zinc-900 dark:text-zinc-100"
>{sortedPosts.length}</span
> articles tagged with <span class="font-medium text-zinc-900 dark:text-zinc-100"
>"{tag}"</span
>
</p>
</div>
</div>
<!-- Related tags section -->
{
relatedTags.length > 0 && (
<div class="hero-text hide-scrollbar mb-8 overflow-x-auto pb-4 sm:mb-12">
<h2 class="mb-3 text-center text-lg font-medium text-zinc-900 sm:text-left dark:text-zinc-100">
Related topics
</h2>
<div class="flex flex-nowrap justify-center gap-2 sm:justify-start">
{relatedTags.map((relatedTag) => (
<a
href={`/tags/${relatedTag}`}
class="inline-flex shrink-0 items-center rounded-full bg-zinc-100 px-3 py-1.5 text-sm font-medium text-zinc-900 transition-colors hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
>
#{relatedTag}
</a>
))}
</div>
</div>
)
}
<!-- Posts list -->
<div class="relative">
<div
class="hero-text bg-grid-pattern pointer-events-none absolute inset-0 opacity-5 dark:opacity-10"
>
</div>
<div class="relative space-y-6 sm:space-y-8">
{
sortedPosts.map((post) => (
<article class="hover-3d theme-transition-element group relative mx-auto flex max-w-2xl flex-col p-5 sm:mx-0 sm:p-8">
<div class="absolute inset-0 rounded-2xl border border-zinc-200 bg-white/50 transition-all duration-300 group-hover:bg-zinc-50 hover:bg-zinc-50/80 hover:shadow-md dark:border-zinc-800 dark:bg-zinc-900/50 dark:group-hover:bg-zinc-800/70 dark:hover:bg-zinc-900/50" />
<div class="flex flex-col gap-5 sm:flex-row sm:gap-6">
{post.image && (
<div class="z-10 mx-auto h-40 w-full shrink-0 overflow-hidden rounded-xl sm:mx-0 sm:w-56">
<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="lazy"
/>
</div>
)}
<div class="z-10 flex-1">
<h2 class="mb-2 text-center text-xl font-semibold text-zinc-900 sm:mb-3 sm:text-left sm:text-2xl dark:text-zinc-100">
<a href={`/blog/${post.slug}/`} class="before:absolute before:inset-0">
{post.title}
</a>
</h2>
<p class="mb-4 line-clamp-2 text-center text-sm text-zinc-600 sm:line-clamp-3 sm:text-left sm:text-base dark:text-zinc-400">
{post.description}
</p>
<div class="mb-2 flex flex-wrap items-center justify-center gap-3 text-xs text-zinc-500 sm:mb-3 sm:justify-start sm:gap-4 sm:text-sm dark:text-zinc-400">
<FormattedDate date={post.published_date} />
</div>
</div>
</div>
<div class="z-10 mt-4 flex flex-wrap items-end justify-center border-t border-zinc-100 pt-4 sm:justify-between dark:border-zinc-800">
{post.tags && post.tags.length > 0 && (
<div class="mb-3 flex flex-wrap justify-center gap-2 sm:mb-0 sm:justify-start">
{post.tags.slice(0, 3).map((postTag) => (
<a
href={`/blog/${postTag}`}
class={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors ${
postTag === tag
? 'bg-zinc-900/10 text-zinc-900 dark:bg-zinc-100/20 dark:text-zinc-100'
: 'bg-zinc-100 text-zinc-600 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:bg-zinc-700'
}`}
>
#{postTag}
</a>
))}
{post.tags.length > 3 && (
<span class="inline-flex items-center rounded-full bg-zinc-50 px-2 py-0.5 text-xs text-zinc-500 dark:bg-zinc-800/50 dark:text-zinc-400">
+{post.tags.length - 3}
</span>
)}
</div>
)}
<div class="mx-auto sm:mr-0 sm:ml-auto">
<a
href={`/blog/${post.slug}`}
class="theme-transition-color relative z-10 mx-auto mt-3 flex min-h-[44px] items-center text-sm font-medium text-zinc-700 transition-colors group-hover:text-zinc-900 sm:mx-0 sm:mt-4 dark:text-zinc-300 dark:group-hover:text-zinc-100"
>
<span class="relative inline-block overflow-hidden">
<span class="relative z-10">Read article</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>
<svg
viewBox="0 0 16 16"
fill="none"
aria-hidden="true"
class="ml-1 h-4 w-4 stroke-current transition-transform duration-300"
>
<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>
</div>
</div>
</article>
))
}
</div>
</div>
<!-- Empty state -->
{
sortedPosts.length === 0 && (
<div class="py-12 text-center sm:py-20">
<div class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full bg-zinc-100 sm:mb-6 sm:h-20 sm:w-20 dark:bg-zinc-800">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-8 w-8 text-zinc-500 sm:h-10 sm:w-10 dark:text-zinc-400"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z"
/>
</svg>
</div>
<h2 class="mb-2 text-xl font-semibold text-zinc-900 sm:text-2xl dark:text-zinc-100">
No posts found
</h2>
<p class="text-zinc-600 dark:text-zinc-400">There are no posts with this tag yet.</p>
<a
href="/blog"
class="mt-6 inline-flex items-center gap-2 rounded-md bg-zinc-100 px-4 py-2 text-sm font-medium text-zinc-800 transition-all duration-300 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-200 dark:hover:bg-zinc-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="h-4 w-4"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 12h-15m0 0l6.75 6.75M4.5 12l6.75-6.75"
/>
</svg>
<span>Browse all articles</span>
</a>
</div>
)
}
</div>
</BaseLayout>
<script>
document.addEventListener('astro:page-load', () => {
// Add smooth reveal animations for content after loading
const animateContent = () => {
// Animate hero section
const heroElements = document.querySelectorAll(
'.hero-text ~ div, .hero-text h1, .hero-text span, .hero-text p'
);
heroElements.forEach((el, index) => {
setTimeout(
() => {
el.classList.add('animate-reveal');
},
100 + index * 150
);
});
// Animate posts with staggered delay
const articles = document.querySelectorAll('article.group');
articles.forEach((article, index) => {
setTimeout(
() => {
article.classList.add('animate-reveal');
},
500 + index * 150
);
});
};
animateContent();
// Add hover effect for cards on touch devices
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
if (isTouchDevice) {
const cards = document.querySelectorAll('.hover-3d');
cards.forEach((card) => {
card.addEventListener('touchstart', () => {
card.classList.add('is-touched');
});
card.addEventListener('touchend', () => {
setTimeout(() => {
card.classList.remove('is-touched');
}, 300);
});
});
// Disable hover animations on touch devices
document.documentElement.classList.add('touch-device');
}
});
</script>
<style>
/* Grid pattern background */
.bg-grid-pattern {
background-size: 30px 30px;
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.05) 1px, transparent 1px);
}
:global(.dark) .bg-grid-pattern {
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.05) 1px, transparent 1px);
}
/* Hide scrollbar but keep functionality */
.hide-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
.hide-scrollbar::-webkit-scrollbar {
display: none;
}
/* Animated underline */
@keyframes expand {
from {
width: 0;
}
to {
width: 100%;
}
}
.animate-expand {
animation: expand 1s ease-out forwards;
}
/* Content reveal animations */
.hero-text h1,
.hero-text span,
.hero-text p,
.hero-text ~ div,
article.group {
opacity: 0;
transform: translateY(20px);
transition:
opacity 0.8s ease,
transform 0.8s ease;
}
.animate-reveal {
opacity: 1 !important;
transform: translateY(0) !important;
}
/* 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;
}
/* Responsive adjustments */
@media (max-width: 640px) {
.animate-blob {
animation-duration: 10s;
}
}
</style>

View File

@@ -1,39 +0,0 @@
---
import { getCollection } from 'astro:content';
import BaseLayout from '../layouts/BaseLayout.astro';
import ContactCTA from '../components/ContactCTA.astro';
import PortfolioPreview from '../components/PortfolioPreview.astro';
import Hero from '../components/Hero.astro';
import Grid from '../components/Grid.astro';
const projects = (await getCollection('work')).sort(
(a, b) => b.data.publishDate.valueOf() - a.data.publishDate.valueOf(),
);
---
<BaseLayout
title="My Work | Alex Lebens"
description="Learn about Alex Lebens's most recent projects"
>
<div class="stack gap-20">
<main class="wrapper stack gap-8">
<Hero
title="My Work"
tagline="See my most recent projects below to get an idea of my past experience."
align="start"
/>
<Grid variant="offset">
{
projects.map((project) => (
<li>
<PortfolioPreview project={project} />
</li>
))
}
</Grid>
</main>
<ContactCTA />
</div>
</BaseLayout>

View File

@@ -1,151 +0,0 @@
---
import { type CollectionEntry, getCollection } from 'astro:content';
import BaseLayout from '../../layouts/BaseLayout.astro';
import ContactCTA from '../../components/ContactCTA.astro';
import Hero from '../../components/Hero.astro';
import Icon from '../../components/Icon.astro';
import Pill from '../../components/Pill.astro';
interface Props {
entry: CollectionEntry<'work'>;
}
// This is a dynamic route that generates a page for every Markdown file in src/content/
// Read more about dynamic routes and this `getStaticPaths` function in the Astro docs:
// https://docs.astro.build/en/core-concepts/routing/#dynamic-routes
export async function getStaticPaths() {
const work = await getCollection('work');
return work.map((entry) => ({
params: { slug: entry.slug },
props: { entry },
}));
}
const { entry } = Astro.props;
const { Content } = await entry.render();
---
<BaseLayout title={entry.data.title} description={entry.data.description}>
<div class="stack gap-20">
<div class="stack gap-15">
<header>
<div class="wrapper stack gap-2">
<a class="back-link" href="/work/"><Icon icon="arrow-left" /> Work</a>
<Hero title={entry.data.title} align="start">
<div class="details">
<div class="tags">
{entry.data.tags.map((t) => <Pill>{t}</Pill>)}
</div>
<p class="description">{entry.data.description}</p>
</div>
</Hero>
</div>
</header>
<main class="wrapper">
<div class="stack gap-10 content">
{entry.data.img && <img src={entry.data.img} alt={entry.data.img_alt || ''} />}
<div class="content">
<Content />
</div>
</div>
</main>
</div>
<ContactCTA />
</div>
</BaseLayout>
<style>
header {
padding-bottom: 2.5rem;
border-bottom: 1px solid var(--gray-800);
}
.back-link {
display: none;
}
.details {
display: flex;
flex-direction: column;
padding: 0.5rem;
gap: 1.5rem;
justify-content: space-between;
align-items: center;
}
.tags {
display: flex;
gap: 0.5rem;
}
.description {
font-size: var(--text-lg);
max-width: 54ch;
}
.content {
max-width: 65ch;
margin-inline: auto;
}
.content > :global(* + *) {
margin-top: 1rem;
}
.content :global(h1),
.content :global(h2),
.content :global(h3),
.content :global(h4),
.content :global(h5) {
margin: 1.5rem 0;
}
.content :global(img) {
border-radius: 1.5rem;
box-shadow: var(--shadow-sm);
background: var(--gradient-subtle);
border: 1px solid var(--gray-800);
}
.content :global(blockquote) {
font-size: var(--text-lg);
font-family: var(--font-brand);
font-weight: 600;
line-height: 1.1;
padding-inline-start: 1.5rem;
border-inline-start: 0.25rem solid var(--accent-dark);
color: var(--gray-0);
}
.back-link,
.content :global(a) {
text-decoration: 1px solid underline transparent;
text-underline-offset: 0.25em;
transition: text-decoration-color var(--theme-transition);
}
.back-link:hover,
.back-link:focus,
.content :global(a:hover),
.content :global(a:focus) {
text-decoration-color: currentColor;
}
@media (min-width: 50em) {
.back-link {
display: block;
align-self: flex-start;
}
.details {
flex-direction: row;
gap: 2.5rem;
}
.content :global(blockquote) {
font-size: var(--text-2xl);
}
}
</style>

View File

@@ -1,255 +1,138 @@
/* Global variables */
:root {
/* Colors */
--gray-0: #090b11;
--gray-50: #141925;
--gray-100: #283044;
--gray-200: #3d4663;
--gray-300: #505d84;
--gray-400: #6474a2;
--gray-500: #8490b5;
--gray-600: #a3acc8;
--gray-700: #c3cadb;
--gray-800: #e3e6ee;
--gray-900: #f3f4f7;
--gray-999-basis: 0, 0%, 100%;
--gray-999_40: hsla(var(--gray-999-basis), 0.4);
--gray-999: #ffffff;
@import 'tailwindcss';
--accent-light: #c561f6;
--accent-regular: #7611a6;
--accent-dark: #1c0056;
--accent-overlay: hsla(280, 89%, 67%, 0.33);
--accent-subtle-overlay: var(--accent-overlay);
--accent-text-over: var(--gray-999);
/* Dark mode support for Tailwind CSS v4 */
/* https://tailwindcss.com/docs/dark-mode */
@custom-variant dark (&:where(.dark, .dark *));
--link-color: var(--accent-regular);
/* Gradients */
--gradient-stop-1: var(--accent-light);
--gradient-stop-2: var(--accent-regular);
--gradient-stop-3: var(--accent-dark);
--gradient-subtle: linear-gradient(150deg, var(--gray-900) 19%, var(--gray-999) 150%);
--gradient-accent: linear-gradient(
150deg,
var(--gradient-stop-1),
var(--gradient-stop-2),
var(--gradient-stop-3)
);
--gradient-accent-orange: linear-gradient(
150deg,
#ca7879,
var(--accent-regular),
var(--accent-dark)
);
--gradient-stroke: linear-gradient(180deg, var(--gray-900), var(--gray-700));
/* Shadows */
--shadow-sm: 0px 6px 3px rgba(9, 11, 17, 0.01), 0px 4px 2px rgba(9, 11, 17, 0.01),
0px 2px 2px rgba(9, 11, 17, 0.02), 0px 0px 1px rgba(9, 11, 17, 0.03);
--shadow-md: 0px 28px 11px rgba(9, 11, 17, 0.01), 0px 16px 10px rgba(9, 11, 17, 0.03),
0px 7px 7px rgba(9, 11, 17, 0.05), 0px 2px 4px rgba(9, 11, 17, 0.06);
--shadow-lg: 0px 62px 25px rgba(9, 11, 17, 0.01), 0px 35px 21px rgba(9, 11, 17, 0.05),
0px 16px 16px rgba(9, 11, 17, 0.1), 0px 4px 9px rgba(9, 11, 17, 0.12);
/* Text Sizes */
--text-sm: 0.875rem;
--text-base: 1rem;
--text-md: 1.125rem;
--text-lg: 1.25rem;
--text-xl: 1.625rem;
--text-2xl: 2.125rem;
--text-3xl: 2.625rem;
--text-4xl: 3.5rem;
--text-5xl: 4.5rem;
/* Fonts */
--font-system: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu,
Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
--font-body: 'Public Sans', var(--font-system);
--font-brand: Rubik, var(--font-system);
/* Transitions */
--theme-transition: 0.2s ease-in-out;
}
:root.theme-dark {
--gray-0: #ffffff;
--gray-50: #f3f4f7;
--gray-100: #e3e6ee;
--gray-200: #c3cadb;
--gray-300: #a3acc8;
--gray-400: #8490b5;
--gray-500: #6474a2;
--gray-600: #505d84;
--gray-700: #3d4663;
--gray-800: #283044;
--gray-900: #141925;
--gray-999-basis: 225, 31%, 5%;
--gray-999: #090b11;
--accent-light: #1c0056;
--accent-regular: #7611a6;
--accent-dark: #c561f6;
--accent-overlay: hsla(280, 89%, 67%, 0.33);
--accent-subtle-overlay: hsla(281, 81%, 36%, 0.33);
--accent-text-over: var(--gray-0);
--link-color: var(--accent-dark);
--gradient-stop-1: #4c11c6;
--gradient-subtle: linear-gradient(150deg, var(--gray-900) 19%, var(--gray-999) 81%);
--gradient-accent-orange: linear-gradient(
150deg,
#ca7879,
var(--accent-regular),
var(--accent-light)
);
--gradient-stroke: linear-gradient(180deg, var(--gray-600), var(--gray-800));
--shadow-sm: 0px 6px 3px rgba(255, 255, 255, 0.01), 0px 4px 2px rgba(255, 255, 255, 0.01),
0px 2px 2px rgba(255, 255, 255, 0.02), 0px 0px 1px rgba(255, 255, 255, 0.03);
--shadow-md: 0px 28px 11px rgba(255, 255, 255, 0.01), 0px 16px 10px rgba(255, 255, 255, 0.03),
0px 7px 7px rgba(255, 255, 255, 0.05), 0px 2px 4px rgba(255, 255, 255, 0.06);
--shadow-lg: 0px 62px 25px rgba(255, 255, 255, 0.01), 0px 35px 21px rgba(255, 255, 255, 0.05),
0px 16px 16px rgba(255, 255, 255, 0.1), 0px 4px 9px rgba(255, 255, 255, 0.12);
}
html,
body {
min-height: 100%;
overflow-x: hidden;
}
body {
background-color: var(--gray-999);
color: var(--gray-200);
font-family: var(--font-body);
@layer base {
:root {
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
line-height: 1.5;
}
-moz-osx-font-smoothing: grayscale;
--theme-transition: 0.3s ease;
}
*,
*::after,
*::before {
box-sizing: border-box;
html {
scroll-behavior: smooth;
scroll-padding-top: 5rem;
overflow-y: scroll;
}
body {
@apply min-h-screen bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100;
margin: 0;
}
img {
max-width: 100%;
height: auto;
}
a {
color: var(--link-color);
}
h1,
h2,
h3,
h4,
h5 {
line-height: 1.1;
font-family: var(--font-brand);
font-weight: 600;
color: var(--gray-100);
}
h1 {
font-size: var(--text-5xl);
}
h2 {
font-size: var(--text-4xl);
}
h3 {
font-size: var(--text-3xl);
}
h4 {
font-size: var(--text-2xl);
}
h5 {
font-size: var(--text-xl);
}
/* Utilities */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
overflow-x: hidden;
}
/* Simple theme transition */
body,
a,
button {
transition:
background-color var(--theme-transition),
color var(--theme-transition),
border-color var(--theme-transition);
}
}
.wrapper {
width: 100%;
max-width: 83rem;
margin-inline: auto;
padding-inline: 1.5rem;
/* Minimal responsive styles */
@media (max-width: 640px) {
html {
scroll-padding-top: 4rem;
}
/* Touch targets on mobile */
button,
a {
@apply min-h-[44px];
}
}
.stack {
display: flex;
flex-direction: column;
/* Add smooth animations */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.gap-2 {
gap: 0.5rem;
}
.gap-4 {
gap: 1rem;
}
.gap-8 {
gap: 2rem;
}
.gap-10 {
gap: 2.5rem;
}
.gap-15 {
gap: 3.75rem;
}
.gap-20 {
gap: 5rem;
}
.gap-30 {
gap: 7.5rem;
}
.gap-48 {
gap: 12rem;
@keyframes slideUp {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@media (min-width: 50em) {
.lg\:gap-2 {
gap: 0.5rem;
@keyframes slideDown {
from {
transform: translateY(-20px);
opacity: 0;
}
.lg\:gap-4 {
gap: 1rem;
}
.lg\:gap-8 {
gap: 2rem;
}
.lg\:gap-10 {
gap: 2.5rem;
}
.lg\:gap-15 {
gap: 3.75rem;
}
.lg\:gap-20 {
gap: 5rem;
}
.lg\:gap-30 {
gap: 7.5rem;
}
.lg\:gap-48 {
gap: 12rem;
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes scaleIn {
from {
transform: scale(0.95);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
/* Apply animations to elements */
.animate-fade-in {
animation: fadeIn 0.6s ease forwards;
}
.animate-slide-up {
animation: slideUp 0.6s ease forwards;
}
.animate-slide-down {
animation: slideDown 0.6s ease forwards;
}
.animate-scale-in {
animation: scaleIn 0.6s ease forwards;
}
/* Staggered animation delays */
.delay-100 {
animation-delay: 0.1s;
}
.delay-200 {
animation-delay: 0.2s;
}
.delay-300 {
animation-delay: 0.3s;
}
.delay-400 {
animation-delay: 0.4s;
}
/* Smooth hover transitions */
a,
button {
transition: all 0.5s ease;
}
a.hover:hover,
button:hover {
transform: translateY(-2px);
}

1
src/types/astro.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="astro/client" />

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

@@ -0,0 +1,40 @@
import React from 'react';
import * as FaIcons from 'react-icons/fa';
import * as MdIcons from 'react-icons/md';
import * as AiIcons from 'react-icons/ai';
import * as GiIcons from 'react-icons/gi';
import * as IoIcons from 'react-icons/io';
import * as CiIcons from 'react-icons/ci';
import * as FiIcons from 'react-icons/fi';
import * as LuIcons from 'react-icons/lu';
import * as SiIcons from 'react-icons/si';
// Load React Icon library dynamically from attributes in Directus
const iconSets = {
fa: FaIcons,
md: MdIcons,
ai: AiIcons,
gi: GiIcons,
io: IoIcons,
ci: CiIcons,
fi: FiIcons,
lu: LuIcons,
si: SiIcons,
};
const DynamicIcon = ({ name, set = 'fa' }: { name: string; set: string }) => {
let IconComponent = FaIcons.FaAlignCenter;
if (name.startsWith('Fa')) {
IconComponent = iconSets['fa'][name];
} else if (name.startsWith('Si')) {
IconComponent = iconSets['si'][name];
} else {
IconComponent = iconSets[set][name];
}
return <IconComponent />;
};
export default DynamicIcon;

3
src/utils/debug.ts Normal file
View File

@@ -0,0 +1,3 @@
export function debugObject(obj: any): string {
return JSON.stringify(obj, null, 2);
}

58
tailwind.config.cjs Normal file
View File

@@ -0,0 +1,58 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}', '*.{js,ts,jsx,tsx,mdx}'],
darkMode: 'class',
theme: {
extend: {
typography: (theme) => ({
DEFAULT: {
css: {
a: {
color: theme('colors.zinc.900'),
'&:hover': {
color: theme('colors.zinc.700'),
},
textDecoration: 'underline',
textDecorationColor: theme('colors.zinc.400'),
textUnderlineOffset: '2px',
},
'h1, h2, h3, h4, h5, h6': {
color: theme('colors.zinc.900'),
},
code: {
color: theme('colors.zinc.900'),
backgroundColor: theme('colors.zinc.100'),
borderRadius: theme('borderRadius.md'),
padding: `${theme('padding.1')} ${theme('padding.1.5')}`,
},
'code::before': {
content: '""',
},
'code::after': {
content: '""',
},
},
},
invert: {
css: {
a: {
color: theme('colors.zinc.100'),
'&:hover': {
color: theme('colors.zinc.300'),
},
textDecorationColor: theme('colors.zinc.700'),
},
'h1, h2, h3, h4, h5, h6': {
color: theme('colors.zinc.100'),
},
code: {
color: theme('colors.zinc.100'),
backgroundColor: theme('colors.zinc.800'),
},
},
},
}),
},
},
plugins: [require('@tailwindcss/typography')],
};

View File

@@ -1,3 +1,30 @@
{
"extends": "astro/tsconfigs/strict"
"extends": "astro/tsconfigs/strict",
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"allowImportingTsExtensions": true,
"target": "ES6",
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"baseUrl": ".",
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}