Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68a827a113
|
||
|
|
c047060b7c
|
||
|
|
cd3a288935
|
||
|
|
7ee9c45339
|
||
|
|
799d7ee0bc
|
||
|
|
71b6601360
|
||
|
|
eb43e2f49f
|
||
|
|
79267d0059
|
||
|
|
39f482fc37
|
||
|
|
70ad0420bb
|
||
|
|
641cc4956d
|
||
|
|
e0e2033fdf
|
||
|
|
e18c066629
|
||
|
|
edb63b0316
|
||
|
|
9a8e764f53
|
||
|
|
26a5783d19
|
||
|
|
880beeee07
|
||
|
|
151947b09c
|
||
|
|
4054d0793f
|
||
|
|
2d61e9f927
|
||
|
|
67ffd05566
|
||
|
|
b429074071
|
||
|
|
9dfd16d68d
|
||
|
|
fb0d80d7f6
|
||
|
|
bd30579923
|
||
|
|
d6e7104f82
|
||
|
|
24b4e45116
|
||
|
|
6b347bb845
|
||
|
|
c5a16d2797
|
||
|
|
f7f4de8341
|
||
|
|
cd4956fbe6
|
||
|
|
241ac889fc
|
||
|
|
d3f3ef37ed
|
||
|
|
c2402f9360
|
||
|
|
f6fdae6b0c
|
||
|
|
c7fe1f2f60
|
||
|
|
c7490d2e48
|
||
|
|
e1e5ca7a76
|
||
|
|
2eafa3c093
|
||
|
|
07901f9ab2
|
||
|
|
167ddbfb4e
|
||
|
|
b062015f23
|
||
|
|
e808ec9a26
|
||
|
|
3eddb7d196
|
||
|
|
d7ce212095
|
||
|
|
da330db355
|
||
|
|
f65b65c053
|
||
|
|
6fbdc87669
|
||
|
|
afdc98beb3
|
||
|
|
248adf40b0
|
||
|
|
d78beffabd
|
||
|
|
f6a0553b99
|
||
|
|
22311dc5c9
|
||
|
|
2fb747f587
|
||
|
|
0c6d6c76f4
|
||
|
|
4b6881808d
|
||
|
|
12d8c4fa23
|
||
|
|
c9cb0fce27
|
||
|
|
31fbfbde07
|
||
|
|
942645df68
|
||
|
|
1e283c7392
|
||
|
|
9cf2245217
|
||
|
|
3136f40530
|
||
|
|
62fbd5de3a
|
||
|
|
25d7ee0c6d
|
||
|
|
a0a9cac244
|
||
|
|
d02c1fb702
|
||
|
|
e4bd6df2d5
|
||
|
|
76b4887205
|
||
|
|
17351e9f44
|
||
|
|
5101450d59
|
||
|
|
139c6f4ef5
|
||
|
|
d63a0e5bca
|
||
|
|
5428006a4a
|
||
|
|
17629e0691
|
||
|
|
22dc39fe51
|
||
|
|
4a4b62936a
|
||
|
|
5efd3ac5b1
|
||
|
|
c11ab2a52c
|
||
|
|
9a193d28af
|
||
|
|
5979177650
|
||
|
|
e337e1db3e
|
||
|
|
81252bf95d
|
||
|
|
e278c51676
|
||
|
|
16db5394ad
|
||
|
|
a49383154f
|
||
|
|
7734bef1ef
|
||
|
|
ae84fd742c
|
||
|
|
0cd7f7e5a7
|
||
|
|
7152771a48
|
||
|
|
964c247098
|
||
|
|
493c705abc
|
||
|
|
c7d79efcc2
|
||
|
|
55164c3b2f
|
||
|
|
b245aa4a39
|
||
|
|
5b93f547f3
|
||
|
|
3e30e85354
|
||
|
|
f4d8952c54
|
||
|
|
2206b9952b
|
||
|
|
1b7b7cbc68
|
||
|
|
3bcd71d60c
|
||
|
|
cc7a562887
|
||
|
|
3f40e2142c
|
||
|
|
2917d04ffe
|
||
|
|
f1430622dd
|
||
|
|
d5f80eba67
|
||
|
|
f343d99406
|
||
|
|
2060134563 | ||
|
|
3e7f3d3c14
|
||
|
|
d4363f7515
|
||
|
|
80dec73fb0
|
||
|
|
8faace292e
|
||
|
|
7c858d53d8
|
||
|
|
2c10be6b99
|
||
|
|
44a5d326ac
|
||
|
|
e00d3a1d41
|
||
|
|
d8a97678cd
|
||
|
|
8d690ac875
|
||
|
|
235cf04b09
|
||
|
|
8a0d4ac949
|
||
|
|
5740b354f9
|
||
|
|
eea417013d
|
||
|
|
192f3c37a9
|
||
|
|
b729b85ad0
|
||
|
|
a9f805e1fd
|
||
|
|
af418197a4
|
||
|
|
8a718b4bab
|
||
|
|
1473dcf060
|
||
|
|
8702648624
|
||
|
|
88819f5684
|
||
|
|
98940eeec4
|
||
|
|
775e206322
|
||
|
|
7407366f45
|
||
|
|
deaf3935c9
|
||
|
|
5f07e66915
|
||
|
|
122663e2e3
|
||
|
|
5c8073d264
|
||
|
|
970f4a74d1
|
||
|
|
ecf9074522
|
||
|
|
ea87e0832a
|
||
|
|
f4121bf419
|
||
|
|
bf317eea35
|
||
|
|
948d6d41e8
|
||
|
|
867b3a41f8
|
||
|
|
e0268b6e6c
|
||
|
|
156b3b552c
|
||
|
|
39e6c94d09
|
||
|
|
76fda9e5c6
|
||
|
|
d9c19b252e
|
||
|
|
91fa06898f
|
||
|
|
6d7dd7dc40
|
||
|
|
5122d93072
|
||
|
|
dc4c47e2ef
|
||
|
|
a5dede3485
|
||
|
|
eb892d9725
|
||
|
|
27f696247d
|
||
|
|
3a07b0482f
|
||
|
|
88634e10cd
|
||
|
|
98cc6b7556
|
||
|
|
0bb0f23347
|
||
|
|
02af96a07d
|
||
|
|
a6194bb874
|
||
|
|
d6700d6965
|
||
|
|
54ffaf73e3
|
||
|
|
ebe21f7f4d
|
||
|
|
37515268d7
|
||
|
|
c919263c36
|
||
|
|
592eb7222f
|
||
|
|
98579b726b
|
||
|
|
918d6768e8
|
||
|
|
924698df24
|
||
|
|
4e6c94afc8
|
||
|
|
f2f7e9e23c
|
||
|
|
08949d5bb4
|
||
|
|
65153db414
|
||
|
|
58e4640965
|
||
|
|
521f1becb4
|
||
|
|
dc1caac2ed
|
||
|
|
e8cb978a61
|
||
|
|
1c3175a3e4
|
||
|
|
a97ee3c660
|
||
|
|
baa1f6ed44
|
||
|
|
d5b76b0a17
|
||
|
|
5c3d7ce535
|
||
|
|
072f3472f7
|
||
|
|
7d15d29409
|
||
|
|
8ce315e1c7
|
||
|
|
33dfee951a
|
||
|
|
80bc8339ed
|
||
|
|
12f9d85841
|
||
|
|
a78fa93b51
|
||
|
|
06c5e9b2c1
|
||
|
|
bf3e6b2e2e
|
||
|
|
b58ee95ad2
|
||
|
|
093d5840fc
|
||
|
|
352bd5baf5
|
||
|
|
2117c28606
|
||
|
|
7f01a807b9
|
||
|
|
a7babaf94b
|
||
|
|
a14318c4c5
|
||
|
|
c3c7fdb945
|
||
|
|
1418e52cbf
|
||
|
|
2d3d2457cf
|
||
|
|
6c12550ee7
|
||
|
|
9ec2d7fd6f
|
||
|
|
975e84be46
|
||
|
|
a365322a5b
|
||
|
|
67fc878575
|
||
|
|
008e38e54a
|
||
|
|
a0d592d26e
|
||
|
|
0b6adfa41d
|
||
|
|
dc557afbc2
|
||
|
|
88d419fd17
|
||
|
|
37bba31fd4
|
||
|
|
ca70d46df3
|
||
|
|
2a0b5c9b86
|
||
|
|
316a8fd56f
|
||
|
|
7bf44ca720
|
||
|
|
53c7cb1c79
|
||
|
|
368747f323
|
||
|
|
a46a4da005
|
||
|
|
5e15c01ed6
|
||
|
|
c64539539d
|
||
|
|
fea440edaa
|
||
|
|
47744e7be6
|
||
|
|
6a31373b7d
|
||
|
|
a824ee57df
|
||
|
|
9ed8790649
|
||
|
|
dd678fb393
|
||
|
|
17570d400d
|
||
|
|
cabebfb9fe
|
||
|
|
9b175e3923
|
||
|
|
873bd74804
|
||
|
|
0df77ef6ee
|
||
|
|
4daf550568
|
||
|
|
f7a76d5d20
|
||
|
|
34af4c874f
|
||
|
|
c98e8d1f96
|
||
|
|
a2268ea275
|
||
|
|
720fbd5697
|
||
|
|
ba754771f4
|
||
|
|
b0bf852e26
|
||
|
|
83351110b3
|
||
|
|
a30703dc3e
|
||
|
|
f480794817
|
||
|
|
0e9e0ecec7
|
||
|
|
54090751a5
|
||
|
|
274d25d654
|
||
|
|
35db1d65f2
|
||
|
|
20e350d88b
|
||
|
|
534734ce9a
|
||
|
|
1679800272
|
||
|
|
bf3786249b
|
||
|
|
f24749423b
|
||
|
|
b617078e58
|
||
|
|
c5467620c5
|
||
|
|
fc4584f932
|
||
|
|
da319c28d5
|
||
|
|
6f2aaa62f0
|
||
|
|
8f65d3ae60
|
||
|
|
f6d848a714
|
||
|
|
2340a6a193
|
||
|
|
0dcb6f0821
|
||
|
|
f0dc5d4bdc
|
||
|
|
59e19c7666
|
||
|
|
0373ea20f7
|
||
|
|
3b8c387c44
|
||
|
|
d461da90f2
|
||
|
|
295bfc19e6
|
||
|
|
7a7f698b44
|
||
|
|
2fd010ddfa
|
||
|
|
f72a2bf2b1
|
||
|
|
926010f128
|
||
|
|
98c954e361
|
||
|
|
325551d253
|
||
|
|
a33bc047fa
|
||
|
|
1421789e43
|
||
|
|
27a22ce569
|
||
|
|
b28af20d11 | ||
|
|
c81de2dce3
|
||
|
|
1b60984b5c
|
||
|
|
733bc2b16a
|
||
|
|
11800f6ef4
|
||
|
|
b47ab0355e
|
||
|
|
34a7cb3f2c
|
||
|
|
da76d14d15
|
||
|
|
547bc241da
|
||
|
|
4ef6008976
|
||
|
|
035aa1aa77
|
||
|
|
462ae00506
|
||
|
|
5d2064134b
|
||
|
|
aeb348fcfd
|
||
|
|
37ca0baa6d
|
||
|
|
a165c13d7c
|
||
|
|
1378813783
|
||
|
|
e376e9b362
|
||
|
|
d09514f71f
|
||
|
|
f6d72dbea3
|
||
|
|
0d7814d655
|
||
|
|
c387c1486f
|
||
|
|
878b558603
|
||
|
|
0251d5c577
|
||
|
|
280dd9d7c0
|
||
|
|
8e2fc6bfa5
|
||
|
|
69d752f79b
|
||
|
|
4d62ca2b75 | ||
|
|
f5b88df831
|
||
|
|
bd25409d9f
|
||
|
|
fe20a618fe
|
||
|
|
17b49cdb6e
|
||
|
|
287d7ad713
|
||
|
|
d3e4d54c57
|
||
|
|
ead8821b0a
|
||
|
|
d3e8a0125d
|
||
|
|
34b7cfbf5a
|
||
|
|
1ebf5780d2
|
||
|
|
f354c1867c
|
||
|
|
0583de0dfa
|
||
|
|
2a5e79b55d
|
||
|
|
4df51a970d
|
||
|
|
7013c60a20
|
||
|
|
3623265ae0
|
||
|
|
fbefb2e7fd
|
||
|
|
b0d3258369
|
||
|
|
f65c3d58aa
|
||
|
|
33e9ca7cf1
|
||
|
|
def3a84e59
|
||
|
|
b48a487d2f
|
||
|
|
76815c6a93
|
||
|
|
aa47015d3a
|
||
|
|
989664422c
|
||
|
|
2ad95385d2
|
||
|
|
b00aaabbb1
|
||
|
|
5104c4e8e7
|
||
|
|
d1c428eccd
|
||
|
|
ed2b3224b4
|
||
|
|
6cc94b651f
|
||
|
|
16ee164d76
|
||
|
|
2989817278
|
||
|
|
0a65bcdae1
|
||
|
|
d5601b8fdb
|
||
|
|
be96541b3e
|
||
|
|
ad8bbfa666
|
||
|
|
b324b93141
|
||
|
|
7bcdf77ca8
|
||
|
|
6726600c8e
|
||
|
|
027ee2d191
|
||
|
|
6d5c7e4270
|
||
|
|
8901967ab0
|
||
|
|
e177ed279c
|
||
|
|
923942f1b1
|
||
|
|
539991fac7
|
||
|
|
9e049bc5c9
|
||
|
|
495c2379ce
|
||
|
|
c50fe25d04
|
||
|
|
1300ab46a0
|
||
|
|
28fcfa1f92
|
||
|
|
f808525d56 | ||
|
|
1c7a0c0e58
|
||
|
|
0eccd33919
|
||
|
|
1f67e95735
|
||
|
|
3c82baca67
|
||
|
|
6d95601399
|
||
|
|
9aecc195a9 | ||
|
|
2bf433621a
|
||
|
|
abbfd0ad9d
|
||
|
|
619d4065b3
|
||
|
|
3ca938d65b
|
||
|
|
666ecb4e98
|
||
|
|
3af6cb3c16
|
||
|
|
006bb5136e
|
||
|
|
258a4025bc
|
||
|
|
420f47dca4
|
||
|
|
6f164a0256
|
||
|
|
b30aea57ad
|
||
|
|
be19a3c29e
|
||
|
|
f08acad086
|
||
|
|
90f99e4240
|
||
|
|
46d92675eb
|
||
|
|
dc532d3848
|
||
|
|
cb42c9d368
|
||
|
|
6fa3ba6298
|
||
|
|
7c8cf6406d
|
||
|
|
010d29c74e
|
||
|
|
5a04e2a2f1
|
||
|
|
a7ff5e2bf4
|
||
|
|
9e4c9febdb | ||
|
|
2b136e30c8
|
||
|
|
16e69ff072
|
||
|
|
c8f8022f44
|
||
|
|
ffaa6200c3
|
||
|
|
a60526e6d7
|
||
|
|
5d7181bea2
|
||
|
|
dd0f560bc8
|
||
|
|
ced76720cc
|
||
|
|
94fc328737
|
||
|
|
2eb187ec1a
|
||
|
|
2882f78990
|
||
|
|
78cde4641c
|
||
|
|
f37d403636
|
||
|
|
09c645e657
|
||
|
|
66483a3a6a
|
||
|
|
d5c714cbd8
|
||
|
|
d8a4c0023b
|
||
|
|
5b8c13c8cf
|
||
|
|
b172e3cf2b
|
||
|
|
46a2caf845
|
||
|
|
63d14ddefd
|
||
|
|
ba45c0d488 | ||
|
|
a7d1cc0f62
|
||
|
|
28fe027517
|
||
|
|
05b6a5d513
|
||
|
|
c78357b61d
|
||
|
|
678c41e990
|
||
|
|
6fb3d95cd5
|
||
|
|
7f5cbf5665
|
||
|
|
555bdb0cfb
|
||
|
|
cd67bf486f
|
||
|
|
c1525cd2f2
|
||
|
|
5d98f27b6b
|
||
|
|
170f188435
|
||
|
|
c7fd0fe132 | ||
|
|
3313ed40e6
|
||
|
|
50c0ff4a8c | ||
|
|
5e6ff65e68 | ||
|
|
8d2937d4a0
|
||
|
|
7273a732b1
|
||
|
|
62c6784e2c
|
||
|
|
c4d32c2815
|
||
|
|
ceda686d03 | ||
|
|
792b4b80b8
|
||
|
|
22e50adaa5
|
||
|
|
8eab67b78f
|
||
|
|
6e3ef7aae0 | ||
|
|
3ef2b5b089 | ||
|
|
551327b018
|
||
|
|
d7246b9aab
|
||
|
|
5eddee4d4d
|
||
|
|
ae639dbe71
|
||
|
|
31dd40e1d7
|
||
|
|
4bdfa0a872 | ||
|
|
488186d795
|
||
|
|
67adb17936
|
||
|
|
2af6c74696
|
||
|
|
81cebb4636
|
||
|
|
d24e11b466
|
||
|
|
a0eaf36db3
|
||
|
|
0b609598f0
|
||
|
|
fc23a6d185 | ||
|
|
cf7525b32a
|
||
|
|
6175e3b0be
|
||
|
|
22c3aafcc5
|
||
|
|
7b601416ad
|
||
|
|
7098414db1
|
||
|
|
a45e208f9b
|
||
|
|
f152d39db8
|
||
|
|
08c3de5cb2
|
||
|
|
61e4f3d1f2
|
||
|
|
6aca8a6087
|
||
|
|
fb5985dcb1
|
||
|
|
78679fe03b
|
||
|
|
06f7037e11
|
||
|
|
9b04b00c11
|
||
|
|
2956c54496
|
||
|
|
ba6e8e652d
|
||
|
|
237fcd33dc
|
||
|
|
d2302afdc4
|
||
|
|
6f93d42b02
|
||
|
|
25958bf7bb
|
||
|
|
8d93ba2638
|
||
|
|
336a063d1e
|
||
|
|
cda4d58486 | ||
|
|
e7986146a1
|
||
|
|
d58845dad9 | ||
|
|
cd1ed0ab3f
|
||
|
|
40b68dd52d
|
||
|
|
c1f1099848
|
||
|
|
926c1fc463
|
||
|
|
3e605f60d0 | ||
|
|
49d4401d00
|
||
|
|
587a97fa03
|
||
|
|
4d5ddfd7ea
|
||
|
|
c5af7366a9
|
||
|
|
cab550051f
|
||
|
|
d407c2f0e9
|
||
|
|
261657d343
|
||
|
|
e3117110e9
|
||
|
|
1097694a8e
|
||
|
|
9f60c23c60
|
||
|
|
399d7caabc
|
||
|
|
679adb4cf9
|
||
|
|
8f48f3dcb5
|
||
|
|
8cdf16f144
|
||
|
|
24926b7312
|
||
|
|
2ed25157e3
|
||
|
|
f7f8cda2a5
|
||
|
|
95c383d58a
|
||
|
|
933f5ace80
|
||
|
|
7ea64524d1
|
||
|
|
6450c12600
|
||
|
|
94cdf6aa99
|
||
|
|
2718227745
|
||
|
|
257b7ed886
|
||
|
|
0e1148094a
|
||
|
|
61c3d2ff7c
|
||
|
|
de20ae89fc
|
||
|
|
13f026eaab
|
||
|
|
9cdca85a4d
|
||
|
|
69453b947d
|
||
|
|
0e8a49969a
|
||
|
|
32bfcf6b33
|
||
|
|
7a09198e9e | ||
|
|
b1812764f6
|
||
|
|
e69bbf361b
|
||
|
|
6a9c5d4035 | ||
|
|
a243c0bf9a | ||
|
|
0da2d0df76
|
||
|
|
3d9a509581 | ||
|
|
6e7e512a9b
|
||
|
|
41500560f3
|
||
|
|
a1ddf0d145
|
||
|
|
0f8d974d31
|
||
|
|
703410e0e3
|
||
|
|
f2a941efca | ||
|
|
9799a40958 | ||
|
|
ecc6598167
|
||
|
|
4edc240b10
|
||
|
|
e4ba19bc9b
|
||
|
|
93aec1c44d
|
||
|
|
bb13b22810
|
||
|
|
1a3d1080b9 | ||
|
|
f846307eee | ||
|
|
d54b913bfd
|
||
|
|
3585b7fc1f
|
||
|
|
d4814edc65
|
||
|
|
5ba4485bf5
|
||
|
|
6a36b8de68
|
||
|
|
8703fe4555
|
||
|
|
59ffc4daff
|
||
|
|
d5e19b6aee
|
||
|
|
a4bc7698c7
|
||
|
|
4f8240457c
|
||
|
|
d454405cb7
|
||
|
|
7892fe2ed6
|
||
|
|
06e3601d70
|
||
|
|
2593b65baf
|
||
|
|
c5a06b6ae4
|
||
|
|
28c79ac203
|
||
|
|
dd43667bed
|
||
|
|
f0c4bb9d06
|
||
|
|
63685121f8
|
||
|
|
703c4c1632
|
||
|
|
87c0e031f1
|
||
|
|
7fdcbfe3ee | ||
|
|
d928f2d25a
|
||
|
|
35915e8c7f
|
||
|
|
01151d476c
|
||
|
|
cc38b00033
|
||
|
|
d50bf475b6
|
||
|
|
0a28644dd7
|
||
|
|
377aa919cb
|
||
|
|
b43bac4c79
|
||
|
|
8d4c54b01b
|
||
|
|
4497396a8f
|
||
|
|
5f7838ebbb
|
||
|
|
8d4ef81b0f
|
||
|
|
4d78a742e1
|
||
|
|
ddad350e6d
|
||
|
|
5696c0f992
|
||
|
|
755c297486
|
||
|
|
821afc6e7c | ||
|
|
45ef5b67f8 | ||
|
|
fd910db9e9 | ||
|
|
01631afb73
|
||
|
|
bac2718663
|
||
|
|
c7ea4d23af | ||
|
|
ceb4f74739 | ||
|
|
991eac065b | ||
|
|
123bd22d07 | ||
|
|
c6559e478d
|
||
|
|
2fa206f866 | ||
|
|
95948f11ec | ||
|
|
5b8d6d3e2e | ||
|
|
b9cbdc7392 | ||
|
|
58d52f59da | ||
|
|
8f5426d468 | ||
|
|
4070a7d5ba | ||
|
|
04b5b29cd5 | ||
|
|
25e497ac57 | ||
|
|
691647ae63 | ||
|
|
a75f6bcde6 | ||
|
|
450fb33c0f | ||
|
|
7467a80bb6
|
||
|
|
5333cc9a97 | ||
|
|
920e71d1a2
|
||
|
|
db404a389b
|
||
|
|
0f20b52aa9
|
||
|
|
ab37ca4137
|
||
|
|
5d3278fdb1 | ||
|
|
02ee937ac9 | ||
|
|
c93215e241 | ||
|
|
83be71821f | ||
|
|
b564a5146e | ||
|
|
f2b46c96de | ||
|
|
14d219ce8a
|
||
|
|
49948899b1 | ||
|
|
23b2f78093 | ||
|
|
d27d66c6e4 | ||
|
|
6e6e9cb1bc
|
||
|
|
a79976036a
|
||
|
|
36d584f238 | ||
|
|
80ca6dcb5d | ||
|
|
467b85b524 | ||
|
|
0eb74a78d3 | ||
|
|
3b3614bd62
|
||
|
|
1fd1fbba57
|
||
|
|
cebf33fb47 | ||
|
|
3fad06f9b5
|
||
|
|
47634f017e | ||
|
|
d4772dd35b
|
@@ -0,0 +1,45 @@
|
||||
name: Deploy on dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
name: Deploy on dev
|
||||
|
||||
runs-on: node-minio
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm install
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm run build
|
||||
|
||||
- name: Set alias
|
||||
run: mc alias set minio ${{ vars.S3_DEV_URL }} ${{ secrets.S3_DEV_ACCESS_KEY }} ${{ secrets.S3_DEV_SECRET_KEY }}
|
||||
|
||||
- name: Upload build artifacts to S3
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
BUCKET=${{ vars.S3_DEV_BUCKET_NAME }}
|
||||
BUILD_ID=$(date +%s)-$(git rev-parse --short HEAD)
|
||||
|
||||
echo "Build ID: $BUILD_ID"
|
||||
|
||||
printf "Copying files ... "
|
||||
mc cp --recursive ./dist/ "minio/$BUCKET/builds/$BUILD_ID/"
|
||||
echo "done"
|
||||
|
||||
printf "Update current build pointer ... "
|
||||
echo "$BUILD_ID" | mc pipe "minio/$BUCKET/current"
|
||||
echo "done"
|
||||
@@ -0,0 +1,3 @@
|
||||
.vscode/
|
||||
node_modules/
|
||||
dist/
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
|
||||
# ------------------------------------------------ SUBJECT AREA
|
||||
# ---------------------------------------------------------------------- DETAIL AREA
|
||||
|
||||
# Please enter a subject that is not longer than 50 characters.
|
||||
# If you enter a detailed description, each line should not be longer
|
||||
# than 72 characters. Break the line as soon as you reach this limit.
|
||||
@@ -1,73 +1,202 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
1. Definitions.
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
|
||||
1. Definitions.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
Copyright 2026 Seekra
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
Notice
|
||||
|
||||
This software is provided "as is", without warranty of any kind.
|
||||
|
||||
This software uses the Vue.js framework.
|
||||
|
||||
This software makes use of various open-source components.
|
||||
See THIRD_PARTY_LICENSES for details.
|
||||
@@ -1,3 +1,11 @@
|
||||
# repository-template
|
||||
# frontend
|
||||
|
||||
The template for repositories
|
||||
The frontend of the search engine Seekra.
|
||||
|
||||
## Setup
|
||||
|
||||
Please execute the setup script in `scripts/` for your platform when cloning the repository.
|
||||
|
||||
## Legal
|
||||
|
||||
After each update to the dependencies, please run the script `scripts/generate-license-files.js` to regenerate the third-party license files.
|
||||
@@ -0,0 +1,16 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Start page
|
||||
- Settings page
|
||||
- Footer
|
||||
- Navbar
|
||||
- Searchbar
|
||||
- Icons
|
||||
- Internationalization and some major languages
|
||||
- Color scheme
|
||||
- Search results view with error message which indicates that the search is not available
|
||||
- Settings renderer
|
||||
@@ -0,0 +1,29 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#0158D2" />
|
||||
<title>Seekra</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"test:run": "vitest run"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^3.0.4",
|
||||
"pinia-plugin-persistedstate": "^4.7.1",
|
||||
"terser": "^5.47.1",
|
||||
"vue": "^3.5.32",
|
||||
"vue-i18n": "^11.4.4",
|
||||
"vue-router": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@pinia/testing": "^1.0.3",
|
||||
"@vitejs/plugin-vue": "^6.0.6",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"jsdom": "^29.1.1",
|
||||
"vite": "^8.0.10",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
"vitest": "^4.1.7"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import fs from 'node:fs/promises';
|
||||
import { generateLicenseFile } from 'generate-license-file';
|
||||
|
||||
const packageJson = './package.json';
|
||||
const packageJsonBackup = `${packageJson}~`;
|
||||
|
||||
await generateLicenseFile(
|
||||
packageJson,
|
||||
'THIRD_PARTY_LICENSES'
|
||||
);
|
||||
|
||||
const pkg = JSON.parse(
|
||||
await fs.readFile(packageJson, 'utf-8')
|
||||
);
|
||||
|
||||
await fs.rename(packageJson, packageJsonBackup);
|
||||
|
||||
const devPackage = {
|
||||
...pkg,
|
||||
dependencies: pkg.devDependencies ?? {},
|
||||
devDependencies: {}
|
||||
};
|
||||
|
||||
/*
|
||||
The file including the dependencies for the current license file has
|
||||
to be always package.json due to a bug of generate-license file. See
|
||||
|
||||
https://github.com/TobyAndToby/generate-license-file/issues/752
|
||||
*/
|
||||
await fs.writeFile(
|
||||
packageJson,
|
||||
JSON.stringify(devPackage)
|
||||
);
|
||||
|
||||
await generateLicenseFile(
|
||||
packageJson,
|
||||
'./THIRD_PARTY_LICENSES_DEV'
|
||||
);
|
||||
|
||||
await fs.rename(packageJsonBackup, packageJson);
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/env pwsh
|
||||
|
||||
git config --local commit.template .gitmessage
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/usr/bin/bash
|
||||
|
||||
git config --local commit.template .gitmessage
|
||||
@@ -0,0 +1,84 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Navbar from './features/nav/components/Navbar.vue';
|
||||
import Footer from './features/footer/components/Footer.vue';
|
||||
|
||||
import { updatePageTitle } from './router';
|
||||
import { useColorScheme } from './features/colorScheme/composables/useColorScheme';
|
||||
import { ref, provide, watch, watchEffect } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
|
||||
const { getColorScheme, updateColorScheme } = useColorScheme();
|
||||
const colorScheme = ref(getColorScheme());
|
||||
provide('colorScheme', colorScheme);
|
||||
watch(colorScheme, (newValue) => {
|
||||
updateColorScheme(newValue);
|
||||
document.body.style.setProperty(colorScheme, {
|
||||
auto: 'normal',
|
||||
dark: 'dark',
|
||||
light: 'light'
|
||||
});
|
||||
if (newValue === 'dark') {
|
||||
document.body.classList.add('dark');
|
||||
} else {
|
||||
document.body.classList.remove('dark');
|
||||
}
|
||||
if (newValue === 'auto') {
|
||||
document.body.classList.add('color-scheme-auto');
|
||||
} else {
|
||||
document.body.classList.remove('color-scheme-auto');
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => updatePageTitle(route));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="app-wrapper">
|
||||
<Navbar />
|
||||
|
||||
<router-view class="main-content" />
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#app-wrapper {
|
||||
--main-content-padding-x: 30px;
|
||||
--main-content-padding-y: 40px;
|
||||
--main-content-padding: var(--main-content-padding-y) var(--main-content-padding-x);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--light-bg);
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
#app-wrapper {
|
||||
--main-content-padding-x: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M5 12 l6 8 l8 -16"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 832 B |
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M4 8 l8 8 l8 -8"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 830 B |
@@ -0,0 +1,26 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M8 20 l8 -8 l-8 -8"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 833 B |
@@ -0,0 +1,38 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="
|
||||
M12 2
|
||||
a10 10 0 0 0 -10 10
|
||||
a10 10 0 0 0 10 10
|
||||
a10 10 0 0 0 10 -10
|
||||
a10 10 0 0 0 -10 -10
|
||||
"
|
||||
/>
|
||||
<path
|
||||
style="filter: invert(1)"
|
||||
fill="currentColor"
|
||||
d="
|
||||
M12 3
|
||||
v18
|
||||
a9 9 0 0 0 9 -9
|
||||
a9 9 0 0 0 -9 -9
|
||||
"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
stroke-width="0"
|
||||
d="
|
||||
M12.621094 23.988281
|
||||
c-6.960938 0 -12.621094 -5.664062000000001 -12.621094 -12.621093
|
||||
c0 -4.8203130000000005 2.683594 -9.148438 7.003906 -11.300781800000001
|
||||
c0.19140600000000063 -0.09765619999999942 0.4257819999999999 -0.058593699999999416 0.578125 0.09374980000000058
|
||||
c0.15234400000000026 0.152344 0.19531300000000051 0.38671900000000003 0.09765699999999988 0.582032
|
||||
c-0.7890629999999996 1.601562 -1.1875 3.320312 -1.1875 5.113281000000001
|
||||
c0.0 6.402342999999999 5.207031 11.613281 11.609374000000003 11.613281
|
||||
c1.8125 0.0 3.550781999999998 -0.41015600000000063 5.167968999999999 -1.2148439999999994
|
||||
c0.19531299999999874 -0.09765600000000063 0.42968799999999874 -0.05859399999999937 0.5820310000000006 0.09375
|
||||
c0.15234399999999937 0.15234399999999937 0.1914069999999981 0.38671899999999937 0.0976569999999981 0.5820319999999981
|
||||
c-2.140625 4.351562000000001 -6.480468999999999 7.058593000000002 -11.328125 7.058593000000002
|
||||
"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<path
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M2 22 l8 -8"
|
||||
/>
|
||||
<circle
|
||||
stroke="currentColor"
|
||||
stroke-width="3"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
cx="15.5"
|
||||
cy="8.5"
|
||||
r="6.5"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,45 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
|
||||
<circle fill="currentColor" r="6" cx="12" cy="12" />
|
||||
|
||||
<!-- sunbeams -->
|
||||
|
||||
<!-- left -->
|
||||
<circle fill="currentColor" r="2" cx="3" cy="12" />
|
||||
|
||||
<!-- right -->
|
||||
<circle fill="currentColor" r="2" cx="21" cy="12" />
|
||||
|
||||
<!-- top -->
|
||||
<circle fill="currentColor" r="2" cx="12" cy="3" />
|
||||
|
||||
<!-- top -->
|
||||
<circle fill="currentColor" r="2" cx="12" cy="21" />
|
||||
|
||||
<!-- top left -->
|
||||
<circle fill="currentColor" r="2" cx="5.636038969321072" cy="5.636038969321072" />
|
||||
|
||||
<!-- top right -->
|
||||
<circle fill="currentColor" r="2" cx="18.36396103067893" cy="5.636038969321072" />
|
||||
|
||||
<!-- bottom left -->
|
||||
<circle fill="currentColor" r="2" cx="5.636038969321072" cy="18.36396103067893" />
|
||||
|
||||
<!-- bottom right -->
|
||||
<circle fill="currentColor" r="2" cx="18.36396103067893" cy="18.36396103067893" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
@@ -0,0 +1,19 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="758.75" height="192.5">
|
||||
<defs><linearGradient id="gradient" x1="0%" y1="0%" x2="100%" y2="0%"><stop offset="0%" style="stop-color:#4ba8eb"/><stop offset="100%" style="stop-color:#043485"/></linearGradient></defs>
|
||||
<path stroke="url(#gradient)" stroke-width="15" fill="none" d=" M94 51 a43.75 43.75 0 0 0 -43.75 -43.75 a43.75 43.75 0 0 0 -43.75 43.75 a43.75 43.75 0 0 0 43.75 43.75 a43.75 43.75 0 0 1 43.75 43.75 a43.75 43.75 0 0 1 -43.75 43.75 a43.75 43.75 0 0 1 -43.75 -43.75 M138.75 95.25 h43.75 a43.75 43.75 0 0 0 43.75 -43.75 a43.75 43.75 0 0 0 -43.75 -43.75 a43.75 43.75 0 0 0 -43.75 43.75 v43.75 v43.75 a43.75 43.75 0 0 0 43.75 43.75 a43.75 43.75 0 0 0 43.75 -43.75 M270 95.25 h43.75 a43.75 43.75 0 0 0 43.75 -43.75 a43.75 43.75 0 0 0 -43.75 -43.75 a43.75 43.75 0 0 0 -43.75 43.75 v43.75 v43.75 a43.75 43.75 0 0 0 43.75 43.75 a43.75 43.75 0 0 0 43.75 -43.75 M401.25 0 v192.5 m0 -96.125 l87.5 -87.5 m-87.5 87.5 l87.5 87.5 M532.5 192.5 v-192.5 m0 51.25 a43.75 43.75 0 0 1 43.75 -43.75 a43.75 43.75 0 0 1 43.75 43.75 M663.75 50.25 a43.75 43.75 0 0 1 43.75 -43.75 a43.75 43.75 0 0 1 43.75 43.75 v43.75 v43.75 v43.75 m0 -43.75 a43.75 43.75 0 0 1 -43.75 43.75 a43.75 43.75 0 0 1 -43.75 -43.75 a43.75 43.75 0 0 1 43.75 -43.75 a43.75 43.75 0 0 1 43.75 43.75 v50"/></svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,75 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
|
||||
import { inject } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const colorScheme = inject('colorScheme');
|
||||
|
||||
const colorSchemeNextMapper = {
|
||||
'light': 'dark',
|
||||
'dark': 'auto',
|
||||
'auto': 'light'
|
||||
};
|
||||
|
||||
const colorSchemeIconMapper = {
|
||||
'dark': 'crescent-moon',
|
||||
'light': 'sun',
|
||||
'auto': 'circle-black-white'
|
||||
};
|
||||
|
||||
const getTooltipTranslation = function (colorScheme) {
|
||||
return t(`preferences.colorScheme.switch.${colorSchemeNextMapper[colorScheme]}`);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button class="color-scheme-button"
|
||||
@click="colorScheme = colorSchemeNextMapper[colorScheme]"
|
||||
:aria-label="getTooltipTranslation(colorScheme)"
|
||||
:title="getTooltipTranslation(colorScheme)"
|
||||
>
|
||||
<Icon
|
||||
:name="colorSchemeIconMapper[colorSchemeNextMapper[colorScheme]]"
|
||||
size="16"
|
||||
/>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.color-scheme-button {
|
||||
background: none;
|
||||
border: 1.5px solid var(--border);
|
||||
border-radius: 50%;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.color-scheme-button:hover {
|
||||
background: var(--light-hover);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export const useColorScheme = function () {
|
||||
const getColorScheme = function () {
|
||||
let colorScheme = localStorage.getItem('colorScheme') || 'auto';
|
||||
if (!(colorScheme === 'dark' || colorScheme === 'light' || colorScheme === 'auto')) {
|
||||
colorScheme = 'auto';
|
||||
};
|
||||
return colorScheme;
|
||||
};
|
||||
|
||||
const updateColorScheme = function (newScheme) {
|
||||
let actualNewScheme = newScheme;
|
||||
if (!(actualNewScheme === 'dark' || actualNewScheme === 'light' || actualNewScheme === 'auto')) {
|
||||
actualNewScheme = 'auto';
|
||||
};
|
||||
|
||||
localStorage.setItem('colorScheme', actualNewScheme);
|
||||
};
|
||||
|
||||
return {
|
||||
getColorScheme,
|
||||
updateColorScheme
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import LanguageSwitchButton from '@/features/i18n/components/LanguageSwitchButton.vue';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const startYear = 2026
|
||||
const currentYear = new Date().getFullYear()
|
||||
|
||||
const copyrightPeriod =
|
||||
startYear === currentYear
|
||||
? `${currentYear}`
|
||||
: `${startYear}-${currentYear}`
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="global-footer">
|
||||
<div class="footer-segment">
|
||||
<RouterLink to="/settings" class="link">
|
||||
{{ t('preferences.settings') }}
|
||||
</RouterLink>
|
||||
<LanguageSwitchButton />
|
||||
</div>
|
||||
<div class="footer-segment">
|
||||
© {{ copyrightPeriod }} Seekra
|
||||
</div>
|
||||
</footer>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.global-footer {
|
||||
--padding-y: 16px;
|
||||
}
|
||||
|
||||
.footer-segment {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 32px;
|
||||
padding: var(--padding-y);
|
||||
background-color: var(--light-bg);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.global-footer a {
|
||||
color: var(--dark);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,164 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
|
||||
import { ref } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { loadLanguage, LANGUAGES_RTL, SUPPORTED_LANGUAGES } from '@/i18n';
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
const isOpen = ref(false);
|
||||
const languageDropdown = ref(null);
|
||||
|
||||
async function selectLanguage(code) {
|
||||
await loadLanguage(code);
|
||||
localStorage.setItem('locale', code);
|
||||
document.documentElement.lang = code;
|
||||
document.documentElement.dir = LANGUAGES_RTL.includes(code) ? 'rtl' : 'ltr';
|
||||
close();
|
||||
};
|
||||
|
||||
const close = function () {
|
||||
document.removeEventListener('click', closeWrapperOnClickOutsite);
|
||||
isOpen.value = false;
|
||||
};
|
||||
|
||||
const closeWrapperOnClickOutsite = function (e) {
|
||||
if (languageDropdown.value) {
|
||||
if (!languageDropdown.value.contains(e.target)) {
|
||||
close();
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
const open = function () {
|
||||
if (!isOpen.value) {
|
||||
isOpen.value = true;
|
||||
setTimeout(() => {
|
||||
document.addEventListener('click', closeWrapperOnClickOutsite);
|
||||
}, 0);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="language-switch" tabindex="-1">
|
||||
<button
|
||||
class="language-button button"
|
||||
@click="open"
|
||||
:aria-expanded="isOpen"
|
||||
aria-haspopup="listbox"
|
||||
>
|
||||
<span class="lang-code">{{ t(`preferences.locale.languages.${locale}`) }}</span>
|
||||
<span class="chevron" :class="{ open: isOpen }">
|
||||
<Icon name="chevron-down" size="1em" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<ul
|
||||
v-if="isOpen"
|
||||
ref="languageDropdown"
|
||||
class="language-dropdown"
|
||||
role="listbox"
|
||||
:style="{
|
||||
bottom: 'calc(var(--offset) + 2 * var(--trigger-padding-y) + 1em)' // easier to add auto adaption to the available space
|
||||
}"
|
||||
>
|
||||
<li
|
||||
v-for="lang in SUPPORTED_LANGUAGES"
|
||||
:key="lang"
|
||||
role="option"
|
||||
:aria-selected="lang === locale"
|
||||
:class="{ active: lang === locale }"
|
||||
@click="selectLanguage(lang)"
|
||||
>
|
||||
<span class="lang-label">{{ t(`preferences.locale.languages.${lang}`) }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.language-switch {
|
||||
--trigger-padding-y: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
background: none;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
padding: var(--trigger-padding-y) 10px;
|
||||
cursor: pointer;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.language-button:hover {
|
||||
background-color: var(--light-hover);
|
||||
}
|
||||
|
||||
.chevron {
|
||||
font-size: 0.75rem;
|
||||
transition: transform 0.2s;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.chevron.open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.language-dropdown {
|
||||
--offset: 6px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background-color: var(--light-bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 6px 0;
|
||||
min-width: 160px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.language-dropdown li {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.language-dropdown li:hover {
|
||||
background-color: var(--light-hover);
|
||||
}
|
||||
|
||||
.language-dropdown li.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.flag {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { expect, test, describe, vi, beforeEach } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import LanguageSwitchButton from '../LanguageSwitchButton.vue';
|
||||
import { loadLanguage } from '@/i18n';
|
||||
|
||||
vi.mock('@/i18n', () => ({
|
||||
loadLanguage: vi.fn(() => Promise.resolve()),
|
||||
LANGUAGES_RTL: ['ar', 'he'],
|
||||
SUPPORTED_LANGUAGES: ['en', 'de', 'ar']
|
||||
}));
|
||||
|
||||
vi.mock('vue-i18n', () => ({
|
||||
useI18n: () => ({
|
||||
t: (key) => key,
|
||||
locale: { value: 'de' }
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('@/features/icons/components/Icon.vue', () => ({
|
||||
default: {
|
||||
name: 'Icon',
|
||||
template: '<span>Icon</span>'
|
||||
}
|
||||
}));
|
||||
|
||||
describe('LanguageSwitchButton', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
localStorage.clear();
|
||||
document.documentElement.lang = '';
|
||||
document.documentElement.dir = '';
|
||||
});
|
||||
|
||||
test('renders correctly with initial state closed', () => {
|
||||
const wrapper = mount(LanguageSwitchButton);
|
||||
|
||||
expect(wrapper.find('.language-button').exists()).toBe(true);
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
||||
expect(wrapper.find('.language-button').attributes('aria-expanded')).toBe('false');
|
||||
});
|
||||
|
||||
test('opens the dropdown when language button is clicked', async () => {
|
||||
const wrapper = mount(LanguageSwitchButton);
|
||||
const button = wrapper.find('.language-button');
|
||||
|
||||
await button.trigger('click');
|
||||
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
|
||||
expect(button.attributes('aria-expanded')).toBe('true');
|
||||
});
|
||||
|
||||
const languageTestCases = [
|
||||
{ code: 'en', expectedDir: 'ltr' },
|
||||
{ code: 'de', expectedDir: 'ltr' },
|
||||
{ code: 'ar', expectedDir: 'rtl' }
|
||||
];
|
||||
|
||||
test.for(languageTestCases)('selectLanguage($code) sets localStorage, html attributes and changes layout direction to $expectedDir', async ({ code, expectedDir }) => {
|
||||
const wrapper = mount(LanguageSwitchButton);
|
||||
|
||||
await wrapper.find('.language-button').trigger('click');
|
||||
|
||||
const options = wrapper.findAll('.language-dropdown li');
|
||||
const optionToClick = options.find(opt => opt.text().includes(code));
|
||||
|
||||
await optionToClick.trigger('click');
|
||||
|
||||
expect(loadLanguage).toHaveBeenCalledWith(code);
|
||||
expect(localStorage.getItem('locale')).toBe(code);
|
||||
expect(document.documentElement.lang).toBe(code);
|
||||
expect(document.documentElement.dir).toBe(expectedDir);
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('closes the dropdown when clicking outside the component', async () => {
|
||||
const wrapper = mount(LanguageSwitchButton, {
|
||||
attachTo: document.body
|
||||
});
|
||||
|
||||
await wrapper.find('.language-button').trigger('click');
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(true);
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 0));
|
||||
|
||||
const externalDiv = document.createElement('div');
|
||||
document.body.appendChild(externalDiv);
|
||||
|
||||
const clickEvent = new MouseEvent('click', { bubbles: true });
|
||||
externalDiv.dispatchEvent(clickEvent);
|
||||
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find('.language-dropdown').exists()).toBe(false);
|
||||
|
||||
wrapper.unmount();
|
||||
externalDiv.remove();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,54 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { ensureUnit } from '@/utils/cssDimensions';
|
||||
|
||||
const props = defineProps({
|
||||
name: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 24
|
||||
}
|
||||
})
|
||||
|
||||
const icons = import.meta.glob('@/assets/icons/*.svg', {
|
||||
eager: true,
|
||||
import: 'default'
|
||||
})
|
||||
|
||||
const Icon = computed(() => icons[`/src/assets/icons/${props.name}.svg`])
|
||||
|
||||
const size = computed(() => ensureUnit(props.size))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="icon-container">
|
||||
<img :src="Icon" :style="{ height: `${size}`, width: `${size}` }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
filter: var(--invert);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,94 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import ColorSchemeButton from '@/features/colorScheme/components/ColorSchemeButton.vue';
|
||||
import logo from '@/assets/images/logo.svg';
|
||||
import NavbarSearchBarWrapper from './NavbarSearchBarWrapper.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<nav class="global-nav">
|
||||
<RouterLink to="/" class="link button link">
|
||||
<img :src="logo" alt="Seekra" class="nav-logo" />
|
||||
</RouterLink>
|
||||
<NavbarSearchBarWrapper class="navbar-search-bar-wrapper" />
|
||||
<ul class="right-links">
|
||||
<li>
|
||||
<ColorSchemeButton />
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
<div class="navbar-search-bar-wrapper-small-screens-wrapper">
|
||||
<NavbarSearchBarWrapper class="navbar-search-bar-wrapper-small-screens" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.global-nav {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 18px var(--main-content-padding-x);
|
||||
height: 42px;
|
||||
}
|
||||
|
||||
.global-nav .right-links {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.global-nav .right-links a {
|
||||
text-decoration: none;
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.global-nav .right-links a:hover{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nav-logo {
|
||||
height: 24px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.navbar-search-bar-wrapper {
|
||||
width: 100%;
|
||||
padding: 0 30px;
|
||||
}
|
||||
|
||||
.navbar-search-bar-wrapper-small-screens-wrapper {
|
||||
padding: 0 15px 15px;
|
||||
}
|
||||
|
||||
.navbar-search-bar-wrapper-small-screens {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.navbar-search-bar-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navbar-search-bar-wrapper-small-screens {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,43 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Searchbar from '@/features/search/components/Searchbar.vue';
|
||||
|
||||
import { watch } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const searchQueryModel = defineModel();
|
||||
|
||||
watch(() => route.name, name => {
|
||||
searchQueryModel.value = name === 'searchResults' ? route.query.q || '' : '';
|
||||
});
|
||||
|
||||
watch(() => route.query.q, q => {
|
||||
if (route.name === 'searchResults') {
|
||||
searchQueryModel.value = q || '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Searchbar
|
||||
v-if="route.name === 'searchResults'"
|
||||
v-model="searchQueryModel"
|
||||
auto-submit
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,117 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
|
||||
const searchQuery = defineModel();
|
||||
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const router = useRouter()
|
||||
const props = defineProps(['autoSubmit'])
|
||||
|
||||
const submitSearch = function () {
|
||||
if (props.autoSubmit !== undefined) {
|
||||
router.push({
|
||||
name: 'searchResults',
|
||||
query: { q: searchQuery.value }
|
||||
});
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<form @submit.prevent="submitSearch" class="search-form">
|
||||
<div class="search-wrapper">
|
||||
<input
|
||||
v-model="searchQuery"
|
||||
type="search"
|
||||
:placeholder="t('search.searchBar.placeholder')"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
class="search-button"
|
||||
:title="t('search.searchBar.submit')"
|
||||
:aria-label="t('search.searchBar.submit')"
|
||||
>
|
||||
<Icon class="search-icon" name="magnifying-glass" size="1.1em" />
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-wrapper {
|
||||
--submit-button-padding-y: 8px;
|
||||
--content-height: 32px;
|
||||
--padding: 4px;
|
||||
--padding-left: calc(var(--content-height) + var(--padding));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1.5px solid var(--border);
|
||||
box-shadow: 0 0px 32px var(--blue-box-shadow);
|
||||
border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y) + var(--padding));
|
||||
padding: var(--padding);
|
||||
padding-left: var(--padding-left);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-wrapper input {
|
||||
border: none;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
font-size: 1rem;
|
||||
background: transparent;
|
||||
height: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
font-size: 1rem;
|
||||
height: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
|
||||
width: calc(var(--content-height) + 2 * var(--submit-button-padding-y));
|
||||
border-radius: calc(var(--content-height) * 0.5 + var(--submit-button-padding-y));
|
||||
border: none;
|
||||
padding: var(--submit-button-padding-y) 20px;
|
||||
background: var(--primary-color);
|
||||
color: var(--white);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.search-button .search-icon {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
.search-button:hover {
|
||||
background: var(--primary-color-l-1);
|
||||
}
|
||||
|
||||
.search-form {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="main-content-padding">
|
||||
<div class="search-results-error-message-container">
|
||||
<div class="search-results-error-message">
|
||||
<p>{{ t('search.error.searchNotAvailable') }}</p>
|
||||
<p>{{ t('error.tryAgainToAnotherTime') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="search-results-container">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-bar {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
@media (max-width: 67.5rem) {
|
||||
.search-bar {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.search-results-container {
|
||||
margin-top: 56px;
|
||||
}
|
||||
|
||||
.search-results-error-message-container {
|
||||
--error-message-height: 100px;
|
||||
--error-message-padding: 2em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-results-error-message {
|
||||
padding: var(--error-message-padding);
|
||||
font-size: 18px;
|
||||
box-shadow: 0px 0px 42px var(--gray-box-shadow);
|
||||
border-radius: 28px;
|
||||
text-align: center;
|
||||
background-color: var(--light-bg);
|
||||
animation: fade-in 0.8s ease-out;
|
||||
height: var(--error-message-height);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
outline: 1px solid var(--border);
|
||||
}
|
||||
|
||||
@media (max-width: 48rem) {
|
||||
.search-results-error-message-container {
|
||||
--error-message-height: 150px;
|
||||
}
|
||||
.search-results-error-message {
|
||||
padding: 1em;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
opacity: 0;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
100% {
|
||||
opacity: 100;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,126 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
import { useSettingsStore } from '../stores/settingsStore';
|
||||
import { computed, ref, useId, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const normalizeSelectedValue = function normalizeSelectedValue (value) {
|
||||
if (props.setting.allowMultiple) {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (value === undefined || value === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return [value];
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value[0] ?? null;
|
||||
}
|
||||
|
||||
return value ?? null;
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
setting: {
|
||||
required: true
|
||||
},
|
||||
path: {
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const store = useSettingsStore();
|
||||
|
||||
const optionType = computed(() => props.setting.allowMultiple ? 'checkbox' : 'radio');
|
||||
const selected = ref(normalizeSelectedValue(store.get(props.path) ?? props.setting.default));
|
||||
|
||||
watch(selected, (newValue) => {
|
||||
store.set(props.path, newValue);
|
||||
});
|
||||
|
||||
const labelId = useId();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="label" :id="labelId">
|
||||
{{ t(props.setting.i18n) }}
|
||||
</div>
|
||||
<ul class="options-list" :aria-labelledby="labelId">
|
||||
<li v-for="option in props.setting.options" :key="option.name">
|
||||
<label
|
||||
class="settings-button"
|
||||
:title="t(option.i18n)"
|
||||
:aria-label="t('settings.selection.option.ariaLabel', { option: t(option.i18n) })"
|
||||
>
|
||||
<input
|
||||
:type="optionType"
|
||||
:name="props.path"
|
||||
:value="option.name"
|
||||
class="option-input"
|
||||
v-model="selected"
|
||||
>
|
||||
<div class="option-check-mark-wrapper">
|
||||
<Icon class="option-check-mark" name="check-mark" size="1em" />
|
||||
</div>
|
||||
<div class="input-label">
|
||||
{{ t(option.i18n) }}
|
||||
</div>
|
||||
</label>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped src="./SettingsButton.css"></style>
|
||||
<style scoped>
|
||||
.options-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
.option-input {
|
||||
appearance: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.option-check-mark-wrapper {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.option-check-mark {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.option-input:checked + .option-check-mark-wrapper .option-check-mark {
|
||||
visibility: visible;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,19 @@
|
||||
.settings-button {
|
||||
--padding-x: 0.7em;
|
||||
padding: 0.3em var(--padding-x);
|
||||
margin-top: 0.2em;
|
||||
width: calc(100% - 2 * var(--padding-x));
|
||||
border-radius: 0.8em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5em;
|
||||
cursor: pointer;
|
||||
background-color: var(--light-bg);
|
||||
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
|
||||
.settings-button:hover {
|
||||
background-color: var(--light-hover);
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
|
||||
import { useSettingsStore } from '../stores/settingsStore';
|
||||
|
||||
import { computed, ref, useId, watch } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
setting: {
|
||||
required: true
|
||||
},
|
||||
path: {
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const store = useSettingsStore();
|
||||
|
||||
const inputModel = ref(store.get(props.path) ?? props.setting.default);
|
||||
let oldInputValue = inputModel.value;
|
||||
|
||||
const doneButtonInitial = ref(true);
|
||||
|
||||
const inputId = useId();
|
||||
const inputLabelId = useId();
|
||||
|
||||
const inputType = computed(() => ({
|
||||
number: 'number',
|
||||
string: 'text'
|
||||
}[props.setting.type]
|
||||
));
|
||||
|
||||
const hasChanged = computed(() =>
|
||||
inputModel.value !== store.get(props.path)
|
||||
);
|
||||
|
||||
watch(store, (newValue) => {
|
||||
const newInputValue = newValue.get(props.path);
|
||||
if (inputModel.value === oldInputValue) {
|
||||
inputModel.value = newInputValue;
|
||||
oldInputValue = newInputValue;
|
||||
};
|
||||
});
|
||||
|
||||
watch(inputModel, () => {
|
||||
doneButtonInitial.value = false;
|
||||
});
|
||||
|
||||
const apply = function apply () {
|
||||
store.set(props.path, inputModel.value);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="input-setting-container">
|
||||
<label :for="inputId" :id="inputLabelId">
|
||||
{{ t(props.setting.i18n) }}
|
||||
</label>
|
||||
<form
|
||||
@submit.prevent="apply"
|
||||
class="input-container"
|
||||
:class="{ 'done-button-shown': hasChanged }"
|
||||
>
|
||||
<input
|
||||
:type="inputType"
|
||||
:id="inputId"
|
||||
:aria-labelledby="inputLabelId"
|
||||
v-model="inputModel"
|
||||
class="input"
|
||||
/>
|
||||
<button
|
||||
ref="doneButton"
|
||||
class="done-button button"
|
||||
:class="{ initial: doneButtonInitial }"
|
||||
type="submit"
|
||||
:title="t('settings.input.done.title')"
|
||||
:aria-label="t('settings.input.done.ariaLabel')"
|
||||
>
|
||||
<Icon name="check-mark" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.input-setting-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
position: relative;
|
||||
width: 50%;
|
||||
white-space: nowrap;
|
||||
--total-animation-duration: 0.3s;
|
||||
}
|
||||
|
||||
.input {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: calc(var(--input-height) + 2 * var(--input-padding-y));
|
||||
display: inline;
|
||||
vertical-align: middle;
|
||||
--input-height: 24px;
|
||||
transition: width calc(var(--total-animation-duration) / 3);
|
||||
}
|
||||
|
||||
.input-container:not(.done-button-shown) .input {
|
||||
transition-delay: calc((2 / 3) * var(--total-animation-duration));
|
||||
}
|
||||
|
||||
.input-container.done-button-shown .input {
|
||||
width: calc(100% - 1.5 * var(--input-padding) - 26px - 0.75 * var(--input-height) + 1.5px);
|
||||
}
|
||||
|
||||
.done-button {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 50%;
|
||||
border: 1px solid var(--border);
|
||||
min-height: var(--input-height);
|
||||
padding: var(--input-padding);
|
||||
transform: translateY(-50%) scale(0);
|
||||
transform-origin: center;
|
||||
vertical-align: middle;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: calc(var(--input-padding) + 13px);
|
||||
font-size: 1em;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.done-button:not(.initial) {
|
||||
animation: done-button-disappear var(--total-animation-duration);
|
||||
}
|
||||
|
||||
.input-container.done-button-shown .done-button {
|
||||
animation: done-button-appear var(--total-animation-duration);
|
||||
display: inline-flex;
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
|
||||
@keyframes done-button-appear {
|
||||
0% {
|
||||
transform: translateY(-50%) scale(0);
|
||||
}
|
||||
10% {
|
||||
transform: translateY(-50%) scale(0);
|
||||
}
|
||||
95% {
|
||||
transform: translateY(-50%) scale(1.05);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes done-button-disappear {
|
||||
0% {
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-50%) scale(0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
@keyframes done-button-appear {
|
||||
0% {
|
||||
transform: translateY(-50%) scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
10% {
|
||||
transform: translateY(-50%) scale(1);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(-50%) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes done-button-disappear {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,93 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Switch from './Switch.vue';
|
||||
import SettingsInput from './SettingsInput.vue';
|
||||
import Selection from './Selection.vue';
|
||||
import SettingsPageLink from './SettingsPageLink.vue';
|
||||
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
setting: {
|
||||
required: true
|
||||
},
|
||||
path: {
|
||||
required: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="section-heading">
|
||||
{{ t(props.setting.i18n) }}
|
||||
</h2>
|
||||
<div
|
||||
v-for="setting in props.setting.content"
|
||||
:key="setting"
|
||||
class="setting"
|
||||
>
|
||||
<Switch
|
||||
v-if="setting.type === 'bool'"
|
||||
:setting="setting"
|
||||
:path="`${props.path}.${setting.name}`"
|
||||
/>
|
||||
<SettingsInput
|
||||
v-if="setting.type === 'string' || setting.type === 'number'"
|
||||
:setting="setting"
|
||||
:path="`${props.path}.${setting.name}`"
|
||||
/>
|
||||
<Selection
|
||||
v-if="setting.type === 'selection'"
|
||||
:setting="setting"
|
||||
:path="`${props.path}.${setting.name}`"
|
||||
/>
|
||||
<SettingsPageLink
|
||||
v-if="setting.type === 'section'"
|
||||
:display-name="t(setting.i18n)"
|
||||
:target="`${props.path}.${setting.name}`"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.setting {
|
||||
margin-bottom: 1.2em;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
--size: 1.2em;
|
||||
font-size: var(--size);
|
||||
margin-bottom: calc(var(--size) - 0.2em);
|
||||
}
|
||||
|
||||
.section-heading[aria-level="2"] {
|
||||
--size: 1.8em;
|
||||
}
|
||||
|
||||
.section-heading[aria-level="3"] {
|
||||
--size: 1.6em;
|
||||
}
|
||||
|
||||
.section-heading[aria-level="4"] {
|
||||
--size: 1.4em;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Icon from '@/features/icons/components/Icon.vue';
|
||||
import { useSettingsPage } from '../composables/useSettingsPage';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
const { goToSettingsPage } = useSettingsPage();
|
||||
|
||||
const props = defineProps({
|
||||
target: {
|
||||
required: true,
|
||||
type: String
|
||||
},
|
||||
displayName: {
|
||||
required: true,
|
||||
type: String
|
||||
}
|
||||
});
|
||||
|
||||
const followLink = function followLink () {
|
||||
goToSettingsPage(props.target);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="settings-button button settings-page-link"
|
||||
:title="props.displayName"
|
||||
:aria-label="t('settings.pages.link.ariaLabel', { page: props.displayName })"
|
||||
@click="followLink"
|
||||
>
|
||||
{{ props.displayName }}
|
||||
<Icon name="chevron-right" size="1em" />
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped src="./SettingsButton.css"></style>
|
||||
<style scoped>
|
||||
.settings-page-link {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import { useSettingsStore } from '../stores/settingsStore';
|
||||
import { useId } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps({
|
||||
setting: {
|
||||
required: true
|
||||
},
|
||||
path: {
|
||||
required: true
|
||||
}
|
||||
});
|
||||
|
||||
const labelId = useId();
|
||||
const switchId = useId();
|
||||
|
||||
const store = useSettingsStore();
|
||||
|
||||
const toggle = function toggle () {
|
||||
store.set(props.path, !store.get(props.path));
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="switch-container" @click="toggle">
|
||||
<label :for="switchId" :id="labelId" class="switch-label">
|
||||
{{ t(props.setting.i18n) }}
|
||||
</label>
|
||||
<div
|
||||
class="switch-wrapper"
|
||||
:class="{ enabled: store.get(props.path) }"
|
||||
:title="t('settings.switch.title')"
|
||||
:aria-label="t('settings.switch.ariaLabel')"
|
||||
:aria-labelledby="labelId"
|
||||
>
|
||||
<div class="switch-point"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.switch-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.switch-label {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.switch-wrapper {
|
||||
--point-size: 0.9em;
|
||||
--padding: 2px;
|
||||
--transition-duration: 0.2s;
|
||||
background-color: var(--light-bg);
|
||||
outline: 1px solid var(--border);
|
||||
width: calc(2 * var(--point-size));
|
||||
padding: var(--padding);
|
||||
border-radius: calc(0.5 * var(--point-size) + var(--padding));
|
||||
transition: background-color var(--transition-duration) ease;
|
||||
}
|
||||
|
||||
.switch-point {
|
||||
height: var(--point-size);
|
||||
width: var(--point-size);
|
||||
background-color: var(--border);
|
||||
border-radius: calc(0.5 * var(--point-size));
|
||||
margin-left: 0;
|
||||
transition: margin-left var(--transition-duration) ease;
|
||||
}
|
||||
|
||||
.switch-wrapper.enabled {
|
||||
background-color: var(--primary-color);
|
||||
}
|
||||
|
||||
.switch-wrapper.enabled .switch-point {
|
||||
margin-left: var(--point-size);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,267 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Selection from '../Selection.vue';
|
||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||
import { useSettingsStore } from '../../stores/settingsStore.js';
|
||||
import { expect, describe, test } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const getWrapper = function getWrapper ({
|
||||
i18n = 'selection',
|
||||
options = [],
|
||||
allowMultiple = false,
|
||||
defaultValue = undefined,
|
||||
translations = {},
|
||||
piniaOptions = {}
|
||||
} = {}) {
|
||||
return mountComponent(Selection, {
|
||||
attrs: {
|
||||
setting: {
|
||||
name: 'selection',
|
||||
type: 'selection',
|
||||
i18n,
|
||||
options,
|
||||
allowMultiple,
|
||||
default: defaultValue
|
||||
},
|
||||
path: 'selection'
|
||||
}
|
||||
}, translations, [], piniaOptions);
|
||||
};
|
||||
|
||||
const getChecked = function getChecked (inputs) {
|
||||
return inputs.map((input) => input.element.matches(':checked'));
|
||||
};
|
||||
|
||||
const exampleOptions = [
|
||||
{ name: 'o0', i18n: 'o0' },
|
||||
{ name: 'o1', i18n: 'o1' },
|
||||
{ name: 'o2', i18n: 'o2' }
|
||||
];
|
||||
|
||||
describe('Selection', () => {
|
||||
test('displays options correctly', () => {
|
||||
const wrapper = getWrapper({ options: exampleOptions });
|
||||
|
||||
const listItems = wrapper.findAll('li');
|
||||
expect(listItems.length).toBe(3);
|
||||
|
||||
for (let i = 0; i <= 2; i++) {
|
||||
const option = exampleOptions[i];
|
||||
const listItem = listItems[i];
|
||||
|
||||
expect(listItem.find('.input-label').text()).toBe(option.i18n);
|
||||
|
||||
const input = listItem.find('input');
|
||||
expect(input.attributes('type')).toBe('radio');
|
||||
expect(input.attributes('name')).toBe('selection');
|
||||
|
||||
expect(input.element.matches(':checked')).toBe(false);
|
||||
};
|
||||
});
|
||||
|
||||
test('shows value from store', () => {
|
||||
const wrapper = getWrapper({
|
||||
options: exampleOptions,
|
||||
piniaOptions: {
|
||||
setupStores: () => {
|
||||
const store = useSettingsStore();
|
||||
store.set('selection', 'o1');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const inputs = wrapper.findAll('input');
|
||||
const inputsChecked = getChecked(inputs);
|
||||
expect(inputsChecked).toStrictEqual([false, true, false]);
|
||||
});
|
||||
|
||||
test('shows multiple values from store', () => {
|
||||
const wrapper = getWrapper({
|
||||
options: exampleOptions,
|
||||
piniaOptions: {
|
||||
setupStores: () => {
|
||||
const store = useSettingsStore();
|
||||
store.set('selection', ['o1', 'o2']);
|
||||
}
|
||||
},
|
||||
allowMultiple: true
|
||||
});
|
||||
|
||||
const inputs = wrapper.findAll('input');
|
||||
const inputsChecked = getChecked(inputs);
|
||||
expect(inputsChecked).toStrictEqual([false, true, true]);
|
||||
});
|
||||
|
||||
test('shows default value', () => {
|
||||
const wrapper = getWrapper({
|
||||
options: exampleOptions,
|
||||
defaultValue: 'o2'
|
||||
});
|
||||
|
||||
const inputs = wrapper.findAll('input');
|
||||
const inputsChecked = getChecked(inputs);
|
||||
expect(inputsChecked).toStrictEqual([false, false, true]);
|
||||
});
|
||||
|
||||
test('shows multiple default values', () => {
|
||||
const wrapper = getWrapper({
|
||||
options: exampleOptions,
|
||||
defaultValue: ['o0', 'o1'],
|
||||
allowMultiple: true
|
||||
});
|
||||
|
||||
const inputs = wrapper.findAll('input');
|
||||
const inputsChecked = getChecked(inputs);
|
||||
expect(inputsChecked).toStrictEqual([true, true, false]);
|
||||
});
|
||||
|
||||
test('shows store value even if default value is set', () => {
|
||||
const wrapper = getWrapper({
|
||||
options: exampleOptions,
|
||||
default: 'o2',
|
||||
piniaOptions: {
|
||||
setupStores: () => {
|
||||
const store = useSettingsStore();
|
||||
store.set('selection', 'o1');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const inputs = wrapper.findAll('input');
|
||||
const inputsChecked = getChecked(inputs);
|
||||
expect(inputsChecked).toStrictEqual([false, true, false]);
|
||||
});
|
||||
|
||||
test('stores one value', async () => {
|
||||
const wrapper = getWrapper({ options: exampleOptions });
|
||||
|
||||
const store = useSettingsStore();
|
||||
expect(store.get('selection')).toBeUndefined();
|
||||
|
||||
const inputs = wrapper.findAll('input');
|
||||
const inputO0 = inputs[0];
|
||||
const inputO1 = inputs[1];
|
||||
const inputO2 = inputs[2];
|
||||
|
||||
await inputO0.setValue(true);
|
||||
await nextTick();
|
||||
expect(store.get('selection')).toBe('o0');
|
||||
|
||||
await inputO1.setValue(true);
|
||||
await nextTick();
|
||||
expect(store.get('selection')).toBe('o1');
|
||||
|
||||
await inputO2.setValue(true);
|
||||
await nextTick();
|
||||
expect(store.get('selection')).toBe('o2');
|
||||
expect(inputO2.element.matches(':checked')).toBe(true);
|
||||
});
|
||||
|
||||
test('stores multiple values', async () => {
|
||||
const setValue = async function setValue (input, value) {
|
||||
input.setValue(value);
|
||||
await nextTick();
|
||||
};
|
||||
|
||||
const check = function check (input) {
|
||||
return setValue(input, true);
|
||||
};
|
||||
|
||||
const uncheck = function uncheck (input) {
|
||||
return setValue(input, false);
|
||||
};
|
||||
|
||||
const expectSelectionValue = function expectSelectionValue (value, options = exampleOptions) {
|
||||
value.sort();
|
||||
expect(store.get('selection').sort()).toStrictEqual(value);
|
||||
const checkedInputs = getChecked(inputs);
|
||||
expect(
|
||||
options
|
||||
.map((option) => option.name)
|
||||
.filter((entry, i) => checkedInputs[i])
|
||||
).toStrictEqual(value);
|
||||
};
|
||||
|
||||
const wrapper = getWrapper({
|
||||
options: exampleOptions,
|
||||
allowMultiple: true
|
||||
});
|
||||
|
||||
const store = useSettingsStore();
|
||||
expect(store.get('selection')).toBeUndefined();
|
||||
|
||||
const inputs = wrapper.findAll('input');
|
||||
const inputO0 = inputs[0];
|
||||
const inputO1 = inputs[1];
|
||||
const inputO2 = inputs[2];
|
||||
|
||||
await check(inputO0);
|
||||
expectSelectionValue(['o0'])
|
||||
|
||||
await uncheck(inputO0);
|
||||
expectSelectionValue([]);
|
||||
|
||||
await check(inputO1);
|
||||
expectSelectionValue(['o1']);
|
||||
|
||||
await check(inputO0);
|
||||
expectSelectionValue(['o0', 'o1']);
|
||||
|
||||
await check(inputO2);
|
||||
expectSelectionValue(['o0', 'o1', 'o2']);
|
||||
|
||||
await uncheck(inputO2);
|
||||
expectSelectionValue(['o0', 'o1']);
|
||||
|
||||
await check(inputO2);
|
||||
expectSelectionValue(['o0', 'o1', 'o2']);
|
||||
|
||||
await uncheck(inputO1);
|
||||
expectSelectionValue(['o0', 'o2']);
|
||||
|
||||
await inputO0.setValue(true);
|
||||
expectSelectionValue(['o0', 'o2']);
|
||||
|
||||
await inputO0.setValue(false);
|
||||
expectSelectionValue(['o2']);
|
||||
|
||||
await inputO1.setValue(true);
|
||||
expectSelectionValue(['o1', 'o2']);
|
||||
|
||||
await inputO2.setValue(false);
|
||||
expectSelectionValue(['o1']);
|
||||
|
||||
await inputO1.setValue(true);
|
||||
expectSelectionValue(['o1']);
|
||||
|
||||
await inputO1.setValue(false);
|
||||
expectSelectionValue([]);
|
||||
});
|
||||
|
||||
test('shows correct translation', () => {
|
||||
const translation = 'Selection setting';
|
||||
const wrapper = getWrapper({
|
||||
translations: {
|
||||
selection: translation
|
||||
}
|
||||
});
|
||||
|
||||
const label = wrapper.find('.label');
|
||||
expect(label.text()).toBe(translation);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,210 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SettingsInput from '../SettingsInput.vue';
|
||||
import { mountComponent } from '@/test-utils/mountComponent';
|
||||
import { useSettingsStore } from '../../stores/settingsStore.js';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const getWrapper = function getWrapper({ type = 'string', i18n = 'input', defaultValue = undefined, piniaOptions = {} } = {}) {
|
||||
return mountComponent(SettingsInput, {
|
||||
attrs: {
|
||||
setting: {
|
||||
name: 'input',
|
||||
default: defaultValue,
|
||||
i18n,
|
||||
type
|
||||
},
|
||||
path: 'input'
|
||||
}
|
||||
}, {}, [], piniaOptions);
|
||||
}
|
||||
|
||||
describe('SettingsInput', () => {
|
||||
test('uses correct input type', () => {
|
||||
const stringSettingWrapper = getWrapper();
|
||||
|
||||
const stringInputElement = stringSettingWrapper.find('input');
|
||||
expect(stringInputElement.attributes('type')).toBe('text');
|
||||
|
||||
const numberSettingWrapper = getWrapper({ type: 'number' });
|
||||
|
||||
const numberInputElement = numberSettingWrapper.find('input');
|
||||
expect(numberInputElement.attributes('type')).toBe('number');
|
||||
});
|
||||
|
||||
test('shows empty value', () => {
|
||||
const stringSettingWrapper = getWrapper();
|
||||
|
||||
const inputElement = stringSettingWrapper.find('input');
|
||||
expect(inputElement.attributes('value')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('shows value from store', () => {
|
||||
const value = 'example value';
|
||||
const stringSettingWrapper = getWrapper({
|
||||
type: 'string',
|
||||
i18n: 'input',
|
||||
piniaOptions: {
|
||||
setupStores: () => {
|
||||
const store = useSettingsStore();
|
||||
store.settings.input = value;
|
||||
}
|
||||
}
|
||||
});
|
||||
const inputElement = stringSettingWrapper.find('input');
|
||||
|
||||
expect(inputElement.element.value).toBe(value);
|
||||
});
|
||||
|
||||
test('shows default value', () => {
|
||||
const value = 'example value';
|
||||
const wrapper = getWrapper({
|
||||
type: 'string',
|
||||
i18n: 'input',
|
||||
defaultValue: value
|
||||
});
|
||||
const inputElement = wrapper.find('input');
|
||||
|
||||
expect(inputElement.element.value).toBe(value);
|
||||
});
|
||||
|
||||
test('store value instead of default value', () => {
|
||||
const storeValue = 'store value';
|
||||
const defaultValue = 'default value';
|
||||
const wrapper = getWrapper({
|
||||
type: 'string',
|
||||
i18n: 'input',
|
||||
defaultValue: defaultValue,
|
||||
piniaOptions: {
|
||||
setupStores: () => {
|
||||
const store = useSettingsStore();
|
||||
store.settings.input = storeValue;
|
||||
}
|
||||
}
|
||||
});
|
||||
const inputElement = wrapper.find('input');
|
||||
|
||||
expect(inputElement.element.value).toBe(storeValue);
|
||||
});
|
||||
|
||||
test('stores value', async () => {
|
||||
const value = 'example value';
|
||||
const wrapper = getWrapper();
|
||||
|
||||
const store = useSettingsStore();
|
||||
expect(store.get('input')).toBeUndefined();
|
||||
|
||||
const inputElement = wrapper.find('input');
|
||||
await inputElement.setValue(value);
|
||||
|
||||
const form = wrapper.find('form');
|
||||
await form.trigger('submit');
|
||||
|
||||
expect(store.get('input')).toBe(value);
|
||||
});
|
||||
|
||||
test('reacts on store change', async () => {
|
||||
const value = 'example value';
|
||||
const value2 = 'example value 2';
|
||||
const anotherValue = 'another value';
|
||||
const wrapper = getWrapper();
|
||||
|
||||
const inputElement = wrapper.find('input');
|
||||
expect(inputElement.element.value).toBe('');
|
||||
|
||||
const store = useSettingsStore();
|
||||
store.set('input', value);
|
||||
await nextTick();
|
||||
expect(inputElement.element.value).toBe(value);
|
||||
|
||||
await inputElement.setValue(anotherValue);
|
||||
expect(inputElement.element.value).toBe(anotherValue);
|
||||
|
||||
// input does not update if there are unsaved changes
|
||||
store.set('input', value2);
|
||||
expect(inputElement.element.value).toBe(anotherValue);
|
||||
|
||||
const form = wrapper.find('form');
|
||||
expect(form.classes('done-button-shown')).toBeTruthy();
|
||||
|
||||
await form.trigger('submit');
|
||||
expect(store.get('input')).toBe(anotherValue);
|
||||
|
||||
expect(form.classes('done-button-shown')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('shows done button correctly', async () => {
|
||||
const wrapper = getWrapper();
|
||||
|
||||
const form = wrapper.find('form');
|
||||
const inputElement = wrapper.find('input');
|
||||
|
||||
expect(form.classes('done-button-shown')).toBeFalsy();
|
||||
|
||||
await inputElement.setValue('value');
|
||||
expect(form.classes('done-button-shown')).toBeTruthy();
|
||||
|
||||
await form.trigger('submit');
|
||||
expect(form.classes('done-button-shown')).toBeFalsy();
|
||||
|
||||
await inputElement.setValue('Hello, World!');
|
||||
expect(form.classes('done-button-shown')).toBeTruthy();
|
||||
|
||||
await form.trigger('submit');
|
||||
expect(form.classes('done-button-shown')).toBeFalsy();
|
||||
|
||||
await inputElement.setValue('');
|
||||
expect(form.classes('done-button-shown')).toBeTruthy();
|
||||
|
||||
await form.trigger('submit');
|
||||
expect(form.classes('done-button-shown')).toBeFalsy();
|
||||
|
||||
await inputElement.setValue('a');
|
||||
expect(form.classes('done-button-shown')).toBeTruthy();
|
||||
|
||||
const store = useSettingsStore();
|
||||
store.set('input', 'a');
|
||||
await nextTick();
|
||||
expect(form.classes('done-button-shown')).toBeFalsy();
|
||||
|
||||
await inputElement.setValue('');
|
||||
store.set('input', '');
|
||||
await nextTick();
|
||||
expect(form.classes('done-button-shown')).toBeFalsy();
|
||||
});
|
||||
|
||||
test('shows correct translation', () => {
|
||||
const translation = 'Text input';
|
||||
|
||||
const wrapper = mountComponent(SettingsInput, {
|
||||
attrs: {
|
||||
setting: {
|
||||
name: 'input',
|
||||
i18n: 'input',
|
||||
type: 'string',
|
||||
},
|
||||
path: 'input'
|
||||
}
|
||||
}, {
|
||||
input: translation
|
||||
});
|
||||
|
||||
const label = wrapper.find('label');
|
||||
expect(label.text()).toBe(translation);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Selection from '../Selection.vue';
|
||||
import SettingsInput from '../SettingsInput.vue';
|
||||
import SettingsPage from '../SettingsPage.vue';
|
||||
import SettingsPageLink from '../SettingsPageLink.vue';
|
||||
import Switch from '../Switch.vue';
|
||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
|
||||
const getWrapper = function getWrapper({ content = [], translations = {}, piniaOptions = {} }) {
|
||||
return mountComponent(SettingsPage, {
|
||||
attrs: {
|
||||
setting: {
|
||||
name: 'section',
|
||||
type: 'section',
|
||||
i18n: 'section',
|
||||
content: content
|
||||
},
|
||||
path: 'section'
|
||||
}
|
||||
}, translations, [], piniaOptions);
|
||||
};
|
||||
|
||||
const exampleSettings = [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'b0',
|
||||
i18n: 'settings.section.b0'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'n0',
|
||||
i18n: 'settings.section.n0'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'st0',
|
||||
i18n: 'settings.section1.st0'
|
||||
},
|
||||
{
|
||||
type: 'selection',
|
||||
name: 'se0',
|
||||
i18n: 'settings.section.se0',
|
||||
options: [
|
||||
{ name: 'o0', i18n: 'settings.section.se0.o0' },
|
||||
{ name: 'o1', i18n: 'settings.section.se0.o1' },
|
||||
{ name: 'o2', i18n: 'settings.section.se0.o2' },
|
||||
{ name: 'o3', i18n: 'settings.section.se0.o3' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 's0',
|
||||
i18n: 'settings.section.s0',
|
||||
content: []
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 's1',
|
||||
i18n: 'settings.section.s1',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'b0',
|
||||
i18n: 'settings.section.s1.b0',
|
||||
default: true
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
describe('SettingsPage', () => {
|
||||
test('shows correct heading', () => {
|
||||
const translation = 'Example section';
|
||||
const wrapper = getWrapper({
|
||||
translations: { section: translation }
|
||||
});
|
||||
|
||||
const heading = wrapper.find('.section-heading');
|
||||
expect(heading.text()).toBe(translation);
|
||||
});
|
||||
|
||||
test('shows settings correctly', () => {
|
||||
const wrapper = getWrapper({
|
||||
content: exampleSettings
|
||||
});
|
||||
|
||||
|
||||
const settingsWrappers = wrapper.findAll('.setting');
|
||||
expect(settingsWrappers.length).toBe(exampleSettings.length);
|
||||
|
||||
exampleSettings.forEach((expectedSetting, index) => {
|
||||
const settingWrapper = settingsWrappers[index];
|
||||
|
||||
const switchSetting = settingWrapper.findComponent(Switch);
|
||||
const inputSetting = settingWrapper.findComponent(SettingsInput);
|
||||
const selectionSetting = settingWrapper.findComponent(Selection);
|
||||
const settingsPageLink = settingWrapper.findComponent(SettingsPageLink);
|
||||
|
||||
let setting;
|
||||
if (expectedSetting.type === 'bool') {
|
||||
expect(switchSetting.exists()).toBe(true);
|
||||
expect(inputSetting.exists()).toBe(false);
|
||||
expect(selectionSetting.exists()).toBe(false);
|
||||
expect(settingsPageLink.exists()).toBe(false);
|
||||
|
||||
setting = switchSetting;
|
||||
} else if (expectedSetting.type === 'string' || expectedSetting.type === 'number') {
|
||||
expect(switchSetting.exists()).toBe(false);
|
||||
expect(inputSetting.exists()).toBe(true);
|
||||
expect(selectionSetting.exists()).toBe(false);
|
||||
expect(settingsPageLink.exists()).toBe(false);
|
||||
|
||||
setting = inputSetting;
|
||||
} else if (expectedSetting.type === 'selection') {
|
||||
expect(switchSetting.exists()).toBe(false);
|
||||
expect(inputSetting.exists()).toBe(false);
|
||||
expect(selectionSetting.exists()).toBe(true);
|
||||
expect(settingsPageLink.exists()).toBe(false);
|
||||
|
||||
setting = selectionSetting;
|
||||
} else {
|
||||
expect(switchSetting.exists()).toBe(false);
|
||||
expect(inputSetting.exists()).toBe(false);
|
||||
expect(selectionSetting.exists()).toBe(false);
|
||||
expect(settingsPageLink.exists()).toBe(true);
|
||||
|
||||
setting = settingsPageLink;
|
||||
}
|
||||
if (
|
||||
expectedSetting.type === 'bool' ||
|
||||
expectedSetting.type === 'string' ||
|
||||
expectedSetting.type === 'number' ||
|
||||
expectedSetting.type === 'selection'
|
||||
) {
|
||||
expect(setting.props('setting')).toMatchObject(expectedSetting);
|
||||
expect(setting.props('path')).toBe(`section.${expectedSetting.name}`);
|
||||
} else {
|
||||
expect(setting.props('displayName')).toBe(expectedSetting.i18n);
|
||||
expect(setting.props('target')).toBe(`section.${expectedSetting.name}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SettingsPageLink from '../SettingsPageLink.vue';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
|
||||
const goToSettingsPage = vi.fn();
|
||||
vi.mock('../../composables/useSettingsPage.js', () => ({
|
||||
useSettingsPage: () => ({
|
||||
goToSettingsPage
|
||||
})
|
||||
}));
|
||||
|
||||
const getWrapper = function getWrapper({ target, displayName }) {
|
||||
return mount(SettingsPageLink, {
|
||||
attrs: {
|
||||
target,
|
||||
displayName
|
||||
},
|
||||
global: {
|
||||
plugins: [
|
||||
createI18n()
|
||||
]
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
describe('SettingsPageLink', () => {
|
||||
test('shows correct display name', () => {
|
||||
const displayName = 'A settings page link';
|
||||
const wrapper = getWrapper({ displayName });
|
||||
expect(wrapper.text()).toBe(displayName);
|
||||
});
|
||||
|
||||
test('follows link', async () => {
|
||||
const target = 'section1.subsection2';
|
||||
const wrapper = getWrapper({ target });
|
||||
|
||||
const button = wrapper.find('button');
|
||||
await button.trigger('click');
|
||||
|
||||
expect(goToSettingsPage).toHaveBeenCalledWith(target);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import Switch from '../Switch.vue';
|
||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||
import { useSettingsStore } from '../../stores/settingsStore.js';
|
||||
import { expect, describe, test } from 'vitest';
|
||||
import { nextTick } from 'vue';
|
||||
|
||||
const getWrapper = function getWrapper ({ i18n = 'switch1', translations = {} } = {}) {
|
||||
return mountComponent(Switch, {
|
||||
attrs: {
|
||||
setting: {
|
||||
type: 'bool',
|
||||
name: 'switch',
|
||||
i18n
|
||||
},
|
||||
path: 'switch'
|
||||
}
|
||||
}, translations);
|
||||
};
|
||||
|
||||
describe('Switch', () => {
|
||||
test('shows value from store', async () => {
|
||||
const wrapper = getWrapper();
|
||||
|
||||
const store = useSettingsStore();
|
||||
store.set('switch', true);
|
||||
await nextTick();
|
||||
|
||||
const switchElement = wrapper.find('.switch-wrapper');
|
||||
const switchElementClasses = switchElement.classes();
|
||||
|
||||
expect(switchElementClasses).toContain('enabled');
|
||||
|
||||
store.set('switch', false);
|
||||
await nextTick();
|
||||
|
||||
const switchElementClasses2 = switchElement.classes();
|
||||
expect(switchElementClasses2).not.toContain('enabled');
|
||||
});
|
||||
|
||||
test('toggles value in store', async () => {
|
||||
const wrapper = getWrapper();
|
||||
|
||||
const switchElement = wrapper.find('.switch-wrapper');
|
||||
|
||||
const store = useSettingsStore();
|
||||
expect(store.get('switch')).toBeUndefined();
|
||||
|
||||
switchElement.trigger('click');
|
||||
await nextTick();
|
||||
expect(store.get('switch')).toBe(true);
|
||||
|
||||
switchElement.trigger('click');
|
||||
await nextTick();
|
||||
expect(store.get('switch')).toBe(false);
|
||||
});
|
||||
|
||||
test('shows correct translation', () => {
|
||||
const translation = 'Switch';
|
||||
|
||||
const wrapper = getWrapper({
|
||||
translations: { switch1: translation }
|
||||
})
|
||||
|
||||
const label = wrapper.find('label');
|
||||
expect(label.text()).toBe(translation);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { useSettingsPage } from '../useSettingsPage.js';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const pushRoute = vi.fn();
|
||||
vi.mock('vue-router', () => ({
|
||||
useRouter: () => ({
|
||||
push: pushRoute
|
||||
})
|
||||
}));
|
||||
|
||||
describe('useSettingsPage', () => {
|
||||
describe('goToSettingsPage', () => {
|
||||
test('pushes correct route', async () => {
|
||||
const { goToSettingsPage } = useSettingsPage();
|
||||
await goToSettingsPage('..a.b...c.d..e......');
|
||||
expect(pushRoute).toHaveBeenCalledWith({
|
||||
name: 'settings',
|
||||
params: {
|
||||
rest: ['a', 'b', 'c', 'd', 'e']
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { ref, readonly } from 'vue';
|
||||
import { loadSettingsConfig } from '../utils/settingsParser.js';
|
||||
|
||||
const config = ref(null);
|
||||
const error = ref(null);
|
||||
const loading = ref(false);
|
||||
|
||||
/**
|
||||
* Provides reactive access to the parsed settings configuration.
|
||||
* The config is loaded once and shared across all consumers.
|
||||
*/
|
||||
export function useSettingsConfig() {
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = null;
|
||||
try {
|
||||
config.value = await loadSettingsConfig();
|
||||
} catch (e) {
|
||||
error.value = e.message;
|
||||
config.value = null;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
config: readonly(config),
|
||||
error: readonly(error),
|
||||
loading: readonly(loading),
|
||||
load,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { getSettingsPagePathSegments } from '../utils/settingsPage';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
export const useSettingsPage = function useSettingsPage () {
|
||||
const router = useRouter();
|
||||
|
||||
const goToSettingsPage = function goToSettingsPage (pagePath) {
|
||||
return router.push({
|
||||
name: 'settings',
|
||||
params: {
|
||||
rest: getSettingsPagePathSegments(pagePath)
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return { goToSettingsPage };
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"contents": []
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia';
|
||||
import { reactive } from 'vue';
|
||||
|
||||
export const useSettingsStore = defineStore('settings', () => {
|
||||
const settings = reactive({});
|
||||
|
||||
const set = function set (key, value) {
|
||||
settings[key] = value;
|
||||
};
|
||||
|
||||
const get = function get (key) {
|
||||
return settings[key];
|
||||
};
|
||||
|
||||
const remove = function remove (key) {
|
||||
delete settings[key];
|
||||
};
|
||||
|
||||
const clear = function clear () {
|
||||
Object.keys(settings).forEach(key => delete settings[key]);
|
||||
};
|
||||
|
||||
return { set, get, remove, clear, settings };
|
||||
}, {
|
||||
persist: true
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {'bool' | 'number' | 'string' | 'selection' | 'section'} SettingType
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SelectionOption
|
||||
* @property {string} name
|
||||
* @property {string} i18n
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} BaseSettingConfig
|
||||
* @property {SettingType} type
|
||||
* @property {string} name
|
||||
* @property {string} i18n
|
||||
* @property {string} [description]
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BaseSettingConfig & {
|
||||
* default: string | string[],
|
||||
* allowMultiple: boolean,
|
||||
* options: SelectionOption[]
|
||||
* }} SelectionSettingConfig
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} SectionSettingConfig
|
||||
* @property {'section'} type
|
||||
* @property {string} name
|
||||
* @property {string} [i18n]
|
||||
* @property {string} [description]
|
||||
* @property {SettingConfigEntry[]} content
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ type: 'bool', name: string, i18n: string, description?: string, default: boolean }} BoolSettingConfig
|
||||
* @typedef {{ type: 'number', name: string, i18n: string, description?: string, default: number }} NumberSettingConfig
|
||||
* @typedef {{ type: 'string', name: string, i18n: string, description?: string, default: string }} StringSettingConfig
|
||||
* @typedef {{ type: 'selection', name: string, i18n: string, description?: string, default: string | string[], allowMultiple?: boolean, options: SelectionOption[] }} SelectionSettingConfig
|
||||
* @typedef {{ type: 'section', name: string, i18n?: string, description?: string, content: SettingConfigEntry[] }} SectionSettingConfig
|
||||
* @typedef {BoolSettingConfig | NumberSettingConfig | StringSettingConfig | SelectionSettingConfig | SectionSettingConfig} SettingConfigEntry
|
||||
* @typedef {{ contents: SettingConfigEntry[] }} FirstLevelSettingConfigEntry
|
||||
* @typedef {{ contents: FirstLevelSettingConfigEntry[] }} SettingsConfig
|
||||
*/
|
||||
@@ -0,0 +1,526 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the 'License');
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an 'AS IS' BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { getSetting, getSettingRecursively } from '../getSetting.js';
|
||||
import { test, expect } from 'vitest';
|
||||
|
||||
test.for([
|
||||
({ settings: [], name: '', expected: undefined }),
|
||||
({ settings: [], name: 'a', expected: undefined }),
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'a',
|
||||
i18n: 'a'
|
||||
}
|
||||
], name: 'a', expected: {
|
||||
type: 'bool',
|
||||
name: 'a',
|
||||
i18n: 'a'
|
||||
} })
|
||||
])('returns $expected', ({ settings, name, expected }) => {
|
||||
expect(getSetting(name, settings)).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
const veryNestedSettingsList = [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '3b',
|
||||
i18n: '3b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '4a',
|
||||
i18n: '4a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '4b',
|
||||
i18n: '4b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '5a',
|
||||
i18n: '5a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '5b',
|
||||
i18n: '5b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '6a',
|
||||
i18n: '6a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '6b',
|
||||
i18n: '6b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '7a',
|
||||
i18n: '7a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
test.for([
|
||||
({ settings: [], path: ['a'], expected: undefined }),
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
}
|
||||
], path: ['1a'], expected: {
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
} }),
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1a'], expected: {
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
} }),
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2a'], expected: {
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
} }),
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
default: 42
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2b'], expected: {
|
||||
type: 'number',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
default: 42
|
||||
} }),
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2b', '3a'], expected: {
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
} }),
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b'], expected: {
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
} }),
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2b'], expected: {
|
||||
type: 'section',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
} }),
|
||||
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2b', '3b'], expected: undefined }),
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2a', '3a'], expected: undefined }),
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1b', '2c', '3a'], expected: undefined }),
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1a', '2b', '3a'], expected: undefined }),
|
||||
({ settings: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '1a',
|
||||
i18n: '1a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '1b',
|
||||
i18n: '1b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '2a',
|
||||
i18n: '2a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '2b',
|
||||
i18n: '2b',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: '3a',
|
||||
i18n: '3a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], path: ['1c', '2b', '3a'], expected: undefined }),
|
||||
({ settings: veryNestedSettingsList, path: ['1b', '2b', '3b', '4b', '5b', '6b', '7a'], expected: {
|
||||
type: 'bool',
|
||||
name: '7a',
|
||||
i18n: '7a'
|
||||
} }),
|
||||
({ settings: veryNestedSettingsList, path: ['1b', '2b', '3b', '4b', '5b'], expected: {
|
||||
type: 'section',
|
||||
name: '5b',
|
||||
i18n: '5b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '6a',
|
||||
i18n: '6a'
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: '6b',
|
||||
i18n: '6b',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: '7a',
|
||||
i18n: '7a'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
} }),
|
||||
({ settings: veryNestedSettingsList, path: ['1b', '2b', '3b', '4ba', '5b', '6b', '7a'], expected: undefined })
|
||||
])('returns $expected', ({ settings, path, expected }) => {
|
||||
expect(getSettingRecursively(path, settings)).toStrictEqual(expected);
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { getSettingsPagePathSegments } from '../settingsPage.js';
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
describe('settingsPage', () => {
|
||||
describe('getSettingsPagePathSegments', () => {
|
||||
test.for([
|
||||
{ path: 'a.b.c', expected: ['a', 'b', 'c'] },
|
||||
{ path: 'a', expected: ['a'] },
|
||||
{ path: 'a.b', expected: ['a', 'b'] },
|
||||
{ path: 'a..b', expected: ['a', 'b'] },
|
||||
{ path: '.a.b', expected: ['a', 'b'] },
|
||||
{ path: '..a.b', expected: ['a', 'b'] },
|
||||
{ path: '.a.b.', expected: ['a', 'b'] },
|
||||
{ path: '..a.b..', expected: ['a', 'b'] },
|
||||
{ path: 'a.b.', expected: ['a', 'b'] },
|
||||
{ path: 'a.b..', expected: ['a', 'b'] },
|
||||
{ path: '.a.b..', expected: ['a', 'b'] },
|
||||
{ path: '..a.b.', expected: ['a', 'b'] },
|
||||
{ path: '.a..b', expected: ['a', 'b'] },
|
||||
{ path: '..a..b', expected: ['a', 'b'] },
|
||||
{ path: '.a..b.', expected: ['a', 'b'] },
|
||||
{ path: '..a..b..', expected: ['a', 'b'] },
|
||||
{ path: 'a..b.', expected: ['a', 'b'] },
|
||||
{ path: 'a..b..', expected: ['a', 'b'] },
|
||||
{ path: '.a..b..', expected: ['a', 'b'] },
|
||||
{ path: '..a..b.', expected: ['a', 'b'] },
|
||||
{ path: '..a.....b.c..d....', expected: ['a', 'b', 'c', 'd'] },
|
||||
{ path: 'a.b.c/', expected: ['a', 'b', 'c'] },
|
||||
{ path: 'a.#b.c', expected: ['a', 'b', 'c'] },
|
||||
{ path: 'a.#b.c/', expected: ['a', 'b', 'c'] },
|
||||
{ path: 'a1.#b.c/', expected: ['a1', 'b', 'c'] },
|
||||
{ path: 'a-1.#b.c/', expected: ['a-1', 'b', 'c'] },
|
||||
{ path: 'a-1.b@.c', expected: ['a-1', 'b', 'c'] },
|
||||
{ path: '....@a/#...)!&§[b.#§c..d....', expected: ['a', 'b', 'c', 'd'] },
|
||||
{ path: '....@a/#...)!&§[b.#§c..dä....', expected: ['a', 'b', 'c', 'd'] },
|
||||
{ path: '..,...~..@a/#.+..)!&§[b.#§c..dä..)..', expected: ['a', 'b', 'c', 'd'] },
|
||||
{ path: 'a.@.b', expected: ['a', 'b'] },
|
||||
{ path: 1, expected: [] },
|
||||
{ path: false, expected: [] },
|
||||
{ path: true, expected: [] },
|
||||
{ path: null, expected: [] },
|
||||
{ path: undefined, expected: [] }
|
||||
])('returns $expected', async ({ path, expected }) => {
|
||||
expect(getSettingsPagePathSegments(path)).toStrictEqual(expected);
|
||||
});
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,760 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, test, expect } from 'vitest';
|
||||
import { validateSettingsConfig, validateEntry, validateSelectionOptions, assertType, assertString } from '../settingsValidator';
|
||||
|
||||
describe('settingsValidator', () => {
|
||||
describe('validateSettingsConfig', () => {
|
||||
test.for([
|
||||
{ raw: false, expected: false },
|
||||
{ raw: true, expected: false },
|
||||
{ raw: 0, expected: false },
|
||||
{ raw: 42, expected: false },
|
||||
{ raw: '', expected: false },
|
||||
{ raw: ' ', expected: false },
|
||||
{ raw: 'a', expected: false },
|
||||
{ raw: {}, expected: false },
|
||||
{ raw: {
|
||||
contents: []
|
||||
}, expected: true },
|
||||
{ raw: {
|
||||
contents: [
|
||||
{
|
||||
name: 'general',
|
||||
i18n: 'settings.settings.general',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}, expected: true },
|
||||
{ raw: {
|
||||
contents: [
|
||||
{
|
||||
name: 'general',
|
||||
i18n: 'settings.settings.general',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'copyOfGeneral',
|
||||
i18n: 'settings.settings.copyOfGeneral',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
}, expected: true },
|
||||
{ raw: {
|
||||
contents: [
|
||||
{
|
||||
name: 'general',
|
||||
i18n: 'settings.settings.general',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'copyOfGeneral',
|
||||
i18n: 'settings.settings.copyOfGeneral',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
default: false,
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'aSection',
|
||||
i18n: 'settings.settings.aSection',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 43',
|
||||
i18n: 'settings.settings.aSection.enableFeature43'
|
||||
},
|
||||
{
|
||||
type: 'selection',
|
||||
name: 'language',
|
||||
i18n: 'settings.settings.aSection.language.label',
|
||||
default: 'en',
|
||||
options: [
|
||||
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
|
||||
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'section2',
|
||||
i18n: 'settings.settings.aSection.section2.label',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'string',
|
||||
i18n: 'settings.settings.aSection.sections.string',
|
||||
default: 'str'
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}, expected: true },
|
||||
{ raw: {
|
||||
contents: [
|
||||
{
|
||||
name: 'general',
|
||||
i18n: 'settings.settings.general',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'copyOfGeneral',
|
||||
i18n: 'settings.settings.copyOfGeneral',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
default: false,
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'aSection',
|
||||
i18n: 'settings.settings.aSection',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 43',
|
||||
i18n: 'settings.settings.aSection.enableFeature43'
|
||||
},
|
||||
{
|
||||
type: 'selection',
|
||||
name: 'language',
|
||||
i18n: 'settings.settings.aSection.language.label',
|
||||
default: 'en',
|
||||
allowMultiple: false,
|
||||
options: [
|
||||
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
|
||||
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'section2',
|
||||
i18n: 'settings.settings.aSection.section2.label',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'string',
|
||||
i18n: 'settings.settings.aSection.sections.string',
|
||||
default: 'str'
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}, expected: true },
|
||||
{ raw: {
|
||||
contents: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'aStandaloneBooleanSetting',
|
||||
i18n: 'settings.settings.aStandaloneBooleanSetting',
|
||||
default: true
|
||||
},
|
||||
{
|
||||
name: 'general',
|
||||
i18n: 'settings.settings.general',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'copyOfGeneral',
|
||||
i18n: 'settings.settings.copyOfGeneral',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
default: false,
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 'aSection',
|
||||
i18n: 'settings.settings.aSection',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 43',
|
||||
i18n: 'settings.settings.aSection.enableFeature43'
|
||||
},
|
||||
{
|
||||
type: 'selection',
|
||||
name: 'language',
|
||||
i18n: 'settings.settings.aSection.language.label',
|
||||
default: 'en',
|
||||
allowMultiple: false,
|
||||
options: [
|
||||
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
|
||||
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'section2',
|
||||
i18n: 'settings.settings.aSection.section2.label',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'string',
|
||||
i18n: 'settings.settings.aSection.sections.string',
|
||||
default: 'str'
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}, expected: false },
|
||||
{ raw: {
|
||||
contents: [
|
||||
{
|
||||
type: 'section',
|
||||
name: 'general',
|
||||
i18n: 'settings.settings.general',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'copyOfGeneral',
|
||||
i18n: 'settings.settings.copyOfGeneral',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
default: false,
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'aSection',
|
||||
i18n: 'settings.settings.aSection',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 43',
|
||||
i18n: 'settings.settings.aSection.enableFeature43',
|
||||
default: 'true'
|
||||
},
|
||||
{
|
||||
type: 'selection',
|
||||
name: 'language',
|
||||
i18n: 'settings.settings.aSection.language.label',
|
||||
default: 'en',
|
||||
options: [
|
||||
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
|
||||
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'section2',
|
||||
i18n: 'settings.settings.aSection.section2.label',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'string',
|
||||
i18n: 'settings.settings.aSection.sections.string',
|
||||
default: 42
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}, expected: false },
|
||||
{ raw: {
|
||||
content: [
|
||||
{
|
||||
type: 'section',
|
||||
name: 'general',
|
||||
i18n: 'settings.settings.general',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'copyOfGeneral',
|
||||
i18n: 'settings.settings.copyOfGeneral',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
default: false,
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'aSection',
|
||||
i18n: 'settings.settings.aSection',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 43',
|
||||
i18n: 'settings.settings.aSection.enableFeature43'
|
||||
},
|
||||
{
|
||||
type: 'selection',
|
||||
name: 'language',
|
||||
i18n: 'settings.settings.aSection.language.label',
|
||||
default: 'en',
|
||||
options: [
|
||||
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
|
||||
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'section2',
|
||||
i18n: 'settings.settings.aSection.section2.label',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'string',
|
||||
i18n: 'settings.settings.aSection.sections.string',
|
||||
default: 'str'
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}, expected: false },
|
||||
{ raw: {
|
||||
content: [
|
||||
{
|
||||
type: 'sectio',
|
||||
name: 'general',
|
||||
i18n: 'settings.settings.general',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'copyOfGeneral',
|
||||
i18n: 'settings.settings.copyOfGeneral',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
default: false,
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'aSection',
|
||||
i18n: 'settings.settings.aSection',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 43',
|
||||
i18n: 'settings.settings.aSection.enableFeature43'
|
||||
},
|
||||
{
|
||||
type: 'selection',
|
||||
name: 'language',
|
||||
i18n: 'settings.settings.aSection.language.label',
|
||||
default: 'en',
|
||||
options: [
|
||||
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
|
||||
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'section2',
|
||||
i18n: 'settings.settings.aSection.section2.label',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'string',
|
||||
i18n: 'settings.settings.aSection.sections.string',
|
||||
default: 'str'
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}, expected: false },
|
||||
{ raw: {
|
||||
content: [
|
||||
{
|
||||
type: 'sectio',
|
||||
name: 'general',
|
||||
i18n: 'settings.settings.general',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'copyOfGeneral',
|
||||
i18n: 'settings.settings.copyOfGeneral',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 42',
|
||||
default: false,
|
||||
i18n: 'settings.settings.general.enableFeature42'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'aSection',
|
||||
i18n: 'settings.settings.aSection',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'Enable feature 43',
|
||||
i18n: 'settings.settings.aSection.enableFeature43'
|
||||
},
|
||||
{
|
||||
type: 'selection',
|
||||
name: 'language',
|
||||
i18n: 'settings.settings.aSection.language.label',
|
||||
default: 'en',
|
||||
allowMultiple: 'false',
|
||||
options: [
|
||||
{ name: 'en', i18n: 'settings.settings.aSection.language.options.en' },
|
||||
{ name: 'de', i18n: 'settings.settings.aSection.language.options.de' },
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 'section2',
|
||||
i18n: 'settings.settings.aSection.section2.label',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'string',
|
||||
i18n: 'settings.settings.aSection.sections.string',
|
||||
default: 'str'
|
||||
}
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}, expected: false }
|
||||
])('returns valid: $expected', ({ raw, expected }) => {
|
||||
const result = validateSettingsConfig(raw);
|
||||
if (!result.valid) {
|
||||
console.error('Error message:', result.error);
|
||||
};
|
||||
expect(result.valid).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateEntry', () => {
|
||||
test.for([
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable' }],
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true }],
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: false }],
|
||||
|
||||
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label' }],
|
||||
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 42 }],
|
||||
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 0 }],
|
||||
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: -42.7 }],
|
||||
|
||||
[{ name: 'aString', type: 'string', i18n: 'aString.label' }],
|
||||
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: '' }],
|
||||
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: 'Seekra is great!' }],
|
||||
|
||||
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', options: [
|
||||
{ name: 'yes', i18n: 'selectSomething.options.yes' },
|
||||
{ name: 'no', i18n: 'selectSomething.options.no' },
|
||||
{ name: 'maybe', i18n: 'selectSomething.options.maybe' }
|
||||
] }],
|
||||
|
||||
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'no', options: [
|
||||
{ name: 'yes', i18n: 'selectSomething.options.yes' },
|
||||
{ name: 'no', i18n: 'selectSomething.options.no' },
|
||||
{ name: 'maybe', i18n: 'selectSomething.options.maybe' }
|
||||
] }],
|
||||
|
||||
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [] }],
|
||||
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
|
||||
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true }
|
||||
] }],
|
||||
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
|
||||
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true },
|
||||
{ name: 'enableFeature43', type: 'bool', i18n: 'feature.43.enable', default: true }
|
||||
] }],
|
||||
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
|
||||
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true },
|
||||
{ name: 'enableFeature43', type: 'bool', i18n: 'feature.43.enable', default: true },
|
||||
{ name: 'aSecondSection', type: 'section', i18n: 'sections.aSecondSection.title', content: [
|
||||
{ name: 'enableFeature44', type: 'bool', i18n: 'feature.44.enable', default: true },
|
||||
] }
|
||||
] }]
|
||||
])('throws no error for the entry %s', ([ entry ]) => {
|
||||
expect(() => validateEntry(entry)).not.throws(Error);
|
||||
});
|
||||
|
||||
test.for([
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: '' }],
|
||||
[{ name: 'enableFeature42', type: 'thisTypeDoesNotExistAndHasAVeryLongName', i18n: 'feature.42.enable' }],
|
||||
[{ name: '', type: 'thisTypeDoesNotExistAndHasAVeryLongName', i18n: 'feature.42.enable' }],
|
||||
[{ name: '', type: 'thisTypeDoesNotExistAndHasAVeryLongName', i18n: '' }],
|
||||
[{ name: '', type: 'bool', i18n: '' }],
|
||||
[{ name: '', type: 'bool', i18n: '' }],
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 42 }],
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: '42' }],
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'Seekra' }],
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'true' }],
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'false' }],
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'undefined' }],
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: 'null' }],
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: [] }],
|
||||
[{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: {} }],
|
||||
[{ name: 'enableFeature42', type: '', i18n: 'feature.42.enable', default: {} }],
|
||||
|
||||
[{ name: '', type: 'number', i18n: 'aNumber.label' }],
|
||||
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: '42' }],
|
||||
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 'zero' }],
|
||||
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 'undefined' }],
|
||||
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: 'false' }],
|
||||
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: true }],
|
||||
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: false }],
|
||||
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: [] }],
|
||||
[{ name: 'aNumber', type: 'number', i18n: 'aNumber.label', default: {} }],
|
||||
[{ name: 'aNumber', type: '', i18n: 'aNumber.label', default: {} }],
|
||||
[{ name: 'aNumber', type: '', i18n: 'aNumber.label', default: 42 }],
|
||||
|
||||
[{ name: 'aString', type: 'string', i18n: '' }],
|
||||
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: 42 }],
|
||||
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: true }],
|
||||
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: false }],
|
||||
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: [] }],
|
||||
[{ name: 'aString', type: 'string', i18n: 'aString.label', default: {} }],
|
||||
[{ name: 'aString', type: 'string', i18n: '', default: {} }],
|
||||
|
||||
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'a', options: true }],
|
||||
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'a', options: [] }],
|
||||
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'c', options: [
|
||||
{ name: 'a', i18n: 'a' },
|
||||
{ name: 'b', i18n: 'b' }
|
||||
] }],
|
||||
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'a', options: [
|
||||
{ name: 'a', i18n: 'a' },
|
||||
{ name: 'b', i18n: '' },
|
||||
{ name: 'c' }
|
||||
] }],
|
||||
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'a', allowMultiple: 'false', options: [
|
||||
{ name: 'a', i18n: 'a' },
|
||||
{ name: 'b', i18n: '' },
|
||||
{ name: 'c' }
|
||||
] }],
|
||||
|
||||
[{ name: 'selectSomething', type: 'selection', i18n: 'selectSomething.title', default: 'no', options: [
|
||||
{ name: 'yes', i18n: '' },
|
||||
{ name: '', i18n: 'selectSomething.options.no' },
|
||||
{ name: 'maybe', i18n: 'selectSomething.options.maybe' },
|
||||
{ name: '', i18n: '' },
|
||||
{ name: 42, i18n: 43 },
|
||||
{ name: '' },
|
||||
{ i18n: '' },
|
||||
{}
|
||||
] }],
|
||||
|
||||
[{ name: 'aSection', type: 'section', i18n: '', content: [] }],
|
||||
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: '' }],
|
||||
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: '[]' }],
|
||||
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: 'a' }],
|
||||
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
|
||||
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true },
|
||||
{ name: 'enableFeature43', type: 'bool', i18n: 'feature.42.enable', default: 0 },
|
||||
{ name: 'enableFeature44', type: 'bool' }
|
||||
] }],
|
||||
[{ name: 'aSection', type: 'section', i18n: 'sections.aSection.title', content: [
|
||||
{ name: 'enableFeature42', type: 'bool', i18n: 'feature.42.enable', default: true },
|
||||
{ name: 'enableFeature43', type: 'bool', i18n: 'feature.43.enable', default: true },
|
||||
{ name: 'aSecondSection', type: 'section', i18n: 'sections.aSecondSection.title', content: 'Hello' },
|
||||
{ name: 'aSecondSection', type: 'section', i18n: '', content: [] },
|
||||
{ name: 'aSecondSection', type: 'section', i18n: 'i18n', content: [] },
|
||||
{ name: 'aSecondSection', type: 'section', content: [] }
|
||||
] }]
|
||||
])('throws an error for the entry %s', ([ entry ]) => {
|
||||
expect(() => validateEntry(entry)).throws(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateSelectionOptions', () => {
|
||||
test.for([
|
||||
[[{ name: 'test', i18n: 'test.label' }]],
|
||||
[[{ name: 'test', i18n: 'test.label' }, { name: 'test2', i18n: 'test2.label' }]],
|
||||
[[{ name: 'test', i18n: 'test.label' }, { name: 'test', i18n: 'test2.label' }, { name: 'test3', i18n: 'test.label' }]]
|
||||
])('throws no error for the options %s', ([ options ]) => {
|
||||
expect(() => validateSelectionOptions(options)).not.throws(Error);
|
||||
});
|
||||
|
||||
test.for([
|
||||
[[{}]],
|
||||
[[{ i18n: '' }]],
|
||||
[[{ i18n: 'a' }]],
|
||||
[[{ name: 'a' }]],
|
||||
[[{ name: '' }]],
|
||||
[[{ name: '', i18n: 'test.label' }]],
|
||||
[[{ name: 'test', i18n: '' }]],
|
||||
[[{ name: '', i18n: '' }]],
|
||||
[[{ name: '', i18n: 'test.label' }, { name: 'test2', i18n: '' }]],
|
||||
[[{ name: 'test', i18n: '' }, { name: '', i18n: 'test2.label' }, { name: '', i18n: ' ' }, { name: '42', i18n: '42.i18n' }]],
|
||||
[[]]
|
||||
])('throws an error for the options %s', ([ options ]) => {
|
||||
expect(() => validateSelectionOptions(options)).throws(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assertType', () => {
|
||||
test.for([
|
||||
['bool'],
|
||||
['number'],
|
||||
['string'],
|
||||
['selection'],
|
||||
['section']
|
||||
])('throws no error for the value %s', ([ value ]) => {
|
||||
expect(() => assertType(value)).not.throw(Error);
|
||||
});
|
||||
|
||||
test.for([
|
||||
[''],
|
||||
[' '],
|
||||
[' '],
|
||||
[' '],
|
||||
|
||||
['42'],
|
||||
['0'],
|
||||
['-42'],
|
||||
['-42.0'],
|
||||
['-0.0'],
|
||||
|
||||
['a'],
|
||||
['ab'],
|
||||
['SeekraIsGreat!'],
|
||||
['Seekra is great!'],
|
||||
|
||||
[undefined],
|
||||
[null]
|
||||
])('throws an error for the value %s', ([ value ]) => {
|
||||
expect(() => assertType(value)).throw(Error);
|
||||
});
|
||||
});
|
||||
|
||||
describe('assertString', () => {
|
||||
test.for([
|
||||
['a'],
|
||||
['b'],
|
||||
['ab'],
|
||||
|
||||
['0'],
|
||||
['42'],
|
||||
|
||||
['null'],
|
||||
['undefined'],
|
||||
|
||||
['()&%())']
|
||||
])('throws no error for the value %s', ([ value ]) => {
|
||||
expect(() => assertString(value)).not.throw(Error);
|
||||
});
|
||||
|
||||
test.for([
|
||||
[0],
|
||||
[1],
|
||||
[42],
|
||||
[-1],
|
||||
[-42],
|
||||
|
||||
[''],
|
||||
[' '],
|
||||
[' '],
|
||||
[' ']
|
||||
])('throws an error for the value %s', ([ value ]) => {
|
||||
expect(() => assertString(value)).throws(Error);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export const getSetting = function getSetting (name, settings) {
|
||||
return settings.filter((setting) => setting.name === name)[0];
|
||||
};
|
||||
|
||||
export const getSettingRecursively = function getSettingRecursively (sectionPath, settings) {
|
||||
const oneSettingsLevel = function oneSettingsLevel (cursor, acc) {
|
||||
const setting = getSetting(cursor[0], acc.content);
|
||||
if (!setting) return;
|
||||
|
||||
if (setting.type === undefined) setting.type = 'section';
|
||||
if (setting.type === 'section') {
|
||||
const newCursor = cursor.slice(1, cursor.length);
|
||||
|
||||
if (newCursor.length !== 0) {
|
||||
return oneSettingsLevel(cursor.slice(1, cursor.length), setting);
|
||||
} else {
|
||||
return setting;
|
||||
};
|
||||
} else if (cursor.length <= 1) {
|
||||
return setting;
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
};
|
||||
return oneSettingsLevel(sectionPath, { content: settings });
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Extracts the segments of a settings page path and normalizes each segment.
|
||||
* @param {string} path - The settings page path.
|
||||
* @returns {Array} The settings page path segments.
|
||||
*/
|
||||
export const getSettingsPagePathSegments = function getSettingsPagePathSegments (path) {
|
||||
if (typeof path !== 'string') return [];
|
||||
return path
|
||||
.replace(/^\.+|\.+$/g, '')
|
||||
.split('.')
|
||||
.map(
|
||||
(segment) => segment.replace(/[^a-zA-Z0-9-]/g, '')
|
||||
)
|
||||
.filter(Boolean);
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { validateSettingsConfig } from './settingsValidator.js';
|
||||
|
||||
/**
|
||||
* Loads and parses the settings configuration via dynamic import.
|
||||
* @returns {Promise<import('../types/settingsConfig').SettingsConfig>}
|
||||
*/
|
||||
export async function loadSettingsConfig() {
|
||||
let raw;
|
||||
|
||||
try {
|
||||
raw = (await import('../settings.json')).default;
|
||||
} catch (e) {
|
||||
throw new Error(`[settings] Failed to load settings.json: ${e.message}`);
|
||||
}
|
||||
|
||||
const result = validateSettingsConfig(raw);
|
||||
if (!result.valid) {
|
||||
throw new Error(result.error);
|
||||
}
|
||||
|
||||
return result.config;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
const VALID_TYPES = ['bool', 'number', 'string', 'selection', 'section'];
|
||||
|
||||
export const assertString = function assertString (value, path) {
|
||||
if (typeof value !== 'string' || value.trim() === '') {
|
||||
throw new Error(`[settings] "${path}" must be a non-empty string`);
|
||||
}
|
||||
}
|
||||
|
||||
export const assertType = function assertType (value, path) {
|
||||
if (!VALID_TYPES.includes(value)) {
|
||||
throw new Error(
|
||||
`[settings] "${path}" has invalid type "${value}". Must be one of: ${VALID_TYPES.join(', ')}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const validateSelectionOptions = function validateSelectionOptions (options, path) {
|
||||
if (!Array.isArray(options) || options.length === 0) {
|
||||
throw new Error(`[settings] "${path}.options" must be a non-empty array`);
|
||||
};
|
||||
options.forEach((opt, i) => {
|
||||
assertString(opt.name, `${path}.options[${i}].name`);
|
||||
assertString(opt.i18n, `${path}.options[${i}].i18n`);
|
||||
});
|
||||
}
|
||||
|
||||
export const validateEntry = function validateEntry (entry, path) {
|
||||
assertType(entry.type, `${path}.type`);
|
||||
|
||||
assertString(entry.name, `${path}.name`);
|
||||
assertString(entry.i18n, `${path}.i18n`);
|
||||
|
||||
if (entry.type === 'section') {
|
||||
if (!Array.isArray(entry.content)) {
|
||||
throw new Error(`[settings] "${path}.content" must be an array`);
|
||||
}
|
||||
entry.content.forEach((child, i) =>
|
||||
validateEntry(child, `${path}.content[${i}]`)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.type === 'selection') {
|
||||
validateSelectionOptions(entry.options, path);
|
||||
if (typeof entry.allowMultiple !== 'boolean' && entry.allowMultiple) {
|
||||
throw new Error(`[settings] "${path}.allowMultiple" must be a boolean`);
|
||||
}
|
||||
};
|
||||
if (entry.default !== undefined) {
|
||||
if (entry.type === 'bool' && typeof entry.default !== 'boolean') {
|
||||
throw new Error(`[settings] "${path}.default" must be a boolean`);
|
||||
}
|
||||
if (entry.type === 'number' && typeof entry.default !== 'number') {
|
||||
throw new Error(`[settings] "${path}.default" must be a number`);
|
||||
}
|
||||
if (entry.type === 'string' && typeof entry.default !== 'string') {
|
||||
throw new Error(`[settings] "${path}.default" must be a string`);
|
||||
}
|
||||
if (entry.type === 'selection') {
|
||||
if (typeof entry.default !== 'string') {
|
||||
throw new Error(`[settings] "${path}.default" must be a string`);
|
||||
};
|
||||
if (!entry.options.map((option) => option.name).includes(entry.default)) {
|
||||
throw new Error(`[settings] option "${path}.default" does not exist`);
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const validateFirstLevelSection = function validateFirstLevelSection (section, path) {
|
||||
assertString(section.name);
|
||||
assertString(section.i18n);
|
||||
|
||||
if (!Array.isArray(section.content)) {
|
||||
throw new Error(`[settings] "${path}.content" must be an array`);
|
||||
};
|
||||
|
||||
section.content.forEach((entry, i) =>
|
||||
validateEntry(entry, `${path}.content[${i}]`)
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates a raw settings config object.
|
||||
* @param {unknown} raw
|
||||
* @returns {{ valid: true, config: import('../types/settingsConfig').SettingsConfig } | { valid: false, error: string }}
|
||||
*/
|
||||
export function validateSettingsConfig(raw) {
|
||||
try {
|
||||
if (!raw || typeof raw !== 'object') {
|
||||
throw new Error('[settings] Config must be an object');
|
||||
}
|
||||
if (!Array.isArray(raw.contents)) {
|
||||
throw new Error('[settings] "contents" must be an array');
|
||||
}
|
||||
raw.contents.forEach((entry, i) =>
|
||||
validateFirstLevelSection(entry, `contents[${i}]`)
|
||||
);
|
||||
return { valid: true, config: raw };
|
||||
} catch (e) {
|
||||
return { valid: false, error: e.message };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import LeftSidebarLayout from '@/layouts/LeftSidebarLayout.vue';
|
||||
import SettingsPage from '../components/SettingsPage.vue';
|
||||
|
||||
import { getSettingRecursively } from '../utils/getSetting.js';
|
||||
import { loadSettingsConfig } from '../utils/settingsParser';
|
||||
import { onMounted, ref, watchEffect } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const settingsLoaded = ref(false)
|
||||
const settings = ref([]);
|
||||
const activeSectionContent = ref({});
|
||||
const activeSection = ref(null);
|
||||
|
||||
watchEffect(() => {
|
||||
const segments = route.path
|
||||
.split('/')
|
||||
.filter(Boolean);
|
||||
|
||||
activeSection.value = segments.slice(1, segments.length).join('.');
|
||||
});
|
||||
|
||||
const updateSettings = function updateSettings () {
|
||||
if (activeSection.value) {
|
||||
const setting = getSettingRecursively(activeSection.value.split('.'), settings.value);
|
||||
if (!setting) {
|
||||
router.push('/settings');
|
||||
} else {
|
||||
activeSectionContent.value = setting;
|
||||
};
|
||||
} else {
|
||||
activeSectionContent.value = { content: [] };
|
||||
};
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
settings.value = (await loadSettingsConfig()).contents;
|
||||
watchEffect(() => {
|
||||
updateSettings();
|
||||
});
|
||||
settingsLoaded.value = true;
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="settings-page-wrapper">
|
||||
<header class="header">
|
||||
<h1>
|
||||
{{ t('preferences.settings') }}
|
||||
</h1>
|
||||
</header>
|
||||
<LeftSidebarLayout class="layout">
|
||||
<template #sidebar>
|
||||
<ul class="sidebar-sections-list">
|
||||
<li v-for="section in settings">
|
||||
<RouterLink
|
||||
:to="`/settings/${section.name}`"
|
||||
class="button button-link link sidebar-section"
|
||||
:class="{ active: activeSection === section.name }"
|
||||
>
|
||||
{{ t(section.i18n) }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
<div v-if="!settingsLoaded" class="main-content--loading">
|
||||
{{ t('loading') }}
|
||||
</div>
|
||||
<div v-else-if="activeSection" class="main-content--loaded">
|
||||
<div class="settings-main-content">
|
||||
<SettingsPage :setting="activeSectionContent" :path="activeSection" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="main-content--no-section">
|
||||
{{ t('settings.noSectionSelected') }}
|
||||
</div>
|
||||
</LeftSidebarLayout>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.layout {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.settings-page-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.active {
|
||||
background-color: var(--light-hover);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: var(--main-content-padding-y) var(--main-content-padding-x);
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.sidebar-sections-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.sidebar-section {
|
||||
--padding: 0.8em;
|
||||
border-radius: var(--padding);
|
||||
padding: var(--padding);
|
||||
margin-bottom: 4px;
|
||||
width: calc(100% - 2 * var(--padding));
|
||||
text-align: start;
|
||||
font-size: 1rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.settings-main-content {
|
||||
padding: 0 30%;
|
||||
}
|
||||
|
||||
@media (max-width: 80em) {
|
||||
.settings-main-content {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,306 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import SettingsView from '../SettingsView.vue';
|
||||
import { mountComponent } from '@/test-utils/mountComponent.js';
|
||||
import { flushPromises } from '@vue/test-utils';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
const settingsHoisted = vi.hoisted(() => [
|
||||
{
|
||||
name: 's0',
|
||||
i18n: 'settings.s0',
|
||||
content: []
|
||||
},
|
||||
{
|
||||
name: 's1',
|
||||
i18n: 'settings.s1',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'b0',
|
||||
i18n: 'settings.s1.b0'
|
||||
},
|
||||
{
|
||||
type: 'number',
|
||||
name: 'n0',
|
||||
i18n: 'settings.s1.n0'
|
||||
},
|
||||
{
|
||||
type: 'string',
|
||||
name: 'st0',
|
||||
i18n: 'settings.s1.st0'
|
||||
},
|
||||
{
|
||||
type: 'selection',
|
||||
name: 'se0',
|
||||
i18n: 'settings.s1.se0',
|
||||
options: [
|
||||
{ name: 'o0', i18n: 'settings.s1.se0.o0' },
|
||||
{ name: 'o1', i18n: 'settings.s1.se0.o1' },
|
||||
{ name: 'o2', i18n: 'settings.s1.se0.o2' },
|
||||
{ name: 'o3', i18n: 'settings.s1.se0.o3' }
|
||||
]
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 's0',
|
||||
i18n: 'settings.s1.s0',
|
||||
content: []
|
||||
},
|
||||
{
|
||||
type: 'section',
|
||||
name: 's1',
|
||||
i18n: 'settings.s1.s1',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'b0',
|
||||
i18n: 'settings.s1.s1.b0',
|
||||
default: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 's2',
|
||||
i18n: 'settings.s2',
|
||||
content: [
|
||||
{
|
||||
type: 'section',
|
||||
name: 's0',
|
||||
i18n: 'settings.s2.s0',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'b0',
|
||||
i18n: 'settings.s2.s0.b0',
|
||||
default: true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 's3',
|
||||
i18n: 'settings.s3',
|
||||
content: [
|
||||
{
|
||||
type: 'section',
|
||||
name: 's0',
|
||||
i18n: 'settings.s3.s0',
|
||||
content: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 's4',
|
||||
i18n: 'settings.s4',
|
||||
content: [
|
||||
{
|
||||
type: 'selection',
|
||||
name: 'se0',
|
||||
i18n: 'settings.s4.se0',
|
||||
options: [
|
||||
{ name: 'o0', i18n: 'settings.s4.se0.o0' },
|
||||
{ name: 'o1', i18n: 'settings.s4.se0.o1' },
|
||||
{ name: 'o2', i18n: 'settings.s4.se0.o2' },
|
||||
{ name: 'o3', i18n: 'settings.s4.se0.o3' }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 's5',
|
||||
i18n: 'settings.s5',
|
||||
content: [
|
||||
{
|
||||
type: 'string',
|
||||
name: 'st0',
|
||||
i18n: 'settings.s5.st0'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 's6',
|
||||
i18n: 'settings.s6',
|
||||
content: [
|
||||
{
|
||||
type: 'number',
|
||||
name: 'n0',
|
||||
i18n: 'settings.s6.n0'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 's7',
|
||||
i18n: 'settings.s7',
|
||||
content: [
|
||||
{
|
||||
type: 'bool',
|
||||
name: 'b0',
|
||||
i18n: 'settings.s7.b0'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
name: 's8',
|
||||
i18n: 'settings.s8',
|
||||
content: [
|
||||
{
|
||||
type: 'section',
|
||||
name: 's0',
|
||||
i18n: 'settings.s8.s0',
|
||||
content: [
|
||||
{
|
||||
type: 'section',
|
||||
name: 's0',
|
||||
i18n: 'settings.s8.s0.s0',
|
||||
content: [
|
||||
{
|
||||
type: 'section',
|
||||
name: 's0',
|
||||
i18n: 'settings.s8.s0.s0.s0',
|
||||
content: [
|
||||
{
|
||||
type: 'selection',
|
||||
name: 'se0',
|
||||
i18n: 'settings.s8.s0.s0.s0.se0',
|
||||
allowMultiple: true,
|
||||
options: []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]);
|
||||
const settings = settingsHoisted.values();
|
||||
|
||||
const originalCurrentSection = 's0';
|
||||
let currentSection = originalCurrentSection;
|
||||
|
||||
vi.mock('../../utils/settingsParser', () => ({
|
||||
loadSettingsConfig: vi.fn().mockResolvedValue({
|
||||
contents: settingsHoisted
|
||||
})
|
||||
}));
|
||||
|
||||
vi.mock('vue-router', async (importOriginal) => {
|
||||
const actual = await importOriginal();
|
||||
return {
|
||||
...actual,
|
||||
useRoute: () => ({
|
||||
path: `/settings/${currentSection}`
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
const getWrapper = function getWrapper ({ translations = {} } = {}) {
|
||||
return mountComponent(SettingsView, {}, translations, [], {});
|
||||
};
|
||||
|
||||
describe('SettingsView', () => {
|
||||
describe('heading', () => {
|
||||
test('shows correct translation', () => {
|
||||
const translation = 'Settings';
|
||||
const wrapper = getWrapper({
|
||||
translations: {
|
||||
'preferences.settings': translation
|
||||
}
|
||||
});
|
||||
|
||||
const header = wrapper.find('header');
|
||||
expect(header.text()).toBe(translation);
|
||||
});
|
||||
});
|
||||
|
||||
describe('sidebar', () => {
|
||||
test('shows all top-level sections', async () => {
|
||||
const wrapper = getWrapper();
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const sidebarItems = wrapper.findAll('.sidebar-sections-list > li');
|
||||
settings.forEach((setting, index) => {
|
||||
const sidebarItem = sidebarItems[index];
|
||||
|
||||
expect(sidebarItem.text()).toBe(setting.i18n);
|
||||
|
||||
const link = sidebarItem.find('*');
|
||||
expect(link.attributes('href')).toBe(`/settings/${setting.name}`);
|
||||
expect(link.classes('active')).toBe(setting.name === currentSection);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('main', () => {
|
||||
describe('correct element', () => {
|
||||
test('shows that it is loading', () => {
|
||||
const wrapper = getWrapper();
|
||||
|
||||
const mainContentLoading = wrapper.find('.main-content--loading');
|
||||
const mainContentLoaded = wrapper.find('.main-content--loaded');
|
||||
const mainContentNoSection = wrapper.find('.main-content--no-section');
|
||||
|
||||
expect(mainContentLoading.exists()).toBe(true);
|
||||
expect(mainContentLoaded.exists()).toBe(false);
|
||||
expect(mainContentNoSection.exists()).toBe(false);
|
||||
});
|
||||
|
||||
test('shows that no section is selected', async () => {
|
||||
currentSection = '';
|
||||
|
||||
const wrapper = getWrapper();
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const mainContentLoading = wrapper.find('.main-content--loading');
|
||||
const mainContentLoaded = wrapper.find('.main-content--loaded');
|
||||
const mainContentNoSection = wrapper.find('.main-content--no-section');
|
||||
|
||||
expect(mainContentLoading.exists()).toBe(false);
|
||||
expect(mainContentLoaded.exists()).toBe(false);
|
||||
expect(mainContentNoSection.exists()).toBe(true);
|
||||
});
|
||||
|
||||
test('shows main content', async () => {
|
||||
const wrapper = getWrapper();
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const mainContentLoading = wrapper.find('.main-content--loading');
|
||||
const mainContentLoaded = wrapper.find('.main-content--loaded');
|
||||
const mainContentNoSection = wrapper.find('.main-content--no-section');
|
||||
|
||||
expect(mainContentLoading.exists()).toBe(false);
|
||||
expect(mainContentLoaded.exists()).toBe(true);
|
||||
expect(mainContentNoSection.exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
currentSection = originalCurrentSection;
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<nav class="sidebar">
|
||||
<slot />
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.sidebar {
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import getCurrentLanguage from './utils/currentLanguage';
|
||||
|
||||
export const fallbackLocale = 'en';
|
||||
|
||||
|
||||
export const LANGUAGES_RTL = [
|
||||
'ar'
|
||||
];
|
||||
export const SUPPORTED_LANGUAGES = [
|
||||
'en',
|
||||
'de',
|
||||
'fr',
|
||||
'es',
|
||||
'pt',
|
||||
'it'
|
||||
];
|
||||
|
||||
export const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: getCurrentLanguage(),
|
||||
fallbackLocale: fallbackLocale,
|
||||
messages: {}
|
||||
});
|
||||
|
||||
const loadedLanguages = new Set();
|
||||
|
||||
export async function loadLanguage (locale) {
|
||||
if (!SUPPORTED_LANGUAGES.includes(locale)) {
|
||||
locale = fallbackLocale;
|
||||
}
|
||||
|
||||
if (loadedLanguages.has(locale)) {
|
||||
i18n.global.locale.value = locale;
|
||||
return;
|
||||
};
|
||||
|
||||
const messages = (await import(`./locales/${locale}.json`)).default;
|
||||
|
||||
i18n.global.setLocaleMessage(locale, messages);
|
||||
i18n.global.locale.value = locale;
|
||||
|
||||
loadedLanguages.add(locale);
|
||||
};
|
||||
@@ -0,0 +1,42 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Sidebar from '@/features/sidebar/components/Sidebar.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="layout-container">
|
||||
<Sidebar>
|
||||
<slot name="sidebar" />
|
||||
</Sidebar>
|
||||
<main class="main-content">
|
||||
<slot />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.layout-container {
|
||||
display: grid;
|
||||
grid-template-columns: min(24%, 280px) 1fr;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.main-content {
|
||||
padding: 20px;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"loading": "Laden ...",
|
||||
"search": {
|
||||
"searchBar": {
|
||||
"submit": "Suchen",
|
||||
"placeholder": "Suchen..."
|
||||
},
|
||||
"error": {
|
||||
"searchNotAvailable": "Die Suche ist momentan nicht verfügbar."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"tryAgainToAnotherTime": "Bitte versuchen Sie es zu einem anderen Zeitpunkt erneut.",
|
||||
"pageNotFound": "Die gesuchte Seite existiert nicht. Bitte überprüfen Sie die URL oder kehren Sie zur Suchseite zurück."
|
||||
},
|
||||
"links": {
|
||||
"back": {
|
||||
"search": "Zurück zur Suche"
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"settings": "Einstellungen",
|
||||
"colorScheme": {
|
||||
"switch": {
|
||||
"light": "Zum hellen Modus wechseln",
|
||||
"dark": "Zum dunklen Modus wechseln",
|
||||
"auto": "Zum System-Farbschema wechseln"
|
||||
}
|
||||
},
|
||||
"locale": {
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "Deutsch",
|
||||
"fr": "Français",
|
||||
"es": "Español",
|
||||
"it": "Italiano",
|
||||
"pt": "Português"
|
||||
}
|
||||
}
|
||||
},
|
||||
"slogan": "Gebaut zum Suchen.",
|
||||
"settings": {
|
||||
"input": {
|
||||
"done": {
|
||||
"title": "Wert speichern",
|
||||
"ariaLabel": "Speichert den eingegebenen Wert"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"title": "Wert umschalten",
|
||||
"ariaLabel": "Schaltet den Wert um"
|
||||
},
|
||||
"pages": {
|
||||
"link": {
|
||||
"ariaLabel": "Geht zur Einstellungsseite {page}"
|
||||
}
|
||||
},
|
||||
"noSectionSelected": "Kein Einstellungsabschnitt ist ausgewählt.",
|
||||
"selection": {
|
||||
"option": {
|
||||
"ariaLabel": "Wählt die Option {option} aus"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"loading": "Loading ...",
|
||||
"search": {
|
||||
"searchBar": {
|
||||
"submit": "Search",
|
||||
"placeholder": "Search..."
|
||||
},
|
||||
"error": {
|
||||
"searchNotAvailable": "Search is not available right now."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"tryAgainToAnotherTime": "Please try again to another time.",
|
||||
"pageNotFound": "The page you are looking for does not exist. Please check the URL or return to the search page."
|
||||
},
|
||||
"links": {
|
||||
"back": {
|
||||
"search": "Back to Search"
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"settings": "Settings",
|
||||
"colorScheme": {
|
||||
"switch": {
|
||||
"light": "Switch to light mode",
|
||||
"dark": "Switch to dark mode",
|
||||
"auto": "Switch to the system color scheme"
|
||||
}
|
||||
},
|
||||
"locale": {
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "Deutsch",
|
||||
"fr": "Français",
|
||||
"es": "Español",
|
||||
"it": "Italiano",
|
||||
"pt": "Português"
|
||||
}
|
||||
}
|
||||
},
|
||||
"slogan": "Built to search.",
|
||||
"settings": {
|
||||
"input": {
|
||||
"done": {
|
||||
"title": "Save value",
|
||||
"ariaLabel": "Saves the entered value"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"title": "Toggle value",
|
||||
"ariaLabel": "Toggles the value"
|
||||
},
|
||||
"pages": {
|
||||
"link": {
|
||||
"ariaLabel": "Goes to settings page {page}"
|
||||
}
|
||||
},
|
||||
"noSectionSelected": "No settings section is selected.",
|
||||
"selection": {
|
||||
"option": {
|
||||
"ariaLabel": "Selects option {option}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"loading": "Cargando ...",
|
||||
"search": {
|
||||
"searchBar": {
|
||||
"submit": "Buscar",
|
||||
"placeholder": "Buscar..."
|
||||
},
|
||||
"error": {
|
||||
"searchNotAvailable": "La búsqueda no está disponible en este momento."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"tryAgainToAnotherTime": "Por favor, inténtalo de nuevo más tarde.",
|
||||
"pageNotFound": "La página que buscas no existe. Comprueba la URL o vuelve a la página de búsqueda."
|
||||
},
|
||||
"links": {
|
||||
"back": {
|
||||
"search": "Volver a la búsqueda"
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"settings": "Ajustes",
|
||||
"colorScheme": {
|
||||
"switch": {
|
||||
"light": "Cambiar al modo claro",
|
||||
"dark": "Cambiar al modo oscuro",
|
||||
"auto": "Usar el esquema de color del sistema"
|
||||
}
|
||||
},
|
||||
"locale": {
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "Deutsch",
|
||||
"fr": "Français",
|
||||
"es": "Español",
|
||||
"it": "Italiano",
|
||||
"pt": "Português"
|
||||
}
|
||||
}
|
||||
},
|
||||
"slogan": "Hecho para buscar."
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"loading": "Chargement ...",
|
||||
"search": {
|
||||
"searchBar": {
|
||||
"submit": "Rechercher",
|
||||
"placeholder": "Rechercher..."
|
||||
},
|
||||
"error": {
|
||||
"searchNotAvailable": "La recherche n'est pas disponible pour le moment."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"tryAgainToAnotherTime": "Veuillez réessayer ultérieurement.",
|
||||
"pageNotFound": "La page que vous recherchez n'existe pas. Vérifiez l'URL ou retournez à la page de recherche."
|
||||
},
|
||||
"links": {
|
||||
"back": {
|
||||
"search": "Retour à la recherche"
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"settings": "Paramètres",
|
||||
"colorScheme": {
|
||||
"switch": {
|
||||
"light": "Passer en mode clair",
|
||||
"dark": "Passer en mode sombre",
|
||||
"auto": "Utiliser le thème système"
|
||||
}
|
||||
},
|
||||
"locale": {
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "Deutsch",
|
||||
"fr": "Français",
|
||||
"es": "Español",
|
||||
"it": "Italiano",
|
||||
"pt": "Português"
|
||||
}
|
||||
}
|
||||
},
|
||||
"slogan": "Conçu pour chercher."
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"loading": "Caricamento ...",
|
||||
"search": {
|
||||
"searchBar": {
|
||||
"submit": "Cerca",
|
||||
"placeholder": "Cerca..."
|
||||
},
|
||||
"error": {
|
||||
"searchNotAvailable": "La ricerca non è disponibile al momento."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"tryAgainToAnotherTime": "Per favore riprova più tardi.",
|
||||
"pageNotFound": "La pagina che cerchi non esiste. Controlla l'URL o torna alla pagina di ricerca."
|
||||
},
|
||||
"links": {
|
||||
"back": {
|
||||
"search": "Torna alla ricerca"
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"settings": "Impostazioni",
|
||||
"colorScheme": {
|
||||
"switch": {
|
||||
"light": "Passa alla modalità chiara",
|
||||
"dark": "Passa alla modalità scura",
|
||||
"auto": "Usa la combinazione colori di sistema"
|
||||
}
|
||||
},
|
||||
"locale": {
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "Deutsch",
|
||||
"fr": "Français",
|
||||
"es": "Español",
|
||||
"it": "Italiano",
|
||||
"pt": "Português"
|
||||
}
|
||||
}
|
||||
},
|
||||
"slogan": "Costruito per cercare."
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"loading": "A carregar ...",
|
||||
"search": {
|
||||
"searchBar": {
|
||||
"submit": "Pesquisar",
|
||||
"placeholder": "Pesquisar..."
|
||||
},
|
||||
"error": {
|
||||
"searchNotAvailable": "A pesquisa não está disponível no momento."
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"tryAgainToAnotherTime": "Por favor, tente novamente mais tarde.",
|
||||
"pageNotFound": "A página que você procura não existe. Verifique o URL ou volte à página de pesquisa."
|
||||
},
|
||||
"links": {
|
||||
"back": {
|
||||
"search": "Voltar à pesquisa"
|
||||
}
|
||||
},
|
||||
"preferences": {
|
||||
"settings": "Configurações",
|
||||
"colorScheme": {
|
||||
"switch": {
|
||||
"light": "Mudar para modo claro",
|
||||
"dark": "Mudar para modo escuro",
|
||||
"auto": "Usar esquema de cores do sistema"
|
||||
}
|
||||
},
|
||||
"locale": {
|
||||
"languages": {
|
||||
"en": "English",
|
||||
"de": "Deutsch",
|
||||
"fr": "Français",
|
||||
"es": "Español",
|
||||
"it": "Italiano",
|
||||
"pt": "Português"
|
||||
}
|
||||
}
|
||||
},
|
||||
"slogan": "Feito para pesquisar."
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia';
|
||||
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
|
||||
import App from './App.vue'
|
||||
import { i18n, loadLanguage, fallbackLocale } from './i18n';
|
||||
import getCurrentLanguage from './utils/currentLanguage';
|
||||
import router from './router'
|
||||
import './styles/common.css'
|
||||
import './styles/variables/colors.css'
|
||||
import './styles/variables/dimensions.css';
|
||||
|
||||
import './styles/common.css';
|
||||
import './styles/variables/colors.css';
|
||||
|
||||
(async () => {
|
||||
await loadLanguage(fallbackLocale);
|
||||
await loadLanguage(getCurrentLanguage());
|
||||
|
||||
const pinia = createPinia();
|
||||
pinia.use(piniaPluginPersistedstate);
|
||||
|
||||
createApp(App)
|
||||
.use(router)
|
||||
.use(i18n)
|
||||
.use(pinia)
|
||||
.mount('#app');
|
||||
})();
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
import SearchView from '../views/SearchView.vue';
|
||||
import SearchResultsView from '@/features/search/views/SearchResultsView.vue';
|
||||
import SettingsView from '@/features/settings/views/SettingsView.vue';
|
||||
import NotFound from '../views/NotFound.vue';
|
||||
|
||||
const routes = [
|
||||
{
|
||||
path: '/',
|
||||
name: 'startPage',
|
||||
component: SearchView
|
||||
},
|
||||
{
|
||||
path: '/search',
|
||||
name: 'searchResults',
|
||||
component: SearchResultsView,
|
||||
props: route => ({
|
||||
searchQuery: route.query.q
|
||||
}),
|
||||
meta: {
|
||||
title: (route) => route.query.q
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/settings/:rest(.*)*',
|
||||
name: 'settings',
|
||||
component: SettingsView,
|
||||
meta: {
|
||||
title: () => i18n.global.t('preferences.settings')
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)*',
|
||||
name: 'notFound',
|
||||
component: NotFound
|
||||
},
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
export const updatePageTitle = function updatePageTitle (route) {
|
||||
const title =
|
||||
typeof route.meta.title === 'function'
|
||||
? route.meta.title(route)
|
||||
: route.meta.title;
|
||||
|
||||
if (title) {
|
||||
document.title = `${title} - Seekra`;
|
||||
} else {
|
||||
document.title = 'Seekra';
|
||||
};
|
||||
};
|
||||
|
||||
// remove trailing slash(es)
|
||||
router.beforeEach((route) => {
|
||||
if (route.path !== '/' && route.path.endsWith('/')) {
|
||||
return {
|
||||
path: route.path.replace(/\/+$/, ''),
|
||||
query: route.query,
|
||||
hash: route.hash,
|
||||
replace: true
|
||||
};
|
||||
};
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
body {
|
||||
background-color: var(--light-bg);
|
||||
color: var(--dark);
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link:hover:not(.button-link), .link:focus:not(.button-link) {
|
||||
outline: none;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.button-link {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.button-link:hover {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.button-link:visited {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
input {
|
||||
color: var(--dark);
|
||||
}
|
||||
|
||||
.input {
|
||||
padding: var(--input-padding);
|
||||
display: block;
|
||||
border-radius: calc(var(--input-padding) + 0.5 * var(--input-height) + 1px);
|
||||
height: var(--input-height);
|
||||
border: 1px solid var(--border);
|
||||
background: none;
|
||||
transition: border-color 0.2s ease;
|
||||
color: inherit;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.input:focus-visible {
|
||||
border-color: var(--primary-color);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.main-content-padding {
|
||||
padding: var(--main-content-padding);
|
||||
width: calc(100% - var(--main-content-padding-x) * 2);
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: var(--light-bg);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.button:focus {
|
||||
background-color: var(--light-hover);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: var(--light-hover);
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
body {
|
||||
--primary-color-l-8: oklch(0.897 0.202 260);
|
||||
--primary-color-l-7: oklch(0.847 0.202 260);
|
||||
--primary-color-l-6: oklch(0.797 0.202 260);
|
||||
--primary-color-l-6: oklch(0.747 0.202 260);
|
||||
--primary-color-l-4: oklch(0.697 0.202 260);
|
||||
--primary-color-l-3: oklch(0.647 0.202 260);
|
||||
--primary-color-l-2: oklch(0.597 0.202 260);
|
||||
--primary-color-l-1: oklch(0.547 0.202 260);
|
||||
--primary-color: oklch(0.497 0.202 260);
|
||||
--primary-color-d-1: oklch(0.447 0.202 260);
|
||||
--primary-color-d-2: oklch(0.397 0.202 260);
|
||||
--primary-color-d-3: oklch(0.347 0.202 260);
|
||||
--primary-color-d-4: oklch(0.297 0.202 260);
|
||||
--primary-color-d-5: oklch(0.247 0.202 260);
|
||||
--primary-color-d-6: oklch(0.197 0.202 260);
|
||||
--primary-color-d-7: oklch(0.147 0.202 260);
|
||||
--primary-color-d-8: oklch(0.097 0.202 260);
|
||||
|
||||
--black-l-8: oklch(0.4 0 0);
|
||||
--black-l-7: oklch(0.35 0 0);
|
||||
--black-l-6: oklch(0.3 0 0);
|
||||
--black-l-5: oklch(0.25 0 0);
|
||||
--black-l-4: oklch(0.2 0 0);
|
||||
--black-l-3: oklch(0.15 0 0);
|
||||
--black-l-2: oklch(0.1 0 0);
|
||||
--black-l-1: oklch(0.05 0 0);
|
||||
--black: oklch(0 0 0);
|
||||
|
||||
--white: oklch(1 0 0);
|
||||
--white-d-1: oklch(0.95 0 0);
|
||||
--white-d-2: oklch(0.9 0 0);
|
||||
--white-d-3: oklch(0.85 0 0);
|
||||
--white-d-4: oklch(0.8 0 0);
|
||||
--white-d-5: oklch(0.75 0 0);
|
||||
--white-d-6: oklch(0.7 0 0);
|
||||
--white-d-7: oklch(0.65 0 0);
|
||||
--white-d-8: oklch(0.6 0 0);
|
||||
|
||||
--dark-l-8: var(--black-l-8);
|
||||
--dark-l-7: var(--black-l-7);
|
||||
--dark-l-6: var(--black-l-6);
|
||||
--dark-l-5: var(--black-l-5);
|
||||
--dark-l-4: var(--black-l-4);
|
||||
--dark-l-3: var(--black-l-3);
|
||||
--dark-l-2: var(--black-l-2);
|
||||
--dark-l-1: var(--black-l-1);
|
||||
--dark: var(--black);
|
||||
|
||||
--light: var(--white);
|
||||
--light-d-1: var(--white-d-1);
|
||||
--light-d-2: var(--white-d-2);
|
||||
--light-d-3: var(--white-d-3);
|
||||
--light-d-4: var(--white-d-4);
|
||||
--light-d-5: var(--white-d-5);
|
||||
--light-d-6: var(--white-d-6);
|
||||
--light-d-7: var(--white-d-7);
|
||||
--light-d-8: var(--white-d-8);
|
||||
|
||||
--dark-bg: var(--black-l-2);
|
||||
--light-bg: var(--white);
|
||||
|
||||
--border: var(--white-d-3);
|
||||
|
||||
--gray-box-shadow: oklch(0.8 0.0001 271 / 0.7);
|
||||
--blue-box-shadow: oklch(0.52 0.15 268 / 0.25);
|
||||
|
||||
--light-hover: var(--light-d-2);
|
||||
|
||||
--invert: invert(0);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body.color-scheme-auto {
|
||||
--dark-l-8: var(--white-d-8);
|
||||
--dark-l-7: var(--white-d-7);
|
||||
--dark-l-6: var(--white-d-6);
|
||||
--dark-l-5: var(--white-d-5);
|
||||
--dark-l-4: var(--white-d-4);
|
||||
--dark-l-3: var(--white-d-3);
|
||||
--dark-l-2: var(--white-d-2);
|
||||
--dark-l-1: var(--white-d-1);
|
||||
--dark: var(--white);
|
||||
|
||||
--light: var(--black);
|
||||
--light-d-1: oklch(0.10 0 0);
|
||||
--light-d-2: var(--black-l-2);
|
||||
--light-d-3: var(--black-l-3);
|
||||
--light-d-4: var(--black-l-4);
|
||||
--light-d-5: var(--black-l-5);
|
||||
--light-d-6: var(--black-l-6);
|
||||
--light-d-7: var(--black-l-7);
|
||||
--light-d-8: var(--black-l-8);
|
||||
|
||||
--dark-bg: var(--white);
|
||||
--light-bg: oklch(0.18 0 0);
|
||||
|
||||
--border: var(--black-l-6);
|
||||
|
||||
--gray-box-shadow: oklch(0.25 0.0001 271 / 0.7);
|
||||
|
||||
--light-hover: var(--light-d-5);
|
||||
|
||||
--invert: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
body.dark {
|
||||
--dark-l-8: var(--white-d-8);
|
||||
--dark-l-7: var(--white-d-7);
|
||||
--dark-l-6: var(--white-d-6);
|
||||
--dark-l-5: var(--white-d-5);
|
||||
--dark-l-4: var(--white-d-4);
|
||||
--dark-l-3: var(--white-d-3);
|
||||
--dark-l-2: var(--white-d-2);
|
||||
--dark-l-1: var(--white-d-1);
|
||||
--dark: var(--white);
|
||||
|
||||
--light: var(--black);
|
||||
--light-d-1: oklch(0.10 0 0);
|
||||
--light-d-2: var(--black-l-2);
|
||||
--light-d-3: var(--black-l-3);
|
||||
--light-d-4: var(--black-l-4);
|
||||
--light-d-5: var(--black-l-5);
|
||||
--light-d-6: var(--black-l-6);
|
||||
--light-d-7: var(--black-l-7);
|
||||
--light-d-8: var(--black-l-8);
|
||||
|
||||
--dark-bg: var(--white);
|
||||
--light-bg: oklch(0.18 0 0);
|
||||
|
||||
--border: var(--black-l-6);
|
||||
|
||||
--gray-box-shadow: oklch(0.25 0.0001 271 / 0.7);
|
||||
|
||||
--light-hover: var(--light-d-5);
|
||||
|
||||
--invert: invert(1);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
:root {
|
||||
--input-padding: 0.7em;
|
||||
--input-height: 1em;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { vi } from 'vitest';
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { createI18n } from 'vue-i18n';
|
||||
import { createMemoryHistory, createRouter } from 'vue-router';
|
||||
|
||||
export const mountComponent = function mountComponent (component, options = {}, i18nMessages = {}, routes = [], piniaOptions = {}) {
|
||||
const {
|
||||
setupStores,
|
||||
...testingPiniaOptions
|
||||
} = piniaOptions;
|
||||
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
locale: 'en',
|
||||
messages: {
|
||||
en: i18nMessages
|
||||
}
|
||||
});
|
||||
|
||||
const router = createRouter({
|
||||
history: createMemoryHistory(),
|
||||
routes
|
||||
});
|
||||
|
||||
const testingPinia = createTestingPinia({
|
||||
...testingPiniaOptions,
|
||||
stubActions: false,
|
||||
createSpy: vi.fn
|
||||
});
|
||||
|
||||
if (setupStores) {
|
||||
setupStores();
|
||||
}
|
||||
|
||||
return mount(component, {
|
||||
global: {
|
||||
plugins: [
|
||||
i18n,
|
||||
router,
|
||||
testingPinia
|
||||
],
|
||||
},
|
||||
...options
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { ensureUnit } from '../cssDimensions';
|
||||
|
||||
describe('cssDimensions', () => {
|
||||
test.for([
|
||||
{ dimension: null, expected: '0px' },
|
||||
{ dimension: undefined, expected: '0px' },
|
||||
|
||||
{ dimension: 0, expected: '0px' },
|
||||
{ dimension: -0, expected: '0px' },
|
||||
{ dimension: -1, expected: '-1px' },
|
||||
{ dimension: -42, expected: '-42px' },
|
||||
{ dimension: 1, expected: '1px' },
|
||||
{ dimension: 42, expected: '42px' },
|
||||
|
||||
{ dimension: 0.0, expected: '0px' },
|
||||
{ dimension: -0.0, expected: '0px' },
|
||||
{ dimension: -1.0, expected: '-1px' },
|
||||
{ dimension: -42.0, expected: '-42px' },
|
||||
{ dimension: 1.0, expected: '1px' },
|
||||
{ dimension: 42.0, expected: '42px' },
|
||||
|
||||
{ dimension: 0.42, expected: '0.42px' },
|
||||
{ dimension: -0.42, expected: '-0.42px' },
|
||||
{ dimension: -1.42, expected: '-1.42px' },
|
||||
{ dimension: -42.42, expected: '-42.42px' },
|
||||
{ dimension: 1.42, expected: '1.42px' },
|
||||
{ dimension: 42.42, expected: '42.42px' },
|
||||
|
||||
{ dimension: '0', expected: '0px' },
|
||||
{ dimension: '-0', expected: '-0px' },
|
||||
{ dimension: '-1', expected: '-1px' },
|
||||
{ dimension: '-42', expected: '-42px' },
|
||||
{ dimension: '1', expected: '1px' },
|
||||
{ dimension: '42', expected: '42px' },
|
||||
|
||||
{ dimension: '0.0', expected: '0.0px' },
|
||||
{ dimension: '-0.0', expected: '-0.0px' },
|
||||
{ dimension: '-1.0', expected: '-1.0px' },
|
||||
{ dimension: '-42.0', expected: '-42.0px' },
|
||||
{ dimension: '1.0', expected: '1.0px' },
|
||||
{ dimension: '42.0', expected: '42.0px' },
|
||||
|
||||
{ dimension: '0.42', expected: '0.42px' },
|
||||
{ dimension: '-0.42', expected: '-0.42px' },
|
||||
{ dimension: '-1.42', expected: '-1.42px' },
|
||||
{ dimension: '-42.42', expected: '-42.42px' },
|
||||
{ dimension: '1.42', expected: '1.42px' },
|
||||
{ dimension: '42.42', expected: '42.42px' },
|
||||
|
||||
{ dimension: '0px', expected: '0px' },
|
||||
{ dimension: '-0px', expected: '-0px' },
|
||||
{ dimension: '-1px', expected: '-1px' },
|
||||
{ dimension: '-42px', expected: '-42px' },
|
||||
{ dimension: '1px', expected: '1px' },
|
||||
{ dimension: '42px', expected: '42px' },
|
||||
|
||||
{ dimension: '0.0px', expected: '0.0px' },
|
||||
{ dimension: '-0.0px', expected: '-0.0px' },
|
||||
{ dimension: '-1.0px', expected: '-1.0px' },
|
||||
{ dimension: '-42.0px', expected: '-42.0px' },
|
||||
{ dimension: '1.0px', expected: '1.0px' },
|
||||
{ dimension: '42.0px', expected: '42.0px' },
|
||||
|
||||
{ dimension: '0.42px', expected: '0.42px' },
|
||||
{ dimension: '-0.42px', expected: '-0.42px' },
|
||||
{ dimension: '-1.42px', expected: '-1.42px' },
|
||||
{ dimension: '-42.42px', expected: '-42.42px' },
|
||||
{ dimension: '1.42px', expected: '1.42px' },
|
||||
{ dimension: '42.42px', expected: '42.42px' },
|
||||
|
||||
{ dimension: '0em', expected: '0em' },
|
||||
{ dimension: '-0em', expected: '-0em' },
|
||||
{ dimension: '-1em', expected: '-1em' },
|
||||
{ dimension: '-42em', expected: '-42em' },
|
||||
{ dimension: '1em', expected: '1em' },
|
||||
{ dimension: '42em', expected: '42em' },
|
||||
|
||||
{ dimension: '0.0em', expected: '0.0em' },
|
||||
{ dimension: '-0.0em', expected: '-0.0em' },
|
||||
{ dimension: '-1.0em', expected: '-1.0em' },
|
||||
{ dimension: '-42.0em', expected: '-42.0em' },
|
||||
{ dimension: '1.0em', expected: '1.0em' },
|
||||
{ dimension: '42.0em', expected: '42.0em' },
|
||||
|
||||
{ dimension: '0.42em', expected: '0.42em' },
|
||||
{ dimension: '-0.42em', expected: '-0.42em' },
|
||||
{ dimension: '-1.42em', expected: '-1.42em' },
|
||||
{ dimension: '-42.42em', expected: '-42.42em' },
|
||||
{ dimension: '1.42em', expected: '1.42em' },
|
||||
{ dimension: '42.42em', expected: '42.42em' },
|
||||
|
||||
{ dimension: 'calc(42px - 1em)', expected: 'calc(42px - 1em)' },
|
||||
{ dimension: 'calc(42px)', expected: 'calc(42px)' },
|
||||
{ dimension: 'calc(42vh)', expected: 'calc(42vh)' },
|
||||
{ dimension: 'min(42px, 1em)', expected: 'min(42px, 1em)' },
|
||||
{ dimension: 'max(42px, 5rem)', expected: 'max(42px, 5rem)' },
|
||||
{ dimension: 'clamp(42vh, 23vw, 13cap)', expected: 'clamp(42vh, 23vw, 13cap)' },
|
||||
|
||||
{ dimension: 'calc(42px-1em)', expected: 'calc(42px-1em)' },
|
||||
{ dimension: 'min(42px,1em)', expected: 'min(42px,1em)' },
|
||||
{ dimension: 'max(42px,5rem)', expected: 'max(42px,5rem)' },
|
||||
{ dimension: 'clamp(42vh,23vw,13cap)', expected: 'clamp(42vh,23vw,13cap)' },
|
||||
{ dimension: 'clamp( 42vh,23vw,13cap )', expected: 'clamp( 42vh,23vw,13cap )' }
|
||||
])('ensureUnit returns $expected with input $dimension', ({ dimension, expected }) => {
|
||||
expect(ensureUnit(dimension)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import getCurrentLanguage from '../currentLanguage';
|
||||
|
||||
describe('currentLanguage', () => {
|
||||
test.for([
|
||||
{ navigatorLanguage: 'en', localStorageLanguage: null, expected: 'en'},
|
||||
{ navigatorLanguage: 'de', localStorageLanguage: null, expected: 'de'},
|
||||
{ navigatorLanguage: 'fr', localStorageLanguage: null, expected: 'fr'},
|
||||
|
||||
{ navigatorLanguage: 'en-US', localStorageLanguage: null, expected: 'en'},
|
||||
{ navigatorLanguage: 'en-AU', localStorageLanguage: null, expected: 'en'},
|
||||
{ navigatorLanguage: 'de-DE', localStorageLanguage: null, expected: 'de'},
|
||||
{ navigatorLanguage: 'fr-FR', localStorageLanguage: null, expected: 'fr'},
|
||||
|
||||
{ navigatorLanguage: 'en-us', localStorageLanguage: null, expected: 'en'},
|
||||
{ navigatorLanguage: 'en-au', localStorageLanguage: null, expected: 'en'},
|
||||
{ navigatorLanguage: 'de-de', localStorageLanguage: null, expected: 'de'},
|
||||
{ navigatorLanguage: 'fr-fr', localStorageLanguage: null, expected: 'fr'},
|
||||
|
||||
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: null, expected: 'zh'},
|
||||
{ navigatorLanguage: 'zh-Hant-TW', localStorageLanguage: null, expected: 'zh'},
|
||||
{ navigatorLanguage: 'uz-Latn-UZ', localStorageLanguage: null, expected: 'uz'},
|
||||
|
||||
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: null, expected: 'en'},
|
||||
{ navigatorLanguage: 'de-DE-u-co-phonebk', localStorageLanguage: null, expected: 'de'},
|
||||
{ navigatorLanguage: 'zh-Hant-TW-u-co-phonebk', localStorageLanguage: null, expected: 'zh'},
|
||||
|
||||
{ navigatorLanguage: 'en', localStorageLanguage: 'de', expected: 'de'},
|
||||
{ navigatorLanguage: 'de-DE', localStorageLanguage: 'en', expected: 'en'},
|
||||
{ navigatorLanguage: 'de-de', localStorageLanguage: 'en', expected: 'en'},
|
||||
{ navigatorLanguage: 'zh-Hans-CN', localStorageLanguage: 'fr', expected: 'fr'},
|
||||
{ navigatorLanguage: 'en-US-u-ca-gregory', localStorageLanguage: 'zh', expected: 'zh'}
|
||||
])('returns the language $expected (navigator: $navigatorLanguage; local storage: $localStorageLanguage)', ({ navigatorLanguage, localStorageLanguage, expected }) => {
|
||||
Object.defineProperty(navigator, 'language', {
|
||||
value: navigatorLanguage,
|
||||
configurable: true
|
||||
});
|
||||
|
||||
if (localStorageLanguage) {
|
||||
localStorage.setItem('locale', localStorageLanguage);
|
||||
};
|
||||
|
||||
expect(getCurrentLanguage()).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export const ensureUnit = function (d) {
|
||||
if (!d) d = 0;
|
||||
|
||||
// ensure that it is a string
|
||||
if (typeof d === 'number') d = d.toString();
|
||||
|
||||
if (d === '') d = '0';
|
||||
|
||||
// if it ends with a number
|
||||
if (/\d$/.test(d)) {
|
||||
d += 'px';
|
||||
};
|
||||
|
||||
return d;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
export default function getCurrentLanguage () {
|
||||
const saved = localStorage.getItem('locale');
|
||||
if (saved) return saved;
|
||||
const locale = new Intl.Locale(navigator.language);
|
||||
return locale.language;
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const { t } = useI18n();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="not-found main-content-padding">
|
||||
<span class="error-message">
|
||||
{{ t('error.pageNotFound') }}
|
||||
</span>
|
||||
<RouterLink to="/" id="link">
|
||||
{{ t('links.back.search') }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding-top: 80px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.error-message{
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
#link {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
padding: 10px 20px;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
<!--
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
-->
|
||||
|
||||
<script setup>
|
||||
import Searchbar from '../features/search/components/Searchbar.vue';
|
||||
import { ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import logo from '@/assets/images/logo.svg';
|
||||
|
||||
const { t } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
const searchQuery = ref('');
|
||||
|
||||
const submitSearch = function () {
|
||||
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-content main-content-padding">
|
||||
<header class="global-header">
|
||||
<img :src="logo" alt="Seekra" class="header-logo" />
|
||||
<span class="slogan">
|
||||
{{ t('slogan') }}
|
||||
</span>
|
||||
</header>
|
||||
|
||||
<div class="search-container">
|
||||
<Searchbar v-model="searchQuery" ref="searchbar" class="search-bar" auto-submit />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 60px;
|
||||
}
|
||||
|
||||
.global-header {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 40px;
|
||||
}
|
||||
|
||||
.header-logo {
|
||||
width: 320px;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
margin-top: 1rem;
|
||||
font-size: small;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 70%;
|
||||
max-width: 624px;
|
||||
}
|
||||
|
||||
@media (max-width: 67.5rem) {
|
||||
.search-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
Copyright 2026 Seekra
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path';
|
||||
import { createHtmlPlugin } from 'vite-plugin-html';
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: 'es2020'
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
createHtmlPlugin({
|
||||
minify: true
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
test: {
|
||||
globals: false,
|
||||
environment: 'jsdom'
|
||||
}
|
||||
})
|
||||