Compare commits
890 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c5cda006bb | |||
| 959d3bd71d | |||
| f3b8d10106 | |||
| 0c63c6bef4 | |||
| 5e37e2bb53 | |||
| b3c377f62d | |||
| 0d87af3aca | |||
| 9eb0f37cb2 | |||
| 76dfef4177 | |||
| d415dda661 | |||
| ea9ae016d7 | |||
| 0416ab7f9e | |||
| 6f1728a909 | |||
| db2711d878 | |||
| 7f2a27248a | |||
| c927235a5a | |||
| 8d5c02e2d1 | |||
| 1a34b932b0 | |||
| 882063ea43 | |||
| ba2477e7af | |||
| 879786484d | |||
| 2c9486f687 | |||
| ba73c1b24f | |||
| 44bd1e4810 | |||
| e52d85f931 | |||
| 21085a1620 | |||
| 744e72efc9 | |||
| 62dd636d4e | |||
| b4d03a286c | |||
| 442da55d5d | |||
|
9b9c982f92
|
|||
|
1820650ada
|
|||
| fa2245e939 | |||
|
12a8363dd2
|
|||
| 4f365a4e60 | |||
|
12e74d29af
|
|||
| 7937090533 | |||
|
ebfd8cf4a7
|
|||
| 8270728e8f | |||
| 20d8c7323f | |||
| 5ac23f08a4 | |||
| c6f3179efb | |||
| 1a8473b964 | |||
| 18211ad485 | |||
| 429cf94023 | |||
| 0497731c45 | |||
| 6c2c6da91d | |||
| 19e17ea947 | |||
| 3d9120c570 | |||
| 875b8a7f47 | |||
| 1ddc76ae69 | |||
| 6423ffba63 | |||
| 505670dbf8 | |||
| b3d7e7af2b | |||
| 440c95224d | |||
| b9ee82e9d8 | |||
|
3af9f08b7c
|
|||
| 0bd56b172f | |||
|
ebf70bd747
|
|||
| 9c5e9b6a5b | |||
|
568f9e5164
|
|||
| a74cc775d0 | |||
| 5271be52a2 | |||
| 8a649b7647 | |||
| c4be4653be | |||
| 47a637353c | |||
| a09a4ee240 | |||
| 342ae8900a | |||
| 2cdef1a553 | |||
| a8d6446674 | |||
| fcd3057f40 | |||
| d464f0fe43 | |||
| 0f403fa274 | |||
| 0fc359a973 | |||
| 104fe35ee8 | |||
| a57f43e082 | |||
| efad6c30d1 | |||
| c2d26228ba | |||
|
94fe56022d
|
|||
| d171292dd2 | |||
| f52d285013 | |||
|
a79f53e90c
|
|||
| 5ad7e33c8a | |||
|
87f266a3e2
|
|||
| dc039046fe | |||
|
9c53f37b39
|
|||
| 093e1e2ccb | |||
| 7a77f0d2d2 | |||
| e29631c4af | |||
| 31aad5511f | |||
| 976bc0c413 | |||
| 0a2979ecfe | |||
| c3e4519682 | |||
| d9833e1c27 | |||
| 19e80809c1 | |||
| 00ef91b644 | |||
| 7f7f710fe8 | |||
| 1573331f87 | |||
| 14f7bdc024 | |||
| 0b116a05df | |||
|
849ca78598
|
|||
| 8377aefaf7 | |||
| 3f5682f80c | |||
| ae84560ddd | |||
|
1f7253d954
|
|||
| b6dfc738f1 | |||
| 63cbcdf39b | |||
| 10c4f9c768 | |||
| 880bafd41e | |||
| 3ebc36174b | |||
| 0abd1a2465 | |||
|
f2b27a01bf
|
|||
|
503cb401fc
|
|||
| a45a4d7dd7 | |||
| 6d3f3a49ab | |||
| 197ad63ada | |||
| 4c4421c8a8 | |||
| d0ff16c8dc | |||
| 9678b3c718 | |||
| 7fafa5c4cf | |||
| a909743feb | |||
|
f116173cb8
|
|||
| ce62de8883 | |||
|
94f2779463
|
|||
| ed3cf80921 | |||
|
63aa6bfdbc
|
|||
| 4343124c3f | |||
|
a48063a694
|
|||
| e476efb96b | |||
| a99201138e | |||
|
9ef86e71dc
|
|||
| 5cd59cd1ff | |||
|
d5cf6fe130
|
|||
| 91136e2e54 | |||
|
7b915cf021
|
|||
| 807b8dd9b9 | |||
|
76c6933682
|
|||
| bd34eb6f75 | |||
|
c8d9def6dc
|
|||
| 5fb2ff16c6 | |||
|
9a86ea4053
|
|||
| 49969e27b0 | |||
| bf73905658 | |||
| 56d841a335 | |||
| 95432d9059 | |||
| c2bf64c6cc | |||
| 1f3fed93a1 | |||
| 754f6a22f0 | |||
| 4203b63893 | |||
| 4d7886b93c | |||
| c7d3ca7252 | |||
| a0f83c874c | |||
| 22860c4714 | |||
| 9b8a7077a7 | |||
| 8bfc744bdb | |||
| d386afa15e | |||
| 3fe324d4c2 | |||
|
a02d417c83
|
|||
|
0d53376c80
|
|||
| a5abfe0d1c | |||
|
3fcf9a0703
|
|||
| 00b63a5bea | |||
| d9860106b1 | |||
| 83940a28ab | |||
|
4baa2bed51
|
|||
| 19a9588919 | |||
|
3c8d3992cf
|
|||
| fb8f642c52 | |||
| fde397386c | |||
| b7f76c5847 | |||
| b3bb769c47 | |||
| f34f4b2532 | |||
| 94f5082729 | |||
| 5e9765f4d7 | |||
|
ac4bc16913
|
|||
| daaca66f42 | |||
| 6fb7846d23 | |||
|
167491fe8d
|
|||
| 1cda8fac20 | |||
| dbf7ae54a4 | |||
|
a857b64029
|
|||
|
6b867ec092
|
|||
| 3e24f3bb4f | |||
|
0c02c71693
|
|||
| 025a5b38aa | |||
|
cdaa3af76c
|
|||
| e87c89afac | |||
|
a00e188f86
|
|||
| bc5f023883 | |||
| 5e272108d4 | |||
| babf0d40cd | |||
| 3925f35c47 | |||
| 3f2c6da690 | |||
| 01ee8fac98 | |||
| c8306e414b | |||
| 42d3891c6b | |||
|
21c08d6853
|
|||
| 6aa62ad76d | |||
|
a95908736b
|
|||
| 6ddec3a558 | |||
|
24a20c4a7e
|
|||
| ecfc907744 | |||
|
44d4837b8e
|
|||
| 6b46e943e3 | |||
|
606424972a
|
|||
| d2a8c007e5 | |||
|
3ac2a5ea1f
|
|||
| 7ef13d8437 | |||
|
4ed5ab769c
|
|||
| ec31419b81 | |||
|
083a5e77da
|
|||
| 4c065f99ab | |||
|
f6cccca140
|
|||
| eedddca9a1 | |||
|
556647977f
|
|||
| d1f189818f | |||
|
5c461d64e2
|
|||
| 6371705b9c | |||
|
92aa4a614c
|
|||
| 6e20d4b8c8 | |||
|
f187c341f6
|
|||
| 16cf8ae2d1 | |||
|
d07b8ab73e
|
|||
| 52ba1108c0 | |||
|
54601905da
|
|||
| 88713b9738 | |||
| 83817cc1b6 | |||
|
0ef1a97f51
|
|||
|
a84e7a1675
|
|||
| fcffbffc02 | |||
|
a2af3015a2
|
|||
| e64e72df0e | |||
|
17dbf719a5
|
|||
| ecb3a2be8b | |||
|
2e0fbff172
|
|||
| cd4bbdea50 | |||
|
98608fba4d
|
|||
| 859d892ba9 | |||
| 797a12f1b6 | |||
|
2ef4429901
|
|||
|
f071535034
|
|||
| 119c570771 | |||
|
c474ed52c1
|
|||
| 4b24da83cb | |||
|
892a333e0e
|
|||
| ab4630fdd1 | |||
|
c318eb9fbb
|
|||
| 310d9779fe | |||
|
63134978b9
|
|||
| 099c4fb251 | |||
|
fa4f31b933
|
|||
| 835ba15cc7 | |||
| eb74233bfb | |||
|
1bb1b0571e
|
|||
|
f569a12edb
|
|||
| 3caee230f2 | |||
|
282d909cfd
|
|||
| 7548131847 | |||
|
ddf42a2d09
|
|||
| f88195b97d | |||
|
daf5acc335
|
|||
| b2246f6858 | |||
|
e424616e12
|
|||
| da20872a1e | |||
|
b43dff833f
|
|||
| 9248b76d8e | |||
|
019413a325
|
|||
| d94e490846 | |||
|
0d6e21618b
|
|||
| 0b03499f81 | |||
|
f9a62cad1c
|
|||
| 2014d0b87a | |||
| 24f237b795 | |||
|
1c985bca47
|
|||
|
291d436c1f
|
|||
| 08c8cb15ca | |||
|
d2b01a7bd3
|
|||
| aa75da2ecb | |||
|
0093b92b23
|
|||
| 6e0253f849 | |||
| 06ada51c0f | |||
|
1cf72e72b5
|
|||
|
181d4b56ac
|
|||
| c5870eba4a | |||
| 8242f153d8 | |||
| 117d2567e5 | |||
| a400c3187c | |||
|
15fb351504
|
|||
| fb492a1028 | |||
|
c427c5ddb7
|
|||
| 9a686eb9f5 | |||
|
2e0e52bdf9
|
|||
| f7a2bb1205 | |||
|
90b00872ee
|
|||
| 7dd5778c89 | |||
|
a062974f45
|
|||
| a7a14dfce5 | |||
|
51cdbd8b0b
|
|||
| f0e52d2f20 | |||
|
c79631cf7d
|
|||
| 6ef9380126 | |||
|
acea361723
|
|||
| c4c33ac9ad | |||
|
1d51d01be7
|
|||
| e9b783a83c | |||
|
d20c93fb8d
|
|||
| 8070cb6636 | |||
|
3f56f7ed2a
|
|||
| 825736458d | |||
|
6016ff4bf0
|
|||
| 9388a90a9d | |||
| 9b0dc6f3ca | |||
| 5203aeb96e | |||
|
94c2e8ded8
|
|||
| fcc0b9fea4 | |||
|
d0a1e6a91a
|
|||
| 62c1996e17 | |||
|
e3aca00d75
|
|||
| fe2bf96b33 | |||
|
8ccc69fe83
|
|||
| 5d5464426f | |||
|
d4be2547ec
|
|||
| 9fbf9b0864 | |||
|
2c5ab270a4
|
|||
| 0b83f7a573 | |||
|
a902c23821
|
|||
| e8a2000b4d | |||
|
c47976bd94
|
|||
| 3aba851dca | |||
|
7d8441c126
|
|||
| 6a1af4515f | |||
|
9995b84cc8
|
|||
| a9d0416576 | |||
|
fb89c5f501
|
|||
| 9a76654ee1 | |||
|
a5d6080b31
|
|||
| d9339214bc | |||
| e40e60023d | |||
| baa979d475 | |||
| 6d854c04a5 | |||
| 019c544d57 | |||
| 2d86be0c8f | |||
| 2ac89d8d35 | |||
|
1874cb6dba
|
|||
|
218797577e
|
|||
|
96e4670f49
|
|||
|
0fde0d1f8b
|
|||
|
f87ad542b2
|
|||
| 0dc48fa375 | |||
|
cda1c254b6
|
|||
| 5acc6f0ccd | |||
|
50b78e4d0d
|
|||
| 9f81114134 | |||
|
4a905f545a
|
|||
| 29f45afce2 | |||
|
dacdfd1e1c
|
|||
| aea953a0bb | |||
|
c20b9887f7
|
|||
| da41ff8939 | |||
|
45a51e301b
|
|||
| 1bd84c77e4 | |||
|
06f9450593
|
|||
| 2d9bfb5873 | |||
|
a90ad7e595
|
|||
| 464488595c | |||
|
49ccf8b417
|
|||
| fc58865824 | |||
|
fd549fd2ab
|
|||
| 59f3aa6992 | |||
|
89ef1e21bf
|
|||
| 20139360d0 | |||
|
7c1ac8432d
|
|||
| 56a624272c | |||
|
39e0a1db54
|
|||
| 5b535239c8 | |||
|
70e87b3732
|
|||
| 6488df5d54 | |||
|
0dc2ac4afc
|
|||
| 569b61f6a8 | |||
|
c602569df7
|
|||
| 24c53cbb53 | |||
|
6f4c6177a4
|
|||
| 826c53174e | |||
|
2775f0e240
|
|||
| e12174c170 | |||
|
5128a60269
|
|||
| 5e133c4fb9 | |||
|
9340faabcd
|
|||
| 9ee670346f | |||
|
ae236af1e4
|
|||
| cccb25f802 | |||
|
aaa37abcd4
|
|||
| 634506a3a5 | |||
| 5a62d0f571 | |||
| aca3566608 | |||
| e11af62c90 | |||
| be31df455f | |||
|
e0d7032727
|
|||
| e5e809fe45 | |||
| 6ac7de8892 | |||
| d417fe92cd | |||
| 98f02dbde1 | |||
| 0222d3d401 | |||
| d6ee4a2698 | |||
|
2929341ad9
|
|||
|
313d8956fb
|
|||
|
2c5deaa49e
|
|||
|
322f297bbe
|
|||
| a383bdc849 | |||
|
0ffee0c0d9
|
|||
| 2b77fb7263 | |||
|
c53dfedb67
|
|||
| 396cf6febb | |||
|
b4900d9516
|
|||
| bfb0c09bed | |||
|
5ccc58b2ab
|
|||
| bfc7dcdfd3 | |||
|
31d0ea5631
|
|||
| b55e04d896 | |||
| 018ec3a337 | |||
|
62cffcec04
|
|||
| c17fe89db2 | |||
|
89aa4d292d
|
|||
| bfa2f030ec | |||
|
d3735a0f64
|
|||
| 7da3a53af7 | |||
|
0954558856
|
|||
| 15af3ee905 | |||
|
01bfad21b6
|
|||
| ac7ec582a9 | |||
| 1754396414 | |||
|
742ced6ae3
|
|||
| 3ffe13043e | |||
| fb2f39d7df | |||
| d945682513 | |||
|
6b46ed04c1
|
|||
|
b6316a08bc
|
|||
|
66e8f50d7f
|
|||
| b044f16987 | |||
| abd14b6828 | |||
|
52be8d9e68
|
|||
|
2de3dd8965
|
|||
| f29a95ef39 | |||
|
e924dbcb55
|
|||
| 525c28092b | |||
| 4beaf9c74c | |||
| 7a0e536514 | |||
|
85f5f362d6
|
|||
|
229aeb332f
|
|||
| 8a1f843d20 | |||
|
f1f40b02cf
|
|||
| a69de08970 | |||
|
567890b771
|
|||
| 6e55e6b4cb | |||
|
6a75d20f3e
|
|||
| 632487898f | |||
|
74d0acb37f
|
|||
| bada42d08c | |||
|
e7ab822b13
|
|||
| 8710c62b45 | |||
|
b79f0ee6f3
|
|||
| f1127532dd | |||
|
7f66de6b4e
|
|||
| ffe976d44c | |||
|
9ae19a4446
|
|||
| 10185a3752 | |||
|
3b04a8a5c4
|
|||
| 32e3821454 | |||
| aced5818d5 | |||
| 16fd92b575 | |||
| 071f52fb0b | |||
| 43db9ab1ef | |||
| 17e1cfdae3 | |||
|
f9a157340f
|
|||
| eaf9e81438 | |||
| 3d4158af52 | |||
|
34317f90bb
|
|||
| a927f785cc | |||
|
0fef6bfe38
|
|||
| 171ad7e633 | |||
|
d6641d3b0c
|
|||
| 45e585d37c | |||
|
078a6c07aa
|
|||
| 42c0be8165 | |||
|
6f873a52ee
|
|||
| 1cf4bb1c98 | |||
| 77e678ac41 | |||
|
8381f1cbff
|
|||
|
1b4fd716ba
|
|||
| b3f738aedb | |||
|
3de6bdf9d5
|
|||
| 886c04047d | |||
| 30e152989f | |||
| b06297591c | |||
|
c5baf3f2f6
|
|||
|
7dd8e57a2f
|
|||
|
9072151d92
|
|||
| 86b3009de0 | |||
|
1b3acfb055
|
|||
| 1f286a8884 | |||
|
2c694045d4
|
|||
| 8644c4c8c1 | |||
|
6e75664908
|
|||
| e03e10170e | |||
|
2a1fa39d5e
|
|||
| d250e43315 | |||
|
d79a6e1c6f
|
|||
| f2da475a10 | |||
|
3dd3541975
|
|||
| 312582cfef | |||
|
add20c234d
|
|||
| e6e14f16d8 | |||
|
7360e4e3bb
|
|||
| e463b9842b | |||
|
cb81b9c873
|
|||
| eaf8184426 | |||
|
be65fbc299
|
|||
| 4893758d10 | |||
| 57e299090a | |||
|
18b69c9ca6
|
|||
|
dcd6f32e7a
|
|||
| c27616efdf | |||
|
c7c78ed2fc
|
|||
| c30dd1b027 | |||
|
96861f43f0
|
|||
| d08855ff40 | |||
|
ef6c656b16
|
|||
| d819c4e6a9 | |||
|
807b188162
|
|||
| 4558eefe8d | |||
|
304921712f
|
|||
| 6b2b648848 | |||
|
2ecfeb0a12
|
|||
| d6d600fe4b | |||
|
c4518c0c27
|
|||
| cc60a3ac14 | |||
|
6d782ad980
|
|||
| 31b9f5d603 | |||
|
98301d9e4f
|
|||
| cd76eb1ba8 | |||
|
c69cdde688
|
|||
| 4edb797817 | |||
|
3a1cfe4b63
|
|||
| 53031f7a0f | |||
| 5c2b3069c6 | |||
| 349817e384 | |||
| cf7098b7ea | |||
|
e9f8191973
|
|||
|
081250dcfc
|
|||
|
8149be36d9
|
|||
|
7f35f74591
|
|||
| 4dd465ef22 | |||
|
05076543ac
|
|||
| 5950ab54e1 | |||
|
1c2891bbba
|
|||
| c49b4a814f | |||
|
1c4c1f7f4a
|
|||
| 1baa5c565a | |||
|
cb9c5033ef
|
|||
| ff9bdc196d | |||
|
30ada35a53
|
|||
| 893a981594 | |||
|
e7227244cc
|
|||
| 507fe2c2ed | |||
|
aa40bf67b6
|
|||
| 17dd45c4e2 | |||
| a2c17a9dc1 | |||
| 365d079dfc | |||
| f29a5c9dd9 | |||
|
cd593905c0
|
|||
| 20f53409ef | |||
|
61c6d2ed08
|
|||
| 0018cfe564 | |||
| 2dd725b00c | |||
| db8f125fe7 | |||
|
3d4265a852
|
|||
|
7a66226be4
|
|||
|
f0b6ee4067
|
|||
| 1cb50c3bec | |||
|
4a7af7f582
|
|||
| 27574e7612 | |||
|
96d96b6e2b
|
|||
| c901fb851b | |||
| e91a2344eb | |||
| 5b20f0a9a3 | |||
|
9652309b67
|
|||
|
9e6fcd62e6
|
|||
|
50a7a09de7
|
|||
| 8a6b762342 | |||
|
55b88e3aca
|
|||
| 21f3b768f1 | |||
|
f41bf11c09
|
|||
| ffafd31d39 | |||
|
fb64aa4cd7
|
|||
| 4ff82866ab | |||
|
c6ec303312
|
|||
| bdf406a3ee | |||
|
831266f2a0
|
|||
| f53f4ad254 | |||
|
d25166ae9c
|
|||
| 0c72977ca7 | |||
|
e1b15ba0f8
|
|||
| a8ddaa5613 | |||
|
b8276d8470
|
|||
| f6e2b6ccf6 | |||
|
fd83f26ab5
|
|||
| d456c1c045 | |||
|
4d8822c187
|
|||
| be3cbe97e5 | |||
|
3d791440f2
|
|||
| 15438a6723 | |||
|
aabe85bc50
|
|||
|
c9c3f21c5a
|
|||
|
502a54186a
|
|||
| 9b248472cc | |||
|
109753e8bd
|
|||
| 17b903afe0 | |||
|
2f264f17d0
|
|||
| e5db1ffca6 | |||
|
f0f48dd16c
|
|||
| 5d8f98f8f0 | |||
|
5de410d577
|
|||
| da75115ea0 | |||
|
b769c130b3
|
|||
| 9a13fc35c3 | |||
|
41675481e8
|
|||
| 6d46dae265 | |||
|
9c280a1c02
|
|||
| 2be6ea9813 | |||
| 6d235806a8 | |||
|
1bc940afd6
|
|||
| 11abbf790d | |||
|
1f0c04a168
|
|||
| 32ddc9129c | |||
|
d9a103a553
|
|||
| 473d1d15cb | |||
|
652955263e
|
|||
| 4cb215625b | |||
|
98a3ed338c
|
|||
| 7e5eb7fd1a | |||
|
0be31cb98f
|
|||
| 6386c76550 | |||
|
b865b93797
|
|||
|
9bed3b30a2
|
|||
| 2556adb7cb | |||
|
4ad9ec7d1e
|
|||
| be91babd39 | |||
|
e189edbfe5
|
|||
|
17f37152a5
|
|||
| 80c7f6ddc2 | |||
|
cac399b924
|
|||
| d7b0b846d2 | |||
|
d04967e435
|
|||
|
866ab47458
|
|||
|
f835e06d6f
|
|||
|
ac31a5a608
|
|||
| 0f93b9d138 | |||
|
2211107a2c
|
|||
| b58cbdbe0a | |||
| 49e2376dbf | |||
| 6b1eaa439a | |||
| f92f911360 | |||
| 1cdbbd4a11 | |||
| da7c5c4a58 | |||
| 931d1009ed | |||
| 43ff986963 | |||
| b9d85a5520 | |||
| 9836b40531 | |||
| ea1c3d9f1a | |||
| 28f73be784 | |||
| 284f30c392 | |||
| 9e4a2d681b | |||
| c8e250c5b2 | |||
|
58f05178a4
|
|||
| b8966e2b88 | |||
|
3f6563a0d3
|
|||
| 4840d15101 | |||
|
f2cb98888a
|
|||
| 9d1402ee82 | |||
|
741338ae9f
|
|||
| 89d8b025d3 | |||
|
5e74f8b01e
|
|||
| bf4835e797 | |||
| fc766599e1 | |||
|
465bda1859
|
|||
|
19d2558436
|
|||
|
2f797ca614
|
|||
|
99e451a934
|
|||
| 1dc4ccfbc6 | |||
| a484feb7cd | |||
| 93d11dca17 | |||
| 3eacf17f61 | |||
| 12ffcc4d72 | |||
|
060400183f
|
|||
|
31ec9908e6
|
|||
| 4180a2eceb | |||
|
fdef90e636
|
|||
|
c369651a70
|
|||
| 75fd981f10 | |||
| 80a4aee41c | |||
| 9e84de0a5a | |||
| 64140cce6b | |||
| 0733fe6a06 | |||
|
0f5c015932
|
|||
| dc17aeb3d5 | |||
|
a852f22409
|
|||
| 130a3866bc | |||
|
2fb0542d36
|
|||
| 8a2be36f17 | |||
|
266d25e0f2
|
|||
| 34dbe6d809 | |||
|
3c654e19e1
|
|||
|
2a0142ee83
|
|||
| 7836f49828 | |||
| 25280a239c | |||
| c56dc99e72 | |||
| 48b7a13729 | |||
| ac026b0264 | |||
| 5332854856 | |||
| 2e0c2f3de5 | |||
| 88d510b06f | |||
| 7843378503 | |||
|
75016fdb4f
|
|||
| 4d74f74ab2 | |||
|
2c1b7f577d
|
|||
| 0e79b32012 | |||
|
c1ef2d2ba2
|
|||
| 020c709b43 | |||
| 9f346ee156 | |||
| e820e4f163 | |||
| 796926316e | |||
|
bf8578045e
|
|||
|
f16af9a98d
|
|||
| ec45ad29ed | |||
| 17afce6710 | |||
| f83fe98b38 | |||
| 2f244761ed | |||
| 649bf4482b | |||
| 2028e2247e | |||
| fcae7676c6 | |||
| cc16b5435a | |||
| 27b5e6a36b | |||
| bcb91972a1 | |||
|
b11666decb
|
|||
| a947a05041 | |||
|
297c573281
|
|||
|
9093594973
|
|||
|
77ce0a1182
|
|||
| 799e6b6090 | |||
|
735e4b4877
|
|||
| 3e12a8647d | |||
| e07210638e | |||
| 22d5b50f73 | |||
| 40acf8f34a | |||
| 543516baba | |||
| e985f905f2 | |||
| e1f09ca4ec | |||
| 0c09eb38e9 | |||
|
95eeb44e4f
|
|||
| d47d67572e | |||
|
fa4841948a
|
|||
| 71e2b0185b | |||
| 7f9fb4d2b9 | |||
| 8420c8dd58 | |||
| fa6ed18edb | |||
| 30860fce1e | |||
| b479e0e22c | |||
| cf01ebcd3c | |||
|
df8ccf81c2
|
|||
| 073911c1b9 | |||
| 3eeea3dd8f | |||
|
43fea76778
|
|||
| d64df6473a | |||
|
63a6a00817
|
|||
| 54759056b3 | |||
| 3cc9762e0d | |||
|
ef757c4a14
|
|||
| 176f92bf67 | |||
| 09d411dd68 | |||
|
54acfcb24d
|
|||
|
6f3b631862
|
|||
| 18cd240a9b | |||
|
bb4fe8ef37
|
|||
|
e0e3c1f61a
|
|||
| 0b5c6ae999 | |||
| a20ba4ab43 | |||
| 550e7dfe52 | |||
|
03174cfb9d
|
|||
|
da50c1928c
|
|||
| f1d1fe979e | |||
| 4d6019d0b0 | |||
|
7dd302b3d4
|
|||
|
8a8f2a6216
|
|||
| 97775f1ceb | |||
| 0a437a26f1 | |||
| ba67b4d0e4 | |||
|
0bcfa9bed4
|
|||
|
ada95481f7
|
|||
|
7c9f4acc00
|
|||
|
0b7b87580a
|
|||
|
08f076e566
|
|||
| 26c27b9353 | |||
| ce8b3a2e19 | |||
| 6d34c0d407 | |||
| 63607bbca3 | |||
|
745d2553a0
|
|||
| 8a19559cc7 | |||
|
42854db0fb
|
|||
| 7b72e3849b | |||
| 6a8dbb0c7c | |||
|
91fdf5a83f
|
|||
|
073f3a7916
|
|||
| 38202841ca | |||
|
0492922cce
|
|||
| a17500835b | |||
| 2f8b97208c | |||
|
d6c30d5e5b
|
|||
|
a7ea9db3aa
|
|||
| 9134e78e8a | |||
| 2ca7d6705d | |||
| 5722e8c7a1 | |||
| e39fd2acb8 | |||
| 0313fd54bc | |||
| dbb0f6d7ff | |||
| 20669d9766 | |||
| 6b2e6353d1 | |||
| 6d112b52df | |||
| ff17af604f | |||
| 32ea0989d7 | |||
| e4ab7d134c | |||
| 5fad13655c | |||
| 8614d40a64 | |||
| 8c417b93b3 | |||
| 1d9519831b | |||
| fa57f2e93f | |||
| 9e01002d4e | |||
| cb52c169a3 | |||
| 3017668cd2 | |||
| 1972b3bc19 | |||
| af77f90a49 | |||
| bdda29f369 | |||
| 644c5fcd6a | |||
| bafd8158d3 | |||
| 4d9c1a3e8c | |||
| 4a4233ac62 | |||
| c71957348d | |||
| 400bf16dd9 | |||
| 85535614a0 | |||
| 38fcbb635b | |||
| b1e57c3f17 | |||
| e22a1985be | |||
| 70b0b86944 | |||
| ba36de8e36 | |||
| d2e44fe046 | |||
| 36ec797d3b | |||
| 086d98ba50 | |||
| 8a05fa4d96 | |||
| dbbf886de9 | |||
| ae7e21eb82 | |||
| ce6f476e8f | |||
| 0ca6be1d91 | |||
| cedcae02ce | |||
| 4ef6e85ed9 | |||
| 1ad039e9ff | |||
| 034d6d1120 | |||
| 2c436100c5 | |||
| 6ea1467653 | |||
| 1ba76ab5cf | |||
| 478482ab01 | |||
| f1e3e4ecaa | |||
| 05eb8a092c | |||
| 633e374a17 | |||
| cd75440a6d | |||
| 3354975e2e | |||
| 1ffe933d6e | |||
| 90318aad14 | |||
| e454a510c6 | |||
| a6d3ec5052 | |||
| 1d134d43da | |||
| 54c7c9e259 | |||
| 0d8cf28be4 | |||
| d78a8d8c45 | |||
| 5b6abeb9f9 | |||
| a3b0301d23 | |||
| 06f7546212 | |||
| abd1d43f79 | |||
| 07f2f5f0e1 | |||
| 91b53a33c2 | |||
| b3e23f3e6c | |||
| ab68b6248f |
@@ -1,3 +1,5 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
.astro
|
||||||
|
.vscode
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
|
|||||||
@@ -3,16 +3,57 @@ name: release-image-gitea
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 0.*
|
- 2.*
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-js
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10.x
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24.13.1
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Lint Code
|
||||||
|
run: pnpm lint
|
||||||
|
|
||||||
|
- name: Build Project
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-js
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Login to Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.REPOSITORY_HOST }}
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.REPOSITORY_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to Docker
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.DH_REGISTRY }}
|
||||||
|
username: ${{ secrets.DH_USERNAME }}
|
||||||
|
password: ${{ secrets.DH_TOKEN }}
|
||||||
|
|
||||||
- name: Create Kubeconfig
|
- name: Create Kubeconfig
|
||||||
run: |
|
run: |
|
||||||
@@ -27,17 +68,13 @@ jobs:
|
|||||||
driver-opts: |
|
driver-opts: |
|
||||||
namespace=gitea
|
namespace=gitea
|
||||||
qemu.install=true
|
qemu.install=true
|
||||||
|
buildkitd-config-inline: |
|
||||||
|
[registry."docker.io"]
|
||||||
|
mirrors = ["harbor.alexlebens.net/proxy-hub.docker/"]
|
||||||
|
|
||||||
- name: Available Platforms
|
- name: Available Platforms
|
||||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||||
|
|
||||||
- name: Login to Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ vars.REPOSITORY_HOST }}
|
|
||||||
username: ${{ gitea.actor }}
|
|
||||||
password: ${{ secrets.REPOSITORY_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract Metadata
|
- name: Extract Metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -45,7 +82,23 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
images: ${{ vars.REPOSITORY_HOST }}/${{ gitea.repository }}
|
type=sha,format=long
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
images: |
|
||||||
|
${{ vars.REPOSITORY_HOST }}/${{ gitea.repository }}
|
||||||
|
|
||||||
|
- name: Get Version Info
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
|
||||||
|
echo "commit=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
if git describe --tags --exact-match HEAD 2>/dev/null; then
|
||||||
|
echo "is_release=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "is_release=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build and Push Image
|
- name: Build and Push Image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -55,13 +108,36 @@ jobs:
|
|||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
COMMIT_SHA=${{ steps.version.outputs.commit }}
|
||||||
|
IS_RELEASE=${{ steps.version.outputs.is_release }}
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|
||||||
- name: Actions Ntfy
|
- name: ntfy Success
|
||||||
run: |
|
uses: niniyas/ntfy-action@master
|
||||||
curl \
|
if: success()
|
||||||
-H "Authorization: Bearer ${{ secrets.NTFY_CRED }}" \
|
with:
|
||||||
-H "Title: Site-Profile Image Released to Gitea: ${{ steps.meta.outputs.tags }}" \
|
url: '${{ secrets.NTFY_URL }}'
|
||||||
-H "Content-Type: text/plain" \
|
topic: '${{ secrets.NTFY_TOPIC }}'
|
||||||
-d 'Repo: ${{ gitea.repository }}\nCommit: ${{ gitea.sha }}\nRef: ${{ gitea.ref }}\nStatus: ${{ job.status}}' \
|
title: 'Release Success - Site Profile'
|
||||||
${{ secrets.NTFY_URL }}
|
priority: 3
|
||||||
|
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
|
||||||
|
tags: action,successfully,completed
|
||||||
|
details: 'Image for Site Profile has been released!'
|
||||||
|
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: 'Release Failure - Site Profile'
|
||||||
|
priority: 4
|
||||||
|
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
|
||||||
|
tags: action,failed
|
||||||
|
details: 'Image for Site Profile has failed to be released.'
|
||||||
|
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
|
||||||
|
|||||||
@@ -3,16 +3,57 @@ name: release-image-harbor
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- 0.*
|
- 2.*
|
||||||
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-js
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10.x
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24.13.1
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Lint Code
|
||||||
|
run: pnpm lint
|
||||||
|
|
||||||
|
- name: Build Project
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-js
|
||||||
|
needs: build
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Login to Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.REGISTRY_HOST }}
|
||||||
|
username: ${{ vars.REGISTRY_USER }}
|
||||||
|
password: ${{ secrets.REGISTRY_SECRET }}
|
||||||
|
|
||||||
|
- name: Login to Docker
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ vars.DH_REGISTRY }}
|
||||||
|
username: ${{ secrets.DH_USERNAME }}
|
||||||
|
password: ${{ secrets.DH_TOKEN }}
|
||||||
|
|
||||||
- name: Create Kubeconfig
|
- name: Create Kubeconfig
|
||||||
run: |
|
run: |
|
||||||
@@ -27,17 +68,13 @@ jobs:
|
|||||||
driver-opts: |
|
driver-opts: |
|
||||||
namespace=gitea
|
namespace=gitea
|
||||||
qemu.install=true
|
qemu.install=true
|
||||||
|
buildkitd-config-inline: |
|
||||||
|
[registry."docker.io"]
|
||||||
|
mirrors = ["harbor.alexlebens.net/proxy-hub.docker/"]
|
||||||
|
|
||||||
- name: Available Platforms
|
- name: Available Platforms
|
||||||
run: echo ${{ steps.buildx.outputs.platforms }}
|
run: echo ${{ steps.buildx.outputs.platforms }}
|
||||||
|
|
||||||
- name: Login to Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ vars.REGISTRY_HOST }}
|
|
||||||
username: ${{ vars.REGISTRY_USER }}
|
|
||||||
password: ${{ secrets.REGISTRY_SECRET }}
|
|
||||||
|
|
||||||
- name: Extract Metadata
|
- name: Extract Metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@v5
|
uses: docker/metadata-action@v5
|
||||||
@@ -45,7 +82,23 @@ jobs:
|
|||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
images: ${{ vars.REGISTRY_HOST }}/images/site-profile
|
type=sha,format=long
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
images: |
|
||||||
|
${{ vars.REGISTRY_HOST }}/images/site-profile
|
||||||
|
|
||||||
|
- name: Get Version Info
|
||||||
|
id: version
|
||||||
|
run: |
|
||||||
|
echo "version=$(git describe --tags --always)" >> $GITHUB_OUTPUT
|
||||||
|
echo "commit=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
|
||||||
|
if git describe --tags --exact-match HEAD 2>/dev/null; then
|
||||||
|
echo "is_release=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "is_release=false" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Build and Push Image
|
- name: Build and Push Image
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v6
|
||||||
@@ -55,13 +108,36 @@ jobs:
|
|||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
VERSION=${{ steps.version.outputs.version }}
|
||||||
|
COMMIT_SHA=${{ steps.version.outputs.commit }}
|
||||||
|
IS_RELEASE=${{ steps.version.outputs.is_release }}
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
|
|
||||||
- name: Actions Ntfy
|
- name: ntfy Success
|
||||||
run: |
|
uses: niniyas/ntfy-action@master
|
||||||
curl \
|
if: success()
|
||||||
-H "Authorization: Bearer ${{ secrets.NTFY_CRED }}" \
|
with:
|
||||||
-H "Title: Site-Profile Image Released to Gitea: ${{ steps.meta.outputs.tags }}" \
|
url: '${{ secrets.NTFY_URL }}'
|
||||||
-H "Content-Type: text/plain" \
|
topic: '${{ secrets.NTFY_TOPIC }}'
|
||||||
-d 'Repo: ${{ gitea.repository }}\nCommit: ${{ gitea.sha }}\nRef: ${{ gitea.ref }}\nStatus: ${{ job.status}}' \
|
title: 'Release Success - Site Profile'
|
||||||
${{ secrets.NTFY_URL }}
|
priority: 3
|
||||||
|
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
|
||||||
|
tags: action,successfully,completed
|
||||||
|
details: 'Image for Site Profile has been released!'
|
||||||
|
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: 'Release Failure - Site Profile'
|
||||||
|
priority: 4
|
||||||
|
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
|
||||||
|
tags: action,failed
|
||||||
|
details: 'Image for Site Profile has failed to be released.'
|
||||||
|
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
|
||||||
|
|||||||
@@ -13,18 +13,22 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
renovate:
|
renovate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container: ghcr.io/renovatebot/renovate:40
|
container: ghcr.io/renovatebot/renovate:43
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- name: Checkout
|
||||||
- run: renovate
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Renovate
|
||||||
|
run: renovate
|
||||||
env:
|
env:
|
||||||
RENOVATE_PLATFORM: gitea
|
RENOVATE_PLATFORM: gitea
|
||||||
RENOVATE_AUTODISCOVER: true
|
RENOVATE_ENDPOINT: ${{ vars.INSTANCE_URL }}
|
||||||
RENOVATE_ONBOARDING: true
|
RENOVATE_REPOSITORIES: alexlebens/site-profile
|
||||||
RENOVATE_ENDPOINT: http://gitea-http.gitea:3000
|
|
||||||
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net>
|
RENOVATE_GIT_AUTHOR: Renovate Bot <renovate-bot@alexlebens.net>
|
||||||
LOG_LEVEL: debug
|
RENOVATE_REDIS_URL: ${{ vars.RENOVATE_REDIS_URL }}
|
||||||
|
LOG_LEVEL: info
|
||||||
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
RENOVATE_TOKEN: ${{ secrets.RENOVATE_TOKEN }}
|
||||||
RENOVATE_GIT_PRIVATE_KEY: ${{ secrets.RENOVATE_GIT_PRIVATE_KEY }}
|
RENOVATE_GIT_PRIVATE_KEY: ${{ secrets.RENOVATE_GIT_PRIVATE_KEY }}
|
||||||
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
|
RENOVATE_GITHUB_COM_TOKEN: ${{ secrets.RENOVATE_GITHUB_COM_TOKEN }}
|
||||||
RENOVATE_REDIS_URL: redis://gitea-renovate-valkey-primary.gitea:6379
|
RENOVATE_REGISTRY_ALIASES: '{"dhi.io": "dhi.io"}'
|
||||||
|
RENOVATE_HOST_RULES: '[{"matchHost":"dhi.io","hostType":"docker","username":"${{ secrets.RENOVATE_DHI_USER }}","password":"${{ secrets.RENOVATE_DHI_TOKEN }}"}]'
|
||||||
|
|||||||
87
.gitea/workflows/test-build.yaml
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
name: test-build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-js
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10.x
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 24.13.1
|
||||||
|
cache: pnpm
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Lint Code
|
||||||
|
run: pnpm lint
|
||||||
|
|
||||||
|
- name: Build Project
|
||||||
|
run: pnpm build
|
||||||
|
|
||||||
|
- name: ntfy Failed
|
||||||
|
uses: niniyas/ntfy-action@master
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
url: '${{ secrets.NTFY_URL }}'
|
||||||
|
topic: '${{ secrets.NTFY_TOPIC }}'
|
||||||
|
title: 'Test Failure - Site Profile'
|
||||||
|
priority: 4
|
||||||
|
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
|
||||||
|
tags: action,failed
|
||||||
|
details: 'Tests have failed for building Site Profile'
|
||||||
|
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=test-build.yaml", "clear": true}]'
|
||||||
|
image: true
|
||||||
|
|
||||||
|
guarddog:
|
||||||
|
runs-on: ubuntu-js
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v6
|
||||||
|
with:
|
||||||
|
python-version: '3.12'
|
||||||
|
|
||||||
|
- name: Install GuardDog
|
||||||
|
run: |
|
||||||
|
python3 -m pip install --upgrade pip
|
||||||
|
python3 -m pip install guarddog
|
||||||
|
|
||||||
|
- name: Run GuardDog
|
||||||
|
run: |
|
||||||
|
guarddog npm scan ./
|
||||||
|
|
||||||
|
- name: ntfy Failed
|
||||||
|
uses: niniyas/ntfy-action@master
|
||||||
|
if: failure()
|
||||||
|
with:
|
||||||
|
url: '${{ secrets.NTFY_URL }}'
|
||||||
|
topic: '${{ secrets.NTFY_TOPIC }}'
|
||||||
|
title: 'Security Failure - Site Profile'
|
||||||
|
priority: 4
|
||||||
|
headers: '{"Authorization": "Bearer ${{ secrets.NTFY_CRED }}"}'
|
||||||
|
tags: action,failed
|
||||||
|
details: 'Guarddog scan failed for Site Profile'
|
||||||
|
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=test-build.yaml", "clear": true}]'
|
||||||
|
image: true
|
||||||
4
.gitignore
vendored
@@ -12,10 +12,9 @@ yarn-debug.log*
|
|||||||
yarn-error.log*
|
yarn-error.log*
|
||||||
pnpm-debug.log*
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
|
||||||
# environment variables
|
# environment variables
|
||||||
.env
|
.env
|
||||||
.env.local
|
|
||||||
.env.development
|
|
||||||
.env.production
|
.env.production
|
||||||
|
|
||||||
# macOS-specific files
|
# macOS-specific files
|
||||||
@@ -24,4 +23,3 @@ pnpm-debug.log*
|
|||||||
# ide
|
# ide
|
||||||
.vscode/
|
.vscode/
|
||||||
site-profile.code-workspace
|
site-profile.code-workspace
|
||||||
.pre-commit-config.yaml
|
|
||||||
|
|||||||
2
.npmrc
@@ -1,3 +1,3 @@
|
|||||||
|
registry=https://registry.npmjs.org/
|
||||||
engine-strict=true
|
engine-strict=true
|
||||||
save-exact=true
|
save-exact=true
|
||||||
|
|
||||||
|
|||||||
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/src/components/ui/sections/Experience.astro
|
||||||
17
.prettierrc
@@ -1,17 +0,0 @@
|
|||||||
{
|
|
||||||
"printWidth": 100,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": true,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "es5",
|
|
||||||
"useTabs": false,
|
|
||||||
"plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"],
|
|
||||||
"overrides": [
|
|
||||||
{
|
|
||||||
"files": "*.astro",
|
|
||||||
"options": {
|
|
||||||
"parser": "astro"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
4
.vscode/extensions.json
vendored
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": ["astro-build.astro-vscode"],
|
|
||||||
"unwantedRecommendations": []
|
|
||||||
}
|
|
||||||
11
.vscode/launch.json
vendored
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"command": "./node_modules/.bin/astro dev",
|
|
||||||
"name": "Development server",
|
|
||||||
"request": "launch",
|
|
||||||
"type": "node-terminal"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
19
Dockerfile
@@ -1,17 +1,13 @@
|
|||||||
FROM node:22.16.0-alpine3.22 AS base
|
FROM docker.io/node:24.13.1-alpine AS builder
|
||||||
|
|
||||||
LABEL version="0.7.0"
|
|
||||||
LABEL description="Astro based website to use as a personal site"
|
|
||||||
|
|
||||||
ENV PNPM_HOME="/pnpm"
|
ENV PNPM_HOME="/pnpm"
|
||||||
ENV PATH="$PNPM_HOME:$PATH"
|
ENV PATH="$PNPM_HOME:$PATH"
|
||||||
RUN corepack enable
|
RUN corepack enable
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package.json pnpm-lock.yaml ./
|
COPY package.json pnpm-lock.yaml ./
|
||||||
|
|
||||||
FROM base AS prod-deps
|
FROM builder AS prod-deps
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
|
||||||
|
|
||||||
FROM prod-deps AS build-deps
|
FROM prod-deps AS build-deps
|
||||||
@@ -21,13 +17,16 @@ FROM build-deps AS build
|
|||||||
COPY . .
|
COPY . .
|
||||||
RUN pnpm run build
|
RUN pnpm run build
|
||||||
|
|
||||||
FROM base AS runtime
|
FROM dhi.io/node:24.13.1 AS runtime
|
||||||
|
WORKDIR /app
|
||||||
COPY --from=prod-deps /app/node_modules /app/node_modules
|
COPY --from=prod-deps /app/node_modules /app/node_modules
|
||||||
COPY --from=build /app/dist /app/dist
|
COPY --from=build /app/dist /app/dist
|
||||||
|
|
||||||
|
LABEL version="2.15.1"
|
||||||
|
LABEL description="Astro based personal website"
|
||||||
|
|
||||||
ENV HOST=0.0.0.0
|
ENV HOST=0.0.0.0
|
||||||
ENV SITE_URL=https://www.alexlebens.dev
|
|
||||||
ENV DIRECTUS_URL=https://directus.alexlebens.dev
|
|
||||||
ENV PORT=4321
|
ENV PORT=4321
|
||||||
|
|
||||||
EXPOSE $PORT
|
EXPOSE $PORT
|
||||||
CMD node ./dist/server/entry.mjs
|
CMD ["node", "./dist/server/entry.mjs"]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Lê Vĩnh Khang
|
Copyright (c) 2025 Alex Lebens
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
79
README.md
@@ -1,78 +1,21 @@
|
|||||||
# Alex Lebens Personal Site
|
# This is an open-source and simple blog built with Astro.
|
||||||
|
|
||||||
Personal site used for information about myself and blog.
|
Personal site used for information about myself and blog.
|
||||||
|
|
||||||
## Features
|
## Development
|
||||||
|
|
||||||
- 🚀 **Maximum Performance** - Built with Astro.js for lightning-fast static sites
|
With dependencies installed, you can utilize the following npm scripts to manage your project's development lifecycle:
|
||||||
- 🎨 **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
|
- `pnpm build`: Bundles your site into static files for production.
|
||||||
|
- `pnpm dev`: Starts a local development server with hot reloading enabled.
|
||||||
|
- `pnpm preview`: Serves your build output locally for preview before deployment.
|
||||||
|
|
||||||
### Requirements
|
For detailed help with Astro CLI commands, visit [Astro's documentation](https://docs.astro.build/en/reference/cli-reference/).
|
||||||
|
|
||||||
- Node.js 16+ and pnpm/yarn
|
## Thanks
|
||||||
|
|
||||||
### Installation
|
Thanks https://github.com/godruoyi/gblog/tree/gblog-template, https://github.com/mearashadowfax/ScrewFast,
|
||||||
|
|
||||||
```bash
|
## License
|
||||||
# Clone repository
|
|
||||||
git clone https://gitea.alexlebens.dev/alexlebens/site-profile
|
|
||||||
|
|
||||||
# Navigate to project directory
|
This project is released under the MIT License. Please read the [LICENSE](https://gitea.alexlebens.dev/alexlebens/site-profile/src/LICENSE.md) file for more details.
|
||||||
cd astro-blog
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
pnpm install
|
|
||||||
|
|
||||||
# Create .env file from template
|
|
||||||
cp .env.example .env
|
|
||||||
|
|
||||||
# Edit .env with your information
|
|
||||||
```
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start development server
|
|
||||||
pnpm run dev
|
|
||||||
|
|
||||||
# Open browser at http://localhost:4321
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create production build
|
|
||||||
pnpm run build
|
|
||||||
|
|
||||||
# Preview production build
|
|
||||||
pnpm run preview
|
|
||||||
```
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
/
|
|
||||||
├── public/ # Static assets
|
|
||||||
├── src/
|
|
||||||
│ ├── components/ # Reusable UI components
|
|
||||||
│ ├── content/ # Blog content (Markdown/MDX)
|
|
||||||
│ ├── layouts/ # Page layouts
|
|
||||||
│ ├── pages/ # Pages and routes
|
|
||||||
│ ├── styles/ # CSS and Tailwind
|
|
||||||
│ └── utils/ # Utilities and helpers
|
|
||||||
├── astro.config.mjs # Astro configuration
|
|
||||||
├── tailwind.config.js # Tailwind configuration
|
|
||||||
└── tsconfig.json # TypeScript configuration
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,21 +1,90 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import tailwindcss from '@tailwindcss/vite';
|
|
||||||
import react from '@astrojs/react';
|
|
||||||
|
|
||||||
const getSiteURL = () => {
|
import node from '@astrojs/node';
|
||||||
if (process.env.SITE_URL) {
|
import partytown from '@astrojs/partytown';
|
||||||
return `https://${process.env.SITE_URL}`;
|
import react from '@astrojs/react';
|
||||||
}
|
import sitemap from '@astrojs/sitemap';
|
||||||
return 'http://localhost:4321';
|
|
||||||
};
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
import icon from 'astro-icon';
|
||||||
|
import swup from '@swup/astro';
|
||||||
|
|
||||||
|
import { getSiteURL } from './src/support/url';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
site: getSiteURL(),
|
site: getSiteURL(),
|
||||||
integrations: [tailwindcss(), react()],
|
|
||||||
plugins: {
|
image: {
|
||||||
"@tailwindcss/postcss": {},
|
remotePatterns: [
|
||||||
|
{ protocol: 'https', hostname: '*.alexlebens.net' },
|
||||||
|
{ protocol: 'https', hostname: '*.jsdelivr.net' },
|
||||||
|
{ protocol: 'https', hostname: '*.icons8.com' },
|
||||||
|
],
|
||||||
|
service: {
|
||||||
|
entrypoint: 'astro/assets/services/sharp',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
prefetch: true,
|
||||||
|
|
||||||
|
integrations: [
|
||||||
|
partytown(),
|
||||||
|
react(),
|
||||||
|
sitemap(),
|
||||||
|
icon({
|
||||||
|
include: {
|
||||||
|
mdi: ['*'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
swup({
|
||||||
|
theme: 'fade',
|
||||||
|
native: true,
|
||||||
|
cache: true,
|
||||||
|
preload: true,
|
||||||
|
accessibility: true,
|
||||||
|
smoothScrolling: true,
|
||||||
|
morph: ['#nav'],
|
||||||
|
}),
|
||||||
|
(await import('@playform/compress')).default({
|
||||||
|
CSS: true,
|
||||||
|
JavaScript: true,
|
||||||
|
HTML: {
|
||||||
|
'html-minifier-terser': {
|
||||||
|
collapseWhitespace: true,
|
||||||
|
minifyCSS: false,
|
||||||
|
minifyJS: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Image: false,
|
||||||
|
SVG: true,
|
||||||
|
Logger: 2,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
markdown: {
|
||||||
|
syntaxHighlight: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
},
|
||||||
|
|
||||||
vite: {
|
vite: {
|
||||||
plugins: [tailwindcss()]
|
plugins: [tailwindcss()],
|
||||||
}
|
},
|
||||||
});
|
|
||||||
|
output: 'static',
|
||||||
|
|
||||||
|
adapter: node({
|
||||||
|
mode: 'standalone',
|
||||||
|
}),
|
||||||
|
|
||||||
|
build: {
|
||||||
|
// Specifies the directory in the build output where Astro-generated assets (bundled JS and CSS for example) should live.
|
||||||
|
// see https://docs.astro.build/en/reference/configuration-reference/#buildassets
|
||||||
|
assets: 'assets',
|
||||||
|
// see https://docs.astro.build/en/reference/configuration-reference/#buildassetsprefix
|
||||||
|
assetsPrefix:
|
||||||
|
!!import.meta.env.S3_ENABLE || !!process.env.S3_ENABLE ? 'https://digitalocean.com' : '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|||||||
11
eslint.config.mjs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import eslintPluginAstro from 'eslint-plugin-astro';
|
||||||
|
import eslintConfigPrettier from "eslint-config-prettier/flat";
|
||||||
|
|
||||||
|
export default [
|
||||||
|
...eslintPluginAstro.configs.recommended,
|
||||||
|
eslintConfigPrettier,
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { createDirectus, rest } from '@directus/sdk';
|
|
||||||
|
|
||||||
type Global = {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
name: string;
|
|
||||||
initals: string;
|
|
||||||
tagline: string;
|
|
||||||
email: string;
|
|
||||||
portrait: string;
|
|
||||||
portrait_alt: string;
|
|
||||||
about: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type About = {
|
|
||||||
background: string;
|
|
||||||
experience: string;
|
|
||||||
education: string;
|
|
||||||
certifications: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Links = {
|
|
||||||
github: string;
|
|
||||||
linkedin: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Skill = {
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
icon: string;
|
|
||||||
level: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type Post = {
|
|
||||||
slug: string;
|
|
||||||
title: string;
|
|
||||||
description: string;
|
|
||||||
content: string;
|
|
||||||
image: string;
|
|
||||||
image_alt: string;
|
|
||||||
published_date: Date;
|
|
||||||
updated_date: Date;
|
|
||||||
tags: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
type Schema = {
|
|
||||||
global: Global;
|
|
||||||
about: About;
|
|
||||||
links: Links;
|
|
||||||
skills: Skill[];
|
|
||||||
posts: Post[];
|
|
||||||
};
|
|
||||||
|
|
||||||
const directus = createDirectus<Schema>(
|
|
||||||
process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'
|
|
||||||
).with(rest());
|
|
||||||
|
|
||||||
export default directus;
|
|
||||||
92
package.json
@@ -1,38 +1,80 @@
|
|||||||
{
|
{
|
||||||
"name": "site-profile",
|
"name": "site-profile",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.7.0",
|
"version": "2.15.1",
|
||||||
"private": true,
|
"homepage": "https://www.alexlebens.dev",
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://gitea.alexlebens.dev/alexlebens/site-profile/issues",
|
||||||
|
"email": "alexander.lebens@gmail.com"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitea.alexlebens.dev/alexlebens/site-profile"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
|
"author": {
|
||||||
|
"name": "Alex Lebens",
|
||||||
|
"email": "alexander.lebens@gmail.com",
|
||||||
|
"url": "https://www.alexlebens.dev"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev",
|
"dev": "astro dev",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
"preview": "astro preview",
|
"preview": "astro preview",
|
||||||
"format": "prettier . --write",
|
"astro": "astro",
|
||||||
"astro": "astro"
|
"format": "prettier --write \"**/*.{js,jsx,ts,tsx,md,mdx,astro}\"",
|
||||||
|
"lint": "eslint \"src/**/*.{js,ts,jsx,tsx,astro}\"",
|
||||||
|
"lint:fix": "eslint --fix \"src/**/*.{js,ts,jsx,tsx,astro}\""
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/mdx": "^4.3.0",
|
"@astrojs/check": "^0.9.6",
|
||||||
"@astrojs/node": "^9.2.2",
|
"@astrojs/node": "^9.5.3",
|
||||||
"@astrojs/react": "^4.3.0",
|
"@astrojs/partytown": "^2.1.4",
|
||||||
"@astrojs/rss": "^4.0.12",
|
"@astrojs/react": "^4.4.2",
|
||||||
"@astrojs/sitemap": "^3.4.1",
|
"@astrojs/rss": "^4.0.15",
|
||||||
"@directus/sdk": "^19.1.0",
|
"@astrojs/sitemap": "^3.7.0",
|
||||||
"@tailwindcss/vite": "^4.1.8",
|
"@directus/sdk": "^21.1.0",
|
||||||
"@tailwindcss/postcss": "^4.1.8",
|
"@giscus/react": "^3.1.0",
|
||||||
"astro": "^5.9.1",
|
"@iconify-json/mdi": "^1.2.3",
|
||||||
"framer-motion": "^12.16.0",
|
"@iconify-json/pajamas": "^1.2.15",
|
||||||
"react": "^19.1.0",
|
"@iconify-json/simple-icons": "^1.2.70",
|
||||||
"react-dom": "^19.1.0",
|
"@playform/compress": "^0.2.1",
|
||||||
"react-hotkeys-hook": "^5.1.0",
|
"@swup/astro": "^1.8.0",
|
||||||
"react-icons": "^5.5.0",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"postcss-preset-env": "^10.2.1",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"sanitize-html": "^2.17.0",
|
"@types/react": "^19.2.14",
|
||||||
"tailwindcss": "^4.1.8"
|
"@types/unist": "^3.0.3",
|
||||||
|
"astro": "^5.17.2",
|
||||||
|
"astro-icon": "^1.1.5",
|
||||||
|
"marked": "^17.0.2",
|
||||||
|
"marked-shiki": "^1.2.1",
|
||||||
|
"mdast-util-to-string": "^4.0.0",
|
||||||
|
"preline": "^4.0.1",
|
||||||
|
"react": "^19.2.4",
|
||||||
|
"react-dom": "^19.2.4",
|
||||||
|
"reading-time": "^1.5.0",
|
||||||
|
"sharp": "^0.34.5",
|
||||||
|
"sharp-ico": "^0.1.5",
|
||||||
|
"shiki": "^3.22.0",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"ultrahtml": "^1.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/typography": "^0.5.16",
|
"@eslint-react/eslint-plugin": "^2.13.0",
|
||||||
"prettier": "^3.5.3",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"prettier-plugin-astro": "^0.14.0",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"prettier-plugin-tailwindcss": "^0.6.0"
|
"eslint": "^10.0.0",
|
||||||
|
"eslint-config-prettier": "^10.1.8",
|
||||||
|
"eslint-plugin-astro": "^1.6.0",
|
||||||
|
"eslint-plugin-format": "^1.4.0",
|
||||||
|
"eslint-plugin-react": "^7.37.5",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.5.0",
|
||||||
|
"prettier": "^3.8.1",
|
||||||
|
"prettier-plugin-astro": "^0.14.1",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||||
|
"timeago.js": "^4.0.2",
|
||||||
|
"typescript": "^5.9.3",
|
||||||
|
"typescript-eslint": "^8.55.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12097
pnpm-lock.yaml
generated
2
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
onlyBuiltDependencies:
|
||||||
|
- swup
|
||||||
@@ -1,13 +1,7 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
'@tailwindcss/postcss': {},
|
||||||
autoprefixer: {},
|
|
||||||
'postcss-preset-env': {
|
|
||||||
features: {
|
|
||||||
'nesting-rules': false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
23
prettier.config.mjs
Normal file
@@ -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;
|
||||||
|
Before Width: | Height: | Size: 713 B After Width: | Height: | Size: 9.9 KiB |
352
public/vendor/preline/collapse2.1.0.min.js
vendored
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
/**
|
||||||
|
* Skipped minification because the original files appears to be already minified.
|
||||||
|
* Original file: /npm/@preline/collapse@2.1.0/index.js
|
||||||
|
*
|
||||||
|
* Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
|
||||||
|
*/
|
||||||
|
!(function (t, e) {
|
||||||
|
if ('object' == typeof exports && 'object' == typeof module) module.exports = e();
|
||||||
|
else if ('function' == typeof define && define.amd) define([], e);
|
||||||
|
else {
|
||||||
|
var n = e();
|
||||||
|
for (var o in n) ('object' == typeof exports ? exports : t)[o] = n[o];
|
||||||
|
}
|
||||||
|
})(self, () =>
|
||||||
|
(() => {
|
||||||
|
'use strict';
|
||||||
|
var t = {
|
||||||
|
737: (t, e) => {
|
||||||
|
/*
|
||||||
|
* HSBasePlugin
|
||||||
|
* @version: 2.1.0
|
||||||
|
* @author: HTMLStream
|
||||||
|
* @license: Licensed under MIT (https://preline.co/docs/license.html)
|
||||||
|
* Copyright 2023 HTMLStream
|
||||||
|
*/
|
||||||
|
Object.defineProperty(e, '__esModule', { value: !0 });
|
||||||
|
var n = (function () {
|
||||||
|
function t(t, e, n) {
|
||||||
|
((this.el = t),
|
||||||
|
(this.options = e),
|
||||||
|
(this.events = n),
|
||||||
|
(this.el = t),
|
||||||
|
(this.options = e),
|
||||||
|
(this.events = {}));
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
(t.prototype.createCollection = function (t, e) {
|
||||||
|
var n;
|
||||||
|
t.push({
|
||||||
|
id:
|
||||||
|
(null === (n = null == e ? void 0 : e.el) || void 0 === n ? void 0 : n.id) ||
|
||||||
|
t.length + 1,
|
||||||
|
element: e,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
(t.prototype.fireEvent = function (t, e) {
|
||||||
|
if ((void 0 === e && (e = null), this.events.hasOwnProperty(t)))
|
||||||
|
return this.events[t](e);
|
||||||
|
}),
|
||||||
|
(t.prototype.on = function (t, e) {
|
||||||
|
this.events[t] = e;
|
||||||
|
}),
|
||||||
|
t
|
||||||
|
);
|
||||||
|
})();
|
||||||
|
e.default = n;
|
||||||
|
},
|
||||||
|
652: function (t, e, n) {
|
||||||
|
/*
|
||||||
|
* HSCollapse
|
||||||
|
* @version: 2.1.0
|
||||||
|
* @author: HTMLStream
|
||||||
|
* @license: Licensed under MIT (https://preline.co/docs/license.html)
|
||||||
|
* Copyright 2023 HTMLStream
|
||||||
|
*/
|
||||||
|
var o,
|
||||||
|
i =
|
||||||
|
(this && this.__extends) ||
|
||||||
|
((o = function (t, e) {
|
||||||
|
return (
|
||||||
|
(o =
|
||||||
|
Object.setPrototypeOf ||
|
||||||
|
({ __proto__: [] } instanceof Array &&
|
||||||
|
function (t, e) {
|
||||||
|
t.__proto__ = e;
|
||||||
|
}) ||
|
||||||
|
function (t, e) {
|
||||||
|
for (var n in e) Object.prototype.hasOwnProperty.call(e, n) && (t[n] = e[n]);
|
||||||
|
}),
|
||||||
|
o(t, e)
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
function (t, e) {
|
||||||
|
if ('function' != typeof e && null !== e)
|
||||||
|
throw new TypeError(
|
||||||
|
'Class extends value ' + String(e) + ' is not a constructor or null'
|
||||||
|
);
|
||||||
|
function n() {
|
||||||
|
this.constructor = t;
|
||||||
|
}
|
||||||
|
(o(t, e),
|
||||||
|
(t.prototype =
|
||||||
|
null === e ? Object.create(e) : ((n.prototype = e.prototype), new n())));
|
||||||
|
});
|
||||||
|
Object.defineProperty(e, '__esModule', { value: !0 });
|
||||||
|
var s = n(969),
|
||||||
|
r = (function (t) {
|
||||||
|
function e(e, n, o) {
|
||||||
|
var i = t.call(this, e, n, o) || this;
|
||||||
|
return (
|
||||||
|
(i.contentId = i.el.dataset.hsCollapse),
|
||||||
|
(i.content = document.querySelector(i.contentId)),
|
||||||
|
(i.animationInProcess = !1),
|
||||||
|
i.content && i.init(),
|
||||||
|
i
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
i(e, t),
|
||||||
|
(e.prototype.init = function () {
|
||||||
|
var t = this;
|
||||||
|
(this.createCollection(window.$hsCollapseCollection, this),
|
||||||
|
this.el.addEventListener('click', function () {
|
||||||
|
t.content.classList.contains('open') ? t.hide() : t.show();
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
(e.prototype.hideAllMegaMenuItems = function () {
|
||||||
|
this.content
|
||||||
|
.querySelectorAll('.hs-mega-menu-content.block')
|
||||||
|
.forEach(function (t) {
|
||||||
|
(t.classList.remove('block'), t.classList.add('hidden'));
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
(e.prototype.show = function () {
|
||||||
|
var t = this;
|
||||||
|
if (this.animationInProcess || this.el.classList.contains('open')) return !1;
|
||||||
|
((this.animationInProcess = !0),
|
||||||
|
this.el.classList.add('open'),
|
||||||
|
this.content.classList.add('open'),
|
||||||
|
this.content.classList.remove('hidden'),
|
||||||
|
(this.content.style.height = '0'),
|
||||||
|
setTimeout(function () {
|
||||||
|
((t.content.style.height = ''.concat(t.content.scrollHeight, 'px')),
|
||||||
|
t.fireEvent('beforeOpen', t.el),
|
||||||
|
(0, s.dispatch)('beforeOpen.hs.collapse', t.el, t.el));
|
||||||
|
}),
|
||||||
|
(0, s.afterTransition)(this.content, function () {
|
||||||
|
((t.content.style.height = ''),
|
||||||
|
t.fireEvent('open', t.el),
|
||||||
|
(0, s.dispatch)('open.hs.collapse', t.el, t.el),
|
||||||
|
(t.animationInProcess = !1));
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
(e.prototype.hide = function () {
|
||||||
|
var t = this;
|
||||||
|
if (this.animationInProcess || !this.el.classList.contains('open')) return !1;
|
||||||
|
((this.animationInProcess = !0),
|
||||||
|
this.el.classList.remove('open'),
|
||||||
|
(this.content.style.height = ''.concat(this.content.scrollHeight, 'px')),
|
||||||
|
setTimeout(function () {
|
||||||
|
t.content.style.height = '0';
|
||||||
|
}),
|
||||||
|
this.content.classList.remove('open'),
|
||||||
|
(0, s.afterTransition)(this.content, function () {
|
||||||
|
(t.content.classList.add('hidden'),
|
||||||
|
(t.content.style.height = ''),
|
||||||
|
t.fireEvent('hide', t.el),
|
||||||
|
(0, s.dispatch)('hide.hs.collapse', t.el, t.el),
|
||||||
|
(t.animationInProcess = !1));
|
||||||
|
}),
|
||||||
|
this.content.querySelectorAll('.hs-mega-menu-content.block').length &&
|
||||||
|
this.hideAllMegaMenuItems());
|
||||||
|
}),
|
||||||
|
(e.getInstance = function (t, e) {
|
||||||
|
void 0 === e && (e = !1);
|
||||||
|
var n = window.$hsCollapseCollection.find(function (e) {
|
||||||
|
return e.element.el === ('string' == typeof t ? document.querySelector(t) : t);
|
||||||
|
});
|
||||||
|
return n ? (e ? n : n.element.el) : null;
|
||||||
|
}),
|
||||||
|
(e.autoInit = function () {
|
||||||
|
(window.$hsCollapseCollection || (window.$hsCollapseCollection = []),
|
||||||
|
document
|
||||||
|
.querySelectorAll('.hs-collapse-toggle:not(.--prevent-on-load-init)')
|
||||||
|
.forEach(function (t) {
|
||||||
|
window.$hsCollapseCollection.find(function (e) {
|
||||||
|
var n;
|
||||||
|
return (
|
||||||
|
(null === (n = null == e ? void 0 : e.element) || void 0 === n
|
||||||
|
? void 0
|
||||||
|
: n.el) === t
|
||||||
|
);
|
||||||
|
}) || new e(t);
|
||||||
|
}));
|
||||||
|
}),
|
||||||
|
(e.show = function (t) {
|
||||||
|
var e = window.$hsCollapseCollection.find(function (e) {
|
||||||
|
return e.element.el === ('string' == typeof t ? document.querySelector(t) : t);
|
||||||
|
});
|
||||||
|
e && e.element.content.classList.contains('hidden') && e.element.show();
|
||||||
|
}),
|
||||||
|
(e.hide = function (t) {
|
||||||
|
var e = window.$hsCollapseCollection.find(function (e) {
|
||||||
|
return e.element.el === ('string' == typeof t ? document.querySelector(t) : t);
|
||||||
|
});
|
||||||
|
e && !e.element.content.classList.contains('hidden') && e.element.hide();
|
||||||
|
}),
|
||||||
|
(e.on = function (t, e, n) {
|
||||||
|
var o = window.$hsCollapseCollection.find(function (t) {
|
||||||
|
return t.element.el === ('string' == typeof e ? document.querySelector(e) : e);
|
||||||
|
});
|
||||||
|
o && (o.element.events[t] = n);
|
||||||
|
}),
|
||||||
|
e
|
||||||
|
);
|
||||||
|
})(n(737).default);
|
||||||
|
(window.addEventListener('load', function () {
|
||||||
|
r.autoInit();
|
||||||
|
}),
|
||||||
|
'undefined' != typeof window && (window.HSCollapse = r),
|
||||||
|
(e.default = r));
|
||||||
|
},
|
||||||
|
969: function (t, e) {
|
||||||
|
var n = this;
|
||||||
|
(Object.defineProperty(e, '__esModule', { value: !0 }),
|
||||||
|
(e.menuSearchHistory =
|
||||||
|
e.classToClassList =
|
||||||
|
e.htmlToElement =
|
||||||
|
e.afterTransition =
|
||||||
|
e.dispatch =
|
||||||
|
e.debounce =
|
||||||
|
e.isFormElement =
|
||||||
|
e.isParentOrElementHidden =
|
||||||
|
e.isEnoughSpace =
|
||||||
|
e.isIpadOS =
|
||||||
|
e.isIOS =
|
||||||
|
e.getClassPropertyAlt =
|
||||||
|
e.getClassProperty =
|
||||||
|
e.stringToBoolean =
|
||||||
|
void 0));
|
||||||
|
e.stringToBoolean = function (t) {
|
||||||
|
return 'true' === t;
|
||||||
|
};
|
||||||
|
e.getClassProperty = function (t, e, n) {
|
||||||
|
return (
|
||||||
|
void 0 === n && (n = ''),
|
||||||
|
(window.getComputedStyle(t).getPropertyValue(e) || n).replace(' ', '')
|
||||||
|
);
|
||||||
|
};
|
||||||
|
e.getClassPropertyAlt = function (t, e, n) {
|
||||||
|
void 0 === n && (n = '');
|
||||||
|
var o = '';
|
||||||
|
return (
|
||||||
|
t.classList.forEach(function (t) {
|
||||||
|
t.includes(e) && (o = t);
|
||||||
|
}),
|
||||||
|
o.match(/:(.*)]/) ? o.match(/:(.*)]/)[1] : n
|
||||||
|
);
|
||||||
|
};
|
||||||
|
e.isIOS = function () {
|
||||||
|
return (
|
||||||
|
!!/iPad|iPhone|iPod/.test(navigator.platform) ||
|
||||||
|
(navigator.maxTouchPoints &&
|
||||||
|
navigator.maxTouchPoints > 2 &&
|
||||||
|
/MacIntel/.test(navigator.platform))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
e.isIpadOS = function () {
|
||||||
|
return (
|
||||||
|
navigator.maxTouchPoints &&
|
||||||
|
navigator.maxTouchPoints > 2 &&
|
||||||
|
/MacIntel/.test(navigator.platform)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
e.isEnoughSpace = function (t, e, n, o, i) {
|
||||||
|
(void 0 === n && (n = 'auto'), void 0 === o && (o = 10), void 0 === i && (i = null));
|
||||||
|
var s = e.getBoundingClientRect(),
|
||||||
|
r = i ? i.getBoundingClientRect() : null,
|
||||||
|
l = window.innerHeight,
|
||||||
|
c = r ? s.top - r.top : s.top,
|
||||||
|
a = (i ? r.bottom : l) - s.bottom,
|
||||||
|
u = t.clientHeight + o;
|
||||||
|
return 'bottom' === n ? a >= u : 'top' === n ? c >= u : c >= u || a >= u;
|
||||||
|
};
|
||||||
|
e.isFormElement = function (t) {
|
||||||
|
return (
|
||||||
|
t instanceof HTMLInputElement ||
|
||||||
|
t instanceof HTMLTextAreaElement ||
|
||||||
|
t instanceof HTMLSelectElement
|
||||||
|
);
|
||||||
|
};
|
||||||
|
var o = function (t) {
|
||||||
|
return !!t && ('none' === window.getComputedStyle(t).display || o(t.parentElement));
|
||||||
|
};
|
||||||
|
e.isParentOrElementHidden = o;
|
||||||
|
e.debounce = function (t, e) {
|
||||||
|
var o;
|
||||||
|
return (
|
||||||
|
void 0 === e && (e = 200),
|
||||||
|
function () {
|
||||||
|
for (var i = [], s = 0; s < arguments.length; s++) i[s] = arguments[s];
|
||||||
|
(clearTimeout(o),
|
||||||
|
(o = setTimeout(function () {
|
||||||
|
t.apply(n, i);
|
||||||
|
}, e)));
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
e.dispatch = function (t, e, n) {
|
||||||
|
void 0 === n && (n = null);
|
||||||
|
var o = new CustomEvent(t, {
|
||||||
|
detail: { payload: n },
|
||||||
|
bubbles: !0,
|
||||||
|
cancelable: !0,
|
||||||
|
composed: !1,
|
||||||
|
});
|
||||||
|
e.dispatchEvent(o);
|
||||||
|
};
|
||||||
|
e.afterTransition = function (t, e) {
|
||||||
|
var n = function () {
|
||||||
|
(e(), t.removeEventListener('transitionend', n, !0));
|
||||||
|
};
|
||||||
|
window.getComputedStyle(t, null).getPropertyValue('transition') !==
|
||||||
|
(navigator.userAgent.includes('Firefox') ? 'all' : 'all 0s ease 0s')
|
||||||
|
? t.addEventListener('transitionend', n, !0)
|
||||||
|
: e();
|
||||||
|
};
|
||||||
|
e.htmlToElement = function (t) {
|
||||||
|
var e = document.createElement('template');
|
||||||
|
return ((t = t.trim()), (e.innerHTML = t), e.content.firstChild);
|
||||||
|
};
|
||||||
|
e.classToClassList = function (t, e, n, o) {
|
||||||
|
(void 0 === n && (n = ' '),
|
||||||
|
void 0 === o && (o = 'add'),
|
||||||
|
t.split(n).forEach(function (t) {
|
||||||
|
return 'add' === o ? e.classList.add(t) : e.classList.remove(t);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
e.menuSearchHistory = {
|
||||||
|
historyIndex: -1,
|
||||||
|
addHistory: function (t) {
|
||||||
|
this.historyIndex = t;
|
||||||
|
},
|
||||||
|
existsInHistory: function (t) {
|
||||||
|
return t > this.historyIndex;
|
||||||
|
},
|
||||||
|
clearHistory: function () {
|
||||||
|
this.historyIndex = -1;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
e = {};
|
||||||
|
var n = (function n(o) {
|
||||||
|
var i = e[o];
|
||||||
|
if (void 0 !== i) return i.exports;
|
||||||
|
var s = (e[o] = { exports: {} });
|
||||||
|
return (t[o].call(s.exports, s, s.exports, n), s.exports);
|
||||||
|
})(652);
|
||||||
|
return n;
|
||||||
|
})()
|
||||||
|
);
|
||||||
@@ -1,10 +1,40 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||||
"extends": ["config:recommended", "mergeConfidence:all-badges", ":rebaseStalePrs"],
|
"extends": [
|
||||||
"timezone": "US/Central",
|
"config:recommended",
|
||||||
"schedule": ["* */1 * * *"],
|
"mergeConfidence:all-badges",
|
||||||
"labels": [],
|
":rebaseStalePrs"
|
||||||
"prHourlyLimit": 0,
|
],
|
||||||
"prConcurrentLimit": 0,
|
"timezone": "US/Central",
|
||||||
"packageRules": []
|
"labels": [],
|
||||||
|
"prHourlyLimit": 0,
|
||||||
|
"prConcurrentLimit": 0,
|
||||||
|
"packageRules": [
|
||||||
|
{
|
||||||
|
"description": "Label dependency",
|
||||||
|
"matchDatasources": [
|
||||||
|
"npm"
|
||||||
|
],
|
||||||
|
"addLabels": [
|
||||||
|
"dependency"
|
||||||
|
],
|
||||||
|
"automerge": false,
|
||||||
|
"minimumReleaseAge": "1 days"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Automerge dependency patch",
|
||||||
|
"matchDatasources": [
|
||||||
|
"npm"
|
||||||
|
],
|
||||||
|
"matchUpdateTypes": [
|
||||||
|
"patch"
|
||||||
|
],
|
||||||
|
"addLabels": [
|
||||||
|
"dependency",
|
||||||
|
"automerge"
|
||||||
|
],
|
||||||
|
"automerge": true,
|
||||||
|
"minimumReleaseAge": "1 days"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
10
site-profile.code-workspace
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
---
|
|
||||||
// Background.astro - Dot pattern and ambient glow background with smooth theme transitions
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class="theme-transition-all fixed inset-0 -z-10 overflow-hidden">
|
|
||||||
<!-- Dot pattern background -->
|
|
||||||
<div
|
|
||||||
class="bg-grid-pattern theme-transition-bg absolute inset-0 bg-[center_top_-1px] [mask-image:radial-gradient(white,transparent_85%)]"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Ambient glow effects -->
|
|
||||||
<div
|
|
||||||
class="animate-glow theme-transition-bg absolute left-1/4 top-1/4 h-96 w-96 -translate-x-1/2 -translate-y-1/2 rounded-full bg-zinc-400/20 opacity-50 blur-3xl dark:bg-zinc-500/20"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="animate-glow animation-delay-1000 theme-transition-bg absolute bottom-1/3 right-1/4 h-64 w-64 translate-x-1/2 translate-y-1/2 rounded-full bg-zinc-300/20 opacity-40 blur-3xl dark:bg-zinc-600/20"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Theme transition overlay -->
|
|
||||||
<div
|
|
||||||
id="theme-transition-overlay"
|
|
||||||
class="pointer-events-none absolute inset-0 bg-white opacity-0 dark:bg-zinc-900"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Theme transition script
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const themeToggle = document.querySelector('[data-theme-toggle]');
|
|
||||||
const overlay = document.getElementById('theme-transition-overlay');
|
|
||||||
|
|
||||||
if (themeToggle && overlay) {
|
|
||||||
themeToggle.addEventListener('click', () => {
|
|
||||||
// Add transitioning class to optimize performance
|
|
||||||
document.documentElement.classList.add('theme-transitioning');
|
|
||||||
|
|
||||||
// Fade in overlay
|
|
||||||
overlay.style.opacity = '0.15';
|
|
||||||
overlay.style.transition = 'opacity 0.3s ease';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// Fade out overlay
|
|
||||||
overlay.style.opacity = '0';
|
|
||||||
|
|
||||||
// Remove transitioning class after animation completes
|
|
||||||
setTimeout(() => {
|
|
||||||
document.documentElement.classList.remove('theme-transitioning');
|
|
||||||
}, 700);
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Grid pattern for dots */
|
|
||||||
.bg-grid-pattern {
|
|
||||||
background-size: 24px 24px;
|
|
||||||
background-image: radial-gradient(circle, rgba(0, 0, 0, 0.15) 1px, transparent 1px);
|
|
||||||
transition: background-image 0.7s cubic-bezier(0.65, 0, 0.35, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode version */
|
|
||||||
:global(.dark) .bg-grid-pattern {
|
|
||||||
background-image: radial-gradient(circle, rgba(255, 255, 255, 0.1) 1px, transparent 1px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ambient glow animations */
|
|
||||||
.animate-glow {
|
|
||||||
animation: glow 12s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
||||||
transition:
|
|
||||||
background-color 0.7s cubic-bezier(0.65, 0, 0.35, 1),
|
|
||||||
opacity 0.7s cubic-bezier(0.65, 0, 0.35, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-1000 {
|
|
||||||
animation-delay: 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes glow {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 0.4;
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
opacity: 0.5;
|
|
||||||
transform: translate(5%, 5%) scale(1.1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.3;
|
|
||||||
transform: translate(0, 10%) scale(0.95);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
opacity: 0.5;
|
|
||||||
transform: translate(-5%, 5%) scale(1.05);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme transition overlay */
|
|
||||||
#theme-transition-overlay {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
90
src/components/BaseHead.astro
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
---
|
||||||
|
import { getImage } from 'astro:assets';
|
||||||
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
import { SEO } from '@/config';
|
||||||
|
|
||||||
|
import brandSrc from '@images/brand_logo.png';
|
||||||
|
import faviconSvgSrc from '@images/favicon_icon.svg';
|
||||||
|
import faviconSrc from '@images/favicon_icon.png';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
ogImage?: any;
|
||||||
|
ogTitle?: string;
|
||||||
|
ogDescription?: string;
|
||||||
|
structuredData?: object;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canonicalURL = Astro.url.href;
|
||||||
|
|
||||||
|
let {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
ogImage,
|
||||||
|
ogTitle = title,
|
||||||
|
ogDescription = description,
|
||||||
|
structuredData = SEO.structuredData,
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
|
||||||
|
let card = 'summary_large_image';
|
||||||
|
if (!ogImage) {
|
||||||
|
ogImage = brandSrc;
|
||||||
|
card = 'summary';
|
||||||
|
}
|
||||||
|
|
||||||
|
const faviconSvg = await getImage({ src: faviconSvgSrc, format: 'svg' });
|
||||||
|
const appleTouchIcon = await getImage({ src: faviconSrc, width: 180, height: 180, format: 'png' });
|
||||||
|
const socialImageRes = await getImage({ src: ogImage, width: 1200, height: 600 });
|
||||||
|
|
||||||
|
let socialImage = socialImageRes.src;
|
||||||
|
if (!socialImage.startsWith('http')) {
|
||||||
|
socialImage = Astro.url.origin + socialImageRes.src;
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- Inject structured data https://developers.google.com/search/docs/advanced/structured-data/intro-structured-data -->{
|
||||||
|
structuredData && <script type="application/ld+json" set:html={JSON.stringify(structuredData)} />
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Global Metadata -->
|
||||||
|
<meta name="title" content={title} />
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="web_author" content={global.name} />
|
||||||
|
<meta
|
||||||
|
name="viewport"
|
||||||
|
content="width=device-width, initial-scale=1.0, maximum-scale=5.0, minimum-scale=1.0"
|
||||||
|
/>
|
||||||
|
<meta name="generator" content={Astro.generator} />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
|
<meta name="theme-color" content="#facc15" />
|
||||||
|
<meta name="robots" content="index, follow" />
|
||||||
|
|
||||||
|
<!-- Open Graph -->
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:locale" content="en_US" />
|
||||||
|
<meta property="og:url" content={Astro.url} />
|
||||||
|
<meta property="og:title" content={ogTitle} />
|
||||||
|
<meta property="og:site_name" content={global.name} />
|
||||||
|
<meta property="og:description" content={ogDescription} />
|
||||||
|
<meta property="og:image" content={socialImage} />
|
||||||
|
<meta content="1200" property="og:image:width" />
|
||||||
|
<meta content="600" property="og:image:height" />
|
||||||
|
<meta content="image/png" property="og:image:type" />
|
||||||
|
|
||||||
|
<!-- Links -->
|
||||||
|
<link href={canonicalURL} rel="canonical" />
|
||||||
|
<link rel="sitemap" href="/sitemap-index.xml" />
|
||||||
|
<link rel="alternate" type="application/rss+xml" title={title} href="/rss.xml" />
|
||||||
|
<!--<link href="/manifest.json" rel="manifest" />-->
|
||||||
|
<link href="/favicon.ico" rel="icon" sizes="any" type="image/x-icon" />
|
||||||
|
<link href={faviconSvg.src} rel="icon" type="image/svg+xml" sizes="any" />
|
||||||
|
<link href={appleTouchIcon.src} rel="apple-touch-icon" />
|
||||||
|
<link href={appleTouchIcon.src} rel="shortcut icon" />
|
||||||
|
<link rel="preconnect" href="https://461ZQ3AX3S-dsn.algolia.net" crossorigin />
|
||||||
@@ -1,244 +1,131 @@
|
|||||||
---
|
---
|
||||||
import directus from '../../lib/directus';
|
import { Image } from 'astro:assets';
|
||||||
import { readSingleton } from '@directus/sdk';
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
const global = await directus.request(readSingleton('global'));
|
import BrandLogo from '@components/images/BrandLogo.astro';
|
||||||
const links = await directus.request(readSingleton('links'));
|
import directus from '@lib/directus';
|
||||||
|
import { NavigationLinks, FooterLinks } from '@/config';
|
||||||
|
|
||||||
|
import footerImg from '@images/flowers.png';
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
|
|
||||||
const navLinks = [
|
|
||||||
{ text: 'About', href: '/about' },
|
|
||||||
{ text: 'Blog', href: '/blog' },
|
|
||||||
{ text: 'Topics', href: '/topics' },
|
|
||||||
{ text: 'RSS', href: '/rss.xml' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const socialLinks = [
|
|
||||||
{
|
|
||||||
name: 'GitHub',
|
|
||||||
href: links.github,
|
|
||||||
icon: `<path fill-rule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clip-rule="evenodd"></path>`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'LinkedIn',
|
|
||||||
href: links.linkedin,
|
|
||||||
icon: `<path d="M19 0h-14c-2.761 0-5 2.239-5 5v14c0 2.761 2.239 5 5 5h14c2.762 0 5-2.239 5-5v-14c0-2.761-2.238-5-5-5zm-11 19h-3v-11h3v11zm-1.5-12.268c-.966 0-1.75-.79-1.75-1.764s.784-1.764 1.75-1.764 1.75.79 1.75 1.764-.783 1.764-1.75 1.764zm13.5 12.268h-3v-5.604c0-3.368-4-3.113-4 0v5.604h-3v-11h3v1.765c1.396-2.586 7-2.777 7 2.476v6.759z"></path>`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<footer
|
<footer
|
||||||
class="theme-transition-all relative mt-20 overflow-hidden border-t border-zinc-100 dark:border-zinc-800"
|
class="bg-background-accent w-full overflow-hidden"
|
||||||
|
transition:animate="none"
|
||||||
>
|
>
|
||||||
<div class="pointer-events-none absolute inset-0 overflow-hidden">
|
<div class="relative px-4 sm:px-6 pt-16 pb-12">
|
||||||
<div
|
<div class="max-w-340 mx-auto">
|
||||||
class="theme-transition-all animate-float-slow absolute -right-40 -top-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
|
<div class="grid grid-cols-1 md:grid-cols-12 gap-10">
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="theme-transition-all animate-float-slow animation-delay-2000 absolute -bottom-40 -left-40 h-80 w-80 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/30"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="theme-transition-all animate-float-slow animation-delay-1000 absolute left-1/4 top-20 h-40 w-40 rounded-full bg-zinc-200/50 opacity-30 blur-2xl dark:bg-zinc-700/20"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative px-4 pb-12 pt-16 sm:px-6">
|
|
||||||
<div class="mx-auto max-w-4xl">
|
|
||||||
<!-- Main footer content -->
|
|
||||||
<div class="grid grid-cols-1 gap-10 md:grid-cols-12">
|
|
||||||
<!-- Brand section -->
|
<!-- Brand section -->
|
||||||
<div class="col-span-1 md:col-span-3">
|
<div class="col-span-1 md:col-span-3">
|
||||||
<a href="/" class="group inline-block">
|
<a href="/" class="group inline-block">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div
|
<div class="mx-auto aspect-square overflow-hidden">
|
||||||
class="relative flex h-10 w-10 transform items-center justify-center overflow-hidden rounded-lg bg-gradient-to-br from-zinc-800 to-zinc-600 shadow-lg transition-transform group-hover:scale-105 dark:from-zinc-200 dark:to-zinc-400"
|
<BrandLogo class="rounded-lg max-h-10 max-w-10"/>
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="theme-transition-all text-xl font-bold text-white transition-transform duration-300 group-hover:scale-110 dark:text-zinc-900"
|
|
||||||
>{global.initals}</span
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="absolute inset-0 bg-gradient-to-br from-zinc-700 to-zinc-900 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-300 dark:to-zinc-100"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span class="text-header text-lg lg:text-2xl font-semibold leading-tight tracking-tight text-balance ml-3">
|
||||||
class="theme-transition-color ml-3 text-xl font-bold text-zinc-900 dark:text-zinc-100"
|
{global.name}
|
||||||
>Blog</span
|
</span>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
<p class="text-primary text-sm lg:text-base text-pretty leading-relaxed mt-4">
|
||||||
<p
|
{global.about}
|
||||||
class="theme-transition-color mt-4 text-sm leading-relaxed text-zinc-600 dark:text-zinc-400"
|
|
||||||
>
|
|
||||||
{global.description}
|
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<!-- Social links -->
|
|
||||||
<div class="mt-6 flex items-center space-x-4">
|
|
||||||
{
|
|
||||||
socialLinks.map((social) => (
|
|
||||||
<a
|
|
||||||
href={social.href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="group relative flex h-10 w-10 transform items-center justify-center rounded-full bg-zinc-100 text-zinc-500 transition-all duration-300 hover:-translate-y-1 hover:text-zinc-900 hover:ring-2 hover:ring-zinc-300 dark:bg-zinc-800 dark:text-zinc-400 dark:hover:text-zinc-100 dark:hover:ring-zinc-700"
|
|
||||||
aria-label={social.name}
|
|
||||||
>
|
|
||||||
<span class="absolute inset-0 rounded-full bg-gradient-to-br from-zinc-200 to-zinc-300 opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-700 dark:to-zinc-600" />
|
|
||||||
<svg
|
|
||||||
class="relative z-10 h-5 w-5 transition-transform duration-300 group-hover:scale-110"
|
|
||||||
fill="currentColor"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-hidden="true"
|
|
||||||
>
|
|
||||||
<Fragment set:html={social.icon} />
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Left links -->
|
||||||
<!-- Quick links -->
|
<div class="col-span-1 md:col-span-2">
|
||||||
<div class="col-span-1 md:col-span-3">
|
<h3 class="relative inline-block text-header after:bg-main text-sm uppercase font-semibold tracking-wider pb-2 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-['']">
|
||||||
<h3
|
Site
|
||||||
class="theme-transition-color relative inline-block pb-2 text-sm font-semibold uppercase tracking-wider text-zinc-900 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:bg-zinc-300 after:content-[''] dark:text-zinc-100 dark:after:bg-zinc-700"
|
|
||||||
>
|
|
||||||
Navigation
|
|
||||||
</h3>
|
</h3>
|
||||||
<ul class="mt-4 space-y-3">
|
<ul class="mt-4 space-y-3">
|
||||||
{
|
{NavigationLinks.map((link) => (
|
||||||
navLinks.map((link) => (
|
<li>
|
||||||
<li>
|
<a
|
||||||
<a
|
href={link.url}
|
||||||
href={link.href}
|
class="inline-flex items-center text-secondary hover:text-secondary-hover text-base transition-all duration-300 overflow-hidden"
|
||||||
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"
|
>
|
||||||
>
|
{link.name}
|
||||||
<span class="relative inline-block overflow-hidden">
|
</a>
|
||||||
<span class="relative z-10">{link.text}</span>
|
</li>
|
||||||
<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>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- Right links -->
|
||||||
<!-- Bottom section -->
|
<div class="col-span-1 md:col-span-3">
|
||||||
<div class="theme-transition-all mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
<h3 class="relative inline-block text-header after:bg-main text-sm uppercase font-semibold tracking-wider pb-2 after:absolute after:bottom-0 after:left-0 after:h-0.5 after:w-8 after:content-['']">
|
||||||
<div class="flex flex-col items-center justify-between gap-4 md:flex-row">
|
Other
|
||||||
<p class="theme-transition-color text-sm text-zinc-600 dark:text-zinc-400">
|
</h3>
|
||||||
© {currentYear} All rights reserved.
|
<ul class="mt-4 space-y-3">
|
||||||
</p>
|
{FooterLinks.map((link) => (
|
||||||
|
<li>
|
||||||
<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
|
<a
|
||||||
href="https://astro.build"
|
href={link.url}
|
||||||
target="_blank"
|
class="inline-flex items-center text-secondary hover:text-secondary-hover text-base transition-all duration-300 overflow-hidden"
|
||||||
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
|
{link.name}
|
||||||
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>
|
</a>
|
||||||
</div>
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<!-- Right image -->
|
||||||
|
<div class="flex justify-center col-span-4 mt-10 md:mt-0">
|
||||||
|
<div class="md:block max-h-115 max-w-55 -mt-10 scale-80 hidden">
|
||||||
|
<Image
|
||||||
|
src={footerImg}
|
||||||
|
alt={global.footer_image_alt}
|
||||||
|
class="h-full w-full object-cover object-center"
|
||||||
|
draggable="false"
|
||||||
|
loading="eager"
|
||||||
|
format="webp"
|
||||||
|
quality="low"
|
||||||
|
widths={[440]}
|
||||||
|
inferSize={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Bottom section -->
|
||||||
|
<div class="border-t border-divider pt-8 mt-12">
|
||||||
|
<div class="flex flex-col md:flex-row items-center justify-between gap-4">
|
||||||
|
<p class="text-secondary text-sm">
|
||||||
|
© {currentYear} All rights reserved.
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<span class="text-secondary text-sm">
|
||||||
|
Weather provided by
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="https://open-meteo.com/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="group inline-flex items-center text-secondary hover:text-secondary-hover text-sm transition-all duration-300"
|
||||||
|
>
|
||||||
|
<span class="relative underline ml-1">
|
||||||
|
Open-Meteo.
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div class="ml-4"/>
|
||||||
|
<span class="text-secondary text-sm">
|
||||||
|
Built with
|
||||||
|
</span>
|
||||||
|
<a
|
||||||
|
href="https://astro.build"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="group inline-flex items-center text-secondary hover:text-secondary-hover text-sm transition-all duration-300"
|
||||||
|
>
|
||||||
|
<span class="relative underline ml-1">
|
||||||
|
Astro.
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
|
||||||
.theme-transition-all {
|
|
||||||
transition-property: background-color, border-color, color, fill, stroke;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 300ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-transition-color {
|
|
||||||
transition-property: color, fill, stroke;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 300ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-transition-bg {
|
|
||||||
transition-property: background-color;
|
|
||||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
|
||||||
transition-duration: 300ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes pulse {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 0.7;
|
|
||||||
transform: scale(1.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes float-slow {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translateY(0) translateX(0);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translateY(-10px) translateX(10px);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translateY(-5px) translateX(-5px);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: translateY(10px) translateX(5px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-pulse {
|
|
||||||
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-float-slow {
|
|
||||||
animation: float-slow 20s ease-in-out infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-1000 {
|
|
||||||
animation-delay: 1s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-2000 {
|
|
||||||
animation-delay: 2s;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</footer>
|
</footer>
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
---
|
|
||||||
export interface Props {
|
|
||||||
date?: Date | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { date } = Astro.props;
|
|
||||||
|
|
||||||
const parsedDate = typeof date === 'string' ? new Date(date) : date;
|
|
||||||
---
|
|
||||||
|
|
||||||
{
|
|
||||||
parsedDate && (
|
|
||||||
<time datetime={parsedDate.toISOString()}>
|
|
||||||
{parsedDate.toLocaleDateString('en-us', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'long',
|
|
||||||
day: 'numeric',
|
|
||||||
})}
|
|
||||||
</time>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
99
src/components/Header.astro
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
---
|
||||||
|
import BrandLogo from '@components/images/BrandLogo.astro';
|
||||||
|
import ThemeToggleButton from '@components/buttons/ThemeToggleButton.astro';
|
||||||
|
import { NavigationLinks } from '@/config';
|
||||||
|
|
||||||
|
const pathname = new URL(Astro.request.url).pathname;
|
||||||
|
const currentPath = pathname.slice(1);
|
||||||
|
---
|
||||||
|
|
||||||
|
<header
|
||||||
|
id="nav"
|
||||||
|
class="fixed flex flex-wrap md:flex-nowrap md:justify-start inset-x-0 top-0 w-full z-50"
|
||||||
|
>
|
||||||
|
<nav
|
||||||
|
class="nav-base relative md:flex md:items-center md:justify-between rounded-[36px] w-full px-4 mx-2 py-3 mt-4"
|
||||||
|
aria-label="Global"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between ml-0">
|
||||||
|
<a
|
||||||
|
class="flex-none rounded-full h-10.5"
|
||||||
|
href="/"
|
||||||
|
aria-label="Brand"
|
||||||
|
>
|
||||||
|
<BrandLogo class="h-full w-auto rounded-full object-cover"/>
|
||||||
|
</a>
|
||||||
|
<div class="md:hidden mr-auto ml-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="hs-collapse-toggle flex items-center justify-center text-secondary text-sm font-bold hover:bg-neutral-200 dark:hover:bg-neutral-700 rounded-full transition duration-300 disabled:pointer-events-none disabled:opacity-50 h-8 w-8"
|
||||||
|
data-hs-collapse="#navbar-collapse-with-animation"
|
||||||
|
aria-controls="navbar-collapse-with-animation"
|
||||||
|
aria-label="Toggle navigation"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="hs-collapse-open:hidden shrink-0 h-5 w-5"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="3" x2="21" y1="6" y2="6"></line>
|
||||||
|
<line x1="3" x2="21" y1="12" y2="12"></line>
|
||||||
|
<line x1="3" x2="21" y1="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
<svg
|
||||||
|
class="hs-collapse-open:block shrink-0 h-5 w-5 hidden"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="M18 6 6 18"></path>
|
||||||
|
<path d="m6 6 12 12"></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="md:hidden ml-2 mr-2">
|
||||||
|
<span class="">
|
||||||
|
<ThemeToggleButton />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex md:flex-row items-center justify-between">
|
||||||
|
<div
|
||||||
|
id="navbar-collapse-with-animation"
|
||||||
|
class="hs-collapse grow basis-full md:block transition-all duration-300 ml-2 mb-2 md:mb-0 hidden overflow-hidden md:overflow-visible"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col md:flex-row md:items-center md:justify-end gap-x-0 md:gap-x-4 lg:gap-x-7 gap-y-4 md:gap-y-0 md:ps-7 mr-2 mt-5 md:mt-0">
|
||||||
|
{NavigationLinks.map((item) => {
|
||||||
|
const isActive = currentPath === (item.url === '/' ? '' : item.url.slice(1));
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={item.url}
|
||||||
|
class={`text-sm font-medium ${isActive ? 'text-active' : 'text-secondary hover:text-secondary-hover'}`}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:flex ml-2">
|
||||||
|
<span class="">
|
||||||
|
<ThemeToggleButton />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<script is:inline src="/vendor/preline/collapse2.1.0.min.js"></script>
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
---
|
|
||||||
import ThemeToggle from './ThemeToggle.astro';
|
|
||||||
|
|
||||||
import directus from '../../lib/directus';
|
|
||||||
import { readSingleton } from '@directus/sdk';
|
|
||||||
|
|
||||||
const global = await directus.request(readSingleton('global'));
|
|
||||||
|
|
||||||
const navItems = [
|
|
||||||
{ text: 'Home', href: '/' },
|
|
||||||
{ text: 'Blog', href: '/blog' },
|
|
||||||
{ text: 'Topics', href: '/topics' },
|
|
||||||
{ text: 'About', href: '/about' },
|
|
||||||
{ text: 'RSS', href: 'rss.xml' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const pathname = new URL(Astro.request.url).pathname;
|
|
||||||
const currentPath = pathname.slice(1);
|
|
||||||
---
|
|
||||||
|
|
||||||
<header
|
|
||||||
class="fixed left-0 right-0 top-0 z-40 border-b border-zinc-100 bg-white py-4 dark:border-zinc-800 dark:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<div class="mx-auto flex max-w-3xl items-center justify-between px-4">
|
|
||||||
<!-- Logo -->
|
|
||||||
<a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">{global.initals}</a>
|
|
||||||
|
|
||||||
<!-- Desktop navigation -->
|
|
||||||
<nav class="hidden items-center space-x-6 sm:flex">
|
|
||||||
{
|
|
||||||
navItems.map((item) => {
|
|
||||||
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={item.href}
|
|
||||||
class={`text-sm font-medium ${
|
|
||||||
isActive
|
|
||||||
? 'text-zinc-900 dark:text-white'
|
|
||||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{item.text}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
<ThemeToggle />
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<!-- Mobile menu button -->
|
|
||||||
<button id="mobile-menu-button" class="flex items-center sm:hidden" aria-label="Menu">
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="h-6 w-6 text-zinc-900 dark:text-white"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<!-- Mobile menu overlay -->
|
|
||||||
<div
|
|
||||||
id="mobile-menu"
|
|
||||||
class="pointer-events-none fixed inset-0 z-50 flex flex-col bg-white opacity-0 transition-all duration-300 ease-in-out dark:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between border-b border-zinc-100 p-4 dark:border-zinc-800">
|
|
||||||
<a href="/" class="text-xl font-bold text-zinc-900 dark:text-white">JD</a>
|
|
||||||
<button
|
|
||||||
id="close-menu-button"
|
|
||||||
class="rounded-md p-2 text-zinc-900 transition-colors hover:bg-zinc-100 dark:text-white dark:hover:bg-zinc-800"
|
|
||||||
aria-label="Close menu"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="1.5"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="h-6 w-6"
|
|
||||||
>
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12"></path>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<nav class="flex flex-1 flex-col items-center justify-center space-y-6 text-center">
|
|
||||||
{
|
|
||||||
navItems.map((item, index) => {
|
|
||||||
const isActive = currentPath === (item.href === '/' ? '' : item.href.slice(1));
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={item.href}
|
|
||||||
class={`mobile-nav-item translate-y-4 text-lg font-medium opacity-0 ${
|
|
||||||
isActive
|
|
||||||
? 'text-zinc-900 dark:text-white'
|
|
||||||
: 'text-zinc-600 hover:text-zinc-900 dark:text-zinc-400 dark:hover:text-white'
|
|
||||||
}`}
|
|
||||||
style={`transition-delay: ${index * 0.05}s;`}
|
|
||||||
>
|
|
||||||
{item.text}
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
<div class="mobile-nav-item translate-y-4 pt-4 opacity-0" style="transition-delay: 0.25s;">
|
|
||||||
<ThemeToggle />
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Spacer to prevent content from hiding behind fixed header -->
|
|
||||||
<div class="h-16"></div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Mobile menu toggle with animations
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
|
||||||
const closeMenuButton = document.getElementById('close-menu-button');
|
|
||||||
const mobileMenu = document.getElementById('mobile-menu');
|
|
||||||
const navItems = document.querySelectorAll('.mobile-nav-item');
|
|
||||||
|
|
||||||
// Open menu with animations
|
|
||||||
mobileMenuButton?.addEventListener('click', () => {
|
|
||||||
if (!mobileMenu) return;
|
|
||||||
|
|
||||||
// Prevent body scrolling
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
|
|
||||||
// Show menu with fade in
|
|
||||||
mobileMenu.classList.remove('pointer-events-none');
|
|
||||||
mobileMenu.classList.add('pointer-events-auto');
|
|
||||||
|
|
||||||
// Animate opacity
|
|
||||||
setTimeout(() => {
|
|
||||||
mobileMenu.style.opacity = '1';
|
|
||||||
|
|
||||||
// Animate each nav item with staggered delay
|
|
||||||
navItems.forEach((item) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
item.classList.remove('opacity-0', 'translate-y-4');
|
|
||||||
}, 150);
|
|
||||||
});
|
|
||||||
}, 50);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close menu with animations
|
|
||||||
const closeMenu = () => {
|
|
||||||
if (!mobileMenu) return;
|
|
||||||
|
|
||||||
// Fade out nav items first
|
|
||||||
navItems.forEach((item) => {
|
|
||||||
item.classList.add('opacity-0', 'translate-y-4');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Then fade out the menu
|
|
||||||
setTimeout(() => {
|
|
||||||
mobileMenu.style.opacity = '0';
|
|
||||||
|
|
||||||
// After animation completes, hide menu and restore scrolling
|
|
||||||
setTimeout(() => {
|
|
||||||
mobileMenu.classList.remove('pointer-events-auto');
|
|
||||||
mobileMenu.classList.add('pointer-events-none');
|
|
||||||
document.body.style.overflow = '';
|
|
||||||
}, 300);
|
|
||||||
}, 100);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Close button event
|
|
||||||
closeMenuButton?.addEventListener('click', closeMenu);
|
|
||||||
|
|
||||||
// Close menu when clicking a link
|
|
||||||
const mobileLinks = mobileMenu?.querySelectorAll('a');
|
|
||||||
mobileLinks?.forEach((link) => {
|
|
||||||
link.addEventListener('click', closeMenu);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Close menu on escape key
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape' && mobileMenu?.classList.contains('pointer-events-auto')) {
|
|
||||||
closeMenu();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add smooth animation to header on scroll
|
|
||||||
const header = document.querySelector('header');
|
|
||||||
let lastScrollY = window.scrollY;
|
|
||||||
|
|
||||||
window.addEventListener('scroll', () => {
|
|
||||||
if (!header) return;
|
|
||||||
|
|
||||||
const currentScrollY = window.scrollY;
|
|
||||||
|
|
||||||
// Add shadow on scroll
|
|
||||||
if (currentScrollY > 10) {
|
|
||||||
header.classList.add('shadow-sm');
|
|
||||||
} else {
|
|
||||||
header.classList.remove('shadow-sm');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update last scroll position
|
|
||||||
lastScrollY = currentScrollY;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Smooth animations for mobile navigation */
|
|
||||||
.mobile-nav-item {
|
|
||||||
transition:
|
|
||||||
opacity 0.5s ease,
|
|
||||||
transform 0.5s ease,
|
|
||||||
color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header transition */
|
|
||||||
header {
|
|
||||||
transition:
|
|
||||||
box-shadow 0.3s ease,
|
|
||||||
transform 0.3s ease,
|
|
||||||
background-color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile menu button hover effect */
|
|
||||||
#mobile-menu-button {
|
|
||||||
transition: transform 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
#mobile-menu-button:hover {
|
|
||||||
transform: scale(1.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mobile menu transition */
|
|
||||||
#mobile-menu {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
---
|
|
||||||
export interface Props {
|
|
||||||
title: string;
|
|
||||||
url: string;
|
|
||||||
class?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { title, url, class: className = '' } = Astro.props;
|
|
||||||
const encodedTitle = encodeURIComponent(title);
|
|
||||||
const encodedUrl = encodeURIComponent(url);
|
|
||||||
---
|
|
||||||
|
|
||||||
<div class={`flex items-center gap-4 mt-8 ${className}`}>
|
|
||||||
<span class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Share:</span>
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<a
|
|
||||||
href={`https://twitter.com/intent/tweet?text=${encodedTitle}&url=${encodedUrl}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
|
|
||||||
aria-label="Share on Twitter"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="h-4 w-4"
|
|
||||||
><path
|
|
||||||
d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z"
|
|
||||||
></path></svg
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={`https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
|
|
||||||
aria-label="Share on Facebook"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="h-4 w-4"
|
|
||||||
><path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z"></path></svg
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={`https://www.linkedin.com/shareArticle?mini=true&url=${encodedUrl}&title=${encodedTitle}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
|
|
||||||
aria-label="Share on LinkedIn"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="h-4 w-4"
|
|
||||||
><path d="M16 8a6 6 0 0 1 6 6v7h-4v-7a2 2 0 0 0-2-2 2 2 0 0 0-2 2v7h-4v-7a6 6 0 0 1 6-6z"
|
|
||||||
></path><rect x="2" y="9" width="4" height="12"></rect><circle cx="4" cy="4" r="2"
|
|
||||||
></circle></svg
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
<button
|
|
||||||
id="copy-link-button"
|
|
||||||
class="relative rounded-full p-2 text-zinc-500 transition-all duration-300 hover:bg-zinc-100 hover:text-zinc-700 dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-300"
|
|
||||||
aria-label="Copy link"
|
|
||||||
title="Copy link to clipboard"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="h-4 w-4"
|
|
||||||
><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path
|
|
||||||
d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
id="copy-tooltip"
|
|
||||||
class="absolute -top-8 left-1/2 -translate-x-1/2 transform whitespace-nowrap rounded bg-zinc-800 px-2 py-1 text-xs text-white opacity-0 transition-opacity duration-300 dark:bg-zinc-700"
|
|
||||||
>
|
|
||||||
Copied!
|
|
||||||
</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Function to handle copy link button
|
|
||||||
function setupCopyLinkButton() {
|
|
||||||
const copyButtons = document.querySelectorAll('#copy-link-button');
|
|
||||||
|
|
||||||
copyButtons.forEach((button) => {
|
|
||||||
button.addEventListener('click', () => {
|
|
||||||
// Get the current URL
|
|
||||||
const url = window.location.href;
|
|
||||||
|
|
||||||
// Copy to clipboard
|
|
||||||
navigator.clipboard
|
|
||||||
.writeText(url)
|
|
||||||
.then(() => {
|
|
||||||
// Show tooltip
|
|
||||||
const tooltip = button.querySelector('#copy-tooltip');
|
|
||||||
if (tooltip) {
|
|
||||||
tooltip.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Hide tooltip after 2 seconds
|
|
||||||
setTimeout(() => {
|
|
||||||
tooltip.classList.remove('opacity-100');
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
console.error('Failed to copy: ', err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up the copy link button when the DOM is loaded
|
|
||||||
document.addEventListener('DOMContentLoaded', setupCopyLinkButton);
|
|
||||||
|
|
||||||
// Also set up when the page content is updated via SPA navigation
|
|
||||||
document.addEventListener('astro:page-load', setupCopyLinkButton);
|
|
||||||
|
|
||||||
// For compatibility with the custom page transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupCopyLinkButton);
|
|
||||||
|
|
||||||
// Handle SPA transitions for share links
|
|
||||||
function setupSpaTransitions() {
|
|
||||||
// Get all share links
|
|
||||||
const shareLinks = document.querySelectorAll('a[target="_blank"][rel="noopener noreferrer"]');
|
|
||||||
|
|
||||||
// Make sure external share links don't trigger page transitions
|
|
||||||
shareLinks.forEach((link) => {
|
|
||||||
link.setAttribute('data-spa-external', 'true');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize SPA transitions
|
|
||||||
document.addEventListener('DOMContentLoaded', setupSpaTransitions);
|
|
||||||
document.addEventListener('astro:page-load', setupSpaTransitions);
|
|
||||||
document.addEventListener('page-transition-complete', setupSpaTransitions);
|
|
||||||
|
|
||||||
// Dispatch custom event when share action is completed
|
|
||||||
function notifyShareComplete() {
|
|
||||||
document.dispatchEvent(new CustomEvent('share-action-complete'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add analytics tracking for share actions if needed
|
|
||||||
function trackShareAction(platform) {
|
|
||||||
// You can implement analytics tracking here
|
|
||||||
console.log(`Shared on ${platform}`);
|
|
||||||
|
|
||||||
// Notify other components that share action is complete
|
|
||||||
notifyShareComplete();
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
export interface Props {
|
|
||||||
tags: string[];
|
|
||||||
class?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { tags = [], class: className = '' } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
{
|
|
||||||
tags.length > 0 && (
|
|
||||||
<div class={`mt-3 flex flex-wrap gap-2 ${className}`}>
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<a
|
|
||||||
href={`/tag/${tag}`}
|
|
||||||
class="inline-flex items-center rounded-full bg-zinc-100 px-2.5 py-0.5 text-xs font-medium text-zinc-800 hover:bg-zinc-200 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700"
|
|
||||||
>
|
|
||||||
{tag}
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
100
src/components/buttons/BookmarkButton.astro
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="button-base button-bg-blue group inline-flex items-center rounded-lg p-2.5"
|
||||||
|
data-bookmark-button="bookmark-button"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
class="h-6 w-6 fill-none transition duration-300"
|
||||||
|
height=24
|
||||||
|
width=24
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12Z"
|
||||||
|
class="fill-current text-neutral-500 transition duration-300 group-hover:text-red-400 group-hover:dark:text-red-400"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
class Bookmark {
|
||||||
|
private static readonly BOOKMARKS_KEY = 'bookmarks';
|
||||||
|
private bookmarkButton: Element | null;
|
||||||
|
|
||||||
|
constructor(private dataAttrValue: string) {
|
||||||
|
this.bookmarkButton = document.querySelector(`[data-bookmark-button="${dataAttrValue}"]`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getStoredBookmarks(): string[] {
|
||||||
|
const item = localStorage.getItem(Bookmark.BOOKMARKS_KEY);
|
||||||
|
return item ? JSON.parse(item) : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
init(): void {
|
||||||
|
if (this.bookmarkButton && this.isStored()) {
|
||||||
|
this.markAsStored();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bookmarkButton?.addEventListener('click', () => this.toggleBookmark());
|
||||||
|
}
|
||||||
|
|
||||||
|
isStored(): boolean {
|
||||||
|
return this.getStoredBookmarks().includes(window.location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
markAsStored(): void {
|
||||||
|
if (this.bookmarkButton) {
|
||||||
|
this.bookmarkButton.classList.add('bookmarked');
|
||||||
|
const svgElement = this.bookmarkButton.querySelector('svg');
|
||||||
|
if (svgElement) {
|
||||||
|
svgElement.setAttribute('class', 'h-6 w-6 fill-red-500 dark:fill-red-500');
|
||||||
|
}
|
||||||
|
const pathElement = svgElement?.querySelector('path');
|
||||||
|
if (pathElement) {
|
||||||
|
pathElement.setAttribute('class', 'fill-current text-red-500 dark:text-red-500');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unmarkAsStored(): void {
|
||||||
|
if (this.bookmarkButton) {
|
||||||
|
this.bookmarkButton.classList.remove('bookmarked');
|
||||||
|
const svgElement = this.bookmarkButton.querySelector('svg');
|
||||||
|
if (svgElement) {
|
||||||
|
svgElement.setAttribute('class', 'h-6 w-6 fill-none');
|
||||||
|
}
|
||||||
|
const pathElement = svgElement?.querySelector('path');
|
||||||
|
if (pathElement) {
|
||||||
|
pathElement.setAttribute(
|
||||||
|
'class',
|
||||||
|
'fill-current text-neutral-500 group-hover:text-red-400 dark:text-neutral-500 group-hover:dark:text-red-400'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleBookmark(): void {
|
||||||
|
const storedBookmarks = this.getStoredBookmarks();
|
||||||
|
const index = storedBookmarks.indexOf(window.location.pathname);
|
||||||
|
if (index !== -1) {
|
||||||
|
storedBookmarks.splice(index, 1);
|
||||||
|
this.unmarkAsStored();
|
||||||
|
} else {
|
||||||
|
storedBookmarks.push(window.location.pathname);
|
||||||
|
this.markAsStored();
|
||||||
|
}
|
||||||
|
localStorage.setItem(Bookmark.BOOKMARKS_KEY, JSON.stringify(storedBookmarks));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new Bookmark('bookmark-button').init();
|
||||||
|
</script>
|
||||||
30
src/components/buttons/GiteaButton.astro
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="button-base button-bg-gitea group inline-flex rounded-full gap-x-2"
|
||||||
|
href={url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<div class="button-text-title flex relative items-center text-center">
|
||||||
|
<Icon
|
||||||
|
name="pajamas:gitea"
|
||||||
|
class="h-4 w-4 md:h-6 md:w-6"
|
||||||
|
/>
|
||||||
|
<span class="ml-2">
|
||||||
|
Continue to Gitea
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="button-hover-arrow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
34
src/components/buttons/GoBackButton.astro
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="button-base button-bg-blue group inline-flex rounded-lg gap-x-2"
|
||||||
|
id="back-button"
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
<div class="button-text-title flex relative items-center text-center">
|
||||||
|
<svg
|
||||||
|
class=" shrink-0 group-hover:-translate-x-1 transition duration-300 h-4 w-4"
|
||||||
|
height=24
|
||||||
|
width=24
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<path d="m15 18-6-6 6-6"/>
|
||||||
|
</svg>
|
||||||
|
<span class="ml-2">
|
||||||
|
Go Back
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('back-button')?.addEventListener('click', () => {
|
||||||
|
window.history.back();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
25
src/components/buttons/GoHomeButton.astro
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { url } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="button-base button-bg-teal group inline-flex rounded-lg gap-x-2"
|
||||||
|
href={url}
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
<div class="button-text-title flex relative items-center text-center">
|
||||||
|
<Icon
|
||||||
|
name="mdi:home-variant-outline"
|
||||||
|
class="card-hover-icon-scale h-3 w-3 md:h-5 md:w-5"
|
||||||
|
/>
|
||||||
|
<span class="ml-2">
|
||||||
|
Return Home
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
29
src/components/buttons/GoLinkPrimaryButton.astro
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
url?: string;
|
||||||
|
noArrow?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, url, noArrow } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="button-base button-bg-teal group inline-flex rounded-lg gap-x-2"
|
||||||
|
href={url}
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
<div class="button-text-title flex relative items-center text-center">
|
||||||
|
<span class="mr-2">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
{noArrow ? null : (
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="button-hover-arrow"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
20
src/components/buttons/GoLinkSecondaryButton.astro
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, url } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<a
|
||||||
|
class="button-base button-bg-neutral group inline-flex rounded-lg gap-x-2"
|
||||||
|
href={url}
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
<div class="button-text-title flex relative items-center text-center">
|
||||||
|
<span>
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
59
src/components/buttons/SocialShareButton.astro
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
import Logo from "@components/images/Logo.astro"
|
||||||
|
|
||||||
|
type SocialPlatform = {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
iconLight: string;
|
||||||
|
iconDark: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
pageTitle: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { pageTitle } = Astro.props;
|
||||||
|
|
||||||
|
const socialPlatforms: SocialPlatform[] = [
|
||||||
|
{
|
||||||
|
name: 'Facebook',
|
||||||
|
url: `https://www.facebook.com/sharer/sharer.php?u=${Astro.url}`,
|
||||||
|
iconLight: 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/facebook.webp',
|
||||||
|
iconDark: 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/facebook.webp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Twitter',
|
||||||
|
url: `https://x.com/intent/tweet?url=${Astro.url}&text=${pageTitle}`,
|
||||||
|
iconLight: 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/twitter.webp',
|
||||||
|
iconDark: 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/twitter.webp',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'LinkedIn',
|
||||||
|
url: `https://www.linkedin.com/sharing/share-offsite/?url=${Astro.url}`,
|
||||||
|
iconLight: 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/linkedin.webp',
|
||||||
|
iconDark: 'https://cdn.jsdelivr.net/gh/selfhst/icons@main/webp/linkedin.webp',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="inline-flex items-center gap-x-2">
|
||||||
|
{socialPlatforms.map((platform) => (
|
||||||
|
<a
|
||||||
|
class="button-base-hidden group inline-flex rounded-lg gap-x-2"
|
||||||
|
href={platform.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
title={`Share on ${platform.name}`}
|
||||||
|
>
|
||||||
|
<div class="button-text-title-hidden flex relative items-center text-center">
|
||||||
|
<Logo
|
||||||
|
srcLight={platform.iconLight}
|
||||||
|
srcDark={platform.iconDark}
|
||||||
|
alt={platform.name}
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
@@ -5,14 +5,14 @@
|
|||||||
<button
|
<button
|
||||||
id="theme-toggle"
|
id="theme-toggle"
|
||||||
data-theme-toggle
|
data-theme-toggle
|
||||||
class="group relative touch-manipulation overflow-hidden rounded-full p-1.5 transition-all duration-300 hover:bg-zinc-100 focus:outline-none focus:ring-2 focus:ring-zinc-300 dark:hover:bg-zinc-800 dark:focus:ring-zinc-700 sm:p-2"
|
class="group dark:hover:bg-steel/30 hover:bg-yellow-300/20 transition-all duration-300 relative rounded-full p-1.5 sm:p-2 touch-manipulation"
|
||||||
aria-label="Toggle dark mode"
|
aria-label="Toggle dark mode"
|
||||||
>
|
>
|
||||||
<div class="relative z-10 flex h-5 w-5 items-center justify-center">
|
<div class="relative flex h-5 w-5 items-center justify-center">
|
||||||
<!-- Sun icon -->
|
<!-- Sun icon -->
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon-light absolute h-5 w-5 rotate-0 scale-100 text-zinc-800 transition-all duration-500 dark:-rotate-90 dark:scale-0 dark:text-zinc-200"
|
class="icon-light absolute h-5 w-5 text-neutral-600 dark:text-neutral-400 scale-100 dark:scale-0 rotate-0 dark:-rotate-90 transition-all duration-500"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
<!-- Moon icon -->
|
<!-- Moon icon -->
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
class="icon-dark absolute h-5 w-5 rotate-90 scale-0 text-zinc-800 transition-all duration-500 dark:rotate-0 dark:scale-100 dark:text-zinc-200"
|
class="icon-dark absolute h-5 w-5 text-neutral-600 dark:text-neutral-400 scale-0 dark:scale-100 rotate-90 dark:rotate-0 transition-all duration-500"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -40,32 +40,26 @@
|
|||||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Ripple effect -->
|
|
||||||
<span
|
|
||||||
class="absolute inset-0 h-full w-full bg-zinc-200 opacity-0 transition-opacity duration-300 group-active:opacity-20 dark:bg-zinc-700"
|
|
||||||
></span>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
const applyTheme = () => {
|
||||||
|
const isDark =
|
||||||
|
localStorage.theme === 'dark' ||
|
||||||
|
(!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
|
document.documentElement.classList.toggle('dark', isDark);
|
||||||
|
};
|
||||||
|
|
||||||
|
applyTheme();
|
||||||
|
|
||||||
|
document.addEventListener('astro:after-swap', applyTheme);
|
||||||
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Use a function to handle theme toggle to ensure it can be called from anywhere
|
|
||||||
function setupThemeToggle() {
|
function setupThemeToggle() {
|
||||||
const themeToggles = document.querySelectorAll('[data-theme-toggle]');
|
const themeToggles = document.querySelectorAll('[data-theme-toggle]');
|
||||||
|
|
||||||
// Check for dark mode preference at the system level
|
// Create theme switch overlay element
|
||||||
const prefersDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
||||||
|
|
||||||
// Check for saved theme preference or use the system preference
|
|
||||||
const currentTheme = localStorage.getItem('theme') || (prefersDarkMode ? 'dark' : 'light');
|
|
||||||
|
|
||||||
// Apply the theme on initial load
|
|
||||||
if (currentTheme === 'dark') {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create theme switch overlay element if it doesn't exist
|
|
||||||
if (!document.querySelector('.theme-switch-overlay')) {
|
if (!document.querySelector('.theme-switch-overlay')) {
|
||||||
const overlay = document.createElement('div');
|
const overlay = document.createElement('div');
|
||||||
overlay.className = 'theme-switch-overlay fixed inset-0 pointer-events-none z-50';
|
overlay.className = 'theme-switch-overlay fixed inset-0 pointer-events-none z-50';
|
||||||
@@ -74,9 +68,7 @@
|
|||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Toggle theme when any theme toggle button is clicked
|
|
||||||
themeToggles.forEach((toggle) => {
|
themeToggles.forEach((toggle) => {
|
||||||
// Add event listeners for both click and touch events
|
|
||||||
['click', 'touchend'].forEach((eventType) => {
|
['click', 'touchend'].forEach((eventType) => {
|
||||||
toggle.addEventListener(
|
toggle.addEventListener(
|
||||||
eventType,
|
eventType,
|
||||||
@@ -96,14 +88,10 @@
|
|||||||
y = e.clientY - rect.top;
|
y = e.clientY - rect.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set the position variables for the radial gradient
|
|
||||||
document.documentElement.style.setProperty('--x', `${x}px`);
|
document.documentElement.style.setProperty('--x', `${x}px`);
|
||||||
document.documentElement.style.setProperty('--y', `${y}px`);
|
document.documentElement.style.setProperty('--y', `${y}px`);
|
||||||
|
|
||||||
// Get the overlay element
|
|
||||||
const overlay = document.querySelector('.theme-switch-overlay');
|
const overlay = document.querySelector('.theme-switch-overlay');
|
||||||
|
|
||||||
// Determine the new theme
|
|
||||||
const isDark = document.documentElement.classList.contains('dark');
|
const isDark = document.documentElement.classList.contains('dark');
|
||||||
const newTheme = isDark ? 'light' : 'dark';
|
const newTheme = isDark ? 'light' : 'dark';
|
||||||
|
|
||||||
@@ -114,14 +102,8 @@
|
|||||||
overlay.style.opacity = '1';
|
overlay.style.opacity = '1';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add transition class
|
|
||||||
document.documentElement.classList.add('theme-switching');
|
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
|
// Force a reflow to ensure all elements update
|
||||||
document.body.offsetHeight;
|
document.body.offsetHeight;
|
||||||
|
|
||||||
@@ -133,10 +115,7 @@
|
|||||||
document.documentElement.classList.add('dark');
|
document.documentElement.classList.add('dark');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the preference
|
|
||||||
localStorage.setItem('theme', newTheme);
|
localStorage.setItem('theme', newTheme);
|
||||||
|
|
||||||
// Dispatch a custom event for other components to react to
|
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
new CustomEvent('themeChanged', {
|
new CustomEvent('themeChanged', {
|
||||||
detail: { isDark: newTheme === 'dark' },
|
detail: { isDark: newTheme === 'dark' },
|
||||||
@@ -146,45 +125,22 @@
|
|||||||
// Force another reflow to ensure all elements update
|
// Force another reflow to ensure all elements update
|
||||||
document.body.offsetHeight;
|
document.body.offsetHeight;
|
||||||
|
|
||||||
// Hide overlay after theme has changed
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (overlay) {
|
if (overlay) {
|
||||||
overlay.style.opacity = '0';
|
overlay.style.opacity = '0';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove transition class after animation completes
|
|
||||||
document.documentElement.classList.remove('theme-switching');
|
document.documentElement.classList.remove('theme-switching');
|
||||||
ripple.remove();
|
|
||||||
}, 300);
|
}, 300);
|
||||||
}, 50);
|
}, 50);
|
||||||
},
|
},
|
||||||
{ passive: false }
|
{ 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
|
// Run setup on load
|
||||||
document.addEventListener('DOMContentLoaded', setupThemeToggle);
|
document.addEventListener('astro:page-load', setupThemeToggle);
|
||||||
|
|
||||||
// Also run on page visibility change to ensure theme is consistent
|
// Also run on page visibility change to ensure theme is consistent
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
@@ -211,85 +167,32 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Smooth transition for the entire page when theme changes */
|
|
||||||
:global(body) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Subtle hover animation */
|
/* Subtle hover animation */
|
||||||
#theme-toggle {
|
#theme-toggle {
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
box-shadow: 0 0 0 rgba(0, 0, 0, 0);
|
||||||
-webkit-tap-highlight-color: transparent; /* Remove default mobile tap highlight */
|
-webkit-tap-highlight-color: transparent;
|
||||||
min-height: 32px; /* Ensure minimum touch target size */
|
min-height: 32px;
|
||||||
min-width: 32px; /* Ensure minimum touch target size */
|
min-width: 32px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Only apply hover effects on non-touch devices */
|
|
||||||
@media (hover: hover) {
|
@media (hover: hover) {
|
||||||
#theme-toggle:hover {
|
#theme-toggle:hover {
|
||||||
transform: translateY(-2px);
|
transform: translateY(-2px);
|
||||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#theme-toggle:hover .icon-light:not(.dark .icon-light) {
|
:global(:root:not(.dark)) #theme-toggle:hover .icon-light {
|
||||||
filter: drop-shadow(0 0 2px rgba(251, 191, 36, 0.6));
|
filter: drop-shadow(0 0 2px rgba(251, 191, 36, 0.6));
|
||||||
transform: scale(1.1) rotate(15deg);
|
transform: scale(1.1) rotate(15deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
#theme-toggle:hover .icon-dark:not(:not(.dark) .icon-dark) {
|
:global(:root.dark) #theme-toggle:hover .icon-dark {
|
||||||
filter: drop-shadow(0 0 2px rgba(129, 140, 248, 0.6));
|
filter: drop-shadow(0 0 2px rgba(129, 140, 248, 0.6));
|
||||||
transform: scale(1.1) rotate(-15deg);
|
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 */
|
/* Optimize animations for mobile */
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.icon-light,
|
.icon-light,
|
||||||
@@ -302,10 +205,6 @@
|
|||||||
transform: none;
|
transform: none;
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-toggle-ripple {
|
|
||||||
animation-duration: 0.4s;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Adjust size for very small screens */
|
/* Adjust size for very small screens */
|
||||||
56
src/components/cards/BlogCard.astro
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
|
||||||
|
import type { Post } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import { formatDate } from '@support/time';
|
||||||
|
import { getDirectusImageURL } from '@/support/url';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
post: Post;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { post } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="smooth-reveal-cards group flex flex-col">
|
||||||
|
<a
|
||||||
|
class="card-base border-none!"
|
||||||
|
href={`/blog/${post.slug}/`}
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
<div class="relative shrink-0 rounded-t-xl w-full overflow-hidden before:absolute before:inset-x-0 before:z-1 before:size-full">
|
||||||
|
<Image
|
||||||
|
class="rounded-t-xl h-auto w-full"
|
||||||
|
src={getDirectusImageURL(post.image)}
|
||||||
|
alt={post.image_alt}
|
||||||
|
draggable="false"
|
||||||
|
loading="eager"
|
||||||
|
format="webp"
|
||||||
|
inferSize={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rounded-xl p-4 md:p-5">
|
||||||
|
<h3 class="card-text-title text-xl">
|
||||||
|
{post.title}
|
||||||
|
</h3>
|
||||||
|
<div class="ml-6 flex">
|
||||||
|
<div class="relative inline-block w-full">
|
||||||
|
<div class="card-text-title card-hover-text-title flex relative items-center mx-auto min-h-11 sm:mx-0 sm:mt-4">
|
||||||
|
<span class="relative inline-block overflow-hidden ml-2">
|
||||||
|
Read more
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
|
||||||
|
/>
|
||||||
|
<p class="card-text-description text-sm ml-auto">
|
||||||
|
{formatDate(post.published_date)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
63
src/components/cards/CategoryCard.astro
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
count: number;
|
||||||
|
publishDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { slug, title, description, count, publishDate } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="smooth-reveal-cards group h-full">
|
||||||
|
<a
|
||||||
|
class="card-base flex flex-col h-full min-h-55"
|
||||||
|
href={`/categories/${slug}/`}
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
<div class="relative grow overflow-hidden">
|
||||||
|
<div class="absolute inset-1 flex flex-col p-3 md:p-4 lg:p-5">
|
||||||
|
<div class="overflow-hidden">
|
||||||
|
<h3 class="card-text-title-major card-hover-text-title whitespace-nowrap mb-4">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
<p class="card-text-description mb-4">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="card-text-description flex items-center justify-between text-xs mt-auto pt-1 md:pt-2">
|
||||||
|
<span class="inline-flex items-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
<path d="M4 19.5v-15A2.5 2.5 0 0 1 6.5 2H20v20H6.5a2.5 2.5 0 0 1 0-5H20"></path>
|
||||||
|
</svg>
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
|
<span class="inline-flex items-center">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="mr-1"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<polyline points="12 6 12 12 16 14"></polyline>
|
||||||
|
</svg>
|
||||||
|
{publishDate}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
65
src/components/cards/EducationCard.astro
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
import Logo from '@components/images/Logo.astro';
|
||||||
|
import { getDirectusImageURL } from '@/support/url';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
topic: string;
|
||||||
|
area: string;
|
||||||
|
date: string;
|
||||||
|
url: string;
|
||||||
|
logoUrlLight?: string;
|
||||||
|
logoUrlDark?: string;
|
||||||
|
logoIcon?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { topic, area, date, url, logoUrlLight, logoIcon } = Astro.props;
|
||||||
|
const logoUrlDark = Astro.props.logoUrlDark || logoUrlLight;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="smooth-reveal group flex flex-col">
|
||||||
|
<a
|
||||||
|
class="card-base flex items-center"
|
||||||
|
href={url}
|
||||||
|
>
|
||||||
|
<div class="p-4 md:p-10">
|
||||||
|
<div class="flex items-center">
|
||||||
|
{logoUrlLight ? (
|
||||||
|
<div class="card-hover-icon-scale mr-5">
|
||||||
|
<Logo
|
||||||
|
srcLight={getDirectusImageURL(logoUrlLight)}
|
||||||
|
srcDark={getDirectusImageURL(logoUrlDark!)}
|
||||||
|
alt={`Logo of ${topic}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : logoIcon ? (
|
||||||
|
<div class="mr-5 text-header">
|
||||||
|
<Icon name={logoIcon} class="card-hover-icon-scale h-12 w-12" />
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div class="grow text-left">
|
||||||
|
<span class="card-text-title block text-lg">
|
||||||
|
{topic}
|
||||||
|
</span>
|
||||||
|
<span class="card-text-description block mt-1 font-medium text-xs uppercase">
|
||||||
|
{area} - {new Date(date).getFullYear()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-6 flex">
|
||||||
|
<div class="relative inline-block">
|
||||||
|
<div class="card-text-title card-hover-text-title flex relative mx-auto min-h-11 items-center font-semibold text-md sm:mx-0 sm:mt-4">
|
||||||
|
<span class="relative inline-block overflow-hidden">
|
||||||
|
Visit Page
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
44
src/components/cards/FeaturesCard.astro
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
import Logo from "@components/images/Logo.astro"
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
url?: string;
|
||||||
|
logoUrlLight?: string;
|
||||||
|
logoUrlDark?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, url, logoUrlLight }: Props = Astro.props;
|
||||||
|
const logoUrlDark = Astro.props.logoUrlDark || logoUrlLight;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="smooth-reveal-2 group flex flex-col">
|
||||||
|
<a
|
||||||
|
class="card-base flex items-center h-30 w-100 md:w-75"
|
||||||
|
href={url}
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
<div class="p-5 w-full">
|
||||||
|
<div class="flex items-center">
|
||||||
|
{logoUrlLight && (
|
||||||
|
<div class="card-hover-icon-scale">
|
||||||
|
<Logo
|
||||||
|
srcLight={logoUrlLight}
|
||||||
|
srcDark={logoUrlDark}
|
||||||
|
alt={`Logo of ${title}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="ms-5 grow text-left">
|
||||||
|
<span class="card-text-title card-hover-text-title block text-lg">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<p class="card-text-description block mt-1">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
72
src/components/cards/HighlightsCard.astro
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
|
||||||
|
import Logo from '@components/images/Logo.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
url?: string;
|
||||||
|
logoUrlLight?: string;
|
||||||
|
logoUrlDark?: string;
|
||||||
|
highlights?: string[];
|
||||||
|
visitSource?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, url, logoUrlLight, logoUrlDark, highlights, visitSource } = Astro.props;
|
||||||
|
|
||||||
|
const visitText = visitSource ? 'Visit Source' : 'Visit Page';
|
||||||
|
const visitClass = visitSource ? 'card-hover-text-gitea' : 'card-hover-text-title';
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="smooth-reveal group flex flex-col">
|
||||||
|
<a
|
||||||
|
class="card-base flex items-center"
|
||||||
|
href={url}
|
||||||
|
>
|
||||||
|
<div class="p-4 md:p-10">
|
||||||
|
<div class="flex items-center mb-4">
|
||||||
|
{logoUrlLight && (
|
||||||
|
<div class="card-hover-icon-scale mr-5">
|
||||||
|
<Logo
|
||||||
|
srcLight={logoUrlLight}
|
||||||
|
srcDark={logoUrlDark}
|
||||||
|
alt={`Logo of ${title}`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="grow text-left">
|
||||||
|
<span class="card-text-title block text-lg">
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
<p class="card-text-description block mt-1">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{highlights && (
|
||||||
|
<ul class="card-text-description text-sm mt-1 flex flex-col list-disc gap-2 [&>li]:ml-4">
|
||||||
|
{highlights.map((highlight) => (
|
||||||
|
<li class="marker:text-accent">
|
||||||
|
{highlight}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
<div class="ml-6 flex">
|
||||||
|
<div class="relative inline-block">
|
||||||
|
<div class={`card-text-title ${visitClass} flex relative items-center font-semibold text-md min-h-11 mx-auto sm:mx-0 sm:mt-4`}>
|
||||||
|
{visitSource && <Icon name="pajamas:gitea" />}
|
||||||
|
<span class="relative inline-block overflow-hidden ml-2">
|
||||||
|
{visitText}
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="translate-y-0.5 transition duration-300 group-hover:translate-x-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
56
src/components/cards/LargeBlogLeftCard.astro
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
|
||||||
|
import { getDirectusImageURL } from '@/support/url';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subTitle: string;
|
||||||
|
url: string;
|
||||||
|
img: string;
|
||||||
|
imgAlt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, subTitle, url, img, imgAlt } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="smooth-reveal flex flex-col px-4 py-10 mx-auto">
|
||||||
|
<a
|
||||||
|
class="md:card-base-hidden group items-center md:grid md:grid-cols-2 lg:grid lg:grid-cols-2 gap-8 xl:gap-16 max-w-340 2xl:max-w-full md:px-8 md:py-8"
|
||||||
|
href={url}
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Image
|
||||||
|
class="rounded-2xl rounded-b-none md:rounded-2xl w-full h-full sm:max-h-80 md:max-h-90 object-cover"
|
||||||
|
src={getDirectusImageURL(img)}
|
||||||
|
alt={imgAlt}
|
||||||
|
draggable="false"
|
||||||
|
loading="lazy"
|
||||||
|
width="850"
|
||||||
|
height="420"
|
||||||
|
inferSize={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="bg-background-card md:bg-transparent group-hover:bg-neutral-100 md:group-hover:bg-transparent dark:group-hover:bg-neutral-800/90 md:dark:group-hover:bg-transparent rounded-b-2xl transition-all duration-300 p-6">
|
||||||
|
<h2 class="card-text-header mb-2">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<p class="card-text-title font-light text-pretty sm:text-lg max-w-prose mb-8">
|
||||||
|
{subTitle}
|
||||||
|
</p>
|
||||||
|
<div class="button-base button-bg-teal inline-flex rounded-lg gap-x-2">
|
||||||
|
<div class="button-text-title flex relative items-center text-center">
|
||||||
|
<span class="mr-2">
|
||||||
|
Read More
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="button-hover-arrow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
86
src/components/cards/LargeBlogRightCard.astro
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
|
||||||
|
import { getDirectusImageURL } from '@/support/url';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subTitle: string;
|
||||||
|
url: string;
|
||||||
|
single?: boolean;
|
||||||
|
imgOne: any;
|
||||||
|
imgOneAlt: any;
|
||||||
|
imgTwo?: any;
|
||||||
|
imgTwoAlt?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, subTitle, url, single, imgOne, imgOneAlt, imgTwo, imgTwoAlt } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="smooth-reveal flex flex-col px-5 py-10 mx-auto">
|
||||||
|
<a
|
||||||
|
class="md:card-base-hidden group flex flex-col-reverse md:items-center md:grid md:grid-cols-2 lg:grid lg:grid-cols-2 md:gap-8 xl:gap-16 max-w-340 2xl:max-w-full md:px-8 md:py-8"
|
||||||
|
href={url}
|
||||||
|
data-astro-prefetch
|
||||||
|
>
|
||||||
|
<div class="bg-background-card md:bg-transparent group-hover:bg-neutral-100 md:group-hover:bg-transparent dark:group-hover:bg-neutral-800/90 md:dark:group-hover:bg-transparent rounded-b-2xl transition-all duration-300 p-6">
|
||||||
|
<h2 class="card-text-header mb-2">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
<p class="card-text-title font-light text-pretty sm:text-lg max-w-prose mb-8">
|
||||||
|
{subTitle}
|
||||||
|
</p>
|
||||||
|
<div class="button-base button-bg-teal inline-flex rounded-lg gap-x-2">
|
||||||
|
<div class="button-text-title flex relative items-center text-center">
|
||||||
|
<span class="mr-2">
|
||||||
|
Read More
|
||||||
|
</span>
|
||||||
|
<Icon
|
||||||
|
name="mdi:keyboard-arrow-right"
|
||||||
|
class="button-hover-arrow"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{single ? (
|
||||||
|
<div>
|
||||||
|
<Image
|
||||||
|
class="rounded-2xl rounded-b-none md:rounded-2xl w-full"
|
||||||
|
src={getDirectusImageURL(imgOne)}
|
||||||
|
alt={imgOneAlt}
|
||||||
|
format="webp"
|
||||||
|
loading="lazy"
|
||||||
|
width="850"
|
||||||
|
height="420"
|
||||||
|
inferSize={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<Image
|
||||||
|
class="rounded-xl w-full"
|
||||||
|
src={getDirectusImageURL(imgOne)}
|
||||||
|
alt={imgOneAlt}
|
||||||
|
draggable="false"
|
||||||
|
format="webp"
|
||||||
|
loading="lazy"
|
||||||
|
width="400"
|
||||||
|
height="230"
|
||||||
|
inferSize={true}
|
||||||
|
/>
|
||||||
|
<Image
|
||||||
|
class="rounded-xl w-full mt-4 lg:mt-10"
|
||||||
|
src={getDirectusImageURL(imgTwo)}
|
||||||
|
alt={imgTwoAlt}
|
||||||
|
draggable="false"
|
||||||
|
format="webp"
|
||||||
|
loading="lazy"
|
||||||
|
width="400"
|
||||||
|
height="230"
|
||||||
|
inferSize={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
35
src/components/cards/WeatherCard.astro
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
dayName: string;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
temp: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { dayName, label, icon, temp } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="smooth-reveal-2 group flex flex-col">
|
||||||
|
<div class="card-base w-32 md:w-40">
|
||||||
|
<div class="p-5 text-center">
|
||||||
|
<span class="card-text-description block font-bold text-xs uppercase tracking-widest">
|
||||||
|
{dayName}
|
||||||
|
</span>
|
||||||
|
<div class="flex justify-center my-2">
|
||||||
|
<img
|
||||||
|
src={`https://openweathermap.org/img/wn/${icon}@2x.png`}
|
||||||
|
alt={label}
|
||||||
|
class="card-hover-icon-scale h-12 w-12"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="card-text-title card-hover-text-title block text-2xl">
|
||||||
|
{temp}°F
|
||||||
|
</span>
|
||||||
|
<span class="card-text-description mt-1 block text-xs capitalize">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
18
src/components/images/BrandLogo.astro
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
---
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
|
import logo from '@images/brand_logo.png';
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
---
|
||||||
|
|
||||||
|
<Image
|
||||||
|
src={logo}
|
||||||
|
alt={global.name}
|
||||||
|
draggable="false"
|
||||||
|
loading="eager"
|
||||||
|
inferSize={true}
|
||||||
|
{...Astro.props}
|
||||||
|
/>
|
||||||
46
src/components/images/ImageTheme.astro
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
---
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
|
||||||
|
const { srcLight, srcDark, alt, style, width, height } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="themed-image-container">
|
||||||
|
<Image
|
||||||
|
src={srcLight}
|
||||||
|
alt={alt}
|
||||||
|
class={`light-logo ${style}`}
|
||||||
|
inferSize={true}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
inferSize={true}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Image
|
||||||
|
src={srcDark}
|
||||||
|
alt={alt}
|
||||||
|
class={`dark-logo ${style}`}
|
||||||
|
inferSize={true}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
inferSize={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.themed-image-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-areas: "stack";
|
||||||
|
}
|
||||||
|
|
||||||
|
.themed-image-container :global(img) {
|
||||||
|
grid-area: stack;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .light-logo {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .dark-logo {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
22
src/components/images/Logo.astro
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
import ImageTheme from '@components/images/ImageTheme.astro';
|
||||||
|
|
||||||
|
const {
|
||||||
|
srcLight,
|
||||||
|
srcDark,
|
||||||
|
alt,
|
||||||
|
width = 48,
|
||||||
|
height = 48,
|
||||||
|
} = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<ImageTheme
|
||||||
|
srcLight={srcLight}
|
||||||
|
srcDark={srcDark}
|
||||||
|
alt={alt}
|
||||||
|
style=`color: transparent; width: ${width}px; height: ${height}px; object-fit: contain; max-height: 100%; max-width: 100%;`
|
||||||
|
draggable="false"
|
||||||
|
loading="lazy"
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
/>
|
||||||
30
src/components/sections/ApplicationSection.astro
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
---
|
||||||
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
|
import type { Application } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import HighlightsCard from '@components/cards/HighlightsCard.astro';
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const applications = ((await directus.request(
|
||||||
|
readItems('site_applications' as any, {
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-isActive'],
|
||||||
|
})
|
||||||
|
)) as unknown) as Application[];
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class:list={['mx-auto max-w-7xl px-4 py-10 sm:px-6 lg:px-8 lg:py-14', Astro.props.className]}>
|
||||||
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:gap-8 print:flex print:flex-col">
|
||||||
|
{applications.map((application: Application) => (
|
||||||
|
<HighlightsCard
|
||||||
|
title={application.name}
|
||||||
|
description={application.description}
|
||||||
|
url={application.url}
|
||||||
|
logoUrlLight={application.logoUrl}
|
||||||
|
logoUrlDark={application.logoUrl}
|
||||||
|
highlights={application.highlights}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
93
src/components/sections/CategorySection.astro
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
|
import type { Post } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import CategoryCard from '@components/cards/CategoryCard.astro';
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
import { timeago } from '@support/time';
|
||||||
|
|
||||||
|
const posts = await directus.request(
|
||||||
|
readItems('posts', {
|
||||||
|
filter: { published: { _eq: true } },
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-published_date'],
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
const layoutPattern = [
|
||||||
|
{ col: 2, row: 2 },
|
||||||
|
{ col: 2, row: 1 },
|
||||||
|
{ col: 1, row: 1 },
|
||||||
|
{ col: 1, row: 1 },
|
||||||
|
{ col: 1, row: 2 },
|
||||||
|
{ col: 2, row: 1 },
|
||||||
|
{ col: 1, row: 1 },
|
||||||
|
{ col: 1, row: 1 },
|
||||||
|
{ col: 1, row: 1 },
|
||||||
|
{ col: 1, row: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const postMap: Map<string, Post[]> = posts
|
||||||
|
.sort((a: Post, b: Post) => b.published_date.valueOf() - a.published_date.valueOf())
|
||||||
|
.reduce((acc, obj) => {
|
||||||
|
let posts = acc.get(obj.category);
|
||||||
|
if (!posts) {
|
||||||
|
posts = [];
|
||||||
|
}
|
||||||
|
posts.push(obj);
|
||||||
|
|
||||||
|
acc.set(obj.category, posts);
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
}, new Map<string, Post[]>());
|
||||||
|
|
||||||
|
const categories = (await getCollection('categories'))
|
||||||
|
.sort((a, b) => {
|
||||||
|
const aCount = postMap.get(a.slug)?.length ?? 0;
|
||||||
|
const bCount = postMap.get(b.slug)?.length ?? 0;
|
||||||
|
return bCount - aCount;
|
||||||
|
})
|
||||||
|
.map((c, index) => {
|
||||||
|
const posts = postMap.get(c.slug);
|
||||||
|
const pattern = layoutPattern[index % layoutPattern.length];
|
||||||
|
const smColSpan = Math.min(pattern.col, 2);
|
||||||
|
const mdColSpan = Math.min(pattern.col, 4);
|
||||||
|
const rowSpan = pattern.row;
|
||||||
|
const rowSpanClass = rowSpan > 1 ? `row-span-${rowSpan}` : 'row-span-1';
|
||||||
|
const gridItemClass = `col-span-${smColSpan} md:col-span-${mdColSpan} ${rowSpanClass}`;
|
||||||
|
return {
|
||||||
|
...c,
|
||||||
|
posts,
|
||||||
|
gridItemClass,
|
||||||
|
layoutPattern: {
|
||||||
|
smCol: smColSpan,
|
||||||
|
mdCol: mdColSpan,
|
||||||
|
row: rowSpan,
|
||||||
|
index,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="mx-auto px-4 py-10 sm:px-6 lg:px-8 lg:py-14 lg:pt-10 2xl:max-w-full">
|
||||||
|
<div class="grid grid-flow-row-dense grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
{categories.map((category) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={category.gridItemClass}
|
||||||
|
style={category.layoutPattern.row > 1 ? 'grid-row: span 2 / span 2;' : ''}
|
||||||
|
>
|
||||||
|
<CategoryCard
|
||||||
|
slug={category.slug}
|
||||||
|
title={category.data.title}
|
||||||
|
description={category.data.description}
|
||||||
|
count={postMap.get(category.slug)?.length ?? 0}
|
||||||
|
publishDate={timeago(postMap.get(category.slug)?.[0]?.published_date)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
65
src/components/sections/EducationSection.astro
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
|
import type { Education, Certificate} from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import EducationCard from '@components/cards/EducationCard.astro';
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const educations = ((await directus.request(
|
||||||
|
readItems('site_education' as any, {
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-graduationDate'],
|
||||||
|
})
|
||||||
|
)) as unknown) as Education[];
|
||||||
|
|
||||||
|
const certificates = ((await directus.request(
|
||||||
|
readItems('site_certificate' as any, {
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-issuerDate'],
|
||||||
|
})
|
||||||
|
)) as unknown) as Certificate[];
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
|
||||||
|
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5">
|
||||||
|
Education
|
||||||
|
</h3>
|
||||||
|
<div class="mx-8">
|
||||||
|
<h4 class="smooth-reveal card-text-header-minor pt-5">
|
||||||
|
College
|
||||||
|
</h4>
|
||||||
|
<div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4 py-3">
|
||||||
|
{educations.map((education: Education) => (
|
||||||
|
<EducationCard
|
||||||
|
topic={education.institution}
|
||||||
|
area={education.area}
|
||||||
|
date={education.graduationDate}
|
||||||
|
url={education.url}
|
||||||
|
logoUrlLight={education.logo}
|
||||||
|
logoUrlDark={education.logoDark}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{certificates.length > 0 && (
|
||||||
|
<div class="mx-8">
|
||||||
|
<h4 class="smooth-reveal card-text-header-minor pt-8">
|
||||||
|
Certificates
|
||||||
|
</h4>
|
||||||
|
<div class="grid md:grid-cols-2 sm:grid-cols-1 gap-4 py-3">
|
||||||
|
{certificates.map((certificate: Certificate) => (
|
||||||
|
<EducationCard
|
||||||
|
topic={certificate.name}
|
||||||
|
area={certificate.issuer}
|
||||||
|
date={certificate.issuerDate}
|
||||||
|
url={certificate.url}
|
||||||
|
logoUrlLight={certificate.logo}
|
||||||
|
logoUrlDark={certificate.logoDark}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
159
src/components/sections/ExperienceSection.astro
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
|
import type { Experience } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const experiences = ((await directus.request(
|
||||||
|
readItems('site_experience'as any, {
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-endDate'],
|
||||||
|
})
|
||||||
|
)) as unknown) as Experience[];
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class:list={['flex flex-col gap-8', Astro.props.className]}>
|
||||||
|
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-10">
|
||||||
|
Experience
|
||||||
|
</h3>
|
||||||
|
<ul class="flex flex-col w-full ml-8 pr-8">
|
||||||
|
{experiences.map((experience: Experience) => {
|
||||||
|
const startYear = new Date(experience.startDate).getFullYear();
|
||||||
|
const endYear = experience.endDate != null ? new Date(experience.endDate).getFullYear() : 'Present';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li class="relative">
|
||||||
|
<div class="smooth-reveal group relative grid sm:grid-cols-18 sm:gap-8 md:gap-6 pb-16">
|
||||||
|
<header class="relative sm:col-span-3 text-header font-semibold text-lg mt-1">
|
||||||
|
<time datetime={experience.startDate} data-title={experience.startDate}>
|
||||||
|
{startYear}
|
||||||
|
</time>
|
||||||
|
{' '}-{' '}
|
||||||
|
<time datetime={experience.endDate} data-title={experience.endDate}>
|
||||||
|
{endYear}
|
||||||
|
</time>
|
||||||
|
</header>
|
||||||
|
<div class="relative flex flex-col sm:col-span-12 pb-6">
|
||||||
|
<div class="absolute bg-accent -translate-x-[1.71rem] rounded-full h-2 w-2 mt-3"/>
|
||||||
|
<h3>
|
||||||
|
<div
|
||||||
|
class="inline-flex items-center text-2xl leading-tight font-semibold"
|
||||||
|
aria-label="{position} - {company}"
|
||||||
|
>
|
||||||
|
<span class="text-header">
|
||||||
|
{experience.position} <span>@</span>
|
||||||
|
{experience.url ? (
|
||||||
|
<a
|
||||||
|
class="hover:text-main"
|
||||||
|
href={experience.url}
|
||||||
|
title={`Ver ${experience.name}`}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{experience.name}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
<span>{experience.name}</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
{(experience.location || experience.location_type) && (
|
||||||
|
<div class="text-secondary text-sm">
|
||||||
|
{experience.location} {experience.location && experience.location_type && '-'} {experience.location_type}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div class="text-md mt-4 flex flex-col gap-4" x-data="{ expanded: false }">
|
||||||
|
{experience.summary && (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h4 class="text-header font-semibold">
|
||||||
|
Summary:
|
||||||
|
</h4>
|
||||||
|
<ul class="flex flex-col text-primary list-disc gap-2 [&>li]:ml-4">
|
||||||
|
<li class="marker:text-main">
|
||||||
|
{experience.summary}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(experience.responsibilities || experience.achievements) && (
|
||||||
|
<div class="relative flex flex-col gap-4" :class="expanded ? '' : 'mask-[linear-gradient(to_bottom,black_50%,transparent)]'" x-show="expanded" x-collapse.min.50px>
|
||||||
|
{experience.responsibilities && (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h4 class="text-header font-semibold">
|
||||||
|
Responsibilities:
|
||||||
|
</h4>
|
||||||
|
<ul class="flex flex-col text-primary list-disc gap-2 [&>li]:ml-4">
|
||||||
|
{experience.responsibilities.map(responsibility => (
|
||||||
|
<li class="marker:text-main">
|
||||||
|
{responsibility}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{experience.achievements && (
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<h4 class="text-header font-semibold">
|
||||||
|
Achievements:
|
||||||
|
</h4>
|
||||||
|
<ul class="flex flex-col text-primary list-disc gap-2 [&>li]:ml-4">
|
||||||
|
{experience.achievements.map(achievement => (
|
||||||
|
<li class="marker:text-main">
|
||||||
|
{achievement}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button @click="expanded = ! expanded" class="group/more flex items-center justify-center text-primary hover:text-primary-hover text-xs underline transition-all gap-1.5 w-fit cursor-pointer">
|
||||||
|
<span x-text="expanded ? 'Show less' : 'Show more'">
|
||||||
|
Show more
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="group-hover/more:translate-y-0.5 ease-out duration-300 h-4 w-4"
|
||||||
|
:class="{ 'rotate-180': expanded }"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<polyline points="6 9 12 15 18 9" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
class="flex print:hidden flex-wrap gap-2 mt-2"
|
||||||
|
aria-label="Technologies used"
|
||||||
|
>
|
||||||
|
{experience.skills && experience.skills.map(skill => {
|
||||||
|
const iconName = skill.toLowerCase();
|
||||||
|
const skillName = skill.split(':')[1].replace(/^language-/, '').replace(/-/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase());
|
||||||
|
return (
|
||||||
|
<li class="flex items-center bg-steel/20 dark:bg-bermuda/20 text-neutral-800 dark:text-neutral-200 text-xs rounded-md border border-solid border-steel/20 dark:border-bermuda/20 gap-1 px-2 py-0.5">
|
||||||
|
<Icon name={`${iconName}`} class="h-4 w-4" /> <span>{skillName}</span>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Alpine Plugins -->
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Alpine Core -->
|
||||||
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||||
41
src/components/sections/FeatureSection.astro
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
|
import FeaturesCard from '@components/cards/FeaturesCard.astro';
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="max-w-340 2xl:max-w-full px-4 sm:px-6 lg:px-8 py-10 lg:py-14 mx-auto mb-2 md:mb-8">
|
||||||
|
<div class="flex flex-col sm:flex-row items-center justify-center gap-y-2 sm:gap-x-12 sm:gap-y-0 lg:gap-x-24">
|
||||||
|
<div class="max-w-5xl sm:px-6 lg:px-8">
|
||||||
|
<div class="flex flex-wrap gap-6 sm:grid-cols-2 sm:gap-6 lg:grid-cols-3 justify-center">
|
||||||
|
<FeaturesCard
|
||||||
|
title="Cloud Engineer"
|
||||||
|
description="Full stack and cloud engineer."
|
||||||
|
url="/about"
|
||||||
|
logoUrlLight="https://img.icons8.com/cotton/64/cloud-development--v2.png"
|
||||||
|
/>
|
||||||
|
<FeaturesCard
|
||||||
|
title="Homelab"
|
||||||
|
description="Tinkering, testing, deploying, etc, etc ..."
|
||||||
|
url="/categories/homelab/"
|
||||||
|
logoUrlLight="https://img.icons8.com/cotton/64/smart-home-connection.png"
|
||||||
|
/>
|
||||||
|
<FeaturesCard
|
||||||
|
title="Documentation"
|
||||||
|
description="Reference and guides for my homelab."
|
||||||
|
url="https://docs.alexlebens.dev"
|
||||||
|
logoUrlLight="https://img.icons8.com/cotton/64/bookmarked-document--v1.png"
|
||||||
|
/>
|
||||||
|
<FeaturesCard
|
||||||
|
title="Email"
|
||||||
|
description={`Send me a message.`}
|
||||||
|
url=`mailto:${global.email}`
|
||||||
|
logoUrlLight="https://img.icons8.com/cotton/64/secured-letter--v3.png"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
155
src/components/sections/GiteaSection.astro
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
---
|
||||||
|
import GiteaButton from '@components/buttons/GiteaButton.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subTitle?: string;
|
||||||
|
url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, subTitle, url } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="relative max-w-340 pt-30 pb-30 px-4 sm:px-6 lg:px- mx-auto mb-2 md:mb-10">
|
||||||
|
<!-- Animated shapes -->
|
||||||
|
<div class="smooth-reveal absolute top-[55%] left-0 scale-90 md:top-[20%] xl:top-[25%] xl:left-[10%]">
|
||||||
|
<svg
|
||||||
|
class="gitea-animate-hover gitea-animate-hover-1"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1.5"
|
||||||
|
color="#ea580c"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill="#ea580c"
|
||||||
|
stroke="#ea580c"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 23a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM3 8a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM3 18a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke="#ea580c"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M21 7.353v9.294a.6.6 0 0 1-.309.525l-8.4 4.666a.6.6 0 0 1-.582 0l-8.4-4.666A.6.6 0 0 1 3 16.647V7.353a.6.6 0 0 1 .309-.524l8.4-4.667a.6.6 0 0 1 .582 0l8.4 4.667a.6.6 0 0 1 .309.524Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke="#ea580c"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="m3.528 7.294 8.18 4.544a.6.6 0 0 0 .583 0l8.209-4.56M12 21v-9"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="smooth-reveal absolute top-0 left-[85%] scale-75">
|
||||||
|
<svg
|
||||||
|
class="gitea-animate-hover gitea-animate-hover-2"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1.5"
|
||||||
|
color="#fbbf24"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="#fbbf24"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10Z"></path>
|
||||||
|
<path
|
||||||
|
fill="#fbbf24"
|
||||||
|
stroke="#fbbf24"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M5 6a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"></path>
|
||||||
|
<path stroke="#fbbf24" stroke-linecap="round" stroke-linejoin="round" d="M5 10.5V9M5 15v-1.5"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
fill="#fbbf24"
|
||||||
|
stroke="#fbbf24"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M5 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2ZM19 20a1 1 0 1 0 0-2 1 1 0 0 0 0 2Z"></path>
|
||||||
|
<path
|
||||||
|
stroke="#fbbf24"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M10.5 19H9M15 19h-1.5"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="smooth-reveal absolute bottom-[5%] left-[60%] scale-[.6] xl:bottom-[15%] xl:left-[35%]">
|
||||||
|
<svg
|
||||||
|
class="gitea-animate-hover gitea-animate-hover-3"
|
||||||
|
width="64"
|
||||||
|
height="64"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="1.5"
|
||||||
|
color="#a3a3a3"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke="#a3a3a3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M5.164 17c.29-1.049.67-2.052 1.132-3M11.5 7.794A16.838 16.838 0 0 1 14 6.296M4.5 22a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
stroke="#a3a3a3"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M9.5 12a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5ZM19.5 7a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5Z"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<!-- Heading -->
|
||||||
|
<div class="smooth-reveal-2 mx-auto mt-5 max-w-xl text-center">
|
||||||
|
<h1 class="card-text-header block">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<!-- Sub-heading -->
|
||||||
|
<div class="smooth-reveal-2 mx-auto mt-5 max-w-3xl text-center">
|
||||||
|
{subTitle && (
|
||||||
|
<p class="card-text-header-description">
|
||||||
|
{subTitle}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<!-- Gitea Button -->
|
||||||
|
{url && (
|
||||||
|
<div class="smooth-reveal-2 flex justify-center mt-8 gap-3">
|
||||||
|
<GiteaButton url={url}/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes gitea-animate-hover {
|
||||||
|
from {
|
||||||
|
transform: translateY(15px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(-15px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gitea-animate-hover {
|
||||||
|
animation: gitea-animate-hover ease-in-out;
|
||||||
|
|
||||||
|
animation-iteration-count: infinite;
|
||||||
|
animation-direction: alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gitea-animate-hover-1 {
|
||||||
|
animation-duration: 5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gitea-animate-hover-2 {
|
||||||
|
animation-duration: 5.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gitea-animate-hover-3 {
|
||||||
|
animation-duration: 6s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
31
src/components/sections/HeaderSection.astro
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
---
|
||||||
|
import GoLinkPrimaryButton from '@components/buttons/GoLinkPrimaryButton.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subTitle: string;
|
||||||
|
btnExists?: boolean;
|
||||||
|
btnTitle?: string;
|
||||||
|
btnURL?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, subTitle, btnExists, btnTitle, btnURL } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="mx-auto mt-10 px-4 sm:px-6 lg:px-8 lg:pt-10 2xl:max-w-full">
|
||||||
|
<div class="flex-wrap md:flex md:items-center md:justify-between">
|
||||||
|
<div class="w-full md:w-auto">
|
||||||
|
<h1 class="smooth-reveal card-text-header block lg:text-6xl">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<p class="smooth-reveal card-text-header-description mt-4">
|
||||||
|
{subTitle}
|
||||||
|
</p>
|
||||||
|
{btnExists ? (
|
||||||
|
<div class="smooth-reveal mt-4 md:mt-8">
|
||||||
|
<GoLinkPrimaryButton title={btnTitle} url={btnURL}/>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
57
src/components/sections/HeroSection.astro
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
|
||||||
|
import GoLinkPrimaryButton from '@components/buttons/GoLinkPrimaryButton.astro';
|
||||||
|
import GoLinkSecondaryButton from '@components/buttons/GoLinkSecondaryButton.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
subTitle?: string;
|
||||||
|
primaryBtn?: string;
|
||||||
|
primaryBtnURL?: string;
|
||||||
|
secondaryBtn?: string;
|
||||||
|
secondaryBtnURL?: string;
|
||||||
|
src?: any;
|
||||||
|
alt?: string;
|
||||||
|
rounded?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, subTitle, primaryBtn, primaryBtnURL, secondaryBtn, secondaryBtnURL, src, alt } = Astro.props;
|
||||||
|
|
||||||
|
const roundedClasses = Astro.props.rounded ? "rounded-2xl" : null;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="mx-auto grid max-w-340 gap-4 px-4 py-14 sm:px-6 md:grid-cols-2 md:items-center md:gap-8 lg:px-8 2xl:max-w-full">
|
||||||
|
<div>
|
||||||
|
<h1 class="smooth-reveal card-text-header block lg:text-7xl">
|
||||||
|
<Fragment set:html={title} />
|
||||||
|
</h1>
|
||||||
|
{subTitle && (
|
||||||
|
<p class="smooth-reveal card-text-header-description lg:w-4/5 mt-6">
|
||||||
|
{subTitle}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div class="smooth-reveal grid sm:inline-flex mt-7 w-full gap-3">
|
||||||
|
{primaryBtn && <GoLinkPrimaryButton title={primaryBtn} url={primaryBtnURL} />}
|
||||||
|
{secondaryBtn && <GoLinkSecondaryButton title={secondaryBtn} url={secondaryBtnURL} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="smooth-reveal-fade md:block w-full hidden">
|
||||||
|
<div class="flex justify-center w-full top-12 md:ml-4 overflow-hidden">
|
||||||
|
{src && alt && (
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
class={`h-full w-105 scale-100 object-cover object-center ${roundedClasses}`}
|
||||||
|
draggable="false"
|
||||||
|
loading="eager"
|
||||||
|
format="webp"
|
||||||
|
quality="low"
|
||||||
|
widths={[840]}
|
||||||
|
inferSize={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
32
src/components/sections/ProjectSection.astro
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
---
|
||||||
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
|
import type { Project } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import HighlightsCard from '@components/cards/HighlightsCard.astro';
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const projects = ((await directus.request(
|
||||||
|
readItems('site_projects' as any, {
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-isActive'],
|
||||||
|
})
|
||||||
|
)) as unknown) as Project[];
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class:list={['flex flex-col gap-y-8', Astro.props.className]}>
|
||||||
|
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5">
|
||||||
|
Projects
|
||||||
|
</h3>
|
||||||
|
<div class="grid grid-cols-1 gap-6 md:grid-cols-2 lg:gap-8 print:flex print:flex-col">
|
||||||
|
{projects.map((project: Project) => (
|
||||||
|
<HighlightsCard
|
||||||
|
title={project.name}
|
||||||
|
description={project.description}
|
||||||
|
url={project.source}
|
||||||
|
highlights={project.highlights}
|
||||||
|
visitSource={true}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
29
src/components/sections/RecentPostsSection.astro
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
import type { Post } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import BlogCard from '@components/cards/BlogCard.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
posts: Post[];
|
||||||
|
title: string;
|
||||||
|
subTitle?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { posts, title, subTitle } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="max-w-340 2xl:max-w-full px-4 sm:px-6 lg:px-8 py-10 lg:py-14 mx-auto mb-2 md:mb-8">
|
||||||
|
<div class="text-center max-w-2xl mx-auto mb-10 lg:mb-14">
|
||||||
|
<h1 class="smooth-reveal card-text-header block">
|
||||||
|
{title}
|
||||||
|
</h1>
|
||||||
|
<div class="smooth-reveal mx-auto mt-5 max-w-3xl text-center">
|
||||||
|
<span class="card-text-header-description">
|
||||||
|
{subTitle}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{posts.map((b) => <BlogCard post={b} />)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
35
src/components/sections/SelectedPostsSection.astro
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
import type { Post } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import LargeBlogLeftCard from '@components/cards/LargeBlogLeftCard.astro';
|
||||||
|
import LargeBlogRightCard from '@components/cards/LargeBlogRightCard.astro';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
posts: Post[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { posts } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="smooth-reveal flex flex-col gap-4">
|
||||||
|
{posts.map((post, index) => index % 2 === 0 ? (
|
||||||
|
<LargeBlogLeftCard
|
||||||
|
title={post.title}
|
||||||
|
subTitle={post.description}
|
||||||
|
url={`/blog/${post.slug}`}
|
||||||
|
img={post.image}
|
||||||
|
imgAlt={post.image_alt}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<LargeBlogRightCard
|
||||||
|
title={post.title}
|
||||||
|
subTitle={post.description}
|
||||||
|
url={`/blog/${post.slug}`}
|
||||||
|
single={!post.image_second}
|
||||||
|
imgOne={post.image}
|
||||||
|
imgOneAlt={post.image_alt}
|
||||||
|
imgTwo={post?.image_second}
|
||||||
|
imgTwoAlt={post?.image_second_alt}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
223
src/components/sections/SkillsSliderSection.astro
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
---
|
||||||
|
import { Icon } from 'astro-icon/components';
|
||||||
|
import { readItems } from '@directus/sdk';
|
||||||
|
|
||||||
|
import type { Skill } from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const skills = ((await directus.request(
|
||||||
|
readItems('site_skills' as any, {
|
||||||
|
fields: ['*'],
|
||||||
|
sort: ['-date_created'],
|
||||||
|
})
|
||||||
|
)) as unknown) as Skill[];
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class:list={['flex flex-col gap-4', Astro.props.className]}>
|
||||||
|
<h3 class="smooth-reveal card-text-header flex relative items-center w-full gap-3 pb-5">
|
||||||
|
Skills
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8 mask-[linear-gradient(to_right,transparent,black_10%,black_90%,transparent)]">
|
||||||
|
<!-- Main slider container -->
|
||||||
|
<div class="slider-track animate-slide flex">
|
||||||
|
{[...skills, ...skills, ...skills].map((skill: Skill) => {
|
||||||
|
return (
|
||||||
|
<div class="skill-card card-base transform hover:-translate-y-2 hover:scale-105 transition-all duration-300 mx-2 min-w-55 sm:mx-4 sm:min-w-70">
|
||||||
|
<div class="p-4 sm:p-6">
|
||||||
|
<div class="flex items-center justify-between mb-4 sm:mb-6">
|
||||||
|
<div class="flex items-center gap-2 sm:gap-4">
|
||||||
|
<div class="flex items-center justify-center rounded-lg text-primary">
|
||||||
|
<Icon name={skill.icon} class="h-8 w-8 sm:h-12 sm:w-12" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-neutral-900 dark:text-neutral-100 text-base font-semibold sm:text-xl">
|
||||||
|
{skill.title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<span class=" bg-neutral-200 dark:bg-neutral-800 text-neutral-700 dark:text-neutral-300 font-mono text-xs sm:text-sm rounded-full px-2 sm:px-2.5 py-0.5 sm:py-1">
|
||||||
|
{skill.level}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="relative bg-stone-500/20 dark:bg-stone-500/20 rounded-full h-1.5 sm:h-2 w-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="progress-bar-animate bg-linear-to-r from-steel via-bermuda to-steel absolute top-0 left-0 h-full rounded-full transition-all duration-1000"
|
||||||
|
style={`width: ${skill.level}%`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-between text-secondary font-mono text-[10px] mt-1 sm:mt-2 sm:text-xs">
|
||||||
|
<span>Beginner</span>
|
||||||
|
<span>Advanced</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
|
function setupInfiniteScroll() {
|
||||||
|
const cards = document.querySelectorAll('.skill-card');
|
||||||
|
if (!cards.length) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupInfiniteScroll();
|
||||||
|
|
||||||
|
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
||||||
|
const cards = document.querySelectorAll('.skill-card');
|
||||||
|
|
||||||
|
if (!isTouchDevice) {
|
||||||
|
cards.forEach((card) => {
|
||||||
|
card.addEventListener('mousemove', (e) => {
|
||||||
|
const rect = card.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
const centerX = rect.width / 2;
|
||||||
|
const centerY = rect.height / 2;
|
||||||
|
|
||||||
|
const angleX = (y - centerY) / 15;
|
||||||
|
const angleY = (centerX - x) / 15;
|
||||||
|
|
||||||
|
card.style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) scale(1.08) translateZ(20px)`;
|
||||||
|
|
||||||
|
// Dynamic shadow based on tilt
|
||||||
|
const shadowX = (x - centerX) / 25;
|
||||||
|
const shadowY = (y - centerY) / 25;
|
||||||
|
card.style.boxShadow = `
|
||||||
|
${shadowX}px ${shadowY}px 20px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 20px rgba(0, 0, 0, 0.05)
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('mouseleave', () => {
|
||||||
|
card.style.transform = '';
|
||||||
|
card.style.boxShadow = '';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Simpler effects for touch devices
|
||||||
|
cards.forEach((card) => {
|
||||||
|
card.addEventListener('touchstart', () => {
|
||||||
|
card.classList.add('is-touched');
|
||||||
|
});
|
||||||
|
|
||||||
|
card.addEventListener('touchend', () => {
|
||||||
|
setTimeout(() => {
|
||||||
|
card.classList.remove('is-touched');
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Specific css to enable sliding effect */
|
||||||
|
.slider-track {
|
||||||
|
width: fit-content;
|
||||||
|
animation: scroll 40s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(calc(-220px * 6 - 16px * 6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.slider-track {
|
||||||
|
animation: scroll 80s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes scroll {
|
||||||
|
0% {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(calc(-280px * 6 - 32px * 6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-stack-slider:hover .slider-track {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card {
|
||||||
|
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:hover {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reduce animation complexity on mobile */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.skill-card {
|
||||||
|
transition:
|
||||||
|
transform 0.3s ease,
|
||||||
|
box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:hover {
|
||||||
|
transform: translateY(-5px) !important;
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -10%;
|
||||||
|
left: -10%;
|
||||||
|
width: 120%;
|
||||||
|
height: 120%;
|
||||||
|
background: radial-gradient(
|
||||||
|
circle at center,
|
||||||
|
rgba(255, 255, 255, 0.1) 0%,
|
||||||
|
rgba(255, 255, 255, 0) 70%
|
||||||
|
);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.5s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-card:hover:before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-animate {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress-bar-animate:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||||
|
animation: progress-shine 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes progress-shine {
|
||||||
|
0% {
|
||||||
|
left: -100%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
37
src/components/sections/WeatherSection.astro
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
import WeatherCard from '@components/cards/WeatherCard.astro';
|
||||||
|
import { getFiveDayForecast } from '@support/weather';
|
||||||
|
|
||||||
|
const { latitude = "44.95", longitude = "-93.09", cityName = "St. Paul, Minnesota", timezone = "America/Chicago" } = Astro.props;
|
||||||
|
|
||||||
|
const { forecastDays, error } = await getFiveDayForecast(latitude, longitude, timezone);
|
||||||
|
---
|
||||||
|
|
||||||
|
<section class="max-w-340 2xl:max-w-fullpx-4 sm:px-6 lg:px-8 py-10 lg:py-14 mx-auto mb-2 md:mb-8">
|
||||||
|
<div class="text-center max-w-2xl mx-auto mb-10 lg:mb-14">
|
||||||
|
<h1 class="smooth-reveal card-text-header block">
|
||||||
|
Weather in my Area
|
||||||
|
</h1>
|
||||||
|
<div class="smooth-reveal mx-auto mt-5 max-w-3xl text-center">
|
||||||
|
<span class="card-text-header-description">
|
||||||
|
Five day forecast for {cityName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error ? (
|
||||||
|
<div class="card-base p-10 text-accent text-center">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div class="flex flex-wrap justify-center gap-4 lg:gap-6">
|
||||||
|
{forecastDays.map((forecastDay) => (
|
||||||
|
<WeatherCard
|
||||||
|
dayName={forecastDay.dayName}
|
||||||
|
label={forecastDay.label}
|
||||||
|
icon={forecastDay.icon}
|
||||||
|
temp={forecastDay.temp}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
44
src/config.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
export interface NavigationLink {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
|
||||||
|
export const NavigationLinks: NavigationLink[] = [
|
||||||
|
{ name: 'Home', url: '/' },
|
||||||
|
{ name: 'Blog', url: '/blog/' },
|
||||||
|
{ name: 'Categories', url: '/categories/' },
|
||||||
|
{ name: 'Apps', url: '/apps/' },
|
||||||
|
{ name: 'About Me', url: '/about/' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FooterLinks: NavigationLink[] = [
|
||||||
|
{ name: 'RSS', url: '/rss.xml' },
|
||||||
|
{ name: 'Gitea', url: 'https://gitea.alexlebens.dev' },
|
||||||
|
{ name: 'Docs', url: 'https://docs.alexlebens.dev' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const SEO = {
|
||||||
|
title: global.name,
|
||||||
|
description: global.about,
|
||||||
|
structuredData: {
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebPage',
|
||||||
|
inLanguage: 'en-US',
|
||||||
|
'@id': global.site_url,
|
||||||
|
url: global.site_url,
|
||||||
|
name: global.name,
|
||||||
|
description: global.about,
|
||||||
|
isPartOf: {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
url: global.site_url,
|
||||||
|
name: global.name,
|
||||||
|
description: global.about,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
4
src/content/categories/books.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Books 📖'
|
||||||
|
description: 'Books I have read or listened to'
|
||||||
|
---
|
||||||
4
src/content/categories/cloud.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Cloud ☁️'
|
||||||
|
description: "Its just someone else's server"
|
||||||
|
---
|
||||||
4
src/content/categories/homelab.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Homelab 🏠'
|
||||||
|
description: 'What happens when rack servers find a home'
|
||||||
|
---
|
||||||
4
src/content/categories/kubernetes.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Kubernetes ☸️'
|
||||||
|
description: 'The container orchestration system'
|
||||||
|
---
|
||||||
4
src/content/categories/life.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Life 🏃🏻'
|
||||||
|
description: 'Just random musings on everyday stuff'
|
||||||
|
---
|
||||||
4
src/content/categories/minnesota.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Minnesota 🌳'
|
||||||
|
description: 'Land of 10,000 Lakes'
|
||||||
|
---
|
||||||
4
src/content/categories/postgresql.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'PostgreSQL'
|
||||||
|
description: 'PostgreSQL is an open-source relational database management system (RDBMS)'
|
||||||
|
---
|
||||||
4
src/content/categories/python.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Python 🐍'
|
||||||
|
description: 'Generally my go to language'
|
||||||
|
---
|
||||||
4
src/content/categories/tool.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'Tool 🪜'
|
||||||
|
description: 'Usually just the software kind'
|
||||||
|
---
|
||||||
4
src/content/categories/whatis.md
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
title: 'What Is?'
|
||||||
|
description: 'A series on discovery'
|
||||||
|
---
|
||||||
12
src/content/config.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { defineCollection, z } from 'astro:content';
|
||||||
|
|
||||||
|
const categoryCollection = defineCollection({
|
||||||
|
type: 'content',
|
||||||
|
schema: () =>
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { categories: categoryCollection };
|
||||||
BIN
src/images/autumn_bench.png
Normal file
|
After Width: | Height: | Size: 11 MiB |
BIN
src/images/autumn_mountain.png
Normal file
|
After Width: | Height: | Size: 13 MiB |
BIN
src/images/autumn_tree.png
Normal file
|
After Width: | Height: | Size: 7.8 MiB |
BIN
src/images/brand_logo.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
src/images/cedar_tree.png
Normal file
|
After Width: | Height: | Size: 9.5 MiB |
1
src/images/favicon.svg
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
src/images/favicon_icon.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
1
src/images/favicon_icon.svg
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
src/images/flowers.png
Normal file
|
After Width: | Height: | Size: 6.8 MiB |
BIN
src/images/portrait.avif
Normal file
|
After Width: | Height: | Size: 76 KiB |
@@ -1,17 +0,0 @@
|
|||||||
---
|
|
||||||
import Layout from './Layout.astro';
|
|
||||||
|
|
||||||
import directus from '../../lib/directus';
|
|
||||||
import { readSingleton } from '@directus/sdk';
|
|
||||||
|
|
||||||
const global = await directus.request(readSingleton('global'));
|
|
||||||
|
|
||||||
export interface Props {
|
|
||||||
title: string;
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={global.title} description={global.description}>
|
|
||||||
<slot />
|
|
||||||
</Layout>
|
|
||||||
@@ -1,59 +1,174 @@
|
|||||||
---
|
---
|
||||||
import Layout from './Layout.astro';
|
import { ClientRouter } from 'astro:transitions';
|
||||||
|
|
||||||
import directus from '../../lib/directus';
|
|
||||||
import { readSingleton } from '@directus/sdk';
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
const global = await directus.request(readSingleton('global'));
|
import BaseHead from '@components/BaseHead.astro';
|
||||||
|
import Footer from '@components/Footer.astro';
|
||||||
|
import Header from '@components/Header.astro';
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
export interface Props {
|
import '@styles/global.css';
|
||||||
title: string;
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
ogImage?: any;
|
||||||
|
lang?: string;
|
||||||
|
structuredData?: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { title, description = 'Alex Lebens', ogImage, lang = 'en', structuredData } = Astro.props;
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
|
||||||
|
const normalizeTitle = !title ? global.name : `${title} | ${global.name}`;
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title={global.title} description={global.description}>
|
<html lang={lang}>
|
||||||
<slot />
|
<head>
|
||||||
</Layout>
|
<title>
|
||||||
|
{normalizeTitle}
|
||||||
|
</title>
|
||||||
|
|
||||||
|
<BaseHead
|
||||||
|
title={normalizeTitle}
|
||||||
|
description={description}
|
||||||
|
ogImage={ogImage}
|
||||||
|
ogTitle={title === '' ? global.name : title}
|
||||||
|
ogDescription={description}
|
||||||
|
structuredData={structuredData}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ClientRouter fallback="swap" />
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
const theme = (() => {
|
||||||
|
if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
|
||||||
|
return localStorage.getItem('theme');
|
||||||
|
}
|
||||||
|
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||||
|
return 'dark';
|
||||||
|
}
|
||||||
|
return 'light';
|
||||||
|
})();
|
||||||
|
|
||||||
|
if (theme === 'light') {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
}
|
||||||
|
window.localStorage.setItem('theme', theme);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Rybbit Tracking Snippet -->
|
||||||
|
<script
|
||||||
|
src="https://rybbit.alexlebens.dev/api/script.js"
|
||||||
|
data-site-id={global.rybbit_site_id}
|
||||||
|
defer
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-background selection:bg-yellow-400 m-0 p-0 overflow-x-hidden">
|
||||||
|
|
||||||
|
<!-- Sliding backgrounds -->
|
||||||
|
<div class="bg"/>
|
||||||
|
<div class="bg bg2"/>
|
||||||
|
<div class="bg bg3"/>
|
||||||
|
|
||||||
|
<!-- Layout -->
|
||||||
|
<div class="grow w-full max-w-(--breakpoint-2xl) px-4 sm:px-6 lg:px-8 py-20 mx-auto">
|
||||||
|
|
||||||
|
<Header />
|
||||||
|
|
||||||
|
<main class="has-js scroll-fade-container min-h-screen">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('astro:page-load', () => {
|
||||||
const themeToggle = document.getElementById('theme-toggle');
|
const onScroll = () => {
|
||||||
|
document.documentElement.style.setProperty('--scroll-offset', `${window.scrollY}px`);
|
||||||
|
document.documentElement.classList.add('has-js');
|
||||||
|
};
|
||||||
|
|
||||||
if (themeToggle) {
|
window.removeEventListener('scroll', onScroll);
|
||||||
themeToggle.addEventListener('click', () => {
|
window.addEventListener('scroll', onScroll, { passive: true });
|
||||||
document.documentElement.classList.add('theme-switching');
|
|
||||||
|
|
||||||
const rippleElements = document.querySelectorAll('.theme-ripple');
|
onScroll();
|
||||||
rippleElements.forEach((el) => {
|
|
||||||
el.classList.add('ripple-active');
|
|
||||||
setTimeout(() => {
|
|
||||||
el.classList.remove('ripple-active');
|
|
||||||
}, 600);
|
|
||||||
});
|
|
||||||
|
|
||||||
const event = new CustomEvent('themeChange', {
|
|
||||||
detail: {
|
|
||||||
theme: document.documentElement.classList.contains('dark') ? 'dark' : 'light',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
document.dispatchEvent(event);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
document.documentElement.classList.remove('theme-switching');
|
|
||||||
}, 600);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const socialLinks = document.querySelectorAll('.social-link');
|
|
||||||
socialLinks.forEach((link) => {
|
|
||||||
link.addEventListener('mouseenter', () => {
|
|
||||||
link.classList.add('hover-active');
|
|
||||||
});
|
|
||||||
|
|
||||||
link.addEventListener('mouseleave', () => {
|
|
||||||
link.classList.remove('hover-active');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* Fade away content below header when scrolling */
|
||||||
|
.has-js .scroll-fade-container {
|
||||||
|
-webkit-mask-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0px,
|
||||||
|
transparent 16px,
|
||||||
|
black 80px,
|
||||||
|
black 100%
|
||||||
|
);
|
||||||
|
|
||||||
|
mask-image: linear-gradient(
|
||||||
|
to bottom,
|
||||||
|
transparent 0px,
|
||||||
|
transparent 16px,
|
||||||
|
black 80px,
|
||||||
|
black 100%
|
||||||
|
);
|
||||||
|
|
||||||
|
-webkit-mask-size: 100vw 100vh;
|
||||||
|
-webkit-mask-repeat: no-repeat;
|
||||||
|
|
||||||
|
-webkit-mask-position-y: var(--scroll-offset);
|
||||||
|
mask-position-y: var(--scroll-offset);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Background that creates the "glimmer" effect */
|
||||||
|
.bg {
|
||||||
|
animation: slide 20s ease-in-out infinite alternate;
|
||||||
|
background-image: linear-gradient(-60deg, var(--bg-primary) 33.3%, var(--bg-secondary) 33.3%, var(--bg-secondary) 66.6%, var(--bg-tertiary) 66.6%);
|
||||||
|
filter: blur(80px);
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: -50%;
|
||||||
|
right: -50%;
|
||||||
|
opacity: .5;
|
||||||
|
position: fixed;
|
||||||
|
z-index: -1;
|
||||||
|
--bg-primary: #e5e5e5;
|
||||||
|
--bg-secondary: #d9d9d9;
|
||||||
|
--bg-tertiary: #ededed;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.dark) .bg {
|
||||||
|
--bg-primary: #292524;
|
||||||
|
--bg-secondary: #44403c;
|
||||||
|
--bg-tertiary: #57534e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg2 {
|
||||||
|
animation-direction: alternate-reverse;
|
||||||
|
animation-duration: 30s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bg3 {
|
||||||
|
animation-duration: 25s;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide {
|
||||||
|
0% {
|
||||||
|
transform:translateX(-25%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform:translateX(25%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@@ -1,398 +0,0 @@
|
|||||||
---
|
|
||||||
import Layout from './Layout.astro';
|
|
||||||
import FormattedDate from '../components/FormattedDate.astro';
|
|
||||||
import ShareButtons from '../components/ShareButtons.astro';
|
|
||||||
import TagList from '../components/TagList.astro';
|
|
||||||
import './styles/markdown.css';
|
|
||||||
|
|
||||||
import directus from '../../lib/directus';
|
|
||||||
import { readItems } from '@directus/sdk';
|
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
|
||||||
const posts = await directus.request(
|
|
||||||
readItems('posts', {
|
|
||||||
fields: ['*'],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
return posts.map((post) => ({ params: { slug: post.slug }, props: post }));
|
|
||||||
}
|
|
||||||
|
|
||||||
const post = Astro.props;
|
|
||||||
const published_date: string = post.published_date.toLocaleString();
|
|
||||||
|
|
||||||
let canonicalURL;
|
|
||||||
try {
|
|
||||||
canonicalURL = new URL(Astro.url.pathname, Astro.site || process.env.SITE_URL);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error creating canonical URL:', error);
|
|
||||||
canonicalURL = new URL('https://www.example.com');
|
|
||||||
}
|
|
||||||
---
|
|
||||||
|
|
||||||
<Layout title={post.title} description={post.description}>
|
|
||||||
<article class="prose prose-zinc mx-auto max-w-4xl dark:prose-invert lg:prose-lg">
|
|
||||||
<div class="mb-12">
|
|
||||||
<h1
|
|
||||||
class="mb-4 text-4xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:text-5xl"
|
|
||||||
>
|
|
||||||
{post.title}
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div class="mb-6 flex items-center gap-x-4 text-sm text-zinc-500 dark:text-zinc-400">
|
|
||||||
<FormattedDate date={published_date} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TagList tags={post.tags} class="mt-2" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Hero image -->
|
|
||||||
{
|
|
||||||
post.image && (
|
|
||||||
<div class="relative mb-8 overflow-hidden rounded-xl shadow-lg sm:mb-12">
|
|
||||||
<div class="aspect-[16/9] w-full">
|
|
||||||
<img
|
|
||||||
src={`${process.env.DIRECTUS_URL ?? 'https://directus.alexlebens.dev'}/assets/${post.image}?width=500`}
|
|
||||||
alt={post.image_alt}
|
|
||||||
class="h-full w-full object-cover"
|
|
||||||
loading="eager"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/30 to-transparent" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
<div class="markdown-content">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add the like button after the content -->
|
|
||||||
<div class="mt-12 border-t border-zinc-200 pt-8 dark:border-zinc-800">
|
|
||||||
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
|
||||||
<ShareButtons url={canonicalURL.toString()} title={post.title} />
|
|
||||||
<!-- Convert URL to string -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{
|
|
||||||
post.updated_date && (
|
|
||||||
<div class="mt-8 text-sm italic text-zinc-500 dark:text-zinc-400">
|
|
||||||
Last updated on <FormattedDate date={post.updated_date} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<slot name="after-article" />
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Blog post SPA transitions
|
|
||||||
function setupBlogPostTransitions() {
|
|
||||||
// Animate article entrance
|
|
||||||
const article = document.querySelector('article');
|
|
||||||
if (article) {
|
|
||||||
article.classList.add('article-entering');
|
|
||||||
|
|
||||||
// Remove class after animation completes
|
|
||||||
setTimeout(() => {
|
|
||||||
article.classList.remove('article-entering');
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure consistent code block styling
|
|
||||||
function updateCodeBlockStyles() {
|
|
||||||
document.querySelectorAll('pre').forEach((pre) => {
|
|
||||||
// Force the background color with !important for both light and dark mode
|
|
||||||
pre.setAttribute('style', 'background-color: #1e293b !important');
|
|
||||||
|
|
||||||
// Also apply to any nested code elements
|
|
||||||
const codeElements = pre.querySelectorAll('code');
|
|
||||||
codeElements.forEach((code) => {
|
|
||||||
code.setAttribute(
|
|
||||||
'style',
|
|
||||||
'background-color: transparent !important; color: #e5e7eb !important;'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial application
|
|
||||||
updateCodeBlockStyles();
|
|
||||||
|
|
||||||
// Watch for theme changes
|
|
||||||
const observer = new MutationObserver(() => {
|
|
||||||
updateCodeBlockStyles();
|
|
||||||
});
|
|
||||||
|
|
||||||
observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
|
|
||||||
|
|
||||||
// Also run on any content changes that might add new code blocks
|
|
||||||
const contentObserver = new MutationObserver((mutations) => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
if (mutation.addedNodes.length) {
|
|
||||||
updateCodeBlockStyles();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
contentObserver.observe(document.body, { childList: true, subtree: true });
|
|
||||||
|
|
||||||
// Clean up observers when navigating away
|
|
||||||
document.addEventListener('spa-navigation-start', () => {
|
|
||||||
observer.disconnect();
|
|
||||||
contentObserver.disconnect();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove the parallax effect for hero image
|
|
||||||
|
|
||||||
// Handle prev/next navigation links
|
|
||||||
const navLinks = document.querySelectorAll('.blog-nav-link');
|
|
||||||
navLinks.forEach((link) => {
|
|
||||||
if (!link.hasAttribute('data-spa-handled')) {
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('mouseenter', () => {
|
|
||||||
link.classList.add('nav-link-hover');
|
|
||||||
});
|
|
||||||
|
|
||||||
link.addEventListener('mouseleave', () => {
|
|
||||||
link.classList.remove('nav-link-hover');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Animate headings when they enter the viewport
|
|
||||||
const animateHeadings = () => {
|
|
||||||
const headings = document.querySelectorAll('article h2, article h3');
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
(entries) => {
|
|
||||||
entries.forEach((entry) => {
|
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add('heading-visible');
|
|
||||||
observer.unobserve(entry.target);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
{
|
|
||||||
threshold: 0.2,
|
|
||||||
rootMargin: '0px 0px -100px 0px',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
headings.forEach((heading) => {
|
|
||||||
heading.classList.add('heading-animated');
|
|
||||||
observer.observe(heading);
|
|
||||||
});
|
|
||||||
|
|
||||||
return observer;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Initialize heading animations
|
|
||||||
const headingObserver = animateHeadings();
|
|
||||||
|
|
||||||
// Enhance code blocks with syntax highlighting and copy button
|
|
||||||
function enhanceCodeBlocks() {
|
|
||||||
const codeBlocks = document.querySelectorAll('pre code');
|
|
||||||
|
|
||||||
codeBlocks.forEach((codeBlock) => {
|
|
||||||
// Skip if already processed
|
|
||||||
if (codeBlock.parentElement.classList.contains('enhanced')) return;
|
|
||||||
|
|
||||||
// Mark as enhanced
|
|
||||||
codeBlock.parentElement.classList.add('enhanced');
|
|
||||||
|
|
||||||
// Create copy button
|
|
||||||
const copyButton = document.createElement('button');
|
|
||||||
copyButton.className = 'copy-code-button';
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
||||||
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add copy functionality
|
|
||||||
copyButton.addEventListener('click', () => {
|
|
||||||
const code = codeBlock.textContent;
|
|
||||||
navigator.clipboard.writeText(code);
|
|
||||||
|
|
||||||
// Show copied feedback
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
|
||||||
<path d="M8 3a1 1 0 011-1h2a1 1 0 110 2H9a1 1 0 01-1-1z" />
|
|
||||||
<path d="M6 3a2 2 0 00-2 2v11a2 2 0 002 2h8a2 2 0 002-2V5a2 2 0 00-2-2 3 3 0 01-3 3H9a3 3 0 01-3-3z" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add copy button to pre element
|
|
||||||
codeBlock.parentElement.appendChild(copyButton);
|
|
||||||
|
|
||||||
// Fix line numbers implementation
|
|
||||||
const codeText = codeBlock.textContent;
|
|
||||||
const lines = codeText.split('\n');
|
|
||||||
|
|
||||||
const lineNumbers = document.createElement('div');
|
|
||||||
lineNumbers.className = 'line-numbers';
|
|
||||||
|
|
||||||
// Always include all lines, including empty ones
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
|
||||||
const lineNumber = document.createElement('span');
|
|
||||||
lineNumber.textContent = i + 1;
|
|
||||||
lineNumbers.appendChild(lineNumber);
|
|
||||||
}
|
|
||||||
|
|
||||||
codeBlock.parentElement.classList.add('with-line-numbers');
|
|
||||||
codeBlock.parentElement.insertBefore(lineNumbers, codeBlock);
|
|
||||||
|
|
||||||
// Fix language label detection and display
|
|
||||||
const className = codeBlock.className;
|
|
||||||
const languageMatch = className.match(/language-(\w+)/);
|
|
||||||
|
|
||||||
if (languageMatch && languageMatch[1]) {
|
|
||||||
const language = languageMatch[1];
|
|
||||||
|
|
||||||
// Add language label at top right
|
|
||||||
const languageLabel = document.createElement('div');
|
|
||||||
languageLabel.className = 'language-label';
|
|
||||||
languageLabel.textContent = language;
|
|
||||||
codeBlock.parentElement.appendChild(languageLabel);
|
|
||||||
|
|
||||||
// Add language badge at bottom right with markdown syntax
|
|
||||||
const languageBadge = document.createElement('div');
|
|
||||||
languageBadge.className = 'language-badge';
|
|
||||||
languageBadge.textContent = `\`\`\`${language}`;
|
|
||||||
languageBadge.style.position = 'absolute';
|
|
||||||
languageBadge.style.bottom = '0.5rem';
|
|
||||||
languageBadge.style.right = '0.5rem';
|
|
||||||
languageBadge.style.fontSize = '0.7rem';
|
|
||||||
languageBadge.style.padding = '0.1rem 0.3rem';
|
|
||||||
languageBadge.style.backgroundColor = 'rgba(75, 85, 99, 0.7)';
|
|
||||||
languageBadge.style.color = '#e5e7eb';
|
|
||||||
languageBadge.style.borderRadius = '0.25rem';
|
|
||||||
languageBadge.style.fontFamily =
|
|
||||||
'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace';
|
|
||||||
languageBadge.style.zIndex = '10';
|
|
||||||
codeBlock.parentElement.appendChild(languageBadge);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhance tables with better styling
|
|
||||||
function enhanceTables() {
|
|
||||||
const tables = document.querySelectorAll('.markdown-content table');
|
|
||||||
|
|
||||||
tables.forEach((table) => {
|
|
||||||
if (table.classList.contains('enhanced-table')) return;
|
|
||||||
|
|
||||||
table.classList.add('enhanced-table');
|
|
||||||
|
|
||||||
// Wrap table in responsive container
|
|
||||||
const wrapper = document.createElement('div');
|
|
||||||
wrapper.className = 'table-container';
|
|
||||||
table.parentNode.insertBefore(wrapper, table);
|
|
||||||
wrapper.appendChild(table);
|
|
||||||
|
|
||||||
// Add zebra striping to rows
|
|
||||||
const rows = table.querySelectorAll('tbody tr');
|
|
||||||
rows.forEach((row, index) => {
|
|
||||||
if (index % 2 === 0) {
|
|
||||||
row.classList.add('even-row');
|
|
||||||
} else {
|
|
||||||
row.classList.add('odd-row');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enhance blockquotes with icons
|
|
||||||
function enhanceBlockquotes() {
|
|
||||||
const blockquotes = document.querySelectorAll('.markdown-content blockquote');
|
|
||||||
|
|
||||||
blockquotes.forEach((blockquote) => {
|
|
||||||
if (blockquote.classList.contains('enhanced-quote')) return;
|
|
||||||
|
|
||||||
blockquote.classList.add('enhanced-quote');
|
|
||||||
|
|
||||||
// Add quote icon
|
|
||||||
const icon = document.createElement('div');
|
|
||||||
icon.className = 'quote-icon';
|
|
||||||
icon.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 01.865-.501 48.172 48.172 0 003.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0012 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018z" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
blockquote.insertBefore(icon, blockquote.firstChild);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all enhancements
|
|
||||||
enhanceCodeBlocks();
|
|
||||||
enhanceTables();
|
|
||||||
enhanceBlockquotes();
|
|
||||||
|
|
||||||
// Clean up observers when navigating away
|
|
||||||
document.addEventListener('spa-navigation-start', () => {
|
|
||||||
if (headingObserver) {
|
|
||||||
headingObserver.disconnect();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', setupBlogPostTransitions);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupBlogPostTransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupBlogPostTransitions);
|
|
||||||
|
|
||||||
// Also initialize when SPA navigation completes
|
|
||||||
document.addEventListener('spa-navigation-complete', setupBlogPostTransitions);
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Enhanced hero image styling */
|
|
||||||
article img:first-of-type {
|
|
||||||
border-radius: 1rem;
|
|
||||||
box-shadow:
|
|
||||||
0 10px 25px -5px rgba(0, 0, 0, 0.1),
|
|
||||||
0 8px 10px -6px rgba(0, 0, 0, 0.1);
|
|
||||||
transition: transform 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
article img:first-of-type:hover {
|
|
||||||
transform: scale(1.01);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Article entrance animation */
|
|
||||||
.article-entering {
|
|
||||||
animation: article-fade-in 0.8s ease-out forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes article-fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Rest of the styles remain unchanged... */
|
|
||||||
</style>
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
---
|
|
||||||
import Navigation from '../components/Navigation.astro';
|
|
||||||
import Footer from '../components/Footer.astro';
|
|
||||||
import Background from '../components/Background.astro';
|
|
||||||
import '../styles/global.css';
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
title?: string | undefined;
|
|
||||||
description?: string | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const { title, description } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<meta name="generator" content={Astro.generator} />
|
|
||||||
<meta name="description" content={description} />
|
|
||||||
<title>{title}</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link
|
|
||||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
</head>
|
|
||||||
<body
|
|
||||||
class="flex min-h-screen flex-col bg-white text-zinc-900 dark:bg-zinc-900 dark:text-zinc-100"
|
|
||||||
>
|
|
||||||
<!-- Page transition overlay - for smooth transitions between pages -->
|
|
||||||
<div
|
|
||||||
id="page-transition"
|
|
||||||
class="pointer-events-none fixed inset-0 z-40 flex items-center justify-center bg-white opacity-0 transition-opacity duration-300 dark:bg-zinc-900"
|
|
||||||
>
|
|
||||||
<div class="transition-spinner"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Background component with dot pattern and ambient glow -->
|
|
||||||
<Background />
|
|
||||||
|
|
||||||
<div class="mx-auto w-full max-w-3xl flex-grow px-4 sm:px-6">
|
|
||||||
<Navigation />
|
|
||||||
<main class="py-12">
|
|
||||||
<slot />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
<Footer />
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// SPA transition system with history API
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
const mainContent = document.querySelector('main');
|
|
||||||
|
|
||||||
// Initialize content with entrance animation
|
|
||||||
if (mainContent) {
|
|
||||||
mainContent.classList.add('content-entering');
|
|
||||||
setTimeout(() => {
|
|
||||||
mainContent.classList.remove('content-entering');
|
|
||||||
}, 800);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to load content via fetch
|
|
||||||
async function loadContent(url) {
|
|
||||||
try {
|
|
||||||
// Show transition overlay
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0', 'pointer-events-none');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fade out current content
|
|
||||||
if (mainContent) {
|
|
||||||
mainContent.style.opacity = '0';
|
|
||||||
mainContent.style.transform = 'translateY(10px)';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the new page content
|
|
||||||
const response = await fetch(url);
|
|
||||||
if (!response.ok) throw new Error(`Failed to fetch ${url}`);
|
|
||||||
const html = await response.text();
|
|
||||||
|
|
||||||
// Create a temporary element to parse the HTML
|
|
||||||
const parser = new DOMParser();
|
|
||||||
const doc = parser.parseFromString(html, 'text/html');
|
|
||||||
|
|
||||||
// Extract the main content
|
|
||||||
const newContent = doc.querySelector('main');
|
|
||||||
if (!newContent) throw new Error('Could not find main content in the fetched page');
|
|
||||||
|
|
||||||
// Extract the title
|
|
||||||
const newTitle = doc.querySelector('title');
|
|
||||||
if (newTitle) {
|
|
||||||
document.title = newTitle.textContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract meta description
|
|
||||||
const newDescription = doc.querySelector('meta[name="description"]');
|
|
||||||
if (newDescription) {
|
|
||||||
const currentDescription = document.querySelector('meta[name="description"]');
|
|
||||||
if (currentDescription) {
|
|
||||||
currentDescription.setAttribute(
|
|
||||||
'content',
|
|
||||||
newDescription.getAttribute('content') || ''
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait a bit for transition effect
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
||||||
|
|
||||||
// Replace the content
|
|
||||||
if (mainContent && newContent) {
|
|
||||||
mainContent.innerHTML = newContent.innerHTML;
|
|
||||||
|
|
||||||
// Run scripts in the new content
|
|
||||||
Array.from(newContent.querySelectorAll('script')).forEach((oldScript) => {
|
|
||||||
const newScript = document.createElement('script');
|
|
||||||
Array.from(oldScript.attributes).forEach((attr) => {
|
|
||||||
newScript.setAttribute(attr.name, attr.value);
|
|
||||||
});
|
|
||||||
newScript.textContent = oldScript.textContent;
|
|
||||||
if (oldScript.parentNode) {
|
|
||||||
mainContent.appendChild(newScript);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fade in new content with animation
|
|
||||||
if (mainContent) {
|
|
||||||
mainContent.style.opacity = '0';
|
|
||||||
mainContent.style.transform = 'translateY(10px)';
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
mainContent.style.transition = 'opacity 0.5s ease, transform 0.5s ease';
|
|
||||||
mainContent.style.opacity = '1';
|
|
||||||
mainContent.style.transform = 'translateY(0)';
|
|
||||||
|
|
||||||
// Add entrance animation class
|
|
||||||
mainContent.classList.add('content-entering');
|
|
||||||
setTimeout(() => {
|
|
||||||
mainContent.classList.remove('content-entering');
|
|
||||||
}, 800);
|
|
||||||
}, 50);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hide transition overlay
|
|
||||||
if (pageTransition) {
|
|
||||||
setTimeout(() => {
|
|
||||||
pageTransition.classList.add('opacity-0', 'pointer-events-none');
|
|
||||||
pageTransition.classList.remove('opacity-100');
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatch custom event for content loaded
|
|
||||||
document.dispatchEvent(
|
|
||||||
new CustomEvent('spa-content-loaded', {
|
|
||||||
detail: { url },
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
// Scroll to top or to saved position
|
|
||||||
window.scrollTo(0, 0);
|
|
||||||
|
|
||||||
// Re-attach event listeners to new content
|
|
||||||
attachLinkListeners();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading content:', error);
|
|
||||||
|
|
||||||
// Fallback to traditional navigation on error
|
|
||||||
window.location.href = url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function to attach event listeners to all links
|
|
||||||
function attachLinkListeners() {
|
|
||||||
document.querySelectorAll('a').forEach((link) => {
|
|
||||||
// Skip links that are already handled, anchor links, external links, or have special attributes
|
|
||||||
if (
|
|
||||||
link.hasAttribute('data-spa-handled') ||
|
|
||||||
!link.href.startsWith(window.location.origin) ||
|
|
||||||
link.href.includes('#') ||
|
|
||||||
link.hasAttribute('target') ||
|
|
||||||
link.hasAttribute('download') ||
|
|
||||||
link.getAttribute('rel') === 'external' ||
|
|
||||||
link.getAttribute('rel') === 'nofollow'
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.href;
|
|
||||||
|
|
||||||
// Don't transition if clicking the current page
|
|
||||||
if (targetHref === window.location.href) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update browser history
|
|
||||||
window.history.pushState({ path: targetHref }, '', targetHref);
|
|
||||||
|
|
||||||
// Load the new content
|
|
||||||
loadContent(targetHref);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initial attachment of link listeners
|
|
||||||
attachLinkListeners();
|
|
||||||
|
|
||||||
// Handle browser back/forward navigation
|
|
||||||
window.addEventListener('popstate', (e) => {
|
|
||||||
if (e.state && e.state.path) {
|
|
||||||
loadContent(e.state.path);
|
|
||||||
} else {
|
|
||||||
loadContent(window.location.href);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check RSS feed availability
|
|
||||||
const checkAndGenerateRSS = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/rss.xml');
|
|
||||||
if (!response.ok) {
|
|
||||||
console.warn('RSS feed not found. Please generate it using an RSS plugin for Astro.');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn('Could not check RSS feed status.');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Check RSS feed availability
|
|
||||||
checkAndGenerateRSS();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Theme handling with transition effects
|
|
||||||
function setupThemeHandling() {
|
|
||||||
// Apply theme from localStorage or system preference
|
|
||||||
const theme = localStorage.getItem('theme');
|
|
||||||
if (
|
|
||||||
theme === 'dark' ||
|
|
||||||
(!theme && window.matchMedia('(prefers-color-scheme: dark)').matches)
|
|
||||||
) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Listen for theme changes
|
|
||||||
document.addEventListener('themeChanged', () => {
|
|
||||||
// Add transition class to body
|
|
||||||
document.body.classList.add('theme-transitioning');
|
|
||||||
|
|
||||||
// Remove class after transition completes
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.classList.remove('theme-transitioning');
|
|
||||||
}, 500);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize theme handling
|
|
||||||
document.addEventListener('DOMContentLoaded', setupThemeHandling);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Page transition effects */
|
|
||||||
#page-transition {
|
|
||||||
transition: opacity 0.3s ease;
|
|
||||||
backdrop-filter: blur(4px);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Transition spinner animation */
|
|
||||||
.transition-spinner {
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
border: 2px solid rgba(0, 0, 0, 0.1);
|
|
||||||
border-radius: 50%;
|
|
||||||
border-top-color: #3b82f6;
|
|
||||||
animation: spin 0.7s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
:global(.dark) .transition-spinner {
|
|
||||||
border-color: rgba(255, 255, 255, 0.1);
|
|
||||||
border-top-color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Content entrance animation */
|
|
||||||
main {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
transition:
|
|
||||||
opacity 0.5s ease,
|
|
||||||
transform 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
main.content-entering {
|
|
||||||
animation: content-fade-in 0.6s ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes content-fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme transition effect */
|
|
||||||
body.theme-transitioning * {
|
|
||||||
transition-duration: 0.3s !important;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
---
|
|
||||||
import { ViewTransitions } from 'astro:transitions';
|
|
||||||
import BaseLayout from './BaseLayout.astro';
|
|
||||||
|
|
||||||
const { title, description } = Astro.props;
|
|
||||||
---
|
|
||||||
|
|
||||||
<BaseLayout title={title} description={description}>
|
|
||||||
<ViewTransitions fallback="swap" />
|
|
||||||
|
|
||||||
<div transition:animate="slide">
|
|
||||||
<slot />
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Custom transition styles */
|
|
||||||
::view-transition-old(root) {
|
|
||||||
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-to-left;
|
|
||||||
}
|
|
||||||
|
|
||||||
::view-transition-new(root) {
|
|
||||||
animation: 0.5s cubic-bezier(0.76, 0, 0.24, 1) both slide-from-right;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -1,890 +0,0 @@
|
|||||||
/* Article entrance animation */
|
|
||||||
article {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
article.article-entering {
|
|
||||||
animation: article-fade-in 0.8s ease forwards;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes article-fade-in {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(20px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Hero image hover effect */
|
|
||||||
article img {
|
|
||||||
transition: transform 0.7s cubic-bezier(0.33, 1, 0.68, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Heading animations */
|
|
||||||
article .heading-animated {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(10px);
|
|
||||||
transition:
|
|
||||||
opacity 0.5s ease,
|
|
||||||
transform 0.5s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
article .heading-visible {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Navigation link hover effect */
|
|
||||||
.blog-nav-link {
|
|
||||||
transition:
|
|
||||||
transform 0.3s ease,
|
|
||||||
box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blog-nav-link.nav-link-hover {
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 10px 20px -10px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Ensure dark mode compatibility */
|
|
||||||
:global(.dark) .blog-nav-link.nav-link-hover {
|
|
||||||
box-shadow: 0 10px 20px -10px rgba(0, 0, 0, 0.3);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced Markdown Content Styling */
|
|
||||||
.markdown-content {
|
|
||||||
font-family:
|
|
||||||
system-ui,
|
|
||||||
-apple-system,
|
|
||||||
BlinkMacSystemFont,
|
|
||||||
'Segoe UI',
|
|
||||||
Roboto,
|
|
||||||
Oxygen,
|
|
||||||
Ubuntu,
|
|
||||||
Cantarell,
|
|
||||||
'Open Sans',
|
|
||||||
'Helvetica Neue',
|
|
||||||
sans-serif;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Headings */
|
|
||||||
.markdown-content h1 {
|
|
||||||
font-size: 2.5rem;
|
|
||||||
font-weight: 800;
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
line-height: 1.2;
|
|
||||||
color: #111827;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content h1 {
|
|
||||||
color: #f9fafb;
|
|
||||||
border-bottom-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h2 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
color: #111827;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
padding-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content h2 {
|
|
||||||
color: #f9fafb;
|
|
||||||
border-bottom-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h3 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 2rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content h3 {
|
|
||||||
color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h4 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content h4 {
|
|
||||||
color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h5 {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content h5 {
|
|
||||||
color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h6 {
|
|
||||||
font-size: 1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-top: 1.5rem;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content h6 {
|
|
||||||
color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Paragraphs */
|
|
||||||
.markdown-content p {
|
|
||||||
margin-top: 1.25rem;
|
|
||||||
margin-bottom: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Links */
|
|
||||||
.markdown-content a {
|
|
||||||
color: #2563eb;
|
|
||||||
text-decoration: none;
|
|
||||||
border-bottom: 1px solid transparent;
|
|
||||||
transition:
|
|
||||||
border-color 0.2s ease,
|
|
||||||
color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content a:hover {
|
|
||||||
color: #1d4ed8;
|
|
||||||
border-bottom-color: #1d4ed8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content a {
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content a:hover {
|
|
||||||
color: #60a5fa;
|
|
||||||
border-bottom-color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Bold text styling - enhanced */
|
|
||||||
.markdown-content strong {
|
|
||||||
font-weight: 700;
|
|
||||||
color: #0f766e;
|
|
||||||
background: linear-gradient(to bottom, transparent 60%, rgba(20, 184, 166, 0.2) 40%);
|
|
||||||
padding: 0 0.2em;
|
|
||||||
border-radius: 0.2em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content strong {
|
|
||||||
color: #14b8a6;
|
|
||||||
background: linear-gradient(to bottom, transparent 60%, rgba(20, 184, 166, 0.15) 40%);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Lists */
|
|
||||||
.markdown-content ul,
|
|
||||||
.markdown-content ol {
|
|
||||||
margin-top: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul {
|
|
||||||
list-style-type: disc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ol {
|
|
||||||
list-style-type: decimal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content li {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content li > ul,
|
|
||||||
.markdown-content li > ol {
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Blockquotes */
|
|
||||||
.markdown-content blockquote {
|
|
||||||
border-left: 4px solid #3b82f6;
|
|
||||||
padding: 1rem 1.5rem;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
border-radius: 0.375rem;
|
|
||||||
font-style: italic;
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content blockquote {
|
|
||||||
background-color: #1f2937;
|
|
||||||
border-left-color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content blockquote p {
|
|
||||||
margin-top: 0.5rem;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content blockquote .quote-icon {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
opacity: 0.1;
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content blockquote .quote-icon {
|
|
||||||
color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code blocks */
|
|
||||||
.markdown-content pre {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
padding: 1rem;
|
|
||||||
background-color: #1e293b !important;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow-x: auto;
|
|
||||||
position: relative;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode code blocks - ensure consistency */
|
|
||||||
.dark .markdown-content pre {
|
|
||||||
background-color: #1e293b !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre code {
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: #e5e7eb !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content pre code {
|
|
||||||
color: #e5e7eb !important;
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre.with-line-numbers {
|
|
||||||
padding-left: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre code {
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: #e5e7eb;
|
|
||||||
background-color: transparent;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .line-numbers {
|
|
||||||
position: absolute;
|
|
||||||
top: 1rem;
|
|
||||||
left: 0;
|
|
||||||
width: 2.5rem;
|
|
||||||
text-align: right;
|
|
||||||
padding-right: 0.75rem;
|
|
||||||
color: #6b7280;
|
|
||||||
user-select: none;
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 1.7;
|
|
||||||
border-right: 1px solid #4b5563;
|
|
||||||
height: calc(100% - 2rem);
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .line-numbers span {
|
|
||||||
display: block;
|
|
||||||
height: 1.7em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .copy-code-button {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.25rem;
|
|
||||||
right: 0.25rem;
|
|
||||||
background-color: #4b5563;
|
|
||||||
color: #e5e7eb;
|
|
||||||
border: none;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 0.15rem;
|
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0.6;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
z-index: 10;
|
|
||||||
width: 1.25rem;
|
|
||||||
height: 1.25rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .copy-code-button:hover {
|
|
||||||
opacity: 1;
|
|
||||||
background-color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .copy-code-button svg {
|
|
||||||
width: 0.875rem;
|
|
||||||
height: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Language label */
|
|
||||||
.markdown-content .language-label {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 2.5rem;
|
|
||||||
background-color: #4b5563;
|
|
||||||
color: #e5e7eb;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
padding: 0.125rem 0.375rem;
|
|
||||||
border-bottom-left-radius: 0.25rem;
|
|
||||||
border-bottom-right-radius: 0.25rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre:hover .language-label {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Language badge at bottom right */
|
|
||||||
.markdown-content .language-badge {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
padding: 0.1rem 0.3rem;
|
|
||||||
background-color: rgba(75, 85, 99, 0.7);
|
|
||||||
color: #e5e7eb;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
opacity: 0.8;
|
|
||||||
transition: opacity 0.2s ease;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre:hover .language-badge {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Inline code */
|
|
||||||
.markdown-content code:not(pre code) {
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
font-size: 0.875em;
|
|
||||||
color: #ef4444;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
padding: 0.2em 0.4em;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content code:not(pre code) {
|
|
||||||
color: #f87171;
|
|
||||||
background-color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tables */
|
|
||||||
.markdown-content .table-container {
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content table {
|
|
||||||
width: 100%;
|
|
||||||
border-collapse: collapse;
|
|
||||||
text-align: left;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content table th {
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
color: #111827;
|
|
||||||
font-weight: 600;
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-bottom: 2px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content table th {
|
|
||||||
background-color: #1f2937;
|
|
||||||
color: #f9fafb;
|
|
||||||
border-bottom-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content table td {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content table td {
|
|
||||||
border-bottom-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content table tr.even-row {
|
|
||||||
background-color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content table tr.even-row {
|
|
||||||
background-color: #111827;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content table tr.odd-row {
|
|
||||||
background-color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content table tr.odd-row {
|
|
||||||
background-color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content table tr:last-child td {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Images */
|
|
||||||
.markdown-content img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Horizontal rule */
|
|
||||||
.markdown-content hr {
|
|
||||||
border: 0;
|
|
||||||
height: 1px;
|
|
||||||
background-color: #e5e7eb;
|
|
||||||
margin: 2rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content hr {
|
|
||||||
background-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Task lists */
|
|
||||||
.markdown-content ul li[data-task-list-item] {
|
|
||||||
list-style-type: none;
|
|
||||||
position: relative;
|
|
||||||
padding-left: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul li[data-task-list-item]::before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0.25rem;
|
|
||||||
width: 1rem;
|
|
||||||
height: 1rem;
|
|
||||||
border: 1px solid #9ca3af;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content ul li[data-task-list-item][data-checked]::before {
|
|
||||||
background-color: #3b82f6;
|
|
||||||
border-color: #3b82f6;
|
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 24 24' stroke='%23ffffff'%3E%3Cpath stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 13l4 4L19 7'%3E%3C/path%3E%3C/svg%3E");
|
|
||||||
background-size: 0.75rem;
|
|
||||||
background-position: center;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Footnotes */
|
|
||||||
.markdown-content .footnotes {
|
|
||||||
margin-top: 2rem;
|
|
||||||
padding-top: 1rem;
|
|
||||||
border-top: 1px solid #e5e7eb;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content .footnotes {
|
|
||||||
border-top-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .footnotes ol {
|
|
||||||
padding-left: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .footnotes li {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .footnote-backref {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
vertical-align: super;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Definition lists */
|
|
||||||
.markdown-content dl {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content dt {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #111827;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content dt {
|
|
||||||
color: #f9fafb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content dd {
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Callouts and admonitions */
|
|
||||||
.markdown-content .callout {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
padding: 1rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
border-left: 4px solid;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content .callout {
|
|
||||||
background-color: #1f2937;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .callout.info {
|
|
||||||
border-left-color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .callout.warning {
|
|
||||||
border-left-color: #f59e0b;
|
|
||||||
background-color: rgba(245, 158, 11, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content .callout.warning {
|
|
||||||
background-color: rgba(245, 158, 11, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .callout.danger {
|
|
||||||
border-left-color: #ef4444;
|
|
||||||
background-color: rgba(239, 68, 68, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content .callout.danger {
|
|
||||||
background-color: rgba(239, 68, 68, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .callout.tip {
|
|
||||||
border-left-color: #10b981;
|
|
||||||
background-color: rgba(16, 185, 129, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content .callout.tip {
|
|
||||||
background-color: rgba(16, 185, 129, 0.05);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Code syntax highlighting - Light theme */
|
|
||||||
.markdown-content .token.comment,
|
|
||||||
.markdown-content .token.prolog,
|
|
||||||
.markdown-content .token.doctype,
|
|
||||||
.markdown-content .token.cdata {
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.punctuation {
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.namespace {
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.property,
|
|
||||||
.markdown-content .token.tag,
|
|
||||||
.markdown-content .token.boolean,
|
|
||||||
.markdown-content .token.number,
|
|
||||||
.markdown-content .token.constant,
|
|
||||||
.markdown-content .token.symbol {
|
|
||||||
color: #ef4444;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.selector,
|
|
||||||
.markdown-content .token.attr-name,
|
|
||||||
.markdown-content .token.string,
|
|
||||||
.markdown-content .token.char,
|
|
||||||
.markdown-content .token.builtin {
|
|
||||||
color: #10b981;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.operator,
|
|
||||||
.markdown-content .token.entity,
|
|
||||||
.markdown-content .token.url,
|
|
||||||
.markdown-content .language-css .token.string,
|
|
||||||
.markdown-content .style .token.string {
|
|
||||||
color: #9333ea;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.atrule,
|
|
||||||
.markdown-content .token.attr-value,
|
|
||||||
.markdown-content .token.keyword {
|
|
||||||
color: #3b82f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.function,
|
|
||||||
.markdown-content .token.class-name {
|
|
||||||
color: #f59e0b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.regex,
|
|
||||||
.markdown-content .token.important,
|
|
||||||
.markdown-content .token.variable {
|
|
||||||
color: #ec4899;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.important,
|
|
||||||
.markdown-content .token.bold {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.italic {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .token.entity {
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive adjustments */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.markdown-content h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h3 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre {
|
|
||||||
padding: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre.with-line-numbers {
|
|
||||||
padding-left: 3rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content .line-numbers {
|
|
||||||
width: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content blockquote {
|
|
||||||
padding: 0.75rem 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Print styles */
|
|
||||||
@media print {
|
|
||||||
.markdown-content {
|
|
||||||
font-size: 12pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content pre,
|
|
||||||
.markdown-content code {
|
|
||||||
font-size: 10pt;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content a {
|
|
||||||
color: #000 !important;
|
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content blockquote {
|
|
||||||
border-left: 2pt solid #000;
|
|
||||||
padding: 0.5cm 1cm;
|
|
||||||
background: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content img {
|
|
||||||
max-width: 100% !important;
|
|
||||||
page-break-inside: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content h2,
|
|
||||||
.markdown-content h3,
|
|
||||||
.markdown-content h4 {
|
|
||||||
page-break-after: avoid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content p,
|
|
||||||
.markdown-content h2,
|
|
||||||
.markdown-content h3 {
|
|
||||||
orphans: 3;
|
|
||||||
widows: 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Additional elements */
|
|
||||||
.markdown-content details {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
border: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content details {
|
|
||||||
background-color: #1f2937;
|
|
||||||
border-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content details summary {
|
|
||||||
font-weight: 600;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content details[open] summary {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
border-bottom: 1px solid #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content details[open] summary {
|
|
||||||
border-bottom-color: #374151;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Keyboard shortcuts */
|
|
||||||
.markdown-content kbd {
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
font-size: 0.8em;
|
|
||||||
padding: 0.2em 0.4em;
|
|
||||||
margin: 0 0.1em;
|
|
||||||
background-color: #f3f4f6;
|
|
||||||
border: 1px solid #d1d5db;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
box-shadow: 0 1px 0 #d1d5db;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content kbd {
|
|
||||||
background-color: #1f2937;
|
|
||||||
border-color: #4b5563;
|
|
||||||
box-shadow: 0 1px 0 #4b5563;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Abbreviations */
|
|
||||||
.markdown-content abbr {
|
|
||||||
cursor: help;
|
|
||||||
text-decoration: underline dotted;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Highlight text */
|
|
||||||
.markdown-content mark {
|
|
||||||
background-color: #fef3c7;
|
|
||||||
color: #92400e;
|
|
||||||
padding: 0.1em 0.2em;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .markdown-content mark {
|
|
||||||
background-color: rgba(254, 243, 199, 0.2);
|
|
||||||
color: #fbbf24;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Subscript and superscript */
|
|
||||||
.markdown-content sub,
|
|
||||||
.markdown-content sup {
|
|
||||||
font-size: 0.75em;
|
|
||||||
line-height: 0;
|
|
||||||
position: relative;
|
|
||||||
vertical-align: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content sup {
|
|
||||||
top: -0.5em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-content sub {
|
|
||||||
bottom: -0.25em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Diagrams and charts */
|
|
||||||
.markdown-content .mermaid {
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Math equations */
|
|
||||||
.markdown-content .math {
|
|
||||||
overflow-x: auto;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Embedded content */
|
|
||||||
.markdown-content iframe {
|
|
||||||
max-width: 100%;
|
|
||||||
margin: 1.5rem 0;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
box-shadow:
|
|
||||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
|
||||||
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
|
||||||
}
|
|
||||||
31
src/lib/directus.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { createDirectus, rest } from '@directus/sdk';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Global,
|
||||||
|
Weather,
|
||||||
|
Post,
|
||||||
|
Application,
|
||||||
|
Experience,
|
||||||
|
Education,
|
||||||
|
Certificate,
|
||||||
|
Project,
|
||||||
|
Skill,
|
||||||
|
} from '@lib/directusTypes';
|
||||||
|
|
||||||
|
import { getDirectusURL } from '@/support/url';
|
||||||
|
|
||||||
|
type Schema = {
|
||||||
|
site_global: Global;
|
||||||
|
site_weather: Weather;
|
||||||
|
posts: Post[];
|
||||||
|
site_applications: Application;
|
||||||
|
site_experience: Experience;
|
||||||
|
site_education: Education;
|
||||||
|
site_certificate: Certificate;
|
||||||
|
site_projects: Project;
|
||||||
|
site_skills: Skill;
|
||||||
|
};
|
||||||
|
|
||||||
|
const directus = createDirectus<Schema>(getDirectusURL()).with(rest());
|
||||||
|
|
||||||
|
export default directus;
|
||||||
116
src/lib/directusTypes.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
export type Global = {
|
||||||
|
name: string;
|
||||||
|
about: string;
|
||||||
|
about_description: string;
|
||||||
|
about_blog: string;
|
||||||
|
about_applications: string;
|
||||||
|
about_categories: string;
|
||||||
|
initials: string;
|
||||||
|
email: string;
|
||||||
|
site_url: string;
|
||||||
|
rybbit_site_id: string;
|
||||||
|
logo: string;
|
||||||
|
portrait: string;
|
||||||
|
portrait_alt: string;
|
||||||
|
home_image: string;
|
||||||
|
home_image_alt: string;
|
||||||
|
blog_image: string;
|
||||||
|
blog_image_alt: string;
|
||||||
|
categories_image: string;
|
||||||
|
categories_image_alt: string;
|
||||||
|
applications_image: string;
|
||||||
|
applications_image_alt: string;
|
||||||
|
footer_image: string;
|
||||||
|
footer_image_alt: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Weather = {
|
||||||
|
id: string;
|
||||||
|
location: string;
|
||||||
|
latitude: string;
|
||||||
|
longitude: string;
|
||||||
|
timezone: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Post = {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
tags: string[];
|
||||||
|
category: string;
|
||||||
|
selected: boolean;
|
||||||
|
published: boolean;
|
||||||
|
content: string;
|
||||||
|
image: string;
|
||||||
|
image_alt: string;
|
||||||
|
image_second: string;
|
||||||
|
image_second_alt: string;
|
||||||
|
published_date: Date;
|
||||||
|
updated_date: Date;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Application = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isActive: boolean;
|
||||||
|
description: string;
|
||||||
|
highlights: string[];
|
||||||
|
url: string;
|
||||||
|
logoUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Experience = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
location: string;
|
||||||
|
location_type: string;
|
||||||
|
url: string;
|
||||||
|
startDate: string;
|
||||||
|
endDate: string;
|
||||||
|
position: string;
|
||||||
|
summary: string;
|
||||||
|
responsibilities: string[];
|
||||||
|
highlights: string[];
|
||||||
|
achievements: string[];
|
||||||
|
skills: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Education = {
|
||||||
|
id: string;
|
||||||
|
institution: string;
|
||||||
|
url: string;
|
||||||
|
area: string;
|
||||||
|
studyType: string;
|
||||||
|
graduationDate: string;
|
||||||
|
logo: string;
|
||||||
|
logoDark: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Certificate = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
issuer: string;
|
||||||
|
issuerDate: string;
|
||||||
|
url: string;
|
||||||
|
logo: string;
|
||||||
|
logoDark: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Project = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
isActive: boolean;
|
||||||
|
description: string;
|
||||||
|
highlights: string[];
|
||||||
|
url: string;
|
||||||
|
source: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Skill = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: string;
|
||||||
|
level: string;
|
||||||
|
date_created: string;
|
||||||
|
};
|
||||||
@@ -1,115 +1,74 @@
|
|||||||
---
|
---
|
||||||
import Layout from '../layouts/Layout.astro';
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
|
import GoBackButton from '@/components/buttons/GoBackButton.astro';
|
||||||
|
import GoHomeButton from '@/components/buttons/GoHomeButton.astro';
|
||||||
|
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="404 - Page Not Found">
|
<BaseLayout
|
||||||
<div
|
title="Page Not Found"
|
||||||
class="relative flex min-h-[80vh] flex-col items-center justify-center overflow-hidden px-4 py-20 text-center"
|
description="Page Not Found"
|
||||||
>
|
structuredData={{
|
||||||
<!-- Animated background elements -->
|
'@context': 'https://schema.org',
|
||||||
<div class="absolute inset-0 overflow-hidden">
|
'@type': 'WebPage',
|
||||||
<div
|
inLanguage: 'en-US',
|
||||||
class="animate-blob absolute -left-20 -top-20 h-64 w-64 rounded-full bg-zinc-100 opacity-50 blur-3xl dark:bg-zinc-800/50"
|
'@id': Astro.url.href,
|
||||||
>
|
url: Astro.url.href,
|
||||||
</div>
|
name: `Page Not Found | ${global.name}`,
|
||||||
<div
|
description: 'Page Not Found',
|
||||||
class="animate-blob animation-delay-2000 absolute right-1/4 top-1/2 h-96 w-96 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30"
|
isPartOf: {
|
||||||
>
|
'@type': 'WebSite',
|
||||||
</div>
|
url: global.site_url,
|
||||||
<div
|
name: global.name,
|
||||||
class="animate-blob animation-delay-4000 absolute bottom-20 left-1/3 h-72 w-72 rounded-full bg-zinc-100 opacity-40 blur-3xl dark:bg-zinc-800/40"
|
description: global.about,
|
||||||
>
|
},
|
||||||
</div>
|
}}
|
||||||
</div>
|
>
|
||||||
|
|
||||||
<!-- Main content with animation -->
|
<section class="grid place-content-center mt-20">
|
||||||
<div class="relative z-10 mx-auto max-w-xl">
|
<div class="max-w-7xl px-4 lg:px-6 py-8 lg:py-16 mx-auto">
|
||||||
<div class="glitch-wrapper">
|
<div class="text-center max-w-screen-sm mx-auto">
|
||||||
<h1
|
<div class="glitch-wrapper smooth-reveal">
|
||||||
class="glitch text-9xl font-bold leading-none text-zinc-900 dark:text-zinc-100 sm:text-[12rem]"
|
<h1
|
||||||
data-text="404"
|
class="glitch text-header text-9xl font-bold leading-none sm:text-[12rem]"
|
||||||
>
|
data-text="404"
|
||||||
404
|
>
|
||||||
|
Not Found
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<h1 class="smooth-reveal text-yellow-500 dark:text-yellow-400 text-4xl md:text-5xl font-bold leading-tight tracking-tight text-balance mt-30">
|
||||||
|
Page Not Found:
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
<h1 class="smooth-reveal card-text-header mt-8 mb-30">
|
||||||
|
{Astro.url.pathname.replace('/', '')}
|
||||||
<h2 class="mt-6 text-2xl font-bold text-zinc-800 dark:text-zinc-200 sm:text-3xl">
|
</h1>
|
||||||
Page Not Found
|
<div class="smooth-reveal card-base max-w-md p-6 mx-auto mt-16">
|
||||||
</h2>
|
<h3 class="card-text-title text-sm tracking-wider uppercase">
|
||||||
|
Did you know?
|
||||||
<p class="mx-auto mt-6 max-w-md text-lg text-zinc-600 dark:text-zinc-400">
|
</h3>
|
||||||
The page you're looking for does not exist.
|
<p
|
||||||
</p>
|
id="fun-fact"
|
||||||
|
class="text-secondary text-sm mt-4 mb-2"
|
||||||
<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
|
The 404 error code originated when CERN's web server displayed room 404 (their server
|
||||||
stroke-linecap="round"
|
room) as the error message when a file wasn't found.
|
||||||
stroke-linejoin="round"
|
</p>
|
||||||
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"
|
</div>
|
||||||
></path>
|
<div class="smooth-reveal flex flex-col sm:flex-row items-center justify-center gap-4 mt-10">
|
||||||
</svg>
|
<GoBackButton/>
|
||||||
<span class="relative z-10 font-medium">Return Home</span>
|
<GoHomeButton url={global.site_url} />
|
||||||
</a>
|
</div>
|
||||||
|
|
||||||
<button
|
|
||||||
id="back-button"
|
|
||||||
class="group inline-flex items-center gap-2 rounded-lg border border-zinc-300 px-6 py-3 text-zinc-700 shadow-sm transition-all duration-300 hover:bg-zinc-100 hover:shadow-md dark:border-zinc-700 dark:text-zinc-300 dark:hover:bg-zinc-800"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke="currentColor"
|
|
||||||
class="h-5 w-5 transition-transform duration-300 group-hover:-translate-x-1"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
d="M10.5 19.5L3 12m0 0l7.5-7.5M3 12h18"></path>
|
|
||||||
</svg>
|
|
||||||
<span class="font-medium">Go Back</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Random fun fact -->
|
|
||||||
<div
|
|
||||||
class="mx-auto mt-16 max-w-md rounded-xl border border-zinc-100 bg-zinc-50 p-6 shadow-sm backdrop-blur-sm dark:border-zinc-700/50 dark:bg-zinc-800/50"
|
|
||||||
>
|
|
||||||
<h3 class="text-sm font-medium uppercase tracking-wider text-zinc-500 dark:text-zinc-400">
|
|
||||||
Did you know?
|
|
||||||
</h3>
|
|
||||||
<p class="mt-2 text-sm text-zinc-700 dark:text-zinc-300" id="fun-fact">
|
|
||||||
The 404 error code originated when CERN's web server displayed room 404 (their server
|
|
||||||
room) as the error message when a file wasn't found.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</Layout>
|
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Go back functionality
|
|
||||||
document.getElementById('back-button')?.addEventListener('click', () => {
|
|
||||||
window.history.back();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Array of fun 404 facts
|
|
||||||
const funFacts = [
|
const funFacts = [
|
||||||
"The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found.",
|
"The 404 error code originated when CERN's web server displayed room 404 (their server room) as the error message when a file wasn't found.",
|
||||||
"In internet slang, '404' has become shorthand for something that's missing or someone who's clueless.",
|
"In internet slang, '404' has become shorthand for something that's missing or someone who's clueless.",
|
||||||
@@ -128,96 +87,26 @@ import Layout from '../layouts/Layout.astro';
|
|||||||
funFactElement.textContent = randomFact;
|
funFactElement.textContent = randomFact;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle SPA transitions for 404 page
|
// Add smooth reveal animations for content after loading
|
||||||
function setupSPATransitions() {
|
document.addEventListener('astro:page-load', () => {
|
||||||
// Handle all internal links for SPA transitions
|
const animateContent = () => {
|
||||||
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
// Animate group 1
|
||||||
// Skip links that are anchor links, external links, or already processed
|
const smoothReveal = document.querySelectorAll('.smooth-reveal');
|
||||||
if (
|
smoothReveal.forEach((el, index) => {
|
||||||
link.getAttribute('href').includes('#') ||
|
setTimeout(
|
||||||
link.getAttribute('target') === '_blank' ||
|
() => {
|
||||||
link.hasAttribute('data-spa-handled')
|
el.classList.add('animate-reveal');
|
||||||
) {
|
},
|
||||||
return;
|
100 + index * 150
|
||||||
}
|
);
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
|
|
||||||
// Re-initialize back button after SPA navigation
|
animateContent();
|
||||||
const backButton = document.getElementById('back-button');
|
});
|
||||||
if (backButton) {
|
|
||||||
backButton.addEventListener('click', () => {
|
|
||||||
window.history.back();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* Animation for floating blobs */
|
|
||||||
@keyframes blob {
|
|
||||||
0% {
|
|
||||||
transform: translate(0px, 0px) scale(1);
|
|
||||||
}
|
|
||||||
33% {
|
|
||||||
transform: translate(30px, -50px) scale(1.1);
|
|
||||||
}
|
|
||||||
66% {
|
|
||||||
transform: translate(-20px, 20px) scale(0.9);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(0px, 0px) scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.animate-blob {
|
|
||||||
animation: blob 7s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-2000 {
|
|
||||||
animation-delay: 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-4000 {
|
|
||||||
animation-delay: 4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Glitch effect for 404 text */
|
/* Glitch effect for 404 text */
|
||||||
.glitch-wrapper {
|
.glitch-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -1,583 +1,84 @@
|
|||||||
---
|
---
|
||||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
import { readSingleton } from '@directus/sdk';
|
||||||
import { FaJs, FaReact, FaNodeJs, FaPython } from 'react-icons/fa';
|
|
||||||
import { SiTypescript, SiAstro } from 'react-icons/si';
|
|
||||||
|
|
||||||
import directus from '../../lib/directus';
|
import HeroSection from '@components/sections/HeroSection.astro';
|
||||||
import { readSingleton, readItems } from '@directus/sdk';
|
import ExperienceSection from '@components/sections/ExperienceSection.astro';
|
||||||
|
import EducationSection from '@components/sections/EducationSection.astro';
|
||||||
|
import ProjectSection from '@components/sections/ProjectSection.astro';
|
||||||
|
import SkillsSliderSection from '@components/sections/SkillsSliderSection.astro';
|
||||||
|
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
const global = await directus.request(readSingleton('global'));
|
import portraitImg from '@images/portrait.avif';
|
||||||
const about = await directus.request(readSingleton('about'));
|
|
||||||
|
|
||||||
const skills = await directus.request(
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
readItems('skills', {
|
|
||||||
fields: ['*'],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout title="About Me" description={global.description}>
|
<BaseLayout
|
||||||
<div class="theme-transition-all mx-auto max-w-6xl px-4 py-8 sm:px-6 sm:py-12 md:py-16">
|
title="About Me"
|
||||||
<!-- Hero Section -->
|
description="About me."
|
||||||
<div class="relative mb-12 sm:mb-16 md:mb-20">
|
structuredData={{
|
||||||
<!-- Decorative elements -->
|
'@context': 'https://schema.org',
|
||||||
<div
|
'@type': 'WebPage',
|
||||||
class="animate-blob theme-transition-bg absolute -left-10 -top-10 h-36 w-36 rounded-full bg-zinc-100 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-left-20 sm:-top-20 sm:h-48 sm:w-48 md:h-72 md:w-72"
|
inLanguage: 'en-US',
|
||||||
>
|
'@id': Astro.url.href,
|
||||||
</div>
|
url: Astro.url.href,
|
||||||
<div
|
name: `About | ${global.name}`,
|
||||||
class="animate-blob animation-delay-2000 theme-transition-bg absolute -bottom-10 -right-10 h-36 w-36 rounded-full bg-zinc-200 opacity-30 blur-3xl dark:bg-zinc-800/30 sm:-bottom-20 sm:-right-20 sm:h-48 sm:w-48 md:h-72 md:w-72"
|
description: 'About me.',
|
||||||
>
|
isPartOf: {
|
||||||
</div>
|
'@type': 'WebSite',
|
||||||
|
url: global.site_url,
|
||||||
|
name: global.name,
|
||||||
|
description: global.about,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
<div class="relative grid grid-cols-1 items-center gap-8 md:grid-cols-2 md:gap-12">
|
<HeroSection
|
||||||
<div class="order-2 text-center md:order-1 md:text-left">
|
title="About Me"
|
||||||
<h1
|
subTitle={global.about}
|
||||||
class="theme-transition-color mb-4 text-3xl font-bold tracking-tight text-zinc-900 dark:text-zinc-100 sm:mb-6 sm:text-4xl md:text-5xl"
|
src={portraitImg}
|
||||||
>
|
alt={global.portrait_alt}
|
||||||
Hello, I'm <span
|
rounded={true}
|
||||||
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
|
<section class="max-w-7xl px-4 sm:px-6 lg:px-8 py-10 lg:py-14 mx-auto">
|
||||||
class="theme-transition-color mb-6 text-lg leading-relaxed text-zinc-600 dark:text-zinc-400 sm:mb-8 sm:text-xl"
|
<div class="flex flex-col gap-y-12 md:gap-y-20">
|
||||||
>
|
<ExperienceSection className="smooth-reveal" />
|
||||||
{about.background}
|
<EducationSection className="smooth-reveal" />
|
||||||
</p>
|
<ProjectSection className="smooth-reveal" />
|
||||||
|
<SkillsSliderSection className="smooth-reveal" />
|
||||||
<div
|
|
||||||
class="social-links-container theme-transition-children flex flex-wrap justify-center gap-4 md:justify-start"
|
|
||||||
>
|
|
||||||
<!-- Social links remain the same -->
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative order-1 md:order-2">
|
|
||||||
<div
|
|
||||||
class="theme-transition-all mx-auto aspect-square w-full max-w-[280px] overflow-hidden rounded-3xl border-4 border-white shadow-xl dark:border-zinc-800 sm:max-w-[320px] sm:border-8 sm:shadow-2xl md:max-w-md"
|
|
||||||
>
|
|
||||||
<img
|
|
||||||
src=`${process.env.DIRECTUS_URL ?? "https://directus.alexlebens.dev"}/assets/${global.portrait}`
|
|
||||||
alt={global.portrait_alt}
|
|
||||||
class="h-full w-full object-cover"
|
|
||||||
loading="eager"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Decorative elements -->
|
|
||||||
<div
|
|
||||||
class="theme-transition-all absolute -bottom-4 -right-4 flex h-16 w-16 items-center justify-center rounded-full border-2 border-white bg-zinc-100 shadow-lg dark:border-zinc-900 dark:bg-zinc-800 sm:-bottom-6 sm:-right-6 sm:h-20 sm:w-20 sm:border-4 md:h-24 md:w-24"
|
|
||||||
>
|
|
||||||
<span class="text-2xl sm:text-3xl">👋</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- About Section -->
|
|
||||||
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
|
|
||||||
<div class="mx-auto max-w-3xl">
|
|
||||||
<h2
|
|
||||||
class="theme-transition-color mb-6 flex items-center justify-center text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-8 sm:text-3xl md:justify-start"
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
class="theme-transition-bg mr-4 hidden h-1 w-8 bg-zinc-300 dark:bg-zinc-700 sm:inline-block sm:w-12"
|
|
||||||
></span>
|
|
||||||
About Me
|
|
||||||
<span
|
|
||||||
class="theme-transition-bg ml-4 hidden h-1 w-8 bg-zinc-300 dark:bg-zinc-700 sm:inline-block sm:w-12"
|
|
||||||
></span>
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="theme-transition-all prose prose-zinc max-w-none dark:prose-invert">
|
|
||||||
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
|
||||||
{about.experience}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
|
||||||
{about.education}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="theme-transition-color mb-4 text-base leading-relaxed sm:mb-6 sm:text-lg">
|
|
||||||
{about.certifications}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Skills Section -->
|
|
||||||
<div class="theme-transition-all mb-16 sm:mb-20 md:mb-24">
|
|
||||||
<h2
|
|
||||||
class="theme-transition-color mb-8 text-center text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-12 sm:text-3xl"
|
|
||||||
>
|
|
||||||
Tech Stack
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="tech-stack-slider relative overflow-hidden py-4 sm:py-8">
|
|
||||||
<!-- Main slider container -->
|
|
||||||
<div class="slider-track animate-slide flex">
|
|
||||||
{
|
|
||||||
skills.map((skill, index) => (
|
|
||||||
<div
|
|
||||||
key={`${skill.title}-${index}`}
|
|
||||||
class="skill-card theme-transition-element mx-2 min-w-[220px] transform rounded-xl border border-zinc-200 bg-white transition-all duration-300 hover:-translate-y-2 hover:scale-105 hover:border-zinc-300 hover:shadow-xl dark:border-zinc-700 dark:bg-zinc-800/50 dark:hover:border-zinc-600 sm:mx-4 sm:min-w-[280px]"
|
|
||||||
>
|
|
||||||
<div class="p-4 sm:p-6">
|
|
||||||
<div class="mb-4 flex items-center justify-between sm:mb-6">
|
|
||||||
<div class="flex items-center gap-2 sm:gap-4">
|
|
||||||
<div class="theme-transition-bg theme-transition-color flex h-8 w-8 transform items-center justify-center rounded-lg bg-zinc-100 text-zinc-800 transition-transform group-hover:rotate-12 dark:bg-zinc-800 dark:text-zinc-200 sm:h-12 sm:w-12">
|
|
||||||
<skill.icon
|
|
||||||
size={20}
|
|
||||||
className="sm:text-2xl transform transition-all hover:scale-125"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<h3 class="theme-transition-color text-base font-semibold text-zinc-900 dark:text-zinc-100 sm:text-xl">
|
|
||||||
{skill.title}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
<span class="theme-transition-all rounded-full bg-zinc-100 px-2 py-0.5 font-mono text-xs text-zinc-600 dark:bg-zinc-800 dark:text-zinc-400 sm:px-2.5 sm:py-1 sm:text-sm">
|
|
||||||
{skill.level}%
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="theme-transition-bg relative h-1.5 w-full overflow-hidden rounded-full bg-zinc-100 dark:bg-zinc-700 sm:h-2">
|
|
||||||
<div
|
|
||||||
class="progress-bar-animate theme-transition-bg absolute left-0 top-0 h-full rounded-full bg-gradient-to-r from-zinc-700 via-zinc-600 to-zinc-800 transition-all duration-1000 dark:from-zinc-300 dark:via-zinc-400 dark:to-zinc-200"
|
|
||||||
style={`width: ${skill.level}%`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="theme-transition-color mt-1 flex justify-between font-mono text-[10px] text-zinc-400 dark:text-zinc-500 sm:mt-2 sm:text-xs">
|
|
||||||
<span>Beginner</span>
|
|
||||||
<span>Advanced</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Gradient overlays for smooth fade effect -->
|
|
||||||
<div
|
|
||||||
class="theme-transition-bg absolute bottom-0 left-0 top-0 z-10 w-12 bg-gradient-to-r from-white to-transparent dark:from-zinc-900 sm:w-24"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="theme-transition-bg absolute bottom-0 right-0 top-0 z-10 w-12 bg-gradient-to-l from-white to-transparent dark:from-zinc-900 sm:w-24"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contact Section -->
|
|
||||||
<div class="theme-transition-all mx-auto max-w-3xl text-center">
|
|
||||||
<h2
|
|
||||||
class="theme-transition-color mb-4 text-2xl font-bold text-zinc-900 dark:text-zinc-100 sm:mb-6 sm:text-3xl"
|
|
||||||
>
|
|
||||||
Get in Touch
|
|
||||||
</h2>
|
|
||||||
<p
|
|
||||||
class="theme-transition-color mb-6 text-base text-zinc-600 dark:text-zinc-400 sm:mb-8 sm:text-lg"
|
|
||||||
>
|
|
||||||
I'm always open to new opportunities and collaborations. If you'd like to work together or
|
|
||||||
just say hello, feel free to reach out.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<a
|
|
||||||
href=`mailto:${global.email}`
|
|
||||||
class="theme-transition-all inline-flex items-center justify-center rounded-lg bg-zinc-900 px-6 py-3 text-base font-medium text-zinc-100 transition-colors hover:bg-zinc-700 dark:bg-zinc-100 dark:text-zinc-900 dark:hover:bg-zinc-300 sm:px-8 sm:py-4 sm:text-lg"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
class="mr-2 h-4 w-4 sm:h-5 sm:w-5"
|
|
||||||
fill="none"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
stroke-width="2"
|
|
||||||
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
|
|
||||||
></path>
|
|
||||||
</svg>
|
|
||||||
Say Hello
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BaseLayout>
|
</BaseLayout>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Blob animation */
|
|
||||||
.animate-blob {
|
|
||||||
animation: blob-bounce 8s infinite ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-delay-2000 {
|
|
||||||
animation-delay: 2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes blob-bounce {
|
|
||||||
0%,
|
|
||||||
100% {
|
|
||||||
transform: translate(0, 0) scale(1);
|
|
||||||
}
|
|
||||||
25% {
|
|
||||||
transform: translate(5%, 5%) scale(1.05);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: translate(0, 10%) scale(1);
|
|
||||||
}
|
|
||||||
75% {
|
|
||||||
transform: translate(-5%, 5%) scale(0.95);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tech Stack Slider */
|
|
||||||
.slider-track {
|
|
||||||
width: fit-content;
|
|
||||||
animation: scroll 40s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scroll {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(calc(-220px * 6 - 16px * 6)); /* Card width + margin for mobile */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.slider-track {
|
|
||||||
animation: scroll 60s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes scroll {
|
|
||||||
0% {
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translateX(calc(-280px * 6 - 32px * 6)); /* Card width + margin for desktop */
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tech-stack-slider:hover .slider-track {
|
|
||||||
animation-play-state: paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card {
|
|
||||||
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:hover {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Reduce animation complexity on mobile for better performance */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.skill-card {
|
|
||||||
transition:
|
|
||||||
transform 0.3s ease,
|
|
||||||
box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:hover {
|
|
||||||
transform: translateY(-5px) !important;
|
|
||||||
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:before {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: -10%;
|
|
||||||
left: -10%;
|
|
||||||
width: 120%;
|
|
||||||
height: 120%;
|
|
||||||
background: radial-gradient(
|
|
||||||
circle at center,
|
|
||||||
rgba(255, 255, 255, 0.1) 0%,
|
|
||||||
rgba(255, 255, 255, 0) 70%
|
|
||||||
);
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.5s ease;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.skill-card:hover:before {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar-animate {
|
|
||||||
position: relative;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-bar-animate:after {
|
|
||||||
content: '';
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: -100%;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
|
||||||
animation: progress-shine 2s infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes progress-shine {
|
|
||||||
0% {
|
|
||||||
left: -100%;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
left: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Improved touch targets for mobile */
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
a,
|
|
||||||
button {
|
|
||||||
min-height: 44px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-link {
|
|
||||||
min-width: 44px;
|
|
||||||
min-height: 44px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Theme transition effect */
|
|
||||||
:global(.theme-switching) .theme-transition-element {
|
|
||||||
animation: fadeIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth card transition during theme switch */
|
|
||||||
.skill-card.theme-transition-element {
|
|
||||||
transition:
|
|
||||||
background-color var(--theme-transition),
|
|
||||||
border-color var(--theme-transition),
|
|
||||||
color var(--theme-transition),
|
|
||||||
box-shadow var(--theme-transition),
|
|
||||||
transform 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Wait for the DOM to be fully loaded
|
// Add smooth reveal animations for content after loading
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('astro:page-load', () => {
|
||||||
const sliderTrack = document.querySelector('.slider-track');
|
const animateContent = () => {
|
||||||
|
const smoothReveal = document.querySelectorAll('.smooth-reveal');
|
||||||
// Create seamless infinite scrolling effect
|
smoothReveal.forEach((el, index) => {
|
||||||
function setupInfiniteScroll() {
|
|
||||||
const cards = document.querySelectorAll('.skill-card');
|
|
||||||
if (!cards.length) return;
|
|
||||||
|
|
||||||
// Clone the first set of cards and append to create seamless loop
|
|
||||||
const firstSetCount = cards.length / 3; // We have 3 sets in the markup
|
|
||||||
|
|
||||||
// Set proper animation based on screen size
|
|
||||||
function updateScrollAnimation() {
|
|
||||||
if (window.innerWidth >= 640) {
|
|
||||||
sliderTrack.style.animation = 'scroll 60s linear infinite';
|
|
||||||
} else {
|
|
||||||
sliderTrack.style.animation = 'scroll 40s linear infinite';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateScrollAnimation();
|
|
||||||
window.addEventListener('resize', updateScrollAnimation);
|
|
||||||
}
|
|
||||||
|
|
||||||
setupInfiniteScroll();
|
|
||||||
|
|
||||||
// Pause animation on hover/touch
|
|
||||||
sliderTrack?.addEventListener('mouseenter', () => {
|
|
||||||
sliderTrack.style.animationPlayState = 'paused';
|
|
||||||
});
|
|
||||||
|
|
||||||
sliderTrack?.addEventListener('touchstart', () => {
|
|
||||||
sliderTrack.style.animationPlayState = 'paused';
|
|
||||||
});
|
|
||||||
|
|
||||||
sliderTrack?.addEventListener('mouseleave', () => {
|
|
||||||
sliderTrack.style.animationPlayState = 'running';
|
|
||||||
});
|
|
||||||
|
|
||||||
sliderTrack?.addEventListener('touchend', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
sliderTrack.style.animationPlayState = 'running';
|
|
||||||
}, 1000); // Delay resuming animation after touch
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add hover effects to cards - only on non-touch devices
|
|
||||||
const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0;
|
|
||||||
const cards = document.querySelectorAll('.skill-card');
|
|
||||||
|
|
||||||
if (!isTouchDevice) {
|
|
||||||
cards.forEach((card) => {
|
|
||||||
card.addEventListener('mousemove', (e) => {
|
|
||||||
const rect = card.getBoundingClientRect();
|
|
||||||
const x = e.clientX - rect.left;
|
|
||||||
const y = e.clientY - rect.top;
|
|
||||||
|
|
||||||
const centerX = rect.width / 2;
|
|
||||||
const centerY = rect.height / 2;
|
|
||||||
|
|
||||||
const angleX = (y - centerY) / 15;
|
|
||||||
const angleY = (centerX - x) / 15;
|
|
||||||
|
|
||||||
card.style.transform = `perspective(1000px) rotateX(${angleX}deg) rotateY(${angleY}deg) scale(1.08) translateZ(20px)`;
|
|
||||||
|
|
||||||
// Dynamic shadow based on tilt
|
|
||||||
const shadowX = (x - centerX) / 25;
|
|
||||||
const shadowY = (y - centerY) / 25;
|
|
||||||
card.style.boxShadow = `
|
|
||||||
${shadowX}px ${shadowY}px 20px rgba(0, 0, 0, 0.1),
|
|
||||||
0 10px 20px rgba(0, 0, 0, 0.05)
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
|
|
||||||
card.addEventListener('mouseleave', () => {
|
|
||||||
card.style.transform = '';
|
|
||||||
card.style.boxShadow = '';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Simpler effects for touch devices
|
|
||||||
cards.forEach((card) => {
|
|
||||||
card.addEventListener('touchstart', () => {
|
|
||||||
card.classList.add('is-touched');
|
|
||||||
});
|
|
||||||
|
|
||||||
card.addEventListener('touchend', () => {
|
|
||||||
setTimeout(() => {
|
|
||||||
card.classList.remove('is-touched');
|
|
||||||
}, 300);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle theme transition
|
|
||||||
document.addEventListener('themeChange', () => {
|
|
||||||
// Add special effects during theme transition
|
|
||||||
cards.forEach((card, index) => {
|
|
||||||
// Add staggered animation delay
|
|
||||||
setTimeout(() => {
|
|
||||||
card.classList.add('theme-changing');
|
|
||||||
setTimeout(() => {
|
|
||||||
card.classList.remove('theme-changing');
|
|
||||||
}, 600);
|
|
||||||
}, index * 50);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// Handle SPA transitions for about page
|
|
||||||
function setupSPATransitions() {
|
|
||||||
// Handle all internal links for SPA transitions
|
|
||||||
document.querySelectorAll('a[href^="/"]').forEach((link) => {
|
|
||||||
// Skip links that are anchor links, external links, or already processed
|
|
||||||
if (
|
|
||||||
link.getAttribute('href').includes('#') ||
|
|
||||||
link.getAttribute('target') === '_blank' ||
|
|
||||||
link.hasAttribute('data-spa-handled')
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize animations for about page
|
|
||||||
function animateAboutContent() {
|
|
||||||
// Animate hero section elements
|
|
||||||
const heroElements = document.querySelectorAll('h1, .order-2 p, .social-links-container');
|
|
||||||
heroElements.forEach((el, index) => {
|
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
el.classList.add('animate-reveal');
|
el.classList.add('animate-reveal');
|
||||||
},
|
},
|
||||||
100 + index * 150
|
50 + index * 100
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Animate profile image
|
// Animate with just fade in with staggered delay
|
||||||
const profileImage = document.querySelector('.aspect-square');
|
const smoothRevealFade = document.querySelectorAll('.smooth-reveal-fade');
|
||||||
if (profileImage) {
|
smoothRevealFade.forEach((el, index) => {
|
||||||
setTimeout(() => {
|
|
||||||
profileImage.classList.add('animate-reveal');
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Animate skill bars with staggered delay
|
|
||||||
const skillBars = document.querySelectorAll('.skill-bar');
|
|
||||||
skillBars.forEach((bar, index) => {
|
|
||||||
setTimeout(
|
setTimeout(
|
||||||
() => {
|
() => {
|
||||||
bar.classList.add('animate-skill');
|
el.classList.add('animate-reveal-fade');
|
||||||
},
|
},
|
||||||
500 + index * 100
|
100 + index * 250
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
// Animate sections with staggered delay
|
animateContent();
|
||||||
const sections = document.querySelectorAll('section');
|
});
|
||||||
sections.forEach((section, index) => {
|
|
||||||
setTimeout(
|
|
||||||
() => {
|
|
||||||
section.classList.add('animate-reveal');
|
|
||||||
},
|
|
||||||
300 + index * 200
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run animations
|
|
||||||
animateAboutContent();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', setupSPATransitions);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', setupSPATransitions);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', setupSPATransitions);
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
63
src/pages/apps.astro
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
---
|
||||||
|
import { readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
|
import HeroSection from '@components/sections/HeroSection.astro';
|
||||||
|
import ApplicationSection from '@components/sections/ApplicationSection.astro';
|
||||||
|
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
|
||||||
|
import applicationImg from '@images/cedar_tree.png';
|
||||||
|
|
||||||
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout
|
||||||
|
title="Applications"
|
||||||
|
description={global.about_applications}
|
||||||
|
structuredData={{
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'WebPage',
|
||||||
|
inLanguage: 'en-US',
|
||||||
|
'@id': Astro.url.href,
|
||||||
|
url: Astro.url.href,
|
||||||
|
name: `Applications | ${global.name}`,
|
||||||
|
description: global.about_applications,
|
||||||
|
isPartOf: {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
url: global.site_url,
|
||||||
|
name: global.name,
|
||||||
|
description: global.about,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
<HeroSection
|
||||||
|
title="Applications"
|
||||||
|
subTitle={global.about_applications}
|
||||||
|
src={applicationImg}
|
||||||
|
alt={global.applications_image_alt}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ApplicationSection className="smooth-reveal-2" />
|
||||||
|
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Add smooth reveal animations for content after loading
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
|
const animateContent = () => {
|
||||||
|
// Animate group 1
|
||||||
|
const smoothReveal = document.querySelectorAll('.smooth-reveal');
|
||||||
|
smoothReveal.forEach((el, index) => {
|
||||||
|
setTimeout(
|
||||||
|
() => {
|
||||||
|
el.classList.add('animate-reveal');
|
||||||
|
},
|
||||||
|
50 + index * 100
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
animateContent();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -1,347 +1,184 @@
|
|||||||
---
|
---
|
||||||
import BlogPost from '../../layouts/BlogPost.astro';
|
import { type CollectionEntry, getCollection } from 'astro:content';
|
||||||
|
import { Image } from 'astro:assets';
|
||||||
|
import getReadingTime from 'reading-time';
|
||||||
|
import { marked } from 'marked';
|
||||||
|
import markedShiki from 'marked-shiki';
|
||||||
|
import { createHighlighter } from 'shiki';
|
||||||
|
import { readItems, readSingleton } from '@directus/sdk';
|
||||||
|
|
||||||
import directus from '../../../lib/directus';
|
import SocialShareButton from '@components/buttons/SocialShareButton.astro';
|
||||||
import { readItems } from '@directus/sdk';
|
import BaseLayout from '@layouts/BaseLayout.astro';
|
||||||
|
import directus from '@lib/directus';
|
||||||
|
import { formatDate } from '@support/time';
|
||||||
|
import { getDirectusImageURL } from '@/support/url';
|
||||||
|
|
||||||
|
const post = Astro.props;
|
||||||
|
|
||||||
export async function getStaticPaths() {
|
export async function getStaticPaths() {
|
||||||
const posts = await directus.request(
|
const posts = await directus.request(readItems('posts'));
|
||||||
readItems('posts', {
|
return posts.map((post) => ({
|
||||||
fields: ['*'],
|
params: { slug: post.slug },
|
||||||
})
|
props: post,
|
||||||
);
|
}));
|
||||||
|
|
||||||
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;
|
const global = await directus.request(readSingleton('site_global'));
|
||||||
|
const category: CollectionEntry<'categories'> = (await getCollection('categories'))
|
||||||
|
.filter((c) => c.slug === post.category)
|
||||||
|
.pop() as CollectionEntry<'categories'>;
|
||||||
|
|
||||||
|
const readingTime = getReadingTime(post.content);
|
||||||
|
|
||||||
|
const highlighter = await createHighlighter({
|
||||||
|
themes: ['github-light', 'github-dark'],
|
||||||
|
langs: ['typescript', 'python', 'css', 'html', 'yaml', 'bash', 'json'],
|
||||||
|
});
|
||||||
|
|
||||||
|
marked.use(markedShiki({
|
||||||
|
highlight(code, lang) {
|
||||||
|
return highlighter.codeToHtml(code, {
|
||||||
|
lang: lang || 'plaintext',
|
||||||
|
themes: {
|
||||||
|
light: 'github-light',
|
||||||
|
dark: 'github-dark',
|
||||||
|
},
|
||||||
|
defaultColor: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const content = marked.parse(post.content);
|
||||||
---
|
---
|
||||||
|
|
||||||
<BlogPost
|
<BaseLayout
|
||||||
slug={post.slug}
|
|
||||||
title={post.title}
|
title={post.title}
|
||||||
description={post.description}
|
description={post.description}
|
||||||
content={post.content}
|
ogImage={getDirectusImageURL(post.image)}
|
||||||
image={post.image}
|
structuredData={{
|
||||||
image_alt={post.image_alt}
|
'@context': 'https://schema.org',
|
||||||
published_date={post.published_date}
|
'@type': 'NewsArticle',
|
||||||
updated_date={post.updated_date}
|
inLanguage: 'en-US',
|
||||||
tags={post.tags}
|
'@id': Astro.url.href,
|
||||||
|
url: Astro.url.href,
|
||||||
|
description: post.description,
|
||||||
|
isPartOf: {
|
||||||
|
'@type': 'WebSite',
|
||||||
|
url: `${global.site_url}/blog`,
|
||||||
|
name: global.name,
|
||||||
|
description: global.about,
|
||||||
|
},
|
||||||
|
image: [],
|
||||||
|
headline: post.title,
|
||||||
|
datePublished: post.published_date,
|
||||||
|
dateModified: post.updated_date,
|
||||||
|
author: [
|
||||||
|
{
|
||||||
|
'@type': 'Person',
|
||||||
|
name: `${global.name}`,
|
||||||
|
url: `${global.site_url}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<!-- Main Content - Enhanced with better typography and spacing -->
|
|
||||||
<div
|
|
||||||
class="prose prose-sm prose-zinc max-w-none dark:prose-invert sm:prose-base prose-headings:scroll-mt-24 prose-headings:font-semibold prose-a:font-medium prose-a:text-zinc-800 prose-a:underline-offset-4 hover:prose-a:text-zinc-600 prose-img:rounded-xl dark:prose-a:text-zinc-300 dark:hover:prose-a:text-zinc-100"
|
|
||||||
>
|
|
||||||
<div set:html={post.content} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Next/Previous Navigation - Improved responsive design -->
|
<section class="max-w-6xl px-4 sm:px-6 lg:px-8 pt-8 lg:pt-12 pb-12 mx-auto">
|
||||||
<div
|
<div class="smooth-reveal relative w-full">
|
||||||
class="mt-12 grid grid-cols-1 gap-4 border-t border-zinc-200 pt-8 dark:border-zinc-800 sm:mt-16 sm:gap-6 sm:pt-12 md:grid-cols-2"
|
<div class="sm:shadow-xs sm:dark:shadow-md rounded-2xl mt-4 sm:mt-0">
|
||||||
>
|
<Image
|
||||||
{
|
class="rounded-2xl sm:rounded-b-none w-full max-h-150 object-cover"
|
||||||
prevPost && (
|
src={getDirectusImageURL(post.image)}
|
||||||
<a
|
alt={post.image_alt}
|
||||||
href={`/blog/${prevPost.slug}`}
|
draggable="false"
|
||||||
class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/50 sm:p-6"
|
format="webp"
|
||||||
>
|
loading="lazy"
|
||||||
<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" />
|
inferSize={true}
|
||||||
<span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm">
|
/>
|
||||||
<svg
|
<div class="sm:bg-background-card rounded-b-2xl px-0 sm:px-6 md:px-10 lg:px-14 py-6">
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<div class="text-center sm:text-left mt-4">
|
||||||
width="20"
|
<h2 class="card-text-header block">
|
||||||
height="20"
|
{post.title}
|
||||||
viewBox="0 0 24 24"
|
</h2>
|
||||||
fill="none"
|
<ol class="flex items-center justify-center sm:justify-start whitespace-nowrap gap-2 sm:gap-0 mt-6 sm:mt-4">
|
||||||
stroke="currentColor"
|
<li class="inline-flex items-center">
|
||||||
stroke-width="2"
|
<a
|
||||||
stroke-linecap="round"
|
class="inline-flex items-center text-secondary hover:text-secondary-hover text-sm transition-all duration-300"
|
||||||
stroke-linejoin="round"
|
href=`/categories/${category.slug}`
|
||||||
class="h-3 w-3 transition-transform duration-300 group-hover:-translate-x-1 sm:h-4 sm:w-4"
|
data-astro-prefetch
|
||||||
>
|
>
|
||||||
<path d="m15 18-6-6 6-6" />
|
{category?.data?.title}
|
||||||
</svg>
|
</a>
|
||||||
Previous Article
|
<span class="shrink-0 text-secondary text-sm mx-2 sm:mx-4">
|
||||||
</span>
|
/
|
||||||
<h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg">
|
</span>
|
||||||
{prevPost.title}
|
</li>
|
||||||
</h3>
|
<li class="inline-flex items-center">
|
||||||
</a>
|
<span class="shrink-0 text-secondary text-sm">
|
||||||
)
|
{formatDate(post.published_date)}
|
||||||
|
</span>
|
||||||
|
<span class="shrink-0 text-secondary text-sm mx-2 sm:mx-4">
|
||||||
|
/
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
<li class="inline-flex items-center">
|
||||||
|
<span class="shrink-0 text-secondary text-sm">
|
||||||
|
{readingTime.minutes.toPrecision(1)} minutes to read
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
<div class="border-t border-divider mt-10 mb-10"/>
|
||||||
|
|
||||||
|
<article class="text-header prose prose-blog sm:prose-lg dark:prose-invert max-w-none">
|
||||||
|
<div set:html={content} />
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<div class="grid sm:flex sm:items-center sm:justify-between gap-y-5 sm:gap-y-0 max-w-5xl mx-auto mt-10 md:mt-14">
|
||||||
|
<div class="flex flex-wrap sm:flex-nowrap sm:items-center gap-x-2 gap-y-1 sm:gap-y-0">
|
||||||
|
{post.tags.map((tag: string) => (
|
||||||
|
<span class="inline-flex items-center button-base bg-cobalt dark:bg-turquoise text-neutral-100 text-xs font-bold rounded-lg gap-x-1.5 px-3 py-1.5">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<SocialShareButton pageTitle={post.title}/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<style is:inline>
|
||||||
|
code[data-theme*=' '],
|
||||||
|
code[data-theme*=' '] span {
|
||||||
|
color: var(--shiki-light);
|
||||||
}
|
}
|
||||||
{
|
|
||||||
nextPost && (
|
html.dark {
|
||||||
<a
|
code[data-theme*=' '],
|
||||||
href={`/blog/${nextPost.slug}`}
|
code[data-theme*=' '] span {
|
||||||
class="group relative flex h-full flex-col overflow-hidden rounded-xl border border-zinc-200 p-4 transition-all duration-300 hover:-translate-y-1 hover:bg-zinc-50 dark:border-zinc-800 dark:hover:bg-zinc-800/50 sm:p-6 md:text-right"
|
color: var(--shiki-dark);
|
||||||
>
|
}
|
||||||
<div class="absolute inset-0 bg-gradient-to-l from-zinc-100 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100 dark:from-zinc-800 dark:to-transparent" />
|
|
||||||
<span class="relative z-10 mb-1 flex items-center gap-1 text-xs font-medium text-zinc-500 dark:text-zinc-400 sm:mb-2 sm:gap-2 sm:text-sm md:justify-end">
|
|
||||||
Next Article
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="20"
|
|
||||||
height="20"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="h-3 w-3 transition-transform duration-300 group-hover:translate-x-1 sm:h-4 sm:w-4"
|
|
||||||
>
|
|
||||||
<path d="m9 18 6-6-6-6" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
<h3 class="line-clamp-2 text-base font-medium text-zinc-900 transition-colors group-hover:text-zinc-700 dark:text-white dark:group-hover:text-zinc-300 sm:text-lg">
|
|
||||||
{nextPost.title}
|
|
||||||
</h3>
|
|
||||||
</a>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
</div>
|
</style>
|
||||||
</BlogPost>
|
|
||||||
|
</BaseLayout>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Removing TOC-related functions
|
// Add smooth reveal animations for content after loading
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
// Add copy buttons to code blocks
|
const animateContent = () => {
|
||||||
function initializeCodeCopyButtons() {
|
const smoothReveal = document.querySelectorAll('.smooth-reveal');
|
||||||
const codeBlocks = document.querySelectorAll('pre');
|
smoothReveal.forEach((el, index) => {
|
||||||
|
setTimeout(
|
||||||
codeBlocks.forEach((block) => {
|
() => {
|
||||||
// Skip if already processed by either method
|
el.classList.add('animate-reveal');
|
||||||
if (
|
},
|
||||||
block.classList.contains('code-block-processed') ||
|
100 + index * 100
|
||||||
block.classList.contains('enhanced')
|
);
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
block.classList.add('code-block-processed');
|
|
||||||
|
|
||||||
// Create wrapper if not already wrapped
|
|
||||||
let wrapper;
|
|
||||||
if (
|
|
||||||
block.parentNode.classList.contains('relative') &&
|
|
||||||
block.parentNode.classList.contains('group')
|
|
||||||
) {
|
|
||||||
wrapper = block.parentNode;
|
|
||||||
} else {
|
|
||||||
wrapper = document.createElement('div');
|
|
||||||
wrapper.className = 'relative group';
|
|
||||||
block.parentNode.insertBefore(wrapper, block);
|
|
||||||
wrapper.appendChild(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add copy button if not already present
|
|
||||||
if (!wrapper.querySelector('.copy-button') && !wrapper.querySelector('.copy-code-button')) {
|
|
||||||
const copyButton = document.createElement('button');
|
|
||||||
copyButton.className =
|
|
||||||
'copy-button absolute top-2 right-2 p-1.5 rounded-md bg-zinc-700/50 hover:bg-zinc-700 text-zinc-200 opacity-0 group-hover:opacity-100 transition-opacity duration-200';
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
copyButton.addEventListener('click', () => {
|
|
||||||
const code = block.querySelector('code').innerText;
|
|
||||||
navigator.clipboard.writeText(code);
|
|
||||||
|
|
||||||
// Show copied feedback
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
copyButton.innerHTML = `
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
}, 2000);
|
|
||||||
});
|
|
||||||
|
|
||||||
wrapper.appendChild(copyButton);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle SPA transitions for blog post navigation
|
|
||||||
function setupSPATransitions() {
|
|
||||||
// Handle prev/next navigation links
|
|
||||||
document.querySelectorAll('a[href^="/blog/"]').forEach((link) => {
|
|
||||||
// Skip links that are anchor links or already processed
|
|
||||||
if (link.getAttribute('href').includes('#') || link.hasAttribute('data-spa-handled')) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark as handled to avoid duplicate listeners
|
|
||||||
link.setAttribute('data-spa-handled', 'true');
|
|
||||||
|
|
||||||
link.addEventListener('click', (e) => {
|
|
||||||
// Don't handle if modifier keys are pressed (for opening in new tab, etc.)
|
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault();
|
|
||||||
const targetHref = link.getAttribute('href');
|
|
||||||
|
|
||||||
// Trigger page transition animation
|
|
||||||
const pageTransition = document.getElementById('page-transition');
|
|
||||||
if (pageTransition) {
|
|
||||||
pageTransition.classList.remove('opacity-0');
|
|
||||||
pageTransition.classList.add('opacity-100');
|
|
||||||
|
|
||||||
// Navigate after transition effect
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}, 300);
|
|
||||||
} else {
|
|
||||||
// Fallback if transition element doesn't exist
|
|
||||||
window.location.href = targetHref;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
};
|
||||||
}
|
|
||||||
|
|
||||||
// Main initialization function
|
animateContent();
|
||||||
function initializeBlogPost() {
|
});
|
||||||
// Initialize remaining components
|
|
||||||
initializeCodeCopyButtons();
|
|
||||||
setupSPATransitions();
|
|
||||||
|
|
||||||
// Scroll to hash if present in URL
|
|
||||||
if (window.location.hash) {
|
|
||||||
setTimeout(() => {
|
|
||||||
const element = document.querySelector(window.location.hash);
|
|
||||||
if (element) {
|
|
||||||
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on first load
|
|
||||||
document.addEventListener('DOMContentLoaded', initializeBlogPost);
|
|
||||||
|
|
||||||
// Re-initialize when content changes via Astro's view transitions
|
|
||||||
document.addEventListener('astro:page-load', initializeBlogPost);
|
|
||||||
|
|
||||||
// For compatibility with custom transition system
|
|
||||||
document.addEventListener('page-transition-complete', initializeBlogPost);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
|
||||||
/* Removing TOC-related styles */
|
|
||||||
|
|
||||||
/* Language badge styling */
|
|
||||||
.language-badge {
|
|
||||||
font-family:
|
|
||||||
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New',
|
|
||||||
monospace;
|
|
||||||
text-transform: lowercase;
|
|
||||||
letter-spacing: 0.05em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Extra small screens */
|
|
||||||
@media (min-width: 480px) {
|
|
||||||
.xs\:inline {
|
|
||||||
display: inline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.xs\:hidden {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Enhanced typography for blog content - Responsive adjustments */
|
|
||||||
.prose {
|
|
||||||
@reference text-zinc-800 dark:text-zinc-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose h1,
|
|
||||||
.prose h2,
|
|
||||||
.prose h3,
|
|
||||||
.prose h4 {
|
|
||||||
@reference font-semibold text-zinc-900 dark:text-zinc-100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose h1 {
|
|
||||||
@reference text-2xl sm:text-3xl md:text-4xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose h2 {
|
|
||||||
@reference mb-3 mt-8 border-b border-zinc-200 pb-2 text-xl dark:border-zinc-800 sm:mb-4 sm:mt-12 sm:text-2xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose h3 {
|
|
||||||
@reference mb-2 mt-6 text-lg sm:mb-3 sm:mt-8 sm:text-xl;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose p {
|
|
||||||
@reference mb-4 text-sm leading-relaxed sm:mb-6 sm:text-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose a {
|
|
||||||
@reference font-medium text-zinc-800 underline decoration-zinc-400 underline-offset-2 transition-colors hover:text-zinc-600 hover:decoration-zinc-600 dark:text-zinc-300 dark:decoration-zinc-600 dark:hover:text-zinc-100 dark:hover:decoration-zinc-400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose blockquote {
|
|
||||||
@reference my-4 border-l-4 border-zinc-300 pl-4 italic text-zinc-700 dark:border-zinc-700 dark:text-zinc-300 sm:my-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose code {
|
|
||||||
@reference rounded bg-zinc-100 px-1.5 py-0.5 text-sm font-medium text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose pre {
|
|
||||||
@reference my-4 overflow-x-auto rounded-lg bg-[#1e293b] p-3 text-xs text-zinc-200 shadow-md dark:bg-[#1e293b] sm:my-6 sm:p-4 sm:text-sm !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose pre code {
|
|
||||||
@reference bg-transparent p-0 text-zinc-200 dark:text-zinc-200 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose img {
|
|
||||||
@reference mx-auto my-6 h-auto max-w-full rounded-lg shadow-md sm:my-8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose ul,
|
|
||||||
.prose ol {
|
|
||||||
@reference my-4 pl-5 sm:my-6 sm:pl-6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose li {
|
|
||||||
@reference mb-1 text-sm sm:mb-2 sm:text-base;
|
|
||||||
}
|
|
||||||
|
|
||||||
.prose hr {
|
|
||||||
@reference my-8 border-zinc-200 dark:border-zinc-800 sm:my-10;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Line clamp for truncating text */
|
|
||||||
.line-clamp-2 {
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|||||||