Compare commits
602 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74d658f928 | ||
|
|
24a1572752 | ||
|
|
80099635be | ||
|
|
cd28526ccc | ||
|
|
277e10daf1 | ||
|
|
a6fb3c87c4 | ||
|
|
bf6474a488 | ||
|
|
08f8d6ac6a | ||
|
|
0c80efc686 | ||
|
|
e1b6561b70 | ||
|
|
56e91db0b4 | ||
|
|
ce3b6fdc37 | ||
|
|
885427fa85 | ||
|
|
b77365e60f | ||
|
|
b3d601aa35 | ||
|
|
f1aca45ce3 | ||
|
|
46b1902af2 | ||
|
|
96cee7e5c2 | ||
|
|
8a32811226 | ||
|
|
18dbaee4b5 | ||
|
|
9c7cea23a7 | ||
|
|
519b085dd2 | ||
|
|
66d3bc6c7b | ||
|
|
40270f219a | ||
|
|
08fba15fee | ||
|
|
379e1a354b | ||
|
|
eacfca715c | ||
|
|
4bfff4b7e9 | ||
|
|
bbad583e53 | ||
|
|
359202a594 | ||
|
|
7f68f7dd9d | ||
|
|
77a9de570c | ||
|
|
a02b193b90 | ||
|
|
789eb30d2b | ||
|
|
7a653a5bb7 | ||
|
|
c929977773 | ||
|
|
d21ec9d727 | ||
|
|
5332d0ac62 | ||
|
|
6508eb57fc | ||
|
|
b7cc19b719 | ||
|
|
925ebf3210 | ||
|
|
b1ff6805eb | ||
|
|
1f4646dbdc | ||
|
|
6612e77579 | ||
|
|
2fd6d5cd2e | ||
|
|
0531b97e19 | ||
|
|
7336b52e14 | ||
|
|
cc03275ad3 | ||
|
|
09909514d5 | ||
|
|
a3dd8cb00d | ||
|
|
d207f7e2b2 | ||
|
|
cc348e5d35 | ||
|
|
aeed19532f | ||
|
|
b52f870e29 | ||
|
|
7889c32be0 | ||
|
|
1ca91d684f | ||
|
|
648f76bd22 | ||
|
|
f8d772149b | ||
|
|
181156de10 | ||
|
|
a038a3366f | ||
|
|
c43dc175d0 | ||
|
|
fd3f7f4a37 | ||
|
|
20e5c0e292 | ||
|
|
768c639d2b | ||
|
|
9bd7f8347b | ||
|
|
ae57022102 | ||
|
|
12d95dd4b1 | ||
|
|
57ac5cb9bf | ||
|
|
24508be4d4 | ||
|
|
9136ff3ea3 | ||
|
|
4460d80b72 | ||
|
|
eb8c178603 | ||
|
|
ad08cfaad0 | ||
|
|
96845ad7cf | ||
|
|
16412821bf | ||
|
|
3c0f4402a8 | ||
|
|
260087d1fa | ||
|
|
3361ef949a | ||
|
|
dfe0e960dc | ||
|
|
98e0623cb7 | ||
|
|
97084db4de | ||
|
|
165c76bb69 | ||
|
|
e0e4145481 | ||
|
|
8c4e7e0e43 | ||
|
|
dee41dad9e | ||
|
|
47d596af08 | ||
|
|
4019a7c225 | ||
|
|
db0c199674 | ||
|
|
f11a1fd9cc | ||
|
|
892754bbae | ||
|
|
a0de0c2005 | ||
|
|
6b5f5c54dd | ||
|
|
fdac632b1c | ||
|
|
43f804c7a8 | ||
|
|
e85220d727 | ||
|
|
489d46e922 | ||
|
|
b2fdb049e2 | ||
|
|
878031cae8 | ||
|
|
aeb0b90281 | ||
|
|
83eb04eed3 | ||
|
|
aeb3916741 | ||
|
|
c42b0677d1 | ||
|
|
08f7e4900c | ||
|
|
8e5c31f7f3 | ||
|
|
23e777cd94 | ||
|
|
fe22bb1035 | ||
|
|
0da9c7f965 | ||
|
|
48adb0b256 | ||
|
|
b2eeeeddfb | ||
|
|
5ee2b6de48 | ||
|
|
4e0b5d79df | ||
|
|
027fac90b4 | ||
|
|
24118e0dc3 | ||
|
|
faf478a092 | ||
|
|
f4a7b00b1b | ||
|
|
df7680e06c | ||
|
|
bbe68974b2 | ||
|
|
aad300bd9c | ||
|
|
12ce568d3c | ||
|
|
25d3f76f9e | ||
|
|
aece93d63d | ||
|
|
45e667abf7 | ||
|
|
5869b39035 | ||
|
|
d981d57a8f | ||
|
|
1f9d1edc5c | ||
|
|
4944ee6078 | ||
|
|
c4b2b909e0 | ||
|
|
08f3928ebc | ||
|
|
350c177be8 | ||
|
|
d3103cb966 | ||
|
|
eec3721e9d | ||
|
|
8058f8f376 | ||
|
|
eae67fd482 | ||
|
|
d10727b87d | ||
|
|
52d6dc9376 | ||
|
|
6ff38e8068 | ||
|
|
b54f4a846c | ||
|
|
b69fa34b9f | ||
|
|
e1f050ac7e | ||
|
|
76668b65fc | ||
|
|
7524cc31c1 | ||
|
|
a7f832107d | ||
|
|
674b0d7285 | ||
|
|
d618a09f6d | ||
|
|
995b1fe63c | ||
|
|
6200f4f790 | ||
|
|
a16c312292 | ||
|
|
1d907bc2d4 | ||
|
|
d267ba4cde | ||
|
|
07c31ebde1 | ||
|
|
2c353628a3 | ||
|
|
3d170d88fb | ||
|
|
90171b2ebd | ||
|
|
36298893db | ||
|
|
ca7d71eed4 | ||
|
|
d0e2771a2f | ||
|
|
6b80bc5b5e | ||
|
|
016a613fd5 | ||
|
|
c6865f48f5 | ||
|
|
3935bd67f8 | ||
|
|
917993a4fa | ||
|
|
1d807002f8 | ||
|
|
f95e1138a0 | ||
|
|
1d16d74383 | ||
|
|
4713e99c7b | ||
|
|
1e122b36a3 | ||
|
|
c87335ff56 | ||
|
|
b8dd0e2fb1 | ||
|
|
3a754d5703 | ||
|
|
e39fa172cb | ||
|
|
8c0d2b67e4 | ||
|
|
12cba1f8fb | ||
|
|
bf5ae30bda | ||
|
|
2973b47555 | ||
|
|
65cba31306 | ||
|
|
629920c2b3 | ||
|
|
e0f00fd4f5 | ||
|
|
083680b563 | ||
|
|
9a637c484b | ||
|
|
175ef04c70 | ||
|
|
1d11f8316c | ||
|
|
bb8027d663 | ||
|
|
bfa5c66006 | ||
|
|
0a606f6d23 | ||
|
|
e4611508ca | ||
|
|
3a8ac8298d | ||
|
|
6ab6d0d205 | ||
|
|
30d398f5f1 | ||
|
|
85de7d08fd | ||
|
|
9f9724ce36 | ||
|
|
264acd0532 | ||
|
|
7a495c9bd6 | ||
|
|
42549bf1a6 | ||
|
|
56c97001fa | ||
|
|
4ad8b1aef8 | ||
|
|
92e8160505 | ||
|
|
0b19cdc95c | ||
|
|
c4b6674492 | ||
|
|
cd2fa7943b | ||
|
|
6245f25b7c | ||
|
|
3c29d4a19f | ||
|
|
f917952083 | ||
|
|
6fe22a5fa4 | ||
|
|
8ea12399d3 | ||
|
|
2947d9aeec | ||
|
|
17551f813b | ||
|
|
8022a0cd92 | ||
|
|
e5edaeb147 | ||
|
|
4b1bd48487 | ||
|
|
980a539ad2 | ||
|
|
cef329376b | ||
|
|
230546d57f | ||
|
|
b418ef0f99 | ||
|
|
343245f290 | ||
|
|
5ec2db5dca | ||
|
|
c21f4e1d2e | ||
|
|
a49404b877 | ||
|
|
1982408881 | ||
|
|
4e352e1430 | ||
|
|
4fa0f5ffeb | ||
|
|
fc91894336 | ||
|
|
176f66b437 | ||
|
|
0a0a5bd60f | ||
|
|
e5504e0bdf | ||
|
|
220c068822 | ||
|
|
1214c17389 | ||
|
|
a82f6b16b7 | ||
|
|
7f74574a7f | ||
|
|
6d6185dc5e | ||
|
|
21e0b08716 | ||
|
|
651a6d6b3d | ||
|
|
658054b13d | ||
|
|
58ebfb71d1 | ||
|
|
842fb976cd | ||
|
|
8c28183410 | ||
|
|
5ae1036f4d | ||
|
|
cf70854af5 | ||
|
|
da1f7bea4f | ||
|
|
a8180a04ab | ||
|
|
55acdfba2a | ||
|
|
4b4498e84d | ||
|
|
f483653506 | ||
|
|
6e3def2c13 | ||
|
|
6416378b78 | ||
|
|
3591bce1b5 | ||
|
|
174a00c469 | ||
|
|
f07cb350d4 | ||
|
|
3d8fbe05f3 | ||
|
|
1fc1d69dae | ||
|
|
af401e7b27 | ||
|
|
bc274b2cf7 | ||
|
|
488c732f3c | ||
|
|
4d58e52729 | ||
|
|
0b7d7bbe7f | ||
|
|
75ac9fd38d | ||
|
|
d8d7b6a766 | ||
|
|
aaa7bae55f | ||
|
|
1786a5e910 | ||
|
|
d9d93ffad3 | ||
|
|
032b9f1f17 | ||
|
|
eba629e5a7 | ||
|
|
41fb8b6bf4 | ||
|
|
430e6025fa | ||
|
|
57a16c4904 | ||
|
|
4525aee9a1 | ||
|
|
e99aae3ed4 | ||
|
|
a9cff1f49a | ||
|
|
f5b0f06bba | ||
|
|
f65e72fdc1 | ||
|
|
979565691f | ||
|
|
9c115efecb | ||
|
|
1ce7749826 | ||
|
|
ef232aba88 | ||
|
|
0c9619a550 | ||
|
|
8b9d20d908 | ||
|
|
1006254090 | ||
|
|
b784f94a64 | ||
|
|
34c382daea | ||
|
|
1b57be1d64 | ||
|
|
25676cff8c | ||
|
|
476c9f5840 | ||
|
|
d663f047bd | ||
|
|
7663a53e1c | ||
|
|
117d8b99df | ||
|
|
bec69b4706 | ||
|
|
0beb876cb4 | ||
|
|
666eb75779 | ||
|
|
b31bcc2a9c | ||
|
|
2c1458e026 | ||
|
|
8d65b5117e | ||
|
|
1e0c8a594f | ||
|
|
b05da7d27c | ||
|
|
00e6cf537a | ||
|
|
c2c1c6dfcc | ||
|
|
753d4cd981 | ||
|
|
86cfc10741 | ||
|
|
492e2f2e7e | ||
|
|
560cceca9b | ||
|
|
f19f8ca2a7 | ||
|
|
925cf982ca | ||
|
|
30a7e3b429 | ||
|
|
f978c6f7d7 | ||
|
|
facbf8ec02 | ||
|
|
ac338e9299 | ||
|
|
d18e6412f0 | ||
|
|
373096b339 | ||
|
|
bb4bff50b3 | ||
|
|
c9baf04505 | ||
|
|
938fec1ab3 | ||
|
|
ed9f04332e | ||
|
|
c20e8d1aea | ||
|
|
3a13f111eb | ||
|
|
6c95c683ba | ||
|
|
d666f59c1b | ||
|
|
28869498ae | ||
|
|
81104a3f38 | ||
|
|
48cdbf6b6b | ||
|
|
7e9ce00166 | ||
|
|
c3431b0921 | ||
|
|
27210317fd | ||
|
|
312b51a5fd | ||
|
|
96a649ca9c | ||
|
|
445d8168e1 | ||
|
|
24a219853e | ||
|
|
4271159294 | ||
|
|
9ff94a4402 | ||
|
|
448b8fa160 | ||
|
|
aa46eb4c6d | ||
|
|
7e35fa8710 | ||
|
|
66b9e9b5c1 | ||
|
|
ad45bafd62 | ||
|
|
4cb2b34d11 | ||
|
|
2b8482cb06 | ||
|
|
57bc6ad98e | ||
|
|
8ec8816e45 | ||
|
|
801bc2ab66 | ||
|
|
5fec87e688 | ||
|
|
9490c00f9f | ||
|
|
a14dde8183 | ||
|
|
b742cda635 | ||
|
|
2e92acbfd5 | ||
|
|
7af904d860 | ||
|
|
59289a2f40 | ||
|
|
b08d0a76ee | ||
|
|
cc8fdbe4a6 | ||
|
|
64eb477f4f | ||
|
|
68f368dc94 | ||
|
|
219d9b2a59 | ||
|
|
a7e1506ea0 | ||
|
|
f61cfaa781 | ||
|
|
66de395549 | ||
|
|
242f382da3 | ||
|
|
a879bd09b4 | ||
|
|
ed273fbfce | ||
|
|
31cb8f9fab | ||
|
|
2aeb6598dc | ||
|
|
9810083304 | ||
|
|
23fd0482c8 | ||
|
|
39f9e16a61 | ||
|
|
c3a5e759b7 | ||
|
|
f9d5aeee91 | ||
|
|
371631856a | ||
|
|
73f33bc0a3 | ||
|
|
ed595a3815 | ||
|
|
7c8fa27aaf | ||
|
|
b326b1b3d7 | ||
|
|
f527064e55 | ||
|
|
3a074ef6c5 | ||
|
|
7fbae16a53 | ||
|
|
98018ca590 | ||
|
|
7d5724d8d5 | ||
|
|
ba17a5592c | ||
|
|
7332e81e6e | ||
|
|
5aeacef511 | ||
|
|
8f3fd3b37e | ||
|
|
0fd58860a8 | ||
|
|
8edd3774d2 | ||
|
|
dd1a4cba76 | ||
|
|
1123fb3ca6 | ||
|
|
5bfd340176 | ||
|
|
7cb2f203b1 | ||
|
|
412d0a19e7 | ||
|
|
f2a50141b7 | ||
|
|
7f65ce0e84 | ||
|
|
8f577a4368 | ||
|
|
09f89afe2a | ||
|
|
d1dcdc2f50 | ||
|
|
e31a5f705c | ||
|
|
04314d507b | ||
|
|
409c5a0ba3 | ||
|
|
68f09125d4 | ||
|
|
ac2d4911fe | ||
|
|
26c2eaddff | ||
|
|
d41d6bd758 | ||
|
|
86b8bbfb5f | ||
|
|
7658b4c9fe | ||
|
|
f1384eef34 | ||
|
|
c7c4a011ef | ||
|
|
bdd73135f0 | ||
|
|
b367410aaf | ||
|
|
c27404cc3a | ||
|
|
9ff2359b4c | ||
|
|
d28ecb509e | ||
|
|
048e7d0ef6 | ||
|
|
1b49a6f3f7 | ||
|
|
fd6e4880d2 | ||
|
|
f27bbd318f | ||
|
|
79dfb11fa4 | ||
|
|
3bf9126595 | ||
|
|
a5a90dfbdd | ||
|
|
ac767f63ae | ||
|
|
782862294d | ||
|
|
eded4b2e16 | ||
|
|
707ba55e1d | ||
|
|
b71a533024 | ||
|
|
36093b9199 | ||
|
|
bd14a35e23 | ||
|
|
406da9732c | ||
|
|
219aa06835 | ||
|
|
43decb21f4 | ||
|
|
b152da9d21 | ||
|
|
14ce2305ce | ||
|
|
a65cb92f37 | ||
|
|
4a54115e31 | ||
|
|
3a80838b4c | ||
|
|
bb16da0f4a | ||
|
|
714ae7b9f0 | ||
|
|
f5f60cc9eb | ||
|
|
057c5f7ec8 | ||
|
|
d704588624 | ||
|
|
c82a6cc0e1 | ||
|
|
603d83ba1b | ||
|
|
fd15235124 | ||
|
|
0c1e5a9628 | ||
|
|
6403d135ea | ||
|
|
a7b33c48b3 | ||
|
|
4e31190291 | ||
|
|
e99a06654e | ||
|
|
cf4d0565c5 | ||
|
|
6b5a874db5 | ||
|
|
defc7a8fa4 | ||
|
|
06d568e61d | ||
|
|
6064c88f2c | ||
|
|
77b3430339 | ||
|
|
0f125cd63f | ||
|
|
65f84b116f | ||
|
|
2d008a5cc3 | ||
|
|
4dcd4483e9 | ||
|
|
0102ce2144 | ||
|
|
71d5887bcc | ||
|
|
76ce301d91 | ||
|
|
692ccd7838 | ||
|
|
bf97b1c115 | ||
|
|
6eda3c68a8 | ||
|
|
ca9a53bde9 | ||
|
|
9b9327c25e | ||
|
|
a2be4c73a1 | ||
|
|
720987197d | ||
|
|
c52e83fe10 | ||
|
|
e97e03b814 | ||
|
|
ac9b85e10b | ||
|
|
8e80af657f | ||
|
|
93f9afd98d | ||
|
|
c9178db87d | ||
|
|
18743f66e8 | ||
|
|
72ae2ec5c4 | ||
|
|
0a407d869e | ||
|
|
8c8b684cb4 | ||
|
|
63d1b86b5e | ||
|
|
49a9055a73 | ||
|
|
4f444bd3c9 | ||
|
|
1efab98f91 | ||
|
|
b8677b4c02 | ||
|
|
e6400d8305 | ||
|
|
7bdfdcc1f8 | ||
|
|
9d9c12a52d | ||
|
|
1aa9da57aa | ||
|
|
0b57e42652 | ||
|
|
9a988e82c4 | ||
|
|
9e83448cd0 | ||
|
|
8d9a3cbb1e | ||
|
|
fc5a40ace0 | ||
|
|
9f2d0b5b07 | ||
|
|
7e98e98ad0 | ||
|
|
f6085aa89b | ||
|
|
32905aa16b | ||
|
|
8a240d506b | ||
|
|
b6f07fdf85 | ||
|
|
9a7ab172ac | ||
|
|
3d6c70629d | ||
|
|
c8d13a6fa1 | ||
|
|
b24ac01493 | ||
|
|
be21da5e3b | ||
|
|
b6fea7dc9b | ||
|
|
adc2e96bf2 | ||
|
|
807dc612a7 | ||
|
|
1612de8231 | ||
|
|
4576ddf09c | ||
|
|
bb96de47be | ||
|
|
ccfc56e629 | ||
|
|
e6cde49247 | ||
|
|
1de41a8d24 | ||
|
|
953c62cc66 | ||
|
|
c2f5569010 | ||
|
|
baef35d263 | ||
|
|
8b8345d0e6 | ||
|
|
3bab677607 | ||
|
|
79362162d2 | ||
|
|
ca537c6321 | ||
|
|
79382ce6f8 | ||
|
|
ea70aa21a9 | ||
|
|
e9f1b2a9b4 | ||
|
|
981513086b | ||
|
|
9f4bfe75d1 | ||
|
|
dcd8bbbd01 | ||
|
|
cadfa5e034 | ||
|
|
76ea0c4978 | ||
|
|
c1321203c1 | ||
|
|
a8388db1c3 | ||
|
|
f0589c2f33 | ||
|
|
fbe7d64d5d | ||
|
|
a5722b2445 | ||
|
|
18e57e20ed | ||
|
|
5b83bf35d0 | ||
|
|
97fb8d8561 | ||
|
|
fe5a72ae90 | ||
|
|
643a48afec | ||
|
|
e50598dfa3 | ||
|
|
46fb1f0ac0 | ||
|
|
eaaeee6ee7 | ||
|
|
522c5562eb | ||
|
|
ef8c63c176 | ||
|
|
12537ec7f3 | ||
|
|
8b791ed19e | ||
|
|
a1fdd0c0d2 | ||
|
|
0b7097082d | ||
|
|
c903192d7c | ||
|
|
d8b5e0f5a1 | ||
|
|
fb8d476c34 | ||
|
|
b4652e2486 | ||
|
|
b0d37dc997 | ||
|
|
778930015e | ||
|
|
fb8c45a86d | ||
|
|
13b0533277 | ||
|
|
6cb6b1534d | ||
|
|
4098f2e498 | ||
|
|
19093a4ead | ||
|
|
c7e2cb9d06 | ||
|
|
6be22170d8 | ||
|
|
2077b0ab4c | ||
|
|
bc2ca86a95 | ||
|
|
a443582ca9 | ||
|
|
2162397575 | ||
|
|
6cfb32570b | ||
|
|
a8d66fd0a4 | ||
|
|
b46d3d28a9 | ||
|
|
44372059cd | ||
|
|
64668147d9 | ||
|
|
e21e71b442 | ||
|
|
10f646f125 | ||
|
|
187b76e04c | ||
|
|
f2c36cae37 | ||
|
|
77a6357ae4 | ||
|
|
6c1be53f86 | ||
|
|
59e02f047b | ||
|
|
321250f30e | ||
|
|
7559036abe | ||
|
|
6080b57f8a | ||
|
|
ddf654a641 | ||
|
|
bf8e187962 | ||
|
|
cf252c3ec4 | ||
|
|
d65541fb3e | ||
|
|
20ef99cfc7 | ||
|
|
87a003918d | ||
|
|
f5c3c90f37 | ||
|
|
ee690092ba | ||
|
|
34a386abce | ||
|
|
05f4c42bf9 | ||
|
|
1510dc9bc2 | ||
|
|
2a85b621f5 | ||
|
|
97abf2f230 | ||
|
|
1f2d38730d | ||
|
|
5c000f50aa | ||
|
|
581931b679 | ||
|
|
709a91b1db | ||
|
|
6ddb8d01ec | ||
|
|
81d3144da2 | ||
|
|
be2e8b7169 | ||
|
|
a21774031b | ||
|
|
de4dc9b9a0 | ||
|
|
7ae2859c78 | ||
|
|
8228472031 | ||
|
|
7ca5a2bd02 | ||
|
|
8858c7f619 | ||
|
|
992d7e3303 | ||
|
|
bce135cba8 | ||
|
|
8c30bedefb | ||
|
|
e26f2910f3 | ||
|
|
0b1208a11c | ||
|
|
261919f17a | ||
|
|
4206cbd557 | ||
|
|
edc203c9ed |
2
.bundle/config
Normal file
@@ -0,0 +1,2 @@
|
||||
---
|
||||
BUNDLE_PATH: "vendor/bundle"
|
||||
9
.editorconfig
Normal file
@@ -0,0 +1,9 @@
|
||||
# EditorConfig is awesome: https://EditorConfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
2
.env
@@ -1,4 +1,4 @@
|
||||
VITE_APP_API_URL=http://localhost:2021
|
||||
VITE_IS_LANDING_DEFAULT=false
|
||||
VITE_IS_LANDING_DEFAULT=true
|
||||
VITE_OPENREPLAY_PROJECT_KEY=
|
||||
VITE_IS_SELF_HOSTED=false
|
||||
@@ -2,3 +2,4 @@ VITE_APP_API_URL=http://localhost:2021
|
||||
VITE_APP_REDIRECT_URL=http://localhost:5173
|
||||
VITE_APP_GOOGLE_CLIENT_ID=USE_YOUR_OWN_CLIENT_ID
|
||||
VITE_IS_SELF_HOSTED=true
|
||||
VITE_IS_LANDING_DEFAULT=true
|
||||
25
.github/workflows/build.yml
vendored
@@ -2,39 +2,20 @@ name: Build validation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "main", "develop" ]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [ "main", "develop" ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [20.x, 22.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
node-version: 20.x
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
- run: npm run build
|
||||
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Use Node.js 22
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
# Lint currently reporting lots of preexisting issues
|
||||
# - run: npm run lint
|
||||
|
||||
22
.gitignore
vendored
@@ -23,4 +23,24 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
resources/android/**/*
|
||||
resources/android/**/*
|
||||
resources/ios/**/*
|
||||
|
||||
# Secrets (pulled via scripts/pull-secrets.sh)
|
||||
.env.production
|
||||
android/app/google-services.json
|
||||
android/app/release/*.jks
|
||||
android/keystore.properties
|
||||
android/local.properties
|
||||
android/play-service-account.json
|
||||
ios/App/App/GoogleService-Info.plist
|
||||
ios/*.p8
|
||||
|
||||
# Release build outputs
|
||||
build/android
|
||||
build/ios
|
||||
fastlane/report.xml
|
||||
fastlane/Preview.html
|
||||
fastlane/.env
|
||||
fastlane/*.log
|
||||
vendor/bundle
|
||||
85
AUTOMATED_RELEASE.md
Normal file
@@ -0,0 +1,85 @@
|
||||
# Automated Release Pipeline
|
||||
|
||||
Builds and (optionally) uploads signed Android and iOS release artifacts from the CLI — no Android Studio or Xcode GUI needed for routine releases.
|
||||
|
||||
## Quick start
|
||||
|
||||
```sh
|
||||
./scripts/pull-secrets.sh # pulls all signing secrets from Vaultwarden
|
||||
./scripts/release.sh # bump patch version, build + sign both platforms
|
||||
./scripts/release.sh --upload # also upload: Android → Play internal track, iOS → TestFlight
|
||||
```
|
||||
|
||||
Common flags on `release.sh`:
|
||||
|
||||
| Flag | Effect |
|
||||
|---|---|
|
||||
| `--android` / `--ios` | Build only one platform |
|
||||
| `--bump minor\|major` | Bump type (default: `patch`) |
|
||||
| `--skip-bump` | Build with the current version, don't bump |
|
||||
| `--upload` | Upload after building |
|
||||
| `--track TRACK` | Play Store track for `--upload` (default: `internal`) |
|
||||
|
||||
Outputs:
|
||||
- Android: `android/app/build/outputs/bundle/release/app-release.aab`
|
||||
- iOS: `build/ios/Donetick.ipa`
|
||||
|
||||
## One-time machine setup
|
||||
|
||||
1. `bundle install` — installs fastlane (Ruby gems, local to the repo via `vendor/bundle`)
|
||||
2. `./scripts/pull-secrets.sh` — pulls all secrets below from Vaultwarden
|
||||
3. `android/local.properties` needs `sdk.dir=<path to Android SDK>` — not pulled from vault, machine-specific. Android Studio creates this automatically; if building headless-only, create it manually.
|
||||
4. Export `ASC_ISSUER_ID` (App Store Connect API key issuer ID) — either in your shell profile, or add it to the `Donetick Env Production` vault note so `pull-secrets.sh` delivers it into `.env.production` (which `release.sh` auto-sources).
|
||||
5. A working Apple Distribution certificate + the `Donetick App Store(fastline)` provisioning profile must be installed in your keychain / `~/Library/MobileDevice/Provisioning Profiles/` (see "iOS signing model" below) — **not** currently pulled from vault, machine-specific one-time setup.
|
||||
|
||||
## Secrets pulled by `scripts/pull-secrets.sh`
|
||||
|
||||
All secrets live in Vaultwarden (`https://www.bitwarden.com`) as Secure Notes, named exactly as below:
|
||||
|
||||
| Vault item name | Written to | Encoding |
|
||||
|---|---|---|
|
||||
| Donetick Google Services Android | `android/app/google-services.json` | raw |
|
||||
| Donetick Android Keystore | `android/app/release/donetick.jks` | base64 |
|
||||
| Donetick Keystore Password | (used inline for `android/keystore.properties`) | raw |
|
||||
| Donetick Google Play Service Account | `android/play-service-account.json` | raw |
|
||||
| Donetick Google Services iOS | `ios/App/App/GoogleService-Info.plist` | raw |
|
||||
| Donetick App Store Connect Key | `ios/AuthKey_84F695CDQ3.p8` | base64 |
|
||||
| Donetick Env Production | `.env.production` | raw |
|
||||
|
||||
None of these files are committed to git — all covered by `.gitignore`.
|
||||
|
||||
## iOS signing model (important — don't relitigate this)
|
||||
|
||||
The App target uses **manual signing for Release**, not Automatic. This was a deliberate choice, not the default:
|
||||
|
||||
- The project has **two** Apple Distribution-type certificates on the Apple Developer account: an Xcode Cloud–managed one (private key never leaves Apple, can't be used for local CLI signing) and a plain one created via Xcode → Settings → Accounts → Manage Certificates (private key is in the local keychain, usable). Automatic signing kept binding to the wrong (Managed) one.
|
||||
- Fix: `ios/App/App.xcodeproj/project.pbxproj` → App target → Release config has `CODE_SIGN_STYLE = Manual`, `CODE_SIGN_IDENTITY = "Apple Distribution"`, `PROVISIONING_PROFILE_SPECIFIER = "Donetick App Store(fastline)"`. Debug config is untouched (still Automatic, for local dev builds in Xcode).
|
||||
- `fastlane/Fastfile`'s `ios release` lane passes explicit `export_options: { signingStyle: "manual", provisioningProfiles: {...} }` to `build_app` — do **not** switch this back to `-allowProvisioningUpdates`/automatic without a good reason, it re-triggers the ambiguous cert selection.
|
||||
|
||||
**If you ever add/change an entitlement** (new Capability in Signing & Capabilities, e.g. a new permission), the existing provisioning profile becomes stale and exports will fail with an error like `"App.app" requires a provisioning profile with the X feature`. Fix:
|
||||
1. developer.apple.com → Identifiers → `com.donetick.app` → confirm the new capability is checked
|
||||
2. developer.apple.com → Profiles → find `Donetick App Store(fastline)` → regenerate it → download
|
||||
3. Install it locally: get its UUID (`security cms -D -i <file>.mobileprovision | plutil -extract UUID xml1 -o - -`) and copy to `~/Library/MobileDevice/Provisioning Profiles/<uuid>.mobileprovision`
|
||||
|
||||
**If the Distribution certificate ever expires/is revoked**, redo the one-time Xcode step: Settings → Accounts → Manage Certificates → `+` → Apple Distribution, then regenerate the profile as above.
|
||||
|
||||
## Android signing model
|
||||
|
||||
Standard keystore signing via `android/keystore.properties`, read by `android/app/build.gradle`. One historical gotcha already fixed: `storeFile` must be `release/donetick.jks` (relative to the `android/app` module dir), **not** `app/release/donetick.jks` — that extra `app/` silently resolved to a double-nested wrong path and broke CLI builds (Android Studio may have masked this before).
|
||||
|
||||
## Play Store upload gotchas
|
||||
|
||||
- The service account (`fastline-471@donetick-5f910.iam.gserviceaccount.com`) needs the **Google Play Android Developer API enabled** in its GCP project (`donetick-5f910`), **and** explicit per-app release permission granted in Play Console → Users and permissions (not just account-level access) — **and you have to actually click Save**, the UI will silently not persist it otherwise.
|
||||
- Google Play Console permission changes can take time to propagate on Google's backend even after saving (historically documented as up to ~24h, though in practice it was near-instant once actually saved).
|
||||
- The Play Developer API requires the app to have had **at least one manual release** uploaded through the Play Console website before API uploads work at all — not an issue for Donetick (already satisfied) but relevant if this is ever set up for a brand-new app.
|
||||
|
||||
## Fastlane lanes reference
|
||||
|
||||
```sh
|
||||
bundle exec fastlane android release # build signed .aab
|
||||
bundle exec fastlane android apk # build signed .apk (sideloading/testing)
|
||||
bundle exec fastlane android upload [track:X] # upload .aab to Play Store (default track: internal)
|
||||
|
||||
bundle exec fastlane ios release # build signed .ipa
|
||||
bundle exec fastlane ios upload # upload .ipa to TestFlight
|
||||
```
|
||||
233
Gemfile.lock
Normal file
@@ -0,0 +1,233 @@
|
||||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
CFPropertyList (3.0.9)
|
||||
abbrev (0.1.2)
|
||||
addressable (2.9.0)
|
||||
public_suffix (>= 2.0.2, < 8.0)
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.2)
|
||||
aws-partitions (1.1109.0)
|
||||
aws-sdk-core (3.224.1)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
base64
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.101.0)
|
||||
aws-sdk-core (~> 3, >= 3.216.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.188.0)
|
||||
aws-sdk-core (~> 3, >= 3.224.1)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.11.0)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
babosa (1.0.4)
|
||||
base64 (0.2.0)
|
||||
claide (1.1.0)
|
||||
colored (1.2)
|
||||
colored2 (3.1.2)
|
||||
commander (4.6.0)
|
||||
highline (~> 2.0.0)
|
||||
csv (3.3.5)
|
||||
declarative (0.0.20)
|
||||
digest-crc (0.7.0)
|
||||
rake (>= 12.0.0, < 14.0.0)
|
||||
domain_name (0.5.20190701)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
dotenv (2.8.1)
|
||||
emoji_regex (3.2.3)
|
||||
excon (0.109.0)
|
||||
faraday (1.10.6)
|
||||
faraday-em_http (~> 1.0)
|
||||
faraday-em_synchrony (~> 1.0)
|
||||
faraday-excon (~> 1.1)
|
||||
faraday-httpclient (~> 1.0)
|
||||
faraday-multipart (~> 1.0)
|
||||
faraday-net_http (~> 1.0)
|
||||
faraday-net_http_persistent (~> 1.0)
|
||||
faraday-patron (~> 1.0)
|
||||
faraday-rack (~> 1.0)
|
||||
faraday-retry (~> 1.0)
|
||||
ruby2_keywords (>= 0.0.4)
|
||||
faraday-cookie_jar (0.0.8)
|
||||
faraday (>= 0.8.0)
|
||||
http-cookie (>= 1.0.0)
|
||||
faraday-em_http (1.0.0)
|
||||
faraday-em_synchrony (1.0.1)
|
||||
faraday-excon (1.1.0)
|
||||
faraday-httpclient (1.0.1)
|
||||
faraday-multipart (1.2.0)
|
||||
multipart-post (~> 2.0)
|
||||
faraday-net_http (1.0.2)
|
||||
faraday-net_http_persistent (1.2.0)
|
||||
faraday-patron (1.0.0)
|
||||
faraday-rack (1.0.0)
|
||||
faraday-retry (1.0.4)
|
||||
faraday_middleware (1.2.1)
|
||||
faraday (~> 1.0)
|
||||
fastimage (2.4.1)
|
||||
fastlane (2.230.0)
|
||||
CFPropertyList (>= 2.3, < 4.0.0)
|
||||
abbrev (~> 0.1.2)
|
||||
addressable (>= 2.8, < 3.0.0)
|
||||
artifactory (~> 3.0)
|
||||
aws-sdk-s3 (~> 1.0)
|
||||
babosa (>= 1.0.3, < 2.0.0)
|
||||
base64 (~> 0.2.0)
|
||||
bundler (>= 1.12.0, < 3.0.0)
|
||||
colored (~> 1.2)
|
||||
commander (~> 4.6)
|
||||
csv (~> 3.3)
|
||||
dotenv (>= 2.1.1, < 3.0.0)
|
||||
emoji_regex (>= 0.1, < 4.0)
|
||||
excon (>= 0.71.0, < 1.0.0)
|
||||
faraday (~> 1.0)
|
||||
faraday-cookie_jar (~> 0.0.6)
|
||||
faraday_middleware (~> 1.0)
|
||||
fastimage (>= 2.1.0, < 3.0.0)
|
||||
fastlane-sirp (>= 1.0.0)
|
||||
gh_inspector (>= 1.1.2, < 2.0.0)
|
||||
google-apis-androidpublisher_v3 (~> 0.3)
|
||||
google-apis-playcustomapp_v1 (~> 0.1)
|
||||
google-cloud-env (>= 1.6.0, < 2.0.0)
|
||||
google-cloud-storage (~> 1.31)
|
||||
highline (~> 2.0)
|
||||
http-cookie (~> 1.0.5)
|
||||
json (< 3.0.0)
|
||||
jwt (>= 2.1.0, < 3)
|
||||
logger (>= 1.6, < 2.0)
|
||||
mini_magick (>= 4.9.4, < 5.0.0)
|
||||
multipart-post (>= 2.0.0, < 3.0.0)
|
||||
mutex_m (~> 0.3.0)
|
||||
naturally (~> 2.2)
|
||||
nkf (~> 0.2.0)
|
||||
optparse (>= 0.1.1, < 1.0.0)
|
||||
plist (>= 3.1.0, < 4.0.0)
|
||||
rubyzip (>= 2.0.0, < 3.0.0)
|
||||
security (= 0.1.5)
|
||||
simctl (~> 1.6.3)
|
||||
terminal-notifier (>= 2.0.0, < 3.0.0)
|
||||
terminal-table (~> 3)
|
||||
tty-screen (>= 0.6.3, < 1.0.0)
|
||||
tty-spinner (>= 0.8.0, < 1.0.0)
|
||||
word_wrap (~> 1.0.0)
|
||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||
xcpretty (~> 0.4.1)
|
||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||
fastlane-sirp (1.1.0)
|
||||
gh_inspector (1.1.3)
|
||||
google-apis-androidpublisher_v3 (0.54.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-core (0.11.3)
|
||||
addressable (~> 2.5, >= 2.5.1)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
httpclient (>= 2.8.1, < 3.a)
|
||||
mini_mime (~> 1.0)
|
||||
representable (~> 3.0)
|
||||
retriable (>= 2.0, < 4.a)
|
||||
rexml
|
||||
google-apis-iamcredentials_v1 (0.17.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-playcustomapp_v1 (0.13.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-apis-storage_v1 (0.29.0)
|
||||
google-apis-core (>= 0.11.0, < 2.a)
|
||||
google-cloud-core (1.6.1)
|
||||
google-cloud-env (>= 1.0, < 3.a)
|
||||
google-cloud-errors (~> 1.0)
|
||||
google-cloud-env (1.6.0)
|
||||
faraday (>= 0.17.3, < 3.0)
|
||||
google-cloud-errors (1.3.1)
|
||||
google-cloud-storage (1.45.0)
|
||||
addressable (~> 2.8)
|
||||
digest-crc (~> 0.4)
|
||||
google-apis-iamcredentials_v1 (~> 0.1)
|
||||
google-apis-storage_v1 (~> 0.29.0)
|
||||
google-cloud-core (~> 1.6)
|
||||
googleauth (>= 0.16.2, < 2.a)
|
||||
mini_mime (~> 1.0)
|
||||
googleauth (1.8.1)
|
||||
faraday (>= 0.17.3, < 3.a)
|
||||
jwt (>= 1.4, < 3.0)
|
||||
multi_json (~> 1.11)
|
||||
os (>= 0.9, < 2.0)
|
||||
signet (>= 0.16, < 2.a)
|
||||
highline (2.0.3)
|
||||
http-cookie (1.0.8)
|
||||
domain_name (~> 0.5)
|
||||
httpclient (2.9.0)
|
||||
mutex_m
|
||||
jmespath (1.6.2)
|
||||
json (2.7.6)
|
||||
jwt (2.10.3)
|
||||
base64
|
||||
logger (1.7.0)
|
||||
mini_magick (4.13.2)
|
||||
mini_mime (1.1.5)
|
||||
multi_json (1.15.0)
|
||||
multipart-post (2.4.1)
|
||||
mutex_m (0.3.0)
|
||||
nanaimo (0.4.0)
|
||||
naturally (2.3.0)
|
||||
nkf (0.2.0)
|
||||
optparse (0.8.1)
|
||||
os (1.1.4)
|
||||
plist (3.7.2)
|
||||
public_suffix (5.1.1)
|
||||
rake (13.4.2)
|
||||
representable (3.2.0)
|
||||
declarative (< 0.1.0)
|
||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||
uber (< 0.2.0)
|
||||
retriable (3.8.0)
|
||||
rexml (3.4.4)
|
||||
rouge (3.28.0)
|
||||
ruby2_keywords (0.0.5)
|
||||
rubyzip (2.4.1)
|
||||
security (0.1.5)
|
||||
signet (0.18.0)
|
||||
addressable (~> 2.8)
|
||||
faraday (>= 0.17.5, < 3.a)
|
||||
jwt (>= 1.5, < 3.0)
|
||||
multi_json (~> 1.10)
|
||||
simctl (1.6.10)
|
||||
CFPropertyList
|
||||
naturally
|
||||
terminal-notifier (2.0.0)
|
||||
terminal-table (3.0.2)
|
||||
unicode-display_width (>= 1.1.1, < 3)
|
||||
trailblazer-option (0.1.2)
|
||||
tty-cursor (0.7.1)
|
||||
tty-screen (0.8.2)
|
||||
tty-spinner (0.9.3)
|
||||
tty-cursor (~> 0.7)
|
||||
uber (0.1.0)
|
||||
unf (0.2.0)
|
||||
unicode-display_width (2.6.0)
|
||||
word_wrap (1.0.0)
|
||||
xcodeproj (1.28.1)
|
||||
CFPropertyList (>= 2.3.3, < 4.0)
|
||||
atomos (~> 0.1.3)
|
||||
base64
|
||||
claide (>= 1.0.2, < 2.0)
|
||||
colored2 (~> 3.1)
|
||||
nanaimo (~> 0.4.0)
|
||||
nkf
|
||||
rexml (>= 3.3.6, < 4.0)
|
||||
xcpretty (0.4.1)
|
||||
rouge (~> 3.28.0)
|
||||
xcpretty-travis-formatter (1.0.1)
|
||||
xcpretty (~> 0.2, >= 0.0.7)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
fastlane
|
||||
|
||||
BUNDLED WITH
|
||||
1.17.2
|
||||
@@ -20,12 +20,9 @@ As an avid for open-source, I was eager to create a solution that could benefit
|
||||
- Recurring Tasks: Schedule tasks to repeat daily, weekly, monthly, or yearly, with flexible customization options.
|
||||
- Progress Tracking: Track the completion status of tasks and view historical data.
|
||||
|
||||
## Installation
|
||||
## Development Environment
|
||||
|
||||
1. Clone the repository:
|
||||
2. Navigate to the project directory: `cd frontend`
|
||||
3. Download dependency `npm install`
|
||||
4. Run locally `npm start`
|
||||
Follow the full instructions here: https://github.com/donetick/donetick?tab=readme-ov-file#development-environment
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -41,7 +38,7 @@ Contributions are welcome! If you would like to contribute to Donetick, please f
|
||||
|
||||
Donetick is a work in progress and has been a fantastic learning experience for me as I've honed my React skills,I'm looking for collaborators to help improve and refine the Donetick. Feel free to open PR or suggest changes.
|
||||
|
||||
## Plans :
|
||||
## Plans:
|
||||
|
||||
My goal is to expand Donetick by offering a hosted infrastructure option. This will make it even easier for users to access and utilize Donetick's features without the need for self-hosting.
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
apply plugin: 'com.android.application'
|
||||
|
||||
def keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
def keystoreProperties = new Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace "com.donetick.app"
|
||||
compileSdk rootProject.ext.compileSdkVersion
|
||||
@@ -7,8 +13,8 @@ android {
|
||||
applicationId "com.donetick.app"
|
||||
minSdkVersion rootProject.ext.minSdkVersion
|
||||
targetSdkVersion rootProject.ext.targetSdkVersion
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
versionCode 37
|
||||
versionName "1.2.16"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
aaptOptions {
|
||||
// Files and dirs to omit from the packaged assets dir, modified to accommodate modern web apps.
|
||||
@@ -16,8 +22,19 @@ android {
|
||||
ignoreAssetsPattern '!.svn:!.git:!.ds_store:!*.scc:.*:!CVS:!thumbs.db:!picasa.ini:!*~'
|
||||
}
|
||||
}
|
||||
signingConfigs {
|
||||
release {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
storeFile file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
signingConfig keystorePropertiesFile.exists() ? signingConfigs.release : signingConfigs.debug
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
|
||||
@@ -9,13 +9,22 @@ android {
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
implementation project(':capacitor-community-sqlite')
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-browser')
|
||||
implementation project(':capacitor-device')
|
||||
implementation project(':capacitor-local-llm')
|
||||
implementation project(':capacitor-local-notifications')
|
||||
implementation project(':capacitor-network')
|
||||
implementation project(':capacitor-preferences')
|
||||
implementation project(':capacitor-push-notifications')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':capgo-capacitor-document-scanner')
|
||||
implementation project(':capgo-capacitor-nfc')
|
||||
implementation project(':capgo-capacitor-social-login')
|
||||
implementation project(':jcesarmobile-capacitor-ocr')
|
||||
implementation project(':revenuecat-purchases-capacitor')
|
||||
implementation project(':revenuecat-purchases-capacitor-ui')
|
||||
implementation project(':capacitor-plugin-safe-area')
|
||||
|
||||
}
|
||||
|
||||
37
android/app/release/output-metadata.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "com.donetick.app",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 25,
|
||||
"versionName": "1.2.3",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File",
|
||||
"baselineProfiles": [
|
||||
{
|
||||
"minApi": 28,
|
||||
"maxApi": 30,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/1/app-release.dm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"minApi": 31,
|
||||
"maxApi": 2147483647,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/0/app-release.dm"
|
||||
]
|
||||
}
|
||||
],
|
||||
"minSdkVersionForDexing": 28
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|smallestScreenSize|screenLayout|uiMode|navigation"
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/title_activity_main"
|
||||
android:theme="@style/AppTheme.NoActionBarLaunch"
|
||||
@@ -18,6 +20,21 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deep link intent filter for OAuth and other donetick:// URLs -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="donetick" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- NFC NDEF dispatch: open app directly when a donetick:// tag is scanned -->
|
||||
<intent-filter>
|
||||
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="donetick" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
@@ -27,9 +44,29 @@
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
<!-- Push Notifications Icon -->
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_icon"
|
||||
android:resource="@mipmap/ic_launcher" />
|
||||
|
||||
<!-- Push Notifications Color -->
|
||||
<meta-data
|
||||
android:name="com.google.firebase.messaging.default_notification_color"
|
||||
android:resource="@android:color/white" />
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="29" />
|
||||
<!-- Android 14+ partial photo access via system photo picker; no READ_MEDIA_IMAGES needed -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VISUAL_USER_SELECTED" />
|
||||
<!-- Explicitly block READ_MEDIA_IMAGES if any library injects it -->
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.NFC" />
|
||||
</manifest>
|
||||
|
||||
@@ -2,4 +2,48 @@ package com.donetick.app;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {}
|
||||
import ee.forgr.capacitor.social.login.GoogleProvider;
|
||||
import ee.forgr.capacitor.social.login.SocialLoginPlugin;
|
||||
import ee.forgr.capacitor.social.login.ModifiedMainActivityForSocialLoginPlugin;
|
||||
import com.getcapacitor.PluginHandle;
|
||||
import com.getcapacitor.Plugin;
|
||||
import android.content.Intent;
|
||||
import android.nfc.NfcAdapter;
|
||||
import android.util.Log;
|
||||
|
||||
|
||||
public class MainActivity extends BridgeActivity implements ModifiedMainActivityForSocialLoginPlugin {
|
||||
|
||||
// Capacitor only forwards ACTION_VIEW deep links, so normalize Donetick NFC intents before dispatch.
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(intent.getAction())
|
||||
&& intent.getData() != null
|
||||
&& "donetick".equals(intent.getData().getScheme())) {
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
}
|
||||
super.onNewIntent(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode >= GoogleProvider.REQUEST_AUTHORIZE_GOOGLE_MIN && requestCode < GoogleProvider.REQUEST_AUTHORIZE_GOOGLE_MAX) {
|
||||
PluginHandle pluginHandle = getBridge().getPlugin("SocialLogin");
|
||||
if (pluginHandle == null) {
|
||||
Log.i("Google Activity Result", "SocialLogin login handle is null");
|
||||
return;
|
||||
}
|
||||
Plugin plugin = pluginHandle.getInstance();
|
||||
if (!(plugin instanceof SocialLoginPlugin)) {
|
||||
Log.i("Google Activity Result", "SocialLogin plugin instance is not SocialLoginPlugin");
|
||||
return;
|
||||
}
|
||||
((SocialLoginPlugin) plugin).handleGoogleLoginIntent(requestCode, data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void IHaveModifiedTheMainActivityForTheUseWithSocialLoginPlugin() {}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:8.2.1'
|
||||
classpath 'com.google.gms:google-services:4.4.0'
|
||||
classpath 'com.android.tools.build:gradle:8.10.1'
|
||||
classpath 'com.google.gms:google-services:4.4.2'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
||||
@@ -2,15 +2,27 @@
|
||||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
|
||||
include ':capacitor-community-sqlite'
|
||||
project(':capacitor-community-sqlite').projectDir = new File('../node_modules/@capacitor-community/sqlite/android')
|
||||
|
||||
include ':capacitor-app'
|
||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||
|
||||
include ':capacitor-browser'
|
||||
project(':capacitor-browser').projectDir = new File('../node_modules/@capacitor/browser/android')
|
||||
|
||||
include ':capacitor-device'
|
||||
project(':capacitor-device').projectDir = new File('../node_modules/@capacitor/device/android')
|
||||
|
||||
include ':capacitor-local-llm'
|
||||
project(':capacitor-local-llm').projectDir = new File('../node_modules/@capacitor/local-llm/android')
|
||||
|
||||
include ':capacitor-local-notifications'
|
||||
project(':capacitor-local-notifications').projectDir = new File('../node_modules/@capacitor/local-notifications/android')
|
||||
|
||||
include ':capacitor-network'
|
||||
project(':capacitor-network').projectDir = new File('../node_modules/@capacitor/network/android')
|
||||
|
||||
include ':capacitor-preferences'
|
||||
project(':capacitor-preferences').projectDir = new File('../node_modules/@capacitor/preferences/android')
|
||||
|
||||
@@ -20,8 +32,23 @@ project(':capacitor-push-notifications').projectDir = new File('../node_modules/
|
||||
include ':capacitor-status-bar'
|
||||
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
|
||||
|
||||
include ':capgo-capacitor-document-scanner'
|
||||
project(':capgo-capacitor-document-scanner').projectDir = new File('../node_modules/@capgo/capacitor-document-scanner/android')
|
||||
|
||||
include ':capgo-capacitor-nfc'
|
||||
project(':capgo-capacitor-nfc').projectDir = new File('../node_modules/@capgo/capacitor-nfc/android')
|
||||
|
||||
include ':capgo-capacitor-social-login'
|
||||
project(':capgo-capacitor-social-login').projectDir = new File('../node_modules/@capgo/capacitor-social-login/android')
|
||||
|
||||
include ':jcesarmobile-capacitor-ocr'
|
||||
project(':jcesarmobile-capacitor-ocr').projectDir = new File('../node_modules/@jcesarmobile/capacitor-ocr/android')
|
||||
|
||||
include ':revenuecat-purchases-capacitor'
|
||||
project(':revenuecat-purchases-capacitor').projectDir = new File('../node_modules/@revenuecat/purchases-capacitor/android')
|
||||
|
||||
include ':revenuecat-purchases-capacitor-ui'
|
||||
project(':revenuecat-purchases-capacitor-ui').projectDir = new File('../node_modules/@revenuecat/purchases-capacitor-ui/android')
|
||||
|
||||
include ':capacitor-plugin-safe-area'
|
||||
project(':capacitor-plugin-safe-area').projectDir = new File('../node_modules/capacitor-plugin-safe-area/android')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
ext {
|
||||
minSdkVersion = 22
|
||||
compileSdkVersion = 34
|
||||
targetSdkVersion = 34
|
||||
androidxActivityVersion = '1.8.0'
|
||||
androidxAppCompatVersion = '1.6.1'
|
||||
androidxCoordinatorLayoutVersion = '1.2.0'
|
||||
androidxCoreVersion = '1.12.0'
|
||||
androidxFragmentVersion = '1.6.2'
|
||||
minSdkVersion = 28
|
||||
compileSdkVersion = 36
|
||||
targetSdkVersion = 36
|
||||
androidxActivityVersion = '1.11.0'
|
||||
androidxAppCompatVersion = '1.7.1'
|
||||
androidxCoordinatorLayoutVersion = '1.3.0'
|
||||
androidxCoreVersion = '1.17.0'
|
||||
androidxFragmentVersion = '1.8.9'
|
||||
coreSplashScreenVersion = '1.0.1'
|
||||
androidxWebkitVersion = '1.9.0'
|
||||
androidxWebkitVersion = '1.14.0'
|
||||
junitVersion = '4.13.2'
|
||||
androidxJunitVersion = '1.1.5'
|
||||
androidxEspressoCoreVersion = '3.5.1'
|
||||
androidxJunitVersion = '1.3.0'
|
||||
androidxEspressoCoreVersion = '3.7.0'
|
||||
cordovaAndroidVersion = '10.1.1'
|
||||
}
|
||||
firebaseMessagingVersion = '24.1.0'
|
||||
}
|
||||
|
||||
BIN
assets/ipad_dashbard_calendar.png
Normal file
|
After Width: | Height: | Size: 419 KiB |
84
bump-version.js
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { execSync } from 'child_process'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// Read package.json
|
||||
const packageJsonPath = path.join(__dirname, 'package.json')
|
||||
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'))
|
||||
|
||||
// Get current version
|
||||
const currentVersion = packageJson.version
|
||||
console.log(`Current version: ${currentVersion}`)
|
||||
|
||||
// Parse version (assuming semver format: major.minor.patch)
|
||||
const versionParts = currentVersion.split('.').map(Number)
|
||||
const [major, minor, patch] = versionParts
|
||||
|
||||
// Bump patch version by default (can be modified to accept arguments for major/minor)
|
||||
const bumpType = process.argv[2] || 'patch'
|
||||
let newVersion
|
||||
|
||||
switch (bumpType) {
|
||||
case 'major':
|
||||
newVersion = `${major + 1}.0.0`
|
||||
break
|
||||
case 'minor':
|
||||
newVersion = `${major}.${minor + 1}.0`
|
||||
break
|
||||
case 'patch':
|
||||
default:
|
||||
newVersion = `${major}.${minor}.${patch + 1}`
|
||||
break
|
||||
}
|
||||
|
||||
console.log(`New version: ${newVersion}`)
|
||||
|
||||
// Update package.json
|
||||
packageJson.version = newVersion
|
||||
fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n')
|
||||
console.log('✓ Updated package.json')
|
||||
|
||||
// Read Android build.gradle to get current versionCode
|
||||
const buildGradlePath = path.join(__dirname, 'android', 'app', 'build.gradle')
|
||||
let buildGradle = fs.readFileSync(buildGradlePath, 'utf8')
|
||||
|
||||
// Extract current versionCode
|
||||
const versionCodeMatch = buildGradle.match(/versionCode\s+(\d+)/)
|
||||
const currentVersionCode = versionCodeMatch ? parseInt(versionCodeMatch[1]) : 1
|
||||
const newVersionCode = currentVersionCode + 1
|
||||
|
||||
console.log(`Android versionCode: ${currentVersionCode} → ${newVersionCode}`)
|
||||
|
||||
// Use capacitor-set-version to update both iOS and Android
|
||||
try {
|
||||
console.log('\nUpdating iOS and Android versions...')
|
||||
|
||||
// Set version and build number for both platforms
|
||||
execSync(
|
||||
`npx capacitor-set-version set -v ${newVersion} -b ${newVersionCode}`,
|
||||
{
|
||||
stdio: 'inherit',
|
||||
},
|
||||
)
|
||||
|
||||
console.log('\n✓ Successfully bumped version to', newVersion)
|
||||
console.log(`✓ Build number: ${newVersionCode}`)
|
||||
console.log('\nNext steps:')
|
||||
console.log('1. Review the changes')
|
||||
console.log(
|
||||
'2. Commit: git add . && git commit -m "Bump version to ' +
|
||||
newVersion +
|
||||
'"',
|
||||
)
|
||||
console.log('3. Tag: git tag v' + newVersion)
|
||||
console.log('4. Push: git push origin develop --tags')
|
||||
} catch (error) {
|
||||
console.error('Error updating versions:', error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
@@ -1,26 +1,28 @@
|
||||
import type { CapacitorConfig } from '@capacitor/cli';
|
||||
import type { CapacitorConfig } from '@capacitor/cli'
|
||||
|
||||
const config: CapacitorConfig = {
|
||||
appId: 'com.donetick.app',
|
||||
appName: 'Donetick',
|
||||
webDir: 'dist',
|
||||
|
||||
android: {
|
||||
allowMixedContent: true,
|
||||
},
|
||||
plugins: {
|
||||
PushNotifications: {
|
||||
presentationOptions: ['badge', 'sound', 'alert'],
|
||||
},
|
||||
LocalNotifications: {
|
||||
smallIcon: "ic_stat_icon_config_sample",
|
||||
iconColor: "#488AFF",
|
||||
sound: "beep.wav",
|
||||
smallIcon: 'ic_stat_icon_config_sample',
|
||||
iconColor: '#488AFF',
|
||||
sound: 'beep.wav',
|
||||
},
|
||||
GoogleAuth: {
|
||||
scopes: ['profile', 'email', 'openid'],
|
||||
clientId: process.env.VITE_APP_GOOGLE_CLIENT_ID,
|
||||
androidClientId: process.env.VITE_APP_ANDRIOD_CLIENT_ID,
|
||||
iosClientId: process.env.VITE_APP_IOS_CLIENT_ID,
|
||||
// GoogleAuth: {
|
||||
// scopes: ['profile', 'email', 'openid'],
|
||||
// clientId: process.env.VITE_APP_GOOGLE_CLIENT_ID,
|
||||
// androidClientId: process.env.VITE_APP_ANDRIOD_CLIENT_ID,
|
||||
// iosClientId: process.env.VITE_APP_IOS_CLIENT_ID,
|
||||
// },
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default config;
|
||||
export default config
|
||||
|
||||
15
crowdin.yml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Crowdin configuration for Donetick
|
||||
# See: https://support.crowdin.com/configuration-file/
|
||||
|
||||
project_id: "donetick"
|
||||
api_token_env: CROWDIN_API_TOKEN
|
||||
preserve_hierarchy: true
|
||||
|
||||
languages_mapping:
|
||||
two_letters_code:
|
||||
zh-CN: zh-CN
|
||||
|
||||
files:
|
||||
- source: /public/locales/en/*.json
|
||||
translation: /public/locales/%two_letters_code%/%original_file_name%
|
||||
update_option: update_as_unapproved
|
||||
4
fastlane/Appfile
Normal file
@@ -0,0 +1,4 @@
|
||||
app_identifier("com.donetick.app")
|
||||
team_id("6UJJ78R3BS")
|
||||
|
||||
apple_id(ENV["APPLE_ID"]) if ENV["APPLE_ID"]
|
||||
113
fastlane/Fastfile
Normal file
@@ -0,0 +1,113 @@
|
||||
require "shellwords"
|
||||
|
||||
default_platform(:ios)
|
||||
|
||||
IOS_KEY_PATH = File.expand_path("../ios/AuthKey_84F695CDQ3.p8")
|
||||
IOS_OUTPUT_DIR = File.expand_path("../build/ios")
|
||||
ANDROID_PROJECT_DIR = File.expand_path("../android")
|
||||
PLAY_JSON_KEY_PATH = File.expand_path("../android/play-service-account.json")
|
||||
|
||||
platform :ios do
|
||||
desc "Build a signed, App Store-ready .ipa"
|
||||
lane :release do
|
||||
Dir.chdir(File.expand_path("../ios/App")) do
|
||||
Bundler.with_clean_env do
|
||||
sh("pod install --repo-update")
|
||||
end
|
||||
end
|
||||
|
||||
build_app(
|
||||
workspace: "ios/App/App.xcworkspace",
|
||||
scheme: "App",
|
||||
export_method: "app-store",
|
||||
output_directory: IOS_OUTPUT_DIR,
|
||||
output_name: "Donetick.ipa",
|
||||
export_options: {
|
||||
signingStyle: "manual",
|
||||
provisioningProfiles: {
|
||||
"com.donetick.app" => "Donetick App Store(fastline)",
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
UI.success("Signed .ipa at #{IOS_OUTPUT_DIR}/Donetick.ipa")
|
||||
end
|
||||
|
||||
desc "Upload the built .ipa to TestFlight"
|
||||
lane :upload do
|
||||
UI.user_error!("Missing #{IOS_KEY_PATH} — run scripts/pull-secrets.sh first") unless File.exist?(IOS_KEY_PATH)
|
||||
UI.user_error!("ASC_ISSUER_ID is not set (App Store Connect API key issuer id)") if ENV["ASC_ISSUER_ID"].to_s.empty?
|
||||
|
||||
ipa_path = File.join(IOS_OUTPUT_DIR, "Donetick.ipa")
|
||||
UI.user_error!("Missing #{ipa_path} — run `fastlane ios release` first") unless File.exist?(ipa_path)
|
||||
|
||||
api_key = app_store_connect_api_key(
|
||||
key_id: ENV["ASC_KEY_ID"] || "84F695CDQ3",
|
||||
issuer_id: ENV["ASC_ISSUER_ID"],
|
||||
key_filepath: IOS_KEY_PATH,
|
||||
duration: 1200,
|
||||
in_house: false,
|
||||
)
|
||||
|
||||
upload_to_testflight(
|
||||
api_key: api_key,
|
||||
ipa: ipa_path,
|
||||
skip_waiting_for_build_processing: true,
|
||||
skip_submission: true,
|
||||
)
|
||||
|
||||
UI.success("Uploaded to TestFlight")
|
||||
end
|
||||
end
|
||||
|
||||
platform :android do
|
||||
desc "Build a signed release .aab"
|
||||
lane :release do
|
||||
keystore_properties = File.join(ANDROID_PROJECT_DIR, "keystore.properties")
|
||||
UI.user_error!("Missing #{keystore_properties} — run scripts/pull-secrets.sh first") unless File.exist?(keystore_properties)
|
||||
|
||||
gradle(
|
||||
task: "bundle",
|
||||
build_type: "Release",
|
||||
project_dir: "#{ANDROID_PROJECT_DIR}/",
|
||||
)
|
||||
|
||||
aab_path = lane_context[SharedValues::GRADLE_AAB_OUTPUT_PATH]
|
||||
UI.success("Signed .aab at #{aab_path}")
|
||||
end
|
||||
|
||||
desc "Build a signed release .apk (for sideloading/testing)"
|
||||
lane :apk do
|
||||
keystore_properties = File.join(ANDROID_PROJECT_DIR, "keystore.properties")
|
||||
UI.user_error!("Missing #{keystore_properties} — run scripts/pull-secrets.sh first") unless File.exist?(keystore_properties)
|
||||
|
||||
gradle(
|
||||
task: "assemble",
|
||||
build_type: "Release",
|
||||
project_dir: "#{ANDROID_PROJECT_DIR}/",
|
||||
)
|
||||
|
||||
apk_path = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]
|
||||
UI.success("Signed .apk at #{apk_path}")
|
||||
end
|
||||
|
||||
desc "Upload the built .aab to the Play Store (internal testing track by default)"
|
||||
lane :upload do |options|
|
||||
UI.user_error!("Missing #{PLAY_JSON_KEY_PATH} — run scripts/pull-secrets.sh first") unless File.exist?(PLAY_JSON_KEY_PATH)
|
||||
|
||||
aab_path = File.join(ANDROID_PROJECT_DIR, "app/build/outputs/bundle/release/app-release.aab")
|
||||
UI.user_error!("Missing #{aab_path} — run `fastlane android release` first") unless File.exist?(aab_path)
|
||||
|
||||
upload_to_play_store(
|
||||
json_key: PLAY_JSON_KEY_PATH,
|
||||
package_name: "com.donetick.app",
|
||||
aab: aab_path,
|
||||
track: options[:track] || "internal",
|
||||
skip_upload_metadata: true,
|
||||
skip_upload_images: true,
|
||||
skip_upload_screenshots: true,
|
||||
)
|
||||
|
||||
UI.success("Uploaded to Play Store (#{options[:track] || 'internal'} track)")
|
||||
end
|
||||
end
|
||||
69
fastlane/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
fastlane documentation
|
||||
----
|
||||
|
||||
# Installation
|
||||
|
||||
Make sure you have the latest version of the Xcode command line tools installed:
|
||||
|
||||
```sh
|
||||
xcode-select --install
|
||||
```
|
||||
|
||||
For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane)
|
||||
|
||||
# Available Actions
|
||||
|
||||
## iOS
|
||||
|
||||
### ios release
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios release
|
||||
```
|
||||
|
||||
Build a signed, App Store-ready .ipa
|
||||
|
||||
### ios upload
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane ios upload
|
||||
```
|
||||
|
||||
Upload the built .ipa to TestFlight
|
||||
|
||||
----
|
||||
|
||||
|
||||
## Android
|
||||
|
||||
### android release
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane android release
|
||||
```
|
||||
|
||||
Build a signed release .aab
|
||||
|
||||
### android apk
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane android apk
|
||||
```
|
||||
|
||||
Build a signed release .apk (for sideloading/testing)
|
||||
|
||||
### android upload
|
||||
|
||||
```sh
|
||||
[bundle exec] fastlane android upload
|
||||
```
|
||||
|
||||
Upload the built .aab to the Play Store (internal testing track by default)
|
||||
|
||||
----
|
||||
|
||||
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||
|
||||
More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools).
|
||||
|
||||
The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools).
|
||||
@@ -3,8 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<!-- <link rel="icon" type="image/svg+xml" href="/logo.svg" /> -->
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<!-- <title>FE Template</title> -->
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
|
||||
/>
|
||||
|
||||
<title>Donetick</title>
|
||||
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
504EC30F1FED79650016851F /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 504EC30E1FED79650016851F /* Assets.xcassets */; };
|
||||
504EC3121FED79650016851F /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 504EC3101FED79650016851F /* LaunchScreen.storyboard */; };
|
||||
50B271D11FEDC1A000F3C39B /* public in Resources */ = {isa = PBXBuildFile; fileRef = 50B271D01FEDC1A000F3C39B /* public */; };
|
||||
72FA9293C4A649D1BA0E5917 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 71866EB277374608AD02C137 /* PrivacyInfo.xcprivacy */; };
|
||||
A084ECDBA7D38E1E42DFC39D /* Pods_App.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */; };
|
||||
D1115B492C653D60004C6043 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = D1115B482C653D60004C6043 /* GoogleService-Info.plist */; };
|
||||
/* End PBXBuildFile section */
|
||||
@@ -28,6 +29,7 @@
|
||||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
71866EB277374608AD02C137 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
D1115B482C653D60004C6043 /* GoogleService-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
@@ -62,6 +64,7 @@
|
||||
504EC3051FED79650016851F /* Products */,
|
||||
7F8756D8B27F46E3366F6CEA /* Pods */,
|
||||
27E2DDA53C4D2A4D1A88CE4A /* Frameworks */,
|
||||
71866EB277374608AD02C137 /* PrivacyInfo.xcprivacy */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -127,8 +130,8 @@
|
||||
504EC2FC1FED79650016851F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 0920;
|
||||
LastUpgradeCheck = 0920;
|
||||
LastSwiftUpdateCheck = 920;
|
||||
LastUpgradeCheck = 920;
|
||||
TargetAttributes = {
|
||||
504EC3031FED79650016851F = {
|
||||
CreatedOnToolsVersion = 9.2;
|
||||
@@ -169,6 +172,7 @@
|
||||
504EC30D1FED79650016851F /* Main.storyboard in Resources */,
|
||||
2FAD9763203C412B000D30F8 /* config.xml in Resources */,
|
||||
D1115B492C653D60004C6043 /* GoogleService-Info.plist in Resources */,
|
||||
72FA9293C4A649D1BA0E5917 /* PrivacyInfo.xcprivacy in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -270,7 +274,6 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
@@ -289,7 +292,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
@@ -327,7 +330,6 @@
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
@@ -340,7 +342,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
|
||||
@@ -353,18 +355,17 @@
|
||||
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
DEVELOPMENT_TEAM = 6UJJ78R3BS;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
MARKETING_VERSION = 1.2.16;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
@@ -375,14 +376,17 @@
|
||||
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ALLOW_ENTITLEMENTS_MODIFICATION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
CODE_SIGN_IDENTITY = "Apple Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
PROVISIONING_PROFILE_SPECIFIER = "Donetick App Store(fastline)";
|
||||
CURRENT_PROJECT_VERSION = 37;
|
||||
DEVELOPMENT_TEAM = 6UJJ78R3BS;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
MARKETING_VERSION = 1.0;
|
||||
MARKETING_VERSION = 1.2.16;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.donetick.app;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
|
||||
@@ -4,5 +4,13 @@
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.developer.applesignin</key>
|
||||
<array>
|
||||
<string>Default</string>
|
||||
</array>
|
||||
<key>com.apple.developer.nfc.readersession.formats</key>
|
||||
<array>
|
||||
<string>TAG</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import UIKit
|
||||
import Capacitor
|
||||
|
||||
import FirebaseCore
|
||||
import FirebaseMessaging
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
@@ -8,6 +9,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
FirebaseApp.configure()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -46,10 +48,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
return ApplicationDelegateProxy.shared.application(application, continue: userActivity, restorationHandler: restorationHandler)
|
||||
}
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: deviceToken)
|
||||
Messaging.messaging().apnsToken = deviceToken
|
||||
Messaging.messaging().token(completion: { (token, error) in
|
||||
if let error = error {
|
||||
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
|
||||
} else if let token = token {
|
||||
NotificationCenter.default.post(name: .capacitorDidRegisterForRemoteNotifications, object: token)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
||||
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
|
||||
NotificationCenter.default.post(name: .capacitorDidFailToRegisterForRemoteNotifications, object: error)
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 53 KiB |
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"images" : [
|
||||
"images": [
|
||||
{
|
||||
"filename" : "AppIcon-512@2x.png",
|
||||
"idiom" : "universal",
|
||||
"platform" : "ios",
|
||||
"size" : "1024x1024"
|
||||
"idiom": "universal",
|
||||
"size": "1024x1024",
|
||||
"filename": "AppIcon-512@2x.png",
|
||||
"platform": "ios"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
"info": {
|
||||
"author": "xcode",
|
||||
"version": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,56 @@
|
||||
{
|
||||
"images" : [
|
||||
"images": [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732-2.png",
|
||||
"scale" : "1x"
|
||||
"idiom": "universal",
|
||||
"filename": "Default@1x~universal~anyany.png",
|
||||
"scale": "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732-1.png",
|
||||
"scale" : "2x"
|
||||
"idiom": "universal",
|
||||
"filename": "Default@2x~universal~anyany.png",
|
||||
"scale": "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "splash-2732x2732.png",
|
||||
"scale" : "3x"
|
||||
"idiom": "universal",
|
||||
"filename": "Default@3x~universal~anyany.png",
|
||||
"scale": "3x"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"idiom": "universal",
|
||||
"scale": "1x",
|
||||
"filename": "Default@1x~universal~anyany-dark.png"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"idiom": "universal",
|
||||
"scale": "2x",
|
||||
"filename": "Default@2x~universal~anyany-dark.png"
|
||||
},
|
||||
{
|
||||
"appearances": [
|
||||
{
|
||||
"appearance": "luminosity",
|
||||
"value": "dark"
|
||||
}
|
||||
],
|
||||
"idiom": "universal",
|
||||
"scale": "3x",
|
||||
"filename": "Default@3x~universal~anyany-dark.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
"info": {
|
||||
"version": 1,
|
||||
"author": "xcode"
|
||||
}
|
||||
}
|
||||
BIN
ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/Default@1x~universal~anyany.png
vendored
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/Default@2x~universal~anyany.png
vendored
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany-dark.png
vendored
Normal file
|
After Width: | Height: | Size: 169 KiB |
BIN
ios/App/App/Assets.xcassets/Splash.imageset/Default@3x~universal~anyany.png
vendored
Normal file
|
After Width: | Height: | Size: 52 KiB |
80
ios/App/App/Info.plist
Normal file
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NFCReaderUsageDescription</key>
|
||||
<string>Donetick uses NFC to read and write chore tags for quick task completion.</string>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Donetick</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs access to camera to take photos to attach to task or use as profile photo</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app needs access to photo library to select images to attach to task or use as profile photo</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||
<true/>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.donetick.app</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>donetick</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>com.googleusercontent.apps.682262497914-5dacpk46qcc1494lood6ch8ul9c83kop</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
29
ios/App/DonetickWidget/Info.plist
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>DoneTick Widget</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.widgetkit-extension</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,6 +1,6 @@
|
||||
require_relative '../../node_modules/@capacitor/ios/scripts/pods_helpers'
|
||||
|
||||
platform :ios, '13.0'
|
||||
platform :ios, '15.0'
|
||||
use_frameworks!
|
||||
|
||||
# workaround to avoid Xcode caching of Pods that requires
|
||||
@@ -11,19 +11,29 @@ install! 'cocoapods', :disable_input_output_paths => true
|
||||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCommunitySqlite', :path => '../../node_modules/@capacitor-community/sqlite'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
|
||||
pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
|
||||
pod 'CapacitorLocalLlm', :path => '../../node_modules/@capacitor/local-llm'
|
||||
pod 'CapacitorLocalNotifications', :path => '../../node_modules/@capacitor/local-notifications'
|
||||
pod 'CapacitorNetwork', :path => '../../node_modules/@capacitor/network'
|
||||
pod 'CapacitorPreferences', :path => '../../node_modules/@capacitor/preferences'
|
||||
pod 'CapacitorPushNotifications', :path => '../../node_modules/@capacitor/push-notifications'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||
pod 'CapgoCapacitorDocumentScanner', :path => '../../node_modules/@capgo/capacitor-document-scanner'
|
||||
pod 'CapgoCapacitorNfc', :path => '../../node_modules/@capgo/capacitor-nfc'
|
||||
pod 'CapgoCapacitorSocialLogin', :path => '../../node_modules/@capgo/capacitor-social-login'
|
||||
pod 'JcesarmobileCapacitorOcr', :path => '../../node_modules/@jcesarmobile/capacitor-ocr'
|
||||
pod 'RevenuecatPurchasesCapacitor', :path => '../../node_modules/@revenuecat/purchases-capacitor'
|
||||
pod 'RevenuecatPurchasesCapacitorUi', :path => '../../node_modules/@revenuecat/purchases-capacitor-ui'
|
||||
pod 'CapacitorPluginSafeArea', :path => '../../node_modules/capacitor-plugin-safe-area'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
capacitor_pods
|
||||
# Add your Pods here
|
||||
pod 'FirebaseMessaging' # Add this line
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
|
||||
@@ -1,90 +1,171 @@
|
||||
PODS:
|
||||
- Alamofire (5.10.2)
|
||||
- AppAuth (1.7.6):
|
||||
- AppAuth/Core (= 1.7.6)
|
||||
- AppAuth/ExternalUserAgent (= 1.7.6)
|
||||
- AppAuth/Core (1.7.6)
|
||||
- AppAuth/ExternalUserAgent (1.7.6):
|
||||
- AppAuth (2.1.0):
|
||||
- AppAuth/Core (= 2.1.0)
|
||||
- AppAuth/ExternalUserAgent (= 2.1.0)
|
||||
- AppAuth/Core (2.1.0)
|
||||
- AppAuth/ExternalUserAgent (2.1.0):
|
||||
- AppAuth/Core
|
||||
- AppCheckCore (11.2.0):
|
||||
- AppCheckCore (11.3.0):
|
||||
- GoogleUtilities/Environment (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- Capacitor (7.2.0):
|
||||
- PromisesSwift (~> 2.4)
|
||||
- RecaptchaInterop (~> 101.0)
|
||||
- Capacitor (8.4.1):
|
||||
- CapacitorCordova
|
||||
- CapacitorApp (7.0.1):
|
||||
- CapacitorApp (8.1.0):
|
||||
- Capacitor
|
||||
- CapacitorCommunitySqlite (7.0.0):
|
||||
- CapacitorBrowser (8.0.3):
|
||||
- Capacitor
|
||||
- CapacitorCommunitySqlite (8.1.0):
|
||||
- Capacitor
|
||||
- SQLCipher
|
||||
- ZIPFoundation
|
||||
- CapacitorCordova (7.2.0)
|
||||
- CapacitorDevice (7.0.1):
|
||||
- CapacitorCordova (8.4.1)
|
||||
- CapacitorDevice (8.0.2):
|
||||
- Capacitor
|
||||
- CapacitorLocalNotifications (7.0.1):
|
||||
- CapacitorLocalLlm (1.0.0):
|
||||
- Capacitor
|
||||
- CapacitorNetwork (7.0.1):
|
||||
- CapacitorLocalNotifications (8.2.0):
|
||||
- Capacitor
|
||||
- CapacitorPluginSafeArea (4.0.0):
|
||||
- CapacitorNetwork (8.0.1):
|
||||
- Capacitor
|
||||
- CapacitorPreferences (7.0.1):
|
||||
- CapacitorPluginSafeArea (5.0.1):
|
||||
- Capacitor
|
||||
- CapacitorPushNotifications (7.0.1):
|
||||
- CapacitorPreferences (8.0.1):
|
||||
- Capacitor
|
||||
- CapacitorStatusBar (7.0.1):
|
||||
- CapacitorPushNotifications (8.1.1):
|
||||
- Capacitor
|
||||
- CapgoCapacitorSocialLogin (7.5.3):
|
||||
- CapacitorStatusBar (8.0.2):
|
||||
- Capacitor
|
||||
- CapgoCapacitorDocumentScanner (8.4.0):
|
||||
- Capacitor
|
||||
- CapgoCapacitorNfc (8.2.2):
|
||||
- Capacitor
|
||||
- CapgoCapacitorSocialLogin (8.3.36):
|
||||
- Alamofire (~> 5.10.2)
|
||||
- Capacitor
|
||||
- FBSDKCoreKit (= 17.4.0)
|
||||
- FBSDKLoginKit (= 17.4.0)
|
||||
- GoogleSignIn (~> 8.0.0)
|
||||
- FBAEMKit (17.4.0):
|
||||
- FBSDKCoreKit_Basics (= 17.4.0)
|
||||
- FBSDKCoreKit (17.4.0):
|
||||
- FBAEMKit (= 17.4.0)
|
||||
- FBSDKCoreKit_Basics (= 17.4.0)
|
||||
- FBSDKCoreKit_Basics (17.4.0)
|
||||
- FBSDKLoginKit (17.4.0):
|
||||
- FBSDKCoreKit (= 17.4.0)
|
||||
- GoogleSignIn (8.0.0):
|
||||
- AppAuth (< 2.0, >= 1.7.3)
|
||||
- FBSDKCoreKit (~> 18.0)
|
||||
- FBSDKLoginKit (~> 18.0)
|
||||
- GoogleSignIn (~> 9.0.0)
|
||||
- FBAEMKit (18.1.0):
|
||||
- FBSDKCoreKit_Basics (= 18.1.0)
|
||||
- FBSDKCoreKit (18.1.0):
|
||||
- FBAEMKit (= 18.1.0)
|
||||
- FBSDKCoreKit_Basics (= 18.1.0)
|
||||
- FBSDKCoreKit_Basics (18.1.0)
|
||||
- FBSDKLoginKit (18.1.0):
|
||||
- FBSDKCoreKit (= 18.1.0)
|
||||
- FirebaseCore (12.15.0):
|
||||
- FirebaseCoreInternal (~> 12.15.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- FirebaseCoreInternal (12.15.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- FirebaseInstallations (12.15.0):
|
||||
- FirebaseCore (~> 12.15.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (12.15.0):
|
||||
- FirebaseCore (~> 12.15.0)
|
||||
- FirebaseInstallations (~> 12.15.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Reachability (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleDataTransport (10.1.0):
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- GoogleSignIn (9.0.0):
|
||||
- AppAuth (~> 2.0)
|
||||
- AppCheckCore (~> 11.0)
|
||||
- GTMAppAuth (< 5.0, >= 4.1.1)
|
||||
- GTMAppAuth (~> 5.0)
|
||||
- GTMSessionFetcher/Core (~> 3.3)
|
||||
- GoogleUtilities/Environment (8.0.2):
|
||||
- GoogleUtilities/AppDelegateSwizzler (8.1.2):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Network
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Logger (8.0.2):
|
||||
- GoogleUtilities/Environment (8.1.2):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Logger (8.1.2):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (8.0.2)
|
||||
- GoogleUtilities/UserDefaults (8.0.2):
|
||||
- GoogleUtilities/Network (8.1.2):
|
||||
- GoogleUtilities/Logger
|
||||
- "GoogleUtilities/NSData+zlib"
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Reachability
|
||||
- "GoogleUtilities/NSData+zlib (8.1.2)":
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (8.1.2)
|
||||
- GoogleUtilities/Reachability (8.1.2):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GTMAppAuth (4.1.1):
|
||||
- AppAuth/Core (~> 1.7)
|
||||
- GoogleUtilities/UserDefaults (8.1.2):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GTMAppAuth (5.0.0):
|
||||
- AppAuth/Core (~> 2.0)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3)
|
||||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- PromisesObjC (2.4.0)
|
||||
- SQLCipher (4.7.0):
|
||||
- SQLCipher/standard (= 4.7.0)
|
||||
- SQLCipher/common (4.7.0)
|
||||
- SQLCipher/standard (4.7.0):
|
||||
- JcesarmobileCapacitorOcr (0.3.0):
|
||||
- Capacitor
|
||||
- nanopb (3.30910.0):
|
||||
- nanopb/decode (= 3.30910.0)
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- PromisesObjC (2.4.1)
|
||||
- PromisesSwift (2.4.1):
|
||||
- PromisesObjC (= 2.4.1)
|
||||
- PurchasesHybridCommon (17.55.1):
|
||||
- RevenueCat (= 5.67.1)
|
||||
- PurchasesHybridCommonUI (17.55.1):
|
||||
- PurchasesHybridCommon (= 17.55.1)
|
||||
- RevenueCatUI (= 5.67.1)
|
||||
- RecaptchaInterop (101.0.0)
|
||||
- RevenueCat (5.67.1)
|
||||
- RevenuecatPurchasesCapacitor (12.3.2):
|
||||
- Capacitor
|
||||
- PurchasesHybridCommon (= 17.55.1)
|
||||
- RevenuecatPurchasesCapacitorUi (12.3.2):
|
||||
- Capacitor
|
||||
- PurchasesHybridCommonUI (= 17.55.1)
|
||||
- RevenueCatUI (5.67.1):
|
||||
- RevenueCat (= 5.67.1)
|
||||
- SQLCipher (4.10.0):
|
||||
- SQLCipher/standard (= 4.10.0)
|
||||
- SQLCipher/common (4.10.0)
|
||||
- SQLCipher/standard (4.10.0):
|
||||
- SQLCipher/common
|
||||
- ZIPFoundation (0.9.19)
|
||||
- ZIPFoundation (0.9.20)
|
||||
|
||||
DEPENDENCIES:
|
||||
- "Capacitor (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorApp (from `../../node_modules/@capacitor/app`)"
|
||||
- "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)"
|
||||
- "CapacitorCommunitySqlite (from `../../node_modules/@capacitor-community/sqlite`)"
|
||||
- "CapacitorCordova (from `../../node_modules/@capacitor/ios`)"
|
||||
- "CapacitorDevice (from `../../node_modules/@capacitor/device`)"
|
||||
- "CapacitorLocalLlm (from `../../node_modules/@capacitor/local-llm`)"
|
||||
- "CapacitorLocalNotifications (from `../../node_modules/@capacitor/local-notifications`)"
|
||||
- "CapacitorNetwork (from `../../node_modules/@capacitor/network`)"
|
||||
- CapacitorPluginSafeArea (from `../../node_modules/capacitor-plugin-safe-area`)
|
||||
- "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)"
|
||||
- "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)"
|
||||
- "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)"
|
||||
- "CapgoCapacitorDocumentScanner (from `../../node_modules/@capgo/capacitor-document-scanner`)"
|
||||
- "CapgoCapacitorNfc (from `../../node_modules/@capgo/capacitor-nfc`)"
|
||||
- "CapgoCapacitorSocialLogin (from `../../node_modules/@capgo/capacitor-social-login`)"
|
||||
- FirebaseMessaging
|
||||
- "JcesarmobileCapacitorOcr (from `../../node_modules/@jcesarmobile/capacitor-ocr`)"
|
||||
- "RevenuecatPurchasesCapacitor (from `../../node_modules/@revenuecat/purchases-capacitor`)"
|
||||
- "RevenuecatPurchasesCapacitorUi (from `../../node_modules/@revenuecat/purchases-capacitor-ui`)"
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -95,11 +176,23 @@ SPEC REPOS:
|
||||
- FBSDKCoreKit
|
||||
- FBSDKCoreKit_Basics
|
||||
- FBSDKLoginKit
|
||||
- FirebaseCore
|
||||
- FirebaseCoreInternal
|
||||
- FirebaseInstallations
|
||||
- FirebaseMessaging
|
||||
- GoogleDataTransport
|
||||
- GoogleSignIn
|
||||
- GoogleUtilities
|
||||
- GTMAppAuth
|
||||
- GTMSessionFetcher
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
- PromisesSwift
|
||||
- PurchasesHybridCommon
|
||||
- PurchasesHybridCommonUI
|
||||
- RecaptchaInterop
|
||||
- RevenueCat
|
||||
- RevenueCatUI
|
||||
- SQLCipher
|
||||
- ZIPFoundation
|
||||
|
||||
@@ -108,10 +201,16 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/@capacitor/ios"
|
||||
CapacitorApp:
|
||||
:path: "../../node_modules/@capacitor/app"
|
||||
CapacitorBrowser:
|
||||
:path: "../../node_modules/@capacitor/browser"
|
||||
CapacitorCommunitySqlite:
|
||||
:path: "../../node_modules/@capacitor-community/sqlite"
|
||||
CapacitorCordova:
|
||||
:path: "../../node_modules/@capacitor/ios"
|
||||
CapacitorDevice:
|
||||
:path: "../../node_modules/@capacitor/device"
|
||||
CapacitorLocalLlm:
|
||||
:path: "../../node_modules/@capacitor/local-llm"
|
||||
CapacitorLocalNotifications:
|
||||
:path: "../../node_modules/@capacitor/local-notifications"
|
||||
CapacitorNetwork:
|
||||
@@ -124,37 +223,66 @@ EXTERNAL SOURCES:
|
||||
:path: "../../node_modules/@capacitor/push-notifications"
|
||||
CapacitorStatusBar:
|
||||
:path: "../../node_modules/@capacitor/status-bar"
|
||||
CapgoCapacitorDocumentScanner:
|
||||
:path: "../../node_modules/@capgo/capacitor-document-scanner"
|
||||
CapgoCapacitorNfc:
|
||||
:path: "../../node_modules/@capgo/capacitor-nfc"
|
||||
CapgoCapacitorSocialLogin:
|
||||
:path: "../../node_modules/@capgo/capacitor-social-login"
|
||||
JcesarmobileCapacitorOcr:
|
||||
:path: "../../node_modules/@jcesarmobile/capacitor-ocr"
|
||||
RevenuecatPurchasesCapacitor:
|
||||
:path: "../../node_modules/@revenuecat/purchases-capacitor"
|
||||
RevenuecatPurchasesCapacitorUi:
|
||||
:path: "../../node_modules/@revenuecat/purchases-capacitor-ui"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Alamofire: 7193b3b92c74a07f85569e1a6c4f4237291e7496
|
||||
AppAuth: d4f13a8fe0baf391b2108511793e4b479691fb73
|
||||
AppCheckCore: cc8fd0a3a230ddd401f326489c99990b013f0c4f
|
||||
Capacitor: 03bc7cbdde6a629a8b910a9d7d78c3cc7ed09ea7
|
||||
CapacitorApp: febecbb9582cb353aed037e18ec765141f880fe9
|
||||
CapacitorCommunitySqlite: b11e556cf5d149f9e0b6fbbb47959a6cd718c3d0
|
||||
CapacitorCordova: 5967b9ba03915ef1d585469d6e31f31dc49be96f
|
||||
CapacitorDevice: c6f6d587dd310527f8a48bf09c4e7b4a4cf14329
|
||||
CapacitorLocalNotifications: c2212755b33d2513b8bb325096ce3a64d4039ae3
|
||||
CapacitorNetwork: 15cb4385f0913a8ceb5e9a4d7af1ec554bdb8de8
|
||||
CapacitorPluginSafeArea: 22031c3436269ca80fac90ec2c94bc7c1e59a81d
|
||||
CapacitorPreferences: 6c98117d4d7508034a4af9db64d6b26fc75d7b94
|
||||
CapacitorPushNotifications: 6a2794788c583dd89215f1805ca4bced1b13dbdf
|
||||
CapacitorStatusBar: 6e7af040d8fc4dd655999819625cae9c2d74c36f
|
||||
CapgoCapacitorSocialLogin: 0daa087f32775c656fbfba09b8eae7c089b85ce2
|
||||
FBAEMKit: 58cb5f302cdd715a56d4c1d0dfdd2e423ac1421a
|
||||
FBSDKCoreKit: 94d7461d0cecf441b1ba7c41acfff41daa8ccd41
|
||||
FBSDKCoreKit_Basics: 151b43db8b834d3f0e02f95d36a44ffd36265e45
|
||||
FBSDKLoginKit: 5c1cd53c91a2282b3a4fe6e6d3dcf2b8b0d33d55
|
||||
GoogleSignIn: ce8c89bb9b37fb624b92e7514cc67335d1e277e4
|
||||
GoogleUtilities: 26a3abef001b6533cf678d3eb38fd3f614b7872d
|
||||
GTMAppAuth: f69bd07d68cd3b766125f7e072c45d7340dea0de
|
||||
AppAuth: ef4da5a3fc2e10b90c09a0a94a9baeaedc0341d5
|
||||
AppCheckCore: 214137f5c378d1dec88a68425c467fe65aaff637
|
||||
Capacitor: 35242afe195b1e53c58ca1b827d1b444c5e6602b
|
||||
CapacitorApp: 449ffe26375e96f8aaaee625ac6e01e5c57c8650
|
||||
CapacitorBrowser: c987c73d09d8bd3b5ec13f06338b1e14d5d2be69
|
||||
CapacitorCommunitySqlite: eac6acfb852f46e7988fc59604d7f900498d354e
|
||||
CapacitorCordova: eebe6bcf807b1b06f3f48237650f96bbcd0eef09
|
||||
CapacitorDevice: 14cba6f88d1c3074cbf825fea977c8c526453ff8
|
||||
CapacitorLocalLlm: a05516151a02923a9e7dae9949d3817e85e321f0
|
||||
CapacitorLocalNotifications: 2615aa008f608b95d3921a778ee1988abf1e6148
|
||||
CapacitorNetwork: 8812ce60d11fb63d8f2e4ba51a49b2e59892ebe2
|
||||
CapacitorPluginSafeArea: 874619c00586248f1694210e72038123d422c2d9
|
||||
CapacitorPreferences: cca2021f386efb75947c850334447d9ff22b14f1
|
||||
CapacitorPushNotifications: ec08d589c226a2c0db7c032ec1bf5b044ec85f8e
|
||||
CapacitorStatusBar: 01d5763b4ed720de5ce2edbc938de6a98f4c8f32
|
||||
CapgoCapacitorDocumentScanner: 7ad9e8ed9c054d551bfc948660d995b9545723d8
|
||||
CapgoCapacitorNfc: 8ea158143c441e1cf6d231a971e375511558d027
|
||||
CapgoCapacitorSocialLogin: adffda2bec52a765bdaebf3fa9a6083bd9341d60
|
||||
FBAEMKit: 64088ff0380f50e4a56e2ee07d984f7f1aef7595
|
||||
FBSDKCoreKit: 8390ea3a8fac186f444275f31d7512e760b47cba
|
||||
FBSDKCoreKit_Basics: 3a0005c78b355388bf2df5c6dbe6381b77ea4be3
|
||||
FBSDKLoginKit: d8e2711a3a03b0026703735c8d28a2b5a0e1f4fd
|
||||
FirebaseCore: 2e86a4ea1684d4381707069e4a6d89ac808e901e
|
||||
FirebaseCoreInternal: 6ab6a02c94446c026d2cf35cf5383842ebaa4992
|
||||
FirebaseInstallations: eb29ccbf64eaedf86fd5b2ccc7fabde567660b52
|
||||
FirebaseMessaging: 40017d7bc8457ee295b0f41d480a80fdabc9994e
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleSignIn: c7f09cfbc85a1abf69187be091997c317cc33b77
|
||||
GoogleUtilities: 766ace00c6b10d8148408f329d10c4f051931850
|
||||
GTMAppAuth: 217a876b249c3c585a54fd6f73e6b58c4f5c4238
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
SQLCipher: ba9d0076041ed767c5bd3d3f77098318d04a403c
|
||||
ZIPFoundation: b8c29ea7ae353b309bc810586181fd073cb3312c
|
||||
JcesarmobileCapacitorOcr: d3668e104d7ad8968a9d00fd4766e18fea0fe305
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
PromisesObjC: 752c3227f599e3467650e47ea36f433eeb10c273
|
||||
PromisesSwift: 217dea0fd5d2ad65222a109c48698add13cc1c5b
|
||||
PurchasesHybridCommon: ecd9d7c586287625f1225b8de5e24c1021502270
|
||||
PurchasesHybridCommonUI: 3056a3d261529f6adb8f74ecc3ab7f1cc0d3fab4
|
||||
RecaptchaInterop: 11e0b637842dfb48308d242afc3f448062325aba
|
||||
RevenueCat: 9357239a9f0978285e68983637b8a602e65c94f0
|
||||
RevenuecatPurchasesCapacitor: c299be01b1a5c1d5eb118c30eadc84160944ecf2
|
||||
RevenuecatPurchasesCapacitorUi: dc2ec0d97638ebe4ac0078452f5c212b1525ea1c
|
||||
RevenueCatUI: 6f9291da540f98baf405b5bdfc46b2d5547b704a
|
||||
SQLCipher: eb79c64049cb002b4e9fcb30edb7979bf4706dfc
|
||||
ZIPFoundation: dfd3d681c4053ff7e2f7350bc4e53b5dba3f5351
|
||||
|
||||
PODFILE CHECKSUM: 54ec5ec72c1c5e3e8d03bd96cc1ec0deaf577d9e
|
||||
PODFILE CHECKSUM: 21b805bdbbb6ac4b8a3527ee8ef346ba0a55ef65
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
29
ios/App/PrivacyInfo.xcprivacy
Normal file
@@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSPrivacyTracking</key>
|
||||
<false/>
|
||||
<key>NSPrivacyAccessedAPITypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>85F4.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>NSPrivacyAccessedAPIType</key>
|
||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||
<key>NSPrivacyAccessedAPITypeReasons</key>
|
||||
<array>
|
||||
<string>CA92.1</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSPrivacyCollectedDataTypes</key>
|
||||
<array/>
|
||||
</dict>
|
||||
</plist>
|
||||
0
ios/Podfile
Normal file
207
ios/Pods/Pods.xcodeproj/project.pbxproj
generated
Normal file
@@ -0,0 +1,207 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 46;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
93BAFDF14BA360977052E7B9CCC18E94 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
CF1408CF629C7361332E53B88F7BD30C = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9D940727FF8FB9C785EB98E56350EF41 /* Podfile */,
|
||||
D89477F20FB1DE18A04690586D7808C4 /* Frameworks */,
|
||||
93BAFDF14BA360977052E7B9CCC18E94 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
D89477F20FB1DE18A04690586D7808C4 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
BFDFE7DC352907FC980B868725387E98 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1600;
|
||||
LastUpgradeCheck = 1600;
|
||||
};
|
||||
buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */;
|
||||
compatibilityVersion = "Xcode 3.2";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
Base,
|
||||
en,
|
||||
);
|
||||
mainGroup = CF1408CF629C7361332E53B88F7BD30C;
|
||||
minimizedProjectReferenceProxies = 0;
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 93BAFDF14BA360977052E7B9CCC18E94 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
2426522C419C0DE4525883EA7AABC025 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"POD_CONFIGURATION_RELEASE=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRIP_INSTALLED_PRODUCT = NO;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SYMROOT = "${SRCROOT}/../build";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
8F9FEBB7130A3DF8746ED9AA46FE33FA /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"POD_CONFIGURATION_DEBUG=1",
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
STRIP_INSTALLED_PRODUCT = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
SYMROOT = "${SRCROOT}/../build";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
8F9FEBB7130A3DF8746ED9AA46FE33FA /* Debug */,
|
||||
2426522C419C0DE4525883EA7AABC025 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */;
|
||||
}
|
||||
8105
package-lock.json
generated
80
package.json
@@ -1,8 +1,12 @@
|
||||
{
|
||||
"name": "donetick",
|
||||
"private": true,
|
||||
"version": "0.1.99",
|
||||
"version": "1.2.16",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=20.0.0",
|
||||
"npm": ">=9.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,jsx,ts,tsx}": [
|
||||
"eslint --fix",
|
||||
@@ -13,64 +17,102 @@
|
||||
"start": "vite --host",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build-selfhosted": "vite build --mode selfhosted",
|
||||
"build-cf": "rm -rf package-lock.json && npm install && npm install --force @rollup/rollup-linux-x64-gnu@4.34.9 @swc/core-linux-x64-gnu && vite build",
|
||||
"build-selfhosted": "rm -rf package-lock.json && npm install && npm install --force @rollup/rollup-linux-x64-gnu@4.34.9 @swc/core-linux-x64-gnu && vite build --mode selfhosted",
|
||||
"build-win": "del package-lock.json && npm install && npm install --force @rollup/rollup-win32-x64-msvc @swc/core-win32-x64-msvc && vite build --mode selfhosted",
|
||||
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||
"preview": "vite preview",
|
||||
"setup-m1": "rm -rf node_modules package-lock.json && npm install && npm install --force @rollup/rollup-darwin-arm64 @swc/core-darwin-arm64",
|
||||
"setup-apple-silicon": "npm install --no-optional && npm install --force @rollup/rollup-darwin-arm64 @swc/core-darwin-arm64",
|
||||
"test-cloudflare-build": "rm -rf node_modules package-lock.json && npm install && npm install --force @rollup/rollup-linux-x64-gnu@4.34.9 @swc/core-linux-x64-gnu && vite build",
|
||||
"release": "npm version patch && npm run build && git push origin develop --tags",
|
||||
"merge": "git checkout main && git merge develop && git push origin main && git checkout develop",
|
||||
"ionic:build": "npm run build",
|
||||
"ionic:serve": "npm run start"
|
||||
"ionic:serve": "npm run start",
|
||||
"android": "npm run build && npx cap sync android",
|
||||
"android:dev": "npx cap run android --live-reload",
|
||||
"cap": "npm run build && npx cap sync",
|
||||
"bump": "node bump-version.js patch",
|
||||
"bump:minor": "node bump-version.js minor",
|
||||
"bump:major": "node bump-version.js major",
|
||||
"bump:patch": "node bump-version.js patch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@capacitor-community/sqlite": "^6.0.2",
|
||||
"@capacitor/android": "^6.1.1",
|
||||
"@capacitor/app": "^6.0.0",
|
||||
"@capacitor/cli": "^6.1.1",
|
||||
"@capacitor/core": "^6.1.1",
|
||||
"@capacitor/device": "^6.0.2",
|
||||
"@capacitor/ios": "6.1.1",
|
||||
"@capacitor/local-notifications": "^6.0.0",
|
||||
"@capacitor/network": "^6.0.3",
|
||||
"@capacitor/preferences": "^6.0.1",
|
||||
"@capacitor/push-notifications": "^6.0.1",
|
||||
"@codetrix-studio/capacitor-google-auth": "^3.4.0-rc.4",
|
||||
"@capacitor-community/sqlite": "^8.0.0",
|
||||
"@capacitor/android": "^8.0.0",
|
||||
"@capacitor/app": "^8.0.0",
|
||||
"@capacitor/browser": "^8.0.0",
|
||||
"@capacitor/core": "^8.0.0",
|
||||
"@capacitor/device": "^8.0.0",
|
||||
"@capacitor/ios": "^8.0.0",
|
||||
"@capacitor/local-llm": "^1.0.0",
|
||||
"@capacitor/local-notifications": "^8.0.0",
|
||||
"@capacitor/network": "^8.0.0",
|
||||
"@capacitor/preferences": "^8.0.0",
|
||||
"@capacitor/push-notifications": "^8.0.0",
|
||||
"@capacitor/status-bar": "^8.0.0",
|
||||
"@capgo/capacitor-document-scanner": "^8.4.0",
|
||||
"@capgo/capacitor-nfc": "^8.0.0",
|
||||
"@capgo/capacitor-social-login": "^8.0.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
"@emotion/react": "^11.11.3",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@mui/icons-material": "^5.15.2",
|
||||
"@hello-pangea/dnd": "^18.0.1",
|
||||
"@jcesarmobile/capacitor-ocr": "^0.3.0",
|
||||
"@meauxt/react-swipeable-list": "^1.0.0",
|
||||
"@mui/icons-material": "^5.16.13",
|
||||
"@mui/joy": "^5.0.0-beta.20",
|
||||
"@mui/material": "^5.15.2",
|
||||
"@openreplay/tracker": "^14.0.4",
|
||||
"@revenuecat/purchases-capacitor": "^12.0.0",
|
||||
"@revenuecat/purchases-capacitor-ui": "^12.0.0",
|
||||
"@swc/core": "^1.12.5",
|
||||
"@tanstack/react-query": "^5.17.0",
|
||||
"aos": "^2.3.4",
|
||||
"browser-image-compression": "^2.0.2",
|
||||
"caniuse-lite": "^1.0.30001769",
|
||||
"capacitor-plugin-safe-area": "^5.0.0",
|
||||
"chrono-node": "^2.7.7",
|
||||
"dotenv": "^16.4.5",
|
||||
"esm": "^3.2.25",
|
||||
"farmhash": "^4.0.1",
|
||||
"event-source-polyfill": "^1.0.31",
|
||||
"fuse.js": "^7.0.0",
|
||||
"i18next": "^25.8.14",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"i18next-http-backend": "^3.0.2",
|
||||
"js-cookie": "^3.0.5",
|
||||
"moment": "^2.30.1",
|
||||
"murmurhash": "^2.0.1",
|
||||
"prop-types": "^15.8.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"quill": "^2.0.3",
|
||||
"quilljs-markdown": "^1.2.0",
|
||||
"react": "^18.2.0",
|
||||
"react-calendar": "^5.1.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-easy-crop": "^5.4.2",
|
||||
"react-i18next": "^16.5.6",
|
||||
"react-router-dom": "^6.21.1",
|
||||
"react-transition-group": "^4.4.5",
|
||||
"reactjs-social-login": "^2.6.3",
|
||||
"recharts": "^2.15.0",
|
||||
"reusify": "^1.0.4",
|
||||
"tesseract.js": "^7.0.0",
|
||||
"vite-plugin-pwa": "^0.20.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@capacitor/assets": "^3.0.5",
|
||||
"@capacitor/cli": "^8.0.0",
|
||||
"@tanstack/eslint-plugin-query": "^5.14.6",
|
||||
"@types/react": "^18.2.43",
|
||||
"@types/react-dom": "^18.2.17",
|
||||
"@vite-pwa/assets-generator": "^0.2.4",
|
||||
"@vitejs/plugin-react-swc": "^3.5.0",
|
||||
"autoprefixer": "^10.4.16",
|
||||
"baseline-browser-mapping": "^2.9.19",
|
||||
"capacitor-set-version": "^2.2.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.2",
|
||||
@@ -86,6 +128,10 @@
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-tailwindcss": "^0.5.10",
|
||||
"tailwindcss": "^3.4.0",
|
||||
"typescript": "^5.8.0",
|
||||
"vite": "^5.2.13"
|
||||
},
|
||||
"allowScripts": {
|
||||
"tesseract.js@7.0.0": true
|
||||
}
|
||||
}
|
||||
|
||||
75
public/locales/ar/chores.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"title": "المهام",
|
||||
"myChores": "مهامي",
|
||||
"allChores": "جميع المهام",
|
||||
"addChore": "إضافة مهمة",
|
||||
"editChore": "تعديل مهمة",
|
||||
"deleteChore": "حذف مهمة",
|
||||
"completeChore": "إكمال مهمة",
|
||||
"dueDate": "تاريخ الاستحقاق",
|
||||
"assignedTo": "مُسند إلى",
|
||||
"priority": "الأولوية",
|
||||
"status": "الحالة",
|
||||
"description": "الوصف",
|
||||
"choreView": {
|
||||
"assignment": "التعيين",
|
||||
"assigned": "المعين",
|
||||
"last": "الأخير",
|
||||
"schedule": "الجدول",
|
||||
"due": "الاستحقاق",
|
||||
"statistics": "الإحصائيات",
|
||||
"completed": "مكتمل",
|
||||
"times": "مرات",
|
||||
"details": "التفاصيل",
|
||||
"createdBy": "أنشئ بواسطة",
|
||||
"na": "غير متوفر",
|
||||
"taskCompleted": "المهمة مكتملة",
|
||||
"taskCompletedMessage": "تم وضع علامة مكتملة على مهمتك",
|
||||
"taskCompletionUndone": "تم التراجع عن إكمال المهمة.",
|
||||
"taskSkipUndone": "تم التراجع عن تخطي المهمة.",
|
||||
"undoSuccessful": "التراجع ناجح",
|
||||
"undoFailed": "فشل التراجع",
|
||||
"undoFailedMessage": "تعذر التراجع عن الإجراء. يرجى المحاولة مرة أخرى.",
|
||||
"resetTimer": "إعادة تعيين المؤقت",
|
||||
"resetTimerConfirmation": "هل أنت متأكد من أنك تريد إعادة تعيين المؤقت؟ سيؤدي هذا إلى مسح جميع سجلات الوقت منذ بدء المهمة.",
|
||||
"clearAllTimeRecords": "مسح جميع سجلات الوقت",
|
||||
"clearAllTimeConfirmation": "سيؤدي هذا إلى حذف جميع المؤقتات لهذه المهمة بشكل دائم وإعادتها إلى حالة \"لم تبدأ\".",
|
||||
"descriptionTitle": "الوصف",
|
||||
"description": "الوصف :",
|
||||
"previousNote": "الملاحظة السابقة",
|
||||
"previousNoteLabel": "الملاحظة السابقة:",
|
||||
"subtasksLabel": "المهام الفرعية :",
|
||||
"taskActions": "إجراءات المهمة",
|
||||
"addNote": "أضف ملاحظة",
|
||||
"additionalNotes": "ملاحظات إضافية:",
|
||||
"notePlaceholder": "أضف ملاحظة حول الإكمال...",
|
||||
"setCustomCompletionTime": "تعيين وقت إكمال مخصص",
|
||||
"skipTask": "تخطي المهمة",
|
||||
"skipTaskConfirmation": "هل أنت متأكد من أنك تريد تخطي هذه المهمة؟",
|
||||
"markComplete": "وضع علامة مكتمل",
|
||||
"markAsDone": "وضع علامة كمنجز",
|
||||
"edit": "تعديل",
|
||||
"archive": "أرشفة",
|
||||
"unarchive": "إلغاء الأرشفة",
|
||||
"viewHistory": "عرض السجل",
|
||||
"history": "السجل",
|
||||
"startTimer": "بدء المؤقت",
|
||||
"start": "بدء",
|
||||
"pauseTimer": "إيقاف المؤقت مؤقتاً",
|
||||
"approve": "موافقة",
|
||||
"reject": "رفض",
|
||||
"pendingApproval": "في انتظار الموافقة",
|
||||
"undo": "تراجع",
|
||||
"skip": "تخطي",
|
||||
"cancel": "إلغاء",
|
||||
"noPriority": "بدون أولوية",
|
||||
"subtasks": "المهام الفرعية",
|
||||
"noDescription": "لا يوجد وصف متاح",
|
||||
"timer": {
|
||||
"active": "المؤقت نشط",
|
||||
"paused": "المؤقت متوقف مؤقتاً",
|
||||
"reset": "إعادة تعيين المؤقت",
|
||||
"delete": "حذف الجلسة"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
public/locales/ar/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"save": "حفظ",
|
||||
"cancel": "إلغاء",
|
||||
"delete": "حذف",
|
||||
"edit": "تعديل",
|
||||
"close": "إغلاق",
|
||||
"confirm": "تأكيد",
|
||||
"loading": "جارٍ التحميل...",
|
||||
"error": "خطأ",
|
||||
"success": "نجح",
|
||||
"warning": "تحذير",
|
||||
"refresh": "تحديث",
|
||||
"copy": "نسخ",
|
||||
"copied": "تم النسخ!",
|
||||
"settings": "الإعدادات",
|
||||
"yes": "نعم",
|
||||
"no": "لا",
|
||||
"back": "رجوع",
|
||||
"backToCalendar": "العودة إلى التقويم",
|
||||
"logout": "تسجيل الخروج",
|
||||
"version": "النسخة",
|
||||
"navigation": {
|
||||
"allTasks": "جميع المهام",
|
||||
"archived": "المؤرشفة",
|
||||
"things": "الأشياء",
|
||||
"labels": "التسميات",
|
||||
"projects": "المشاريع",
|
||||
"filters": "الفلاتر",
|
||||
"activities": "الأنشطة",
|
||||
"points": "النقاط",
|
||||
"settings": "الإعدادات"
|
||||
}
|
||||
}
|
||||
174
public/locales/ar/settings.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"title": "الإعدادات",
|
||||
"circleSettings": {
|
||||
"title": "إعدادات الدائرة",
|
||||
"description": "يتم ربط حسابك تلقائيًا بدائرة عند إنشاء واحدة أو الانضمام إليها. ادعُ الأصدقاء بسهولة من خلال مشاركة رمز الدائرة الفريد أو الرابط أدناه. ستتلقى إشعارًا أدناه عندما يطلب شخص ما الانضمام إلى دائرتك. إذا كنت ترغب في المغادرة، فما عليك سوى الضغط على زر 'مغادرة الدائرة'.",
|
||||
"circleCode": "رمز الدائرة",
|
||||
"copyCode": "نسخ الرمز",
|
||||
"copyLink": "نسخ الرابط",
|
||||
"codeCopied": "تم نسخ رمز الدائرة!",
|
||||
"linkCopied": "تم نسخ الرابط!",
|
||||
"joinCircle": "الانضمام إلى دائرة",
|
||||
"joinCirclePlaceholder": "أدخل رمز الدائرة",
|
||||
"join": "انضمام",
|
||||
"leave": "مغادرة الدائرة",
|
||||
"leaveConfirmTitle": "مغادرة الدائرة",
|
||||
"leaveConfirmMessage": "هل أنت متأكد من أنك تريد مغادرة هذه الدائرة؟",
|
||||
"circleMembers": "أعضاء الدائرة",
|
||||
"circleMemberRequests": "طلبات انضمام الأعضاء",
|
||||
"admin": "مشرف",
|
||||
"member": "عضو",
|
||||
"pending": "قيد الانتظار",
|
||||
"accept": "قبول",
|
||||
"reject": "رفض",
|
||||
"makeAdmin": "جعله مشرف",
|
||||
"makeMember": "جعله عضو",
|
||||
"remove": "إزالة",
|
||||
"webhookURL": "رابط Webhook",
|
||||
"webhookDescription": "أدخل رابط webhook لتلقي إشعارات أحداث الدائرة",
|
||||
"webhookPlaceholder": "https://your-webhook-url.com"
|
||||
},
|
||||
"accountSettings": {
|
||||
"title": "إعدادات الحساب",
|
||||
"subscription": "الاشتراك",
|
||||
"subscriptionStatus": "الخطة الحالية",
|
||||
"free": "مجاني",
|
||||
"plus": "بلس",
|
||||
"upgrade": "ترقية",
|
||||
"cancel": "إلغاء",
|
||||
"changePassword": "تغيير كلمة المرور",
|
||||
"password": "كلمة المرور",
|
||||
"dangerZone": "منطقة الخطر",
|
||||
"dangerZoneDescription": "بمجرد حذف حسابك، لا يمكن التراجع. يرجى التأكد.",
|
||||
"deleteAccount": "حذف الحساب"
|
||||
},
|
||||
"localization": {
|
||||
"title": "التوطين",
|
||||
"description": "تخصيص اللغة وتنسيق التاريخ والتفضيلات الإقليمية لحسابك.",
|
||||
"language": "اللغة",
|
||||
"languageDescription": "اختر لغتك المفضلة",
|
||||
"dateFormat": "تنسيق التاريخ",
|
||||
"dateFormatDescription": "اختر كيفية عرض التواريخ في التطبيق",
|
||||
"timeFormat": "تنسيق الوقت",
|
||||
"timeFormatDescription": "اختر تنسيق 12 أو 24 ساعة",
|
||||
"12hour": "12 ساعة (ص/م)",
|
||||
"24hour": "24 ساعة",
|
||||
"firstDayOfWeek": "أول يوم في الأسبوع",
|
||||
"firstDayOfWeekDescription": "اختر اليوم الذي يبدأ به أسبوعك",
|
||||
"sunday": "الأحد",
|
||||
"monday": "الاثنين",
|
||||
"saturday": "السبت",
|
||||
"formats": {
|
||||
"mdy": "MM/DD/YYYY (الولايات المتحدة)",
|
||||
"dmy": "DD/MM/YYYY (أوروبا)",
|
||||
"ymd": "YYYY-MM-DD (ISO)",
|
||||
"long": "تنسيق طويل (مثل 1 يناير 2024)",
|
||||
"short": "تنسيق قصير (مثل 1 يناير 2024)"
|
||||
}
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "تخصيص اللوحة الجانبية",
|
||||
"description": "قم بتخصيص تخطيط ورؤية البطاقات في اللوحة الجانبية. هذا القسم متاح فقط على أجهزة الشاشة الكبيرة مثل الأجهزة اللوحية وأجهزة سطح المكتب."
|
||||
},
|
||||
"theme": {
|
||||
"title": "تفضيلات المظهر",
|
||||
"description": "اختر كيف يبدو الموقع لك. حدد مظهرًا واحدًا أو قم بالمزامنة مع نظامك والتبديل تلقائيًا بين مظاهر النهار والليل.",
|
||||
"themeMode": "وضع المظهر",
|
||||
"light": "فاتح",
|
||||
"dark": "داكن",
|
||||
"system": "النظام"
|
||||
},
|
||||
"notifications": {
|
||||
"settingsSaved": "تم حفظ الإعدادات بنجاح",
|
||||
"settingsSaveFailed": "فشل حفظ الإعدادات",
|
||||
"invalidWebhook": "رابط webhook غير صالح"
|
||||
},
|
||||
"profile": {
|
||||
"title": "إعدادات الملف الشخصي",
|
||||
"description": "تحديث اسم العرض وصورة الملف الشخصي.",
|
||||
"photoUpdated": "تم تحديث الصورة",
|
||||
"photoUpdatedMessage": "تم تحديث صورة ملفك الشخصي بنجاح!",
|
||||
"uploadFailed": "فشل التحميل",
|
||||
"uploadFailedMessage": "فشل تحميل صورتك. يرجى المحاولة مرة أخرى.",
|
||||
"profileUpdated": "تم تحديث الملف الشخصي",
|
||||
"profileUpdatedMessage": "تم حفظ معلومات ملفك الشخصي بنجاح!",
|
||||
"updateFailed": "فشل التحديث",
|
||||
"updateFailedMessage": "تعذر تحديث ملفك الشخصي. يرجى التحقق من اتصالك والمحاولة مرة أخرى.",
|
||||
"changePhoto": "تغيير الصورة",
|
||||
"displayName": "اسم العرض",
|
||||
"displayNamePlaceholder": "أدخل اسم العرض الخاص بك",
|
||||
"timezone": "المنطقة الزمنية",
|
||||
"timezonePlaceholder": "اختر منطقتك الزمنية",
|
||||
"save": "حفظ",
|
||||
"cancel": "إلغاء"
|
||||
},
|
||||
"overview": {
|
||||
"title": "الإعدادات",
|
||||
"subtitle": "قم بتخصيص تجربتك وإدارة تفضيلات حسابك",
|
||||
"upgrade": {
|
||||
"title": "الترقية إلى بلس",
|
||||
"description": "افتح ميزات قوية لتعزيز إنتاجيتك",
|
||||
"button": "الترقية الآن",
|
||||
"features": {
|
||||
"richText": "أوصاف نصية منسقة",
|
||||
"notifications": "إشعارات المهام",
|
||||
"apiIntegrations": "تكاملات API",
|
||||
"advancedAutomation": "أتمتة متقدمة"
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"profile": {
|
||||
"title": "إعدادات الملف الشخصي",
|
||||
"description": "تحديث معلومات ملفك الشخصي والصورة واسم العرض وتفضيلات المنطقة الزمنية."
|
||||
},
|
||||
"circle": {
|
||||
"title": "إعدادات الدائرة",
|
||||
"description": "إدارة دائرتك ودعوة الأعضاء والتعامل مع طلبات الانضمام."
|
||||
},
|
||||
"account": {
|
||||
"title": "إعدادات الحساب",
|
||||
"description": "إدارة اشتراكك وتغيير كلمة المرور وخيارات حذف الحساب."
|
||||
},
|
||||
"subaccounts": {
|
||||
"title": "الحسابات المُدارة",
|
||||
"description": "إنشاء وإدارة حسابات فرعية لتسجيل الدخول وإكمال المهام المعينة."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "الإشعارات",
|
||||
"description": "تكوين الإشعارات الفورية وتنبيهات البريد الإلكتروني ووجهات الإشعارات للمهام."
|
||||
},
|
||||
"mfa": {
|
||||
"title": "المصادقة متعددة العوامل",
|
||||
"description": "إضافة طبقة إضافية من الأمان باستخدام MFA مع تطبيقات المصادقة."
|
||||
},
|
||||
"apitokens": {
|
||||
"title": "رموز API",
|
||||
"description": "إنشاء وإدارة رموز الوصول لتكاملات الطرف الثالث والوصول إلى API."
|
||||
},
|
||||
"storage": {
|
||||
"title": "إعدادات التخزين",
|
||||
"description": "نسخ احتياطي واستعادة بياناتك وإدارة التخزين المحلي وتفضيلات المزامنة."
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "تخصيص اللوحة الجانبية",
|
||||
"description": "قم بتخصيص تخطيط ورؤية البطاقات في واجهة اللوحة الجانبية."
|
||||
},
|
||||
"theme": {
|
||||
"title": "تفضيلات المظهر",
|
||||
"description": "اختر مظهرك المفضل وقم بتكوين إعدادات الوضع الداكن/الفاتح."
|
||||
},
|
||||
"localization": {
|
||||
"title": "التوطين",
|
||||
"description": "تخصيص اللغة وتنسيق التاريخ وتنسيق الوقت والتفضيلات الإقليمية."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "الإعدادات المتقدمة",
|
||||
"description": "تكوين webhooks والتحديثات في الوقت الفعلي وميزات متقدمة أخرى لتعزيز الإنتاجية."
|
||||
},
|
||||
"developer": {
|
||||
"title": "إعدادات المطور",
|
||||
"description": "عرض المعلومات الفنية حول رموز المصادقة واتصالات SSE وبيانات التصحيح."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
public/locales/de/chores.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"title": "Aufgaben",
|
||||
"myChores": "Meine Aufgaben",
|
||||
"allChores": "Alle Aufgaben",
|
||||
"addChore": "Aufgabe hinzufügen",
|
||||
"editChore": "Aufgabe bearbeiten",
|
||||
"deleteChore": "Aufgabe löschen",
|
||||
"completeChore": "Aufgabe abschließen",
|
||||
"dueDate": "Fälligkeitsdatum",
|
||||
"assignedTo": "Zugewiesen an",
|
||||
"priority": "Priorität",
|
||||
"status": "Status",
|
||||
"description": "Beschreibung",
|
||||
"choreView": {
|
||||
"assignment": "Zuweisung",
|
||||
"assigned": "Zugewiesen",
|
||||
"last": "Letzte",
|
||||
"schedule": "Zeitplan",
|
||||
"due": "Fällig",
|
||||
"statistics": "Statistiken",
|
||||
"completed": "Abgeschlossen",
|
||||
"times": "mal",
|
||||
"details": "Details",
|
||||
"createdBy": "Erstellt von",
|
||||
"na": "Nicht verfügbar",
|
||||
"taskCompleted": "Aufgabe abgeschlossen",
|
||||
"taskCompletedMessage": "Deine Aufgabe wurde als abgeschlossen markiert",
|
||||
"taskCompletionUndone": "Aufgabenabschluss wurde rückgängig gemacht.",
|
||||
"taskSkipUndone": "Aufgabe überspringen wurde rückgängig gemacht.",
|
||||
"undoSuccessful": "Rückgängig erfolgreich",
|
||||
"undoFailed": "Rückgängig fehlgeschlagen",
|
||||
"undoFailedMessage": "Die Aktion konnte nicht rückgängig gemacht werden. Bitte versuche es erneut.",
|
||||
"resetTimer": "Timer zurücksetzen",
|
||||
"resetTimerConfirmation": "Bist du sicher, dass du den Timer zurücksetzen möchtest? Dies löscht alle Zeitaufzeichnungen seit du die Aufgabe gestartet hast.",
|
||||
"clearAllTimeRecords": "Alle Zeitaufzeichnungen löschen",
|
||||
"clearAllTimeConfirmation": "Dies löscht dauerhaft alle Timer für diese Aufgabe und setzt sie zurück auf \"nicht gestartet\".",
|
||||
"descriptionTitle": "Beschreibung",
|
||||
"description": "Beschreibung:",
|
||||
"previousNote": "Vorherige Notiz",
|
||||
"previousNoteLabel": "Vorherige Notiz:",
|
||||
"subtasksLabel": "Unteraufgaben:",
|
||||
"taskActions": "Aufgabenaktionen",
|
||||
"addNote": "Notiz hinzufügen",
|
||||
"additionalNotes": "Zusätzliche Notizen:",
|
||||
"notePlaceholder": "Notiz zur Fertigstellung hinzufügen...",
|
||||
"setCustomCompletionTime": "Benutzerdefinierte Abschlusszeit festlegen",
|
||||
"skipTask": "Aufgabe überspringen",
|
||||
"skipTaskConfirmation": "Bist du sicher, dass du diese Aufgabe überspringen möchtest?",
|
||||
"markComplete": "Als abgeschlossen markieren",
|
||||
"markAsDone": "Als erledigt markieren",
|
||||
"edit": "Bearbeiten",
|
||||
"archive": "Archivieren",
|
||||
"unarchive": "Archivierung aufheben",
|
||||
"viewHistory": "Verlauf anzeigen",
|
||||
"history": "Verlauf",
|
||||
"startTimer": "Timer starten",
|
||||
"start": "Starten",
|
||||
"pauseTimer": "Timer pausieren",
|
||||
"approve": "Genehmigen",
|
||||
"reject": "Ablehnen",
|
||||
"pendingApproval": "Genehmigung ausstehend",
|
||||
"undo": "Rückgängig",
|
||||
"skip": "Überspringen",
|
||||
"cancel": "Abbrechen",
|
||||
"noPriority": "Keine Priorität",
|
||||
"subtasks": "Unteraufgaben",
|
||||
"noDescription": "Keine Beschreibung verfügbar",
|
||||
"timer": {
|
||||
"active": "Timer aktiv",
|
||||
"paused": "Timer pausiert",
|
||||
"reset": "Timer zurücksetzen",
|
||||
"delete": "Sitzung löschen"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
public/locales/de/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen",
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten",
|
||||
"close": "Schließen",
|
||||
"confirm": "Bestätigen",
|
||||
"loading": "Laden...",
|
||||
"error": "Fehler",
|
||||
"success": "Erfolg",
|
||||
"warning": "Warnung",
|
||||
"refresh": "Aktualisieren",
|
||||
"copy": "Kopieren",
|
||||
"copied": "Kopiert!",
|
||||
"settings": "Einstellungen",
|
||||
"yes": "Ja",
|
||||
"no": "Nein",
|
||||
"back": "Zurück",
|
||||
"backToCalendar": "Zurück zum Kalender",
|
||||
"logout": "Abmelden",
|
||||
"version": "Version",
|
||||
"navigation": {
|
||||
"allTasks": "Alle Aufgaben",
|
||||
"archived": "Archiviert",
|
||||
"things": "Things",
|
||||
"labels": "Beschriftungen",
|
||||
"projects": "Projekte",
|
||||
"filters": "Filter",
|
||||
"activities": "Aktivitäten",
|
||||
"points": "Punkte",
|
||||
"settings": "Einstellungen"
|
||||
}
|
||||
}
|
||||
174
public/locales/de/settings.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"title": "Einstellungen",
|
||||
"circleSettings": {
|
||||
"title": "Kreis-Einstellungen",
|
||||
"description": "Dein Account wird automatisch mit einem Kreis verbunden, wenn du einen erstellst oder beitrittst. Lade einfach Freunde ein, indem du den einzigartigen Kreis-Code oder Link unten teilst. Du erhältst eine Benachrichtigung, wenn jemand deinem Kreis beitreten möchte. Wenn du gehen möchtest, klicke einfach auf 'Kreis verlassen'.",
|
||||
"circleCode": "Kreis-Code",
|
||||
"copyCode": "Code kopieren",
|
||||
"copyLink": "Link kopieren",
|
||||
"codeCopied": "Kreis-Code kopiert!",
|
||||
"linkCopied": "Kreis-Link kopiert!",
|
||||
"joinCircle": "Einem Kreis beitreten",
|
||||
"joinCirclePlaceholder": "Kreis-Code eingeben",
|
||||
"join": "Beitreten",
|
||||
"leave": "Kreis verlassen",
|
||||
"leaveConfirmTitle": "Kreis verlassen",
|
||||
"leaveConfirmMessage": "Bist du sicher, dass du diesen Kreis verlassen möchtest?",
|
||||
"circleMembers": "Kreis-Mitglieder",
|
||||
"circleMemberRequests": "Kreis-Beitrittsanfragen",
|
||||
"admin": "Administrator",
|
||||
"member": "Mitglied",
|
||||
"pending": "Ausstehend",
|
||||
"accept": "Akzeptieren",
|
||||
"reject": "Ablehnen",
|
||||
"makeAdmin": "Zum Administrator machen",
|
||||
"makeMember": "Zum Mitglied machen",
|
||||
"remove": "Entfernen",
|
||||
"webhookURL": "Webhook-URL",
|
||||
"webhookDescription": "Gib eine Webhook-URL ein, um Benachrichtigungen für Kreis-Ereignisse zu erhalten",
|
||||
"webhookPlaceholder": "https://deine-webhook-url.com"
|
||||
},
|
||||
"accountSettings": {
|
||||
"title": "Account-Einstellungen",
|
||||
"subscription": "Abonnement",
|
||||
"subscriptionStatus": "Aktueller Plan",
|
||||
"free": "Kostenlos",
|
||||
"plus": "Plus",
|
||||
"upgrade": "Upgraden",
|
||||
"cancel": "Kündigen",
|
||||
"changePassword": "Passwort ändern",
|
||||
"password": "Passwort",
|
||||
"dangerZone": "Gefahrenbereich",
|
||||
"dangerZoneDescription": "Sobald du dein Konto löschst, gibt es kein Zurück mehr. Bitte sei dir sicher.",
|
||||
"deleteAccount": "Account löschen"
|
||||
},
|
||||
"localization": {
|
||||
"title": "Sprach- und Formateinstellungen",
|
||||
"description": "Passe Sprache, Datumsformat und regionale Einstellungen für dein Konto an.",
|
||||
"language": "Sprache",
|
||||
"languageDescription": "Wähle deine bevorzugte Sprache",
|
||||
"dateFormat": "Datumsformat",
|
||||
"dateFormatDescription": "Wähle, wie Daten in der gesamten Anwendung angezeigt werden sollen",
|
||||
"timeFormat": "Zeitformat",
|
||||
"timeFormatDescription": "Wähle 12-Stunden- oder 24-Stunden-Zeitformat",
|
||||
"12hour": "12-Stunden (AM/PM)",
|
||||
"24hour": "24-Stunden",
|
||||
"firstDayOfWeek": "Erster Tag der Woche",
|
||||
"firstDayOfWeekDescription": "Wähle, welcher Tag deine Woche beginnt",
|
||||
"sunday": "Sonntag",
|
||||
"monday": "Montag",
|
||||
"saturday": "Samstag",
|
||||
"formats": {
|
||||
"mdy": "MM/TT/JJJJ (USA)",
|
||||
"dmy": "TT/MM/JJJJ (Europa)",
|
||||
"ymd": "JJJJ-MM-TT (ISO)",
|
||||
"long": "Langes Format (z.B. 1. Januar 2024)",
|
||||
"short": "Kurzes Format (z.B. 1. Jan 2024)"
|
||||
}
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "Seitenleisten-Anpassung",
|
||||
"description": "Passe das Layout und die Sichtbarkeit von Karten in der Seitenleiste an. Dieser Bereich ist nur auf großen Bildschirmgeräten wie Tablets und Desktops verfügbar."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme-Einstellungen",
|
||||
"description": "Wähle, wie die Seite für dich aussieht. Wähle ein einzelnes Theme oder synchronisiere mit deinem System und wechsle automatisch zwischen Tag- und Nacht-Themes.",
|
||||
"themeMode": "Theme-Modus",
|
||||
"light": "Hell",
|
||||
"dark": "Dunkel",
|
||||
"system": "System"
|
||||
},
|
||||
"notifications": {
|
||||
"settingsSaved": "Einstellungen erfolgreich gespeichert",
|
||||
"settingsSaveFailed": "Speichern der Einstellungen fehlgeschlagen",
|
||||
"invalidWebhook": "Ungültige Webhook-URL"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profil-Einstellungen",
|
||||
"description": "Aktualisiere deinen Anzeigenamen und dein Profilbild.",
|
||||
"photoUpdated": "Foto aktualisiert",
|
||||
"photoUpdatedMessage": "Dein Profilbild wurde erfolgreich aktualisiert!",
|
||||
"uploadFailed": "Upload fehlgeschlagen",
|
||||
"uploadFailedMessage": "Das Hochladen deines Fotos ist fehlgeschlagen. Bitte versuche es erneut.",
|
||||
"profileUpdated": "Profil aktualisiert",
|
||||
"profileUpdatedMessage": "Deine Profilinformationen wurden erfolgreich gespeichert!",
|
||||
"updateFailed": "Update fehlgeschlagen",
|
||||
"updateFailedMessage": "Dein Profil konnte nicht aktualisiert werden. Bitte überprüfe deine Verbindung und versuche es erneut.",
|
||||
"changePhoto": "Foto ändern",
|
||||
"displayName": "Anzeigename",
|
||||
"displayNamePlaceholder": "Gib deinen Anzeigenamen ein",
|
||||
"timezone": "Zeitzone",
|
||||
"timezonePlaceholder": "Wähle deine Zeitzone",
|
||||
"save": "Speichern",
|
||||
"cancel": "Abbrechen"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Einstellungen",
|
||||
"subtitle": "Passe deine Erfahrung an und verwalte deine Account-Einstellungen",
|
||||
"upgrade": {
|
||||
"title": "Auf Plus upgraden",
|
||||
"description": "Schalte mächtige Funktionen frei, um deine Produktivität zu steigern",
|
||||
"button": "Jetzt upgraden",
|
||||
"features": {
|
||||
"richText": "Rich-Text-Beschreibungen",
|
||||
"notifications": "Aufgaben-Benachrichtigungen",
|
||||
"apiIntegrations": "API-Integrationen",
|
||||
"advancedAutomation": "Erweiterte Automatisierung"
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"profile": {
|
||||
"title": "Profil-Einstellungen",
|
||||
"description": "Aktualisiere deine Profilinformationen, Foto, Anzeigenamen und Zeitzonen-Einstellungen."
|
||||
},
|
||||
"circle": {
|
||||
"title": "Kreis-Einstellungen",
|
||||
"description": "Verwalte deinen Kreis, lade Mitglieder ein und bearbeite Beitrittsanfragen."
|
||||
},
|
||||
"account": {
|
||||
"title": "Account-Einstellungen",
|
||||
"description": "Verwalte dein Abonnement, ändere dein Passwort und Account-Löschoptionen."
|
||||
},
|
||||
"subaccounts": {
|
||||
"title": "Verwaltete Accounts",
|
||||
"description": "Erstelle und verwalte Unter-Accounts zum Anmelden und Erledigen zugewiesener Aufgaben."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Benachrichtigungen",
|
||||
"description": "Konfiguriere Push-Benachrichtigungen, E-Mail-Benachrichtigungen und Benachrichtigungsziele für Aufgaben."
|
||||
},
|
||||
"mfa": {
|
||||
"title": "Multi-Faktor-Authentifizierung",
|
||||
"description": "Füge eine zusätzliche Sicherheitsebene mit MFA über Authenticator-Apps hinzu."
|
||||
},
|
||||
"apitokens": {
|
||||
"title": "API-Token",
|
||||
"description": "Generiere und verwalte Zugriffstoken für Drittanbieter-Integrationen und API-Zugang."
|
||||
},
|
||||
"storage": {
|
||||
"title": "Speicher-Einstellungen",
|
||||
"description": "Sichere und stelle deine Daten wieder her, verwalte lokalen Speicher und Synchronisierungseinstellungen."
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "Seitenleisten-Anpassung",
|
||||
"description": "Passe das Layout und die Sichtbarkeit von Karten in der Seitenleisten-Oberfläche an."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme-Einstellungen",
|
||||
"description": "Wähle dein bevorzugtes Theme und konfiguriere Dunkel-/Hell-Modus-Einstellungen."
|
||||
},
|
||||
"localization": {
|
||||
"title": "Sprach- und Formateinstellungen",
|
||||
"description": "Passe Sprache, Datumsformat, Zeitformat und regionale Einstellungen an."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Erweiterte Einstellungen",
|
||||
"description": "Konfiguriere Webhooks, Echtzeit-Updates und andere erweiterte Funktionen für erhöhte Produktivität."
|
||||
},
|
||||
"developer": {
|
||||
"title": "Entwickler-Einstellungen",
|
||||
"description": "Zeige technische Informationen über Authentifizierungs-Token, SSE-Verbindungen und Debug-Daten an."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
public/locales/en/chores.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"title": "Chores",
|
||||
"myChores": "My Chores",
|
||||
"allChores": "All Chores",
|
||||
"addChore": "Add Chore",
|
||||
"editChore": "Edit Chore",
|
||||
"deleteChore": "Delete Chore",
|
||||
"completeChore": "Complete Task",
|
||||
"dueDate": "Due Date",
|
||||
"assignedTo": "Assigned To",
|
||||
"priority": "Priority",
|
||||
"status": "Status",
|
||||
"description": "Description",
|
||||
"choreView": {
|
||||
"assignment": "Assignment",
|
||||
"assigned": "Assigned",
|
||||
"last": "Last",
|
||||
"schedule": "Schedule",
|
||||
"due": "Due",
|
||||
"statistics": "Statistics",
|
||||
"completed": "Completed",
|
||||
"times": "times",
|
||||
"details": "Details",
|
||||
"createdBy": "Created By",
|
||||
"na": "N/A",
|
||||
"taskCompleted": "Task Completed",
|
||||
"taskCompletedMessage": "Your task has been marked as complete",
|
||||
"taskCompletionUndone": "Task completion has been undone.",
|
||||
"taskSkipUndone": "Task skip has been undone.",
|
||||
"undoSuccessful": "Undo Successful",
|
||||
"undoFailed": "Undo Failed",
|
||||
"undoFailedMessage": "Unable to undo the action. Please try again.",
|
||||
"resetTimer": "Reset Timer",
|
||||
"resetTimerConfirmation": "Are you sure you want to reset the timer? This will clear all time records since you started the task.",
|
||||
"clearAllTimeRecords": "Clear All Time Records",
|
||||
"clearAllTimeConfirmation": "This will permanently delete all timers for this task and set it back to \"not started\".",
|
||||
"descriptionTitle": "Description",
|
||||
"description": "Description :",
|
||||
"previousNote": "Previous Note",
|
||||
"previousNoteLabel": "Previous note:",
|
||||
"subtasksLabel": "Subtasks :",
|
||||
"taskActions": "Task Actions",
|
||||
"addNote": "Add a note",
|
||||
"additionalNotes": "Additional Notes:",
|
||||
"notePlaceholder": "Add a note about the completion...",
|
||||
"setCustomCompletionTime": "Set custom completion time",
|
||||
"skipTask": "Skip Task",
|
||||
"skipTaskConfirmation": "Are you sure you want to skip this task?",
|
||||
"markComplete": "Mark Complete",
|
||||
"markAsDone": "Mark as done",
|
||||
"edit": "Edit",
|
||||
"archive": "Archive",
|
||||
"unarchive": "Unarchive",
|
||||
"viewHistory": "View History",
|
||||
"history": "History",
|
||||
"startTimer": "Start Timer",
|
||||
"start": "Start",
|
||||
"pauseTimer": "Pause Timer",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"pendingApproval": "Pending Approval",
|
||||
"undo": "Undo",
|
||||
"skip": "Skip",
|
||||
"cancel": "Cancel",
|
||||
"noPriority": "No Priority",
|
||||
"subtasks": "Subtasks",
|
||||
"noDescription": "No description available",
|
||||
"timer": {
|
||||
"active": "Timer Active",
|
||||
"paused": "Timer Paused",
|
||||
"reset": "Reset Timer",
|
||||
"delete": "Delete Session"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
public/locales/en/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"close": "Close",
|
||||
"confirm": "Confirm",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"refresh": "Refresh",
|
||||
"copy": "Copy",
|
||||
"copied": "Copied!",
|
||||
"settings": "Settings",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"back": "Back",
|
||||
"backToCalendar": "Back to Calendar",
|
||||
"logout": "Logout",
|
||||
"version": "Version",
|
||||
"navigation": {
|
||||
"allTasks": "All Tasks",
|
||||
"archived": "Archived",
|
||||
"things": "Things",
|
||||
"labels": "Labels",
|
||||
"projects": "Projects",
|
||||
"filters": "Filters",
|
||||
"activities": "Activities",
|
||||
"points": "Points",
|
||||
"settings": "Settings"
|
||||
}
|
||||
}
|
||||
174
public/locales/en/settings.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"title": "Settings",
|
||||
"circleSettings": {
|
||||
"title": "Circle settings",
|
||||
"description": "Your account is automatically connected to a Circle when you create or join one. Easily invite friends by sharing the unique Circle code or link below. You'll receive a notification below when someone requests to join your Circle. If you'd like to leave, simply hit the 'Leave Circle' button.",
|
||||
"circleCode": "Circle Code",
|
||||
"copyCode": "Copy Code",
|
||||
"copyLink": "Copy Link",
|
||||
"codeCopied": "Circle code copied!",
|
||||
"linkCopied": "Circle link copied!",
|
||||
"joinCircle": "Join a Circle",
|
||||
"joinCirclePlaceholder": "Enter Circle Code",
|
||||
"join": "Join",
|
||||
"leave": "Leave Circle",
|
||||
"leaveConfirmTitle": "Leave Circle",
|
||||
"leaveConfirmMessage": "Are you sure you want to leave this circle?",
|
||||
"circleMembers": "Circle Members",
|
||||
"circleMemberRequests": "Circle Member Requests",
|
||||
"admin": "Admin",
|
||||
"member": "Member",
|
||||
"pending": "Pending",
|
||||
"accept": "Accept",
|
||||
"reject": "Reject",
|
||||
"makeAdmin": "Make Admin",
|
||||
"makeMember": "Make Member",
|
||||
"remove": "Remove",
|
||||
"webhookURL": "Webhook URL",
|
||||
"webhookDescription": "Enter a webhook URL to receive notifications for circle events",
|
||||
"webhookPlaceholder": "https://your-webhook-url.com"
|
||||
},
|
||||
"accountSettings": {
|
||||
"title": "Account Settings",
|
||||
"subscription": "Subscription",
|
||||
"subscriptionStatus": "Current Plan",
|
||||
"free": "Free",
|
||||
"plus": "Plus",
|
||||
"upgrade": "Upgrade",
|
||||
"cancel": "Cancel",
|
||||
"changePassword": "Change Password",
|
||||
"password": "Password",
|
||||
"dangerZone": "Danger Zone",
|
||||
"dangerZoneDescription": "Once you delete your account, there is no going back. Please be certain.",
|
||||
"deleteAccount": "Delete Account"
|
||||
},
|
||||
"localization": {
|
||||
"title": "Localization",
|
||||
"description": "Customize language, date format, and regional preferences for your account.",
|
||||
"language": "Language",
|
||||
"languageDescription": "Select your preferred language",
|
||||
"dateFormat": "Date Format",
|
||||
"dateFormatDescription": "Choose how dates should be displayed throughout the application",
|
||||
"timeFormat": "Time Format",
|
||||
"timeFormatDescription": "Select 12-hour or 24-hour time format",
|
||||
"12hour": "12-hour (AM/PM)",
|
||||
"24hour": "24-hour",
|
||||
"firstDayOfWeek": "First Day of Week",
|
||||
"firstDayOfWeekDescription": "Select which day starts your week",
|
||||
"sunday": "Sunday",
|
||||
"monday": "Monday",
|
||||
"saturday": "Saturday",
|
||||
"formats": {
|
||||
"mdy": "MM/DD/YYYY (US)",
|
||||
"dmy": "DD/MM/YYYY (Europe)",
|
||||
"ymd": "YYYY-MM-DD (ISO)",
|
||||
"long": "Long format (e.g., January 1, 2024)",
|
||||
"short": "Short format (e.g., Jan 1, 2024)"
|
||||
}
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "Sidepanel Customization",
|
||||
"description": "Customize the layout and visibility of cards in the sidepanel. This section is only available on large screen devices such as tablets and desktops."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme preferences",
|
||||
"description": "Choose how the site looks to you. Select a single theme, or sync with your system and automatically switch between day and night themes.",
|
||||
"themeMode": "Theme mode",
|
||||
"light": "Light",
|
||||
"dark": "Dark",
|
||||
"system": "System"
|
||||
},
|
||||
"notifications": {
|
||||
"settingsSaved": "Settings saved successfully",
|
||||
"settingsSaveFailed": "Failed to save settings",
|
||||
"invalidWebhook": "Invalid webhook URL"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profile Settings",
|
||||
"description": "Update your display name and profile photo.",
|
||||
"photoUpdated": "Photo Updated",
|
||||
"photoUpdatedMessage": "Your profile photo has been updated successfully!",
|
||||
"uploadFailed": "Upload Failed",
|
||||
"uploadFailedMessage": "Failed to upload your photo. Please try again.",
|
||||
"profileUpdated": "Profile Updated",
|
||||
"profileUpdatedMessage": "Your profile information has been saved successfully!",
|
||||
"updateFailed": "Update Failed",
|
||||
"updateFailedMessage": "Unable to update your profile. Please check your connection and try again.",
|
||||
"changePhoto": "Change Photo",
|
||||
"displayName": "Display Name",
|
||||
"displayNamePlaceholder": "Enter your display name",
|
||||
"timezone": "Timezone",
|
||||
"timezonePlaceholder": "Select your timezone",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Settings",
|
||||
"subtitle": "Customize your experience and manage your account preferences",
|
||||
"upgrade": {
|
||||
"title": "Upgrade to Plus",
|
||||
"description": "Unlock powerful features to enhance your productivity",
|
||||
"button": "Upgrade Now",
|
||||
"features": {
|
||||
"richText": "Rich text descriptions",
|
||||
"notifications": "Task notifications",
|
||||
"apiIntegrations": "API integrations",
|
||||
"advancedAutomation": "Advanced automation"
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"profile": {
|
||||
"title": "Profile Settings",
|
||||
"description": "Update your profile information, photo, display name, and timezone preferences."
|
||||
},
|
||||
"circle": {
|
||||
"title": "Circle Settings",
|
||||
"description": "Manage your circle, invite members, and handle join requests."
|
||||
},
|
||||
"account": {
|
||||
"title": "Account Settings",
|
||||
"description": "Manage your subscription, change password, and account deletion options."
|
||||
},
|
||||
"subaccounts": {
|
||||
"title": "Managed Accounts",
|
||||
"description": "Create and manage sub accounts to log in and complete assigned tasks."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"description": "Configure push notifications, email alerts, and notification targets for tasks."
|
||||
},
|
||||
"mfa": {
|
||||
"title": "Multi-Factor Authentication",
|
||||
"description": "Add an extra layer of security with MFA using authenticator apps."
|
||||
},
|
||||
"apitokens": {
|
||||
"title": "API Tokens",
|
||||
"description": "Generate and manage access tokens for third-party integrations and API access."
|
||||
},
|
||||
"storage": {
|
||||
"title": "Storage Settings",
|
||||
"description": "Backup and restore your data, manage local storage and sync preferences."
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "Sidepanel Customization",
|
||||
"description": "Customize the layout and visibility of cards in the sidepanel interface."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Theme Preferences",
|
||||
"description": "Choose your preferred theme and configure dark/light mode settings."
|
||||
},
|
||||
"localization": {
|
||||
"title": "Localization",
|
||||
"description": "Customize language, date format, time format, and regional preferences."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Advanced Settings",
|
||||
"description": "Configure webhooks, real-time updates, and other advanced features for enhanced productivity."
|
||||
},
|
||||
"developer": {
|
||||
"title": "Developer Settings",
|
||||
"description": "View technical information about authentication tokens, SSE connections, and debug data."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
public/locales/es/chores.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"title": "Tareas",
|
||||
"myChores": "Mis Tareas",
|
||||
"allChores": "Todas las Tareas",
|
||||
"addChore": "Agregar Tarea",
|
||||
"editChore": "Editar Tarea",
|
||||
"deleteChore": "Eliminar Tarea",
|
||||
"completeChore": "Completar Tarea",
|
||||
"dueDate": "Fecha de Vencimiento",
|
||||
"assignedTo": "Asignado a",
|
||||
"priority": "Prioridad",
|
||||
"status": "Estado",
|
||||
"description": "Descripción",
|
||||
"choreView": {
|
||||
"assignment": "Asignación",
|
||||
"assigned": "Asignado",
|
||||
"last": "Último",
|
||||
"schedule": "Horario",
|
||||
"due": "Vencimiento",
|
||||
"statistics": "Estadísticas",
|
||||
"completed": "Completado",
|
||||
"times": "veces",
|
||||
"details": "Detalles",
|
||||
"createdBy": "Creado por",
|
||||
"na": "N/D",
|
||||
"taskCompleted": "Tarea Completada",
|
||||
"taskCompletedMessage": "Tu tarea ha sido marcada como completada",
|
||||
"taskCompletionUndone": "La finalización de la tarea ha sido deshecha.",
|
||||
"taskSkipUndone": "El salto de tarea ha sido deshecho.",
|
||||
"undoSuccessful": "Deshacer Exitoso",
|
||||
"undoFailed": "Deshacer Fallido",
|
||||
"undoFailedMessage": "No se pudo deshacer la acción. Por favor, inténtalo de nuevo.",
|
||||
"resetTimer": "Reiniciar Temporizador",
|
||||
"resetTimerConfirmation": "¿Estás seguro de que quieres reiniciar el temporizador? Esto borrará todos los registros de tiempo desde que iniciaste la tarea.",
|
||||
"clearAllTimeRecords": "Borrar Todos los Registros de Tiempo",
|
||||
"clearAllTimeConfirmation": "Esto eliminará permanentemente todos los temporizadores de esta tarea y la volverá al estado \"no iniciada\".",
|
||||
"descriptionTitle": "Descripción",
|
||||
"description": "Descripción :",
|
||||
"previousNote": "Nota Anterior",
|
||||
"previousNoteLabel": "Nota anterior:",
|
||||
"subtasksLabel": "Subtareas :",
|
||||
"taskActions": "Acciones de Tarea",
|
||||
"addNote": "Agregar una nota",
|
||||
"additionalNotes": "Notas Adicionales:",
|
||||
"notePlaceholder": "Agregar una nota sobre la finalización...",
|
||||
"setCustomCompletionTime": "Establecer hora de finalización personalizada",
|
||||
"skipTask": "Saltar Tarea",
|
||||
"skipTaskConfirmation": "¿Estás seguro de que quieres saltar esta tarea?",
|
||||
"markComplete": "Marcar como Completada",
|
||||
"markAsDone": "Marcar como hecha",
|
||||
"edit": "Editar",
|
||||
"archive": "Archivar",
|
||||
"unarchive": "Desarchivar",
|
||||
"viewHistory": "Ver Historial",
|
||||
"history": "Historial",
|
||||
"startTimer": "Iniciar Temporizador",
|
||||
"start": "Iniciar",
|
||||
"pauseTimer": "Pausar Temporizador",
|
||||
"approve": "Aprobar",
|
||||
"reject": "Rechazar",
|
||||
"pendingApproval": "Pendiente de Aprobación",
|
||||
"undo": "Deshacer",
|
||||
"skip": "Saltar",
|
||||
"cancel": "Cancelar",
|
||||
"noPriority": "Sin Prioridad",
|
||||
"subtasks": "Subtareas",
|
||||
"noDescription": "No hay descripción disponible",
|
||||
"timer": {
|
||||
"active": "Temporizador Activo",
|
||||
"paused": "Temporizador Pausado",
|
||||
"reset": "Reiniciar Temporizador",
|
||||
"delete": "Eliminar Sesión"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
public/locales/es/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Eliminar",
|
||||
"edit": "Editar",
|
||||
"close": "Cerrar",
|
||||
"confirm": "Confirmar",
|
||||
"loading": "Cargando...",
|
||||
"error": "Error",
|
||||
"success": "Éxito",
|
||||
"warning": "Advertencia",
|
||||
"refresh": "Actualizar",
|
||||
"copy": "Copiar",
|
||||
"copied": "¡Copiado!",
|
||||
"settings": "Configuración",
|
||||
"yes": "Sí",
|
||||
"no": "No",
|
||||
"back": "Atrás",
|
||||
"backToCalendar": "Volver al Calendario",
|
||||
"logout": "Cerrar Sesión",
|
||||
"version": "Versión",
|
||||
"navigation": {
|
||||
"allTasks": "Todas las Tareas",
|
||||
"archived": "Archivadas",
|
||||
"things": "Cosas",
|
||||
"labels": "Etiquetas",
|
||||
"projects": "Proyectos",
|
||||
"filters": "Filtros",
|
||||
"activities": "Actividades",
|
||||
"points": "Puntos",
|
||||
"settings": "Configuración"
|
||||
}
|
||||
}
|
||||
174
public/locales/es/settings.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"title": "Configuración",
|
||||
"circleSettings": {
|
||||
"title": "Configuración del círculo",
|
||||
"description": "Tu cuenta se conecta automáticamente a un Círculo cuando creas o te unes a uno. Invita fácilmente a amigos compartiendo el código único del Círculo o el enlace a continuación.",
|
||||
"circleCode": "Código del Círculo",
|
||||
"copyCode": "Copiar Código",
|
||||
"copyLink": "Copiar Enlace",
|
||||
"codeCopied": "¡Código del círculo copiado!",
|
||||
"linkCopied": "¡Enlace copiado!",
|
||||
"joinCircle": "Unirse a un Círculo",
|
||||
"joinCirclePlaceholder": "Ingresa el Código del Círculo",
|
||||
"join": "Unirse",
|
||||
"leave": "Salir del Círculo",
|
||||
"leaveConfirmTitle": "Salir del Círculo",
|
||||
"leaveConfirmMessage": "¿Estás seguro de que quieres salir de este círculo?",
|
||||
"circleMembers": "Miembros del Círculo",
|
||||
"circleMemberRequests": "Solicitudes de Miembros del Círculo",
|
||||
"admin": "Administrador",
|
||||
"member": "Miembro",
|
||||
"pending": "Pendiente",
|
||||
"accept": "Aceptar",
|
||||
"reject": "Rechazar",
|
||||
"makeAdmin": "Hacer Administrador",
|
||||
"makeMember": "Hacer Miembro",
|
||||
"remove": "Eliminar",
|
||||
"webhookURL": "URL del Webhook",
|
||||
"webhookDescription": "Ingresa una URL de webhook para recibir notificaciones de eventos del círculo",
|
||||
"webhookPlaceholder": "https://tu-url-webhook.com"
|
||||
},
|
||||
"accountSettings": {
|
||||
"title": "Configuración de la Cuenta",
|
||||
"subscription": "Suscripción",
|
||||
"subscriptionStatus": "Plan Actual",
|
||||
"free": "Gratis",
|
||||
"plus": "Plus",
|
||||
"upgrade": "Actualizar",
|
||||
"cancel": "Cancelar",
|
||||
"changePassword": "Cambiar Contraseña",
|
||||
"password": "Contraseña",
|
||||
"dangerZone": "Zona de Peligro",
|
||||
"dangerZoneDescription": "Una vez que elimines tu cuenta, no hay vuelta atrás. Por favor, está seguro.",
|
||||
"deleteAccount": "Eliminar Cuenta"
|
||||
},
|
||||
"localization": {
|
||||
"title": "Localización",
|
||||
"description": "Personaliza el idioma, formato de fecha y preferencias regionales para tu cuenta.",
|
||||
"language": "Idioma",
|
||||
"languageDescription": "Selecciona tu idioma preferido",
|
||||
"dateFormat": "Formato de Fecha",
|
||||
"dateFormatDescription": "Elige cómo se deben mostrar las fechas en toda la aplicación",
|
||||
"timeFormat": "Formato de Hora",
|
||||
"timeFormatDescription": "Selecciona formato de 12 o 24 horas",
|
||||
"12hour": "12 horas (AM/PM)",
|
||||
"24hour": "24 horas",
|
||||
"firstDayOfWeek": "Primer Día de la Semana",
|
||||
"firstDayOfWeekDescription": "Selecciona qué día comienza tu semana",
|
||||
"sunday": "Domingo",
|
||||
"monday": "Lunes",
|
||||
"saturday": "Sábado",
|
||||
"formats": {
|
||||
"mdy": "MM/DD/AAAA (EE.UU.)",
|
||||
"dmy": "DD/MM/AAAA (Europa)",
|
||||
"ymd": "AAAA-MM-DD (ISO)",
|
||||
"long": "Formato largo (ej., 1 de enero de 2024)",
|
||||
"short": "Formato corto (ej., 1 ene 2024)"
|
||||
}
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "Personalización del Panel Lateral",
|
||||
"description": "Personaliza el diseño y la visibilidad de las tarjetas en el panel lateral. Esta sección solo está disponible en dispositivos de pantalla grande como tabletas y computadoras de escritorio."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Preferencias de tema",
|
||||
"description": "Elige cómo se ve el sitio para ti. Selecciona un solo tema o sincronízalo con tu sistema y cambia automáticamente entre temas de día y noche.",
|
||||
"themeMode": "Modo de tema",
|
||||
"light": "Claro",
|
||||
"dark": "Oscuro",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"notifications": {
|
||||
"settingsSaved": "Configuración guardada con éxito",
|
||||
"settingsSaveFailed": "Error al guardar la configuración",
|
||||
"invalidWebhook": "URL de webhook no válida"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Configuración del Perfil",
|
||||
"description": "Actualiza tu nombre para mostrar y foto de perfil.",
|
||||
"photoUpdated": "Foto Actualizada",
|
||||
"photoUpdatedMessage": "¡Tu foto de perfil ha sido actualizada con éxito!",
|
||||
"uploadFailed": "Error al Subir",
|
||||
"uploadFailedMessage": "Error al subir tu foto. Por favor, inténtalo de nuevo.",
|
||||
"profileUpdated": "Perfil Actualizado",
|
||||
"profileUpdatedMessage": "¡Tu información de perfil ha sido guardada con éxito!",
|
||||
"updateFailed": "Error al Actualizar",
|
||||
"updateFailedMessage": "No se pudo actualizar tu perfil. Por favor, verifica tu conexión e inténtalo de nuevo.",
|
||||
"changePhoto": "Cambiar Foto",
|
||||
"displayName": "Nombre para Mostrar",
|
||||
"displayNamePlaceholder": "Ingresa tu nombre para mostrar",
|
||||
"timezone": "Zona Horaria",
|
||||
"timezonePlaceholder": "Selecciona tu zona horaria",
|
||||
"save": "Guardar",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Configuración",
|
||||
"subtitle": "Personaliza tu experiencia y administra las preferencias de tu cuenta",
|
||||
"upgrade": {
|
||||
"title": "Actualizar a Plus",
|
||||
"description": "Desbloquea funciones potentes para mejorar tu productividad",
|
||||
"button": "Actualizar Ahora",
|
||||
"features": {
|
||||
"richText": "Descripciones en texto enriquecido",
|
||||
"notifications": "Notificaciones de tareas",
|
||||
"apiIntegrations": "Integraciones API",
|
||||
"advancedAutomation": "Automatización avanzada"
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"profile": {
|
||||
"title": "Configuración del Perfil",
|
||||
"description": "Actualiza tu información de perfil, foto, nombre para mostrar y preferencias de zona horaria."
|
||||
},
|
||||
"circle": {
|
||||
"title": "Configuración del Círculo",
|
||||
"description": "Administra tu círculo, invita miembros y gestiona solicitudes de unión."
|
||||
},
|
||||
"account": {
|
||||
"title": "Configuración de la Cuenta",
|
||||
"description": "Administra tu suscripción, cambia la contraseña y opciones de eliminación de cuenta."
|
||||
},
|
||||
"subaccounts": {
|
||||
"title": "Cuentas Administradas",
|
||||
"description": "Crea y administra subcuentas para iniciar sesión y completar tareas asignadas."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notificaciones",
|
||||
"description": "Configura notificaciones push, alertas por correo electrónico y destinos de notificación para tareas."
|
||||
},
|
||||
"mfa": {
|
||||
"title": "Autenticación Multifactor",
|
||||
"description": "Agrega una capa adicional de seguridad con MFA usando aplicaciones de autenticación."
|
||||
},
|
||||
"apitokens": {
|
||||
"title": "Tokens API",
|
||||
"description": "Genera y administra tokens de acceso para integraciones de terceros y acceso a la API."
|
||||
},
|
||||
"storage": {
|
||||
"title": "Configuración de Almacenamiento",
|
||||
"description": "Respalda y restaura tus datos, administra el almacenamiento local y las preferencias de sincronización."
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "Personalización del Panel Lateral",
|
||||
"description": "Personaliza el diseño y la visibilidad de las tarjetas en la interfaz del panel lateral."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Preferencias de Tema",
|
||||
"description": "Elige tu tema preferido y configura los ajustes de modo oscuro/claro."
|
||||
},
|
||||
"localization": {
|
||||
"title": "Localización",
|
||||
"description": "Personaliza el idioma, formato de fecha, formato de hora y preferencias regionales."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Configuración Avanzada",
|
||||
"description": "Configura webhooks, actualizaciones en tiempo real y otras funciones avanzadas para mejorar la productividad."
|
||||
},
|
||||
"developer": {
|
||||
"title": "Configuración de Desarrollador",
|
||||
"description": "Ver información técnica sobre tokens de autenticación, conexiones SSE y datos de depuración."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
public/locales/fr/chores.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"title": "Tâches",
|
||||
"myChores": "Mes Tâches",
|
||||
"allChores": "Toutes les Tâches",
|
||||
"addChore": "Ajouter une Tâche",
|
||||
"editChore": "Modifier la Tâche",
|
||||
"deleteChore": "Supprimer la Tâche",
|
||||
"completeChore": "Terminer la Tâche",
|
||||
"dueDate": "Date d'Échéance",
|
||||
"assignedTo": "Assigné à",
|
||||
"priority": "Priorité",
|
||||
"status": "Statut",
|
||||
"description": "Description",
|
||||
"choreView": {
|
||||
"assignment": "Attribution",
|
||||
"assigned": "Assigné",
|
||||
"last": "Dernier",
|
||||
"schedule": "Calendrier",
|
||||
"due": "Échéance",
|
||||
"statistics": "Statistiques",
|
||||
"completed": "Terminé",
|
||||
"times": "fois",
|
||||
"details": "Détails",
|
||||
"createdBy": "Créé par",
|
||||
"na": "N/A",
|
||||
"taskCompleted": "Tâche Terminée",
|
||||
"taskCompletedMessage": "Votre tâche a été marquée comme terminée",
|
||||
"taskCompletionUndone": "La complétion de la tâche a été annulée.",
|
||||
"taskSkipUndone": "Le saut de tâche a été annulé.",
|
||||
"undoSuccessful": "Annulation Réussie",
|
||||
"undoFailed": "Annulation Échouée",
|
||||
"undoFailedMessage": "Impossible d'annuler l'action. Veuillez réessayer.",
|
||||
"resetTimer": "Réinitialiser le Minuteur",
|
||||
"resetTimerConfirmation": "Êtes-vous sûr de vouloir réinitialiser le minuteur ? Cela effacera tous les enregistrements de temps depuis le début de la tâche.",
|
||||
"clearAllTimeRecords": "Effacer Tous les Enregistrements de Temps",
|
||||
"clearAllTimeConfirmation": "Cela supprimera définitivement tous les minuteurs de cette tâche et la remettra à l'état \"non démarrée\".",
|
||||
"descriptionTitle": "Description",
|
||||
"description": "Description :",
|
||||
"previousNote": "Note Précédente",
|
||||
"previousNoteLabel": "Note précédente :",
|
||||
"subtasksLabel": "Sous-tâches :",
|
||||
"taskActions": "Actions de Tâche",
|
||||
"addNote": "Ajouter une note",
|
||||
"additionalNotes": "Notes Supplémentaires :",
|
||||
"notePlaceholder": "Ajouter une note sur la complétion...",
|
||||
"setCustomCompletionTime": "Définir une heure de complétion personnalisée",
|
||||
"skipTask": "Passer la Tâche",
|
||||
"skipTaskConfirmation": "Êtes-vous sûr de vouloir passer cette tâche ?",
|
||||
"markComplete": "Marquer comme Terminée",
|
||||
"markAsDone": "Marquer comme fait",
|
||||
"edit": "Modifier",
|
||||
"archive": "Archiver",
|
||||
"unarchive": "Désarchiver",
|
||||
"viewHistory": "Voir l'Historique",
|
||||
"history": "Historique",
|
||||
"startTimer": "Démarrer le Minuteur",
|
||||
"start": "Démarrer",
|
||||
"pauseTimer": "Mettre en Pause le Minuteur",
|
||||
"approve": "Approuver",
|
||||
"reject": "Rejeter",
|
||||
"pendingApproval": "En Attente d'Approbation",
|
||||
"undo": "Annuler",
|
||||
"skip": "Passer",
|
||||
"cancel": "Annuler",
|
||||
"noPriority": "Aucune Priorité",
|
||||
"subtasks": "Sous-tâches",
|
||||
"noDescription": "Aucune description disponible",
|
||||
"timer": {
|
||||
"active": "Minuteur Actif",
|
||||
"paused": "Minuteur en Pause",
|
||||
"reset": "Réinitialiser le Minuteur",
|
||||
"delete": "Supprimer la Session"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
public/locales/fr/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"save": "Enregistrer",
|
||||
"cancel": "Annuler",
|
||||
"delete": "Supprimer",
|
||||
"edit": "Modifier",
|
||||
"close": "Fermer",
|
||||
"confirm": "Confirmer",
|
||||
"loading": "Chargement...",
|
||||
"error": "Erreur",
|
||||
"success": "Succès",
|
||||
"warning": "Avertissement",
|
||||
"refresh": "Actualiser",
|
||||
"copy": "Copier",
|
||||
"copied": "Copié !",
|
||||
"settings": "Paramètres",
|
||||
"yes": "Oui",
|
||||
"no": "Non",
|
||||
"back": "Retour",
|
||||
"backToCalendar": "Retour au Calendrier",
|
||||
"logout": "Déconnexion",
|
||||
"version": "Version",
|
||||
"navigation": {
|
||||
"allTasks": "Toutes les Tâches",
|
||||
"archived": "Archivées",
|
||||
"things": "Objets",
|
||||
"labels": "Étiquettes",
|
||||
"projects": "Projets",
|
||||
"filters": "Filtres",
|
||||
"activities": "Activités",
|
||||
"points": "Points",
|
||||
"settings": "Paramètres"
|
||||
}
|
||||
}
|
||||
174
public/locales/fr/settings.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"title": "Paramètres",
|
||||
"circleSettings": {
|
||||
"title": "Paramètres du cercle",
|
||||
"description": "Votre compte est automatiquement connecté à un Cercle lorsque vous en créez un ou en rejoignez un. Invitez facilement des amis en partageant le code unique du Cercle ou le lien ci-dessous.",
|
||||
"circleCode": "Code du Cercle",
|
||||
"copyCode": "Copier le Code",
|
||||
"copyLink": "Copier le Lien",
|
||||
"codeCopied": "Code du cercle copié !",
|
||||
"linkCopied": "Lien copié !",
|
||||
"joinCircle": "Rejoindre un Cercle",
|
||||
"joinCirclePlaceholder": "Entrer le Code du Cercle",
|
||||
"join": "Rejoindre",
|
||||
"leave": "Quitter le Cercle",
|
||||
"leaveConfirmTitle": "Quitter le Cercle",
|
||||
"leaveConfirmMessage": "Êtes-vous sûr de vouloir quitter ce cercle ?",
|
||||
"circleMembers": "Membres du Cercle",
|
||||
"circleMemberRequests": "Demandes de Membres du Cercle",
|
||||
"admin": "Administrateur",
|
||||
"member": "Membre",
|
||||
"pending": "En attente",
|
||||
"accept": "Accepter",
|
||||
"reject": "Rejeter",
|
||||
"makeAdmin": "Nommer Administrateur",
|
||||
"makeMember": "Nommer Membre",
|
||||
"remove": "Supprimer",
|
||||
"webhookURL": "URL du Webhook",
|
||||
"webhookDescription": "Entrez une URL de webhook pour recevoir des notifications pour les événements du cercle",
|
||||
"webhookPlaceholder": "https://votre-url-webhook.com"
|
||||
},
|
||||
"accountSettings": {
|
||||
"title": "Paramètres du Compte",
|
||||
"subscription": "Abonnement",
|
||||
"subscriptionStatus": "Plan Actuel",
|
||||
"free": "Gratuit",
|
||||
"plus": "Plus",
|
||||
"upgrade": "Mettre à Niveau",
|
||||
"cancel": "Annuler",
|
||||
"changePassword": "Changer le Mot de Passe",
|
||||
"password": "Mot de passe",
|
||||
"dangerZone": "Zone Dangereuse",
|
||||
"dangerZoneDescription": "Une fois votre compte supprimé, il n'y a pas de retour en arrière. Veuillez être certain.",
|
||||
"deleteAccount": "Supprimer le Compte"
|
||||
},
|
||||
"localization": {
|
||||
"title": "Localisation",
|
||||
"description": "Personnalisez la langue, le format de date et les préférences régionales pour votre compte.",
|
||||
"language": "Langue",
|
||||
"languageDescription": "Sélectionnez votre langue préférée",
|
||||
"dateFormat": "Format de Date",
|
||||
"dateFormatDescription": "Choisissez comment les dates doivent être affichées dans l'application",
|
||||
"timeFormat": "Format de l'Heure",
|
||||
"timeFormatDescription": "Sélectionnez le format 12 heures ou 24 heures",
|
||||
"12hour": "12 heures (AM/PM)",
|
||||
"24hour": "24 heures",
|
||||
"firstDayOfWeek": "Premier Jour de la Semaine",
|
||||
"firstDayOfWeekDescription": "Sélectionnez quel jour commence votre semaine",
|
||||
"sunday": "Dimanche",
|
||||
"monday": "Lundi",
|
||||
"saturday": "Samedi",
|
||||
"formats": {
|
||||
"mdy": "MM/JJ/AAAA (États-Unis)",
|
||||
"dmy": "JJ/MM/AAAA (Europe)",
|
||||
"ymd": "AAAA-MM-JJ (ISO)",
|
||||
"long": "Format long (ex., 1 janvier 2024)",
|
||||
"short": "Format court (ex., 1 janv. 2024)"
|
||||
}
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "Personnalisation du Panneau Latéral",
|
||||
"description": "Personnalisez la disposition et la visibilité des cartes dans le panneau latéral. Cette section n'est disponible que sur les grands écrans comme les tablettes et les ordinateurs de bureau."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Préférences de thème",
|
||||
"description": "Choisissez comment le site vous apparaît. Sélectionnez un thème unique ou synchronisez avec votre système pour basculer automatiquement entre les thèmes jour et nuit.",
|
||||
"themeMode": "Mode de thème",
|
||||
"light": "Clair",
|
||||
"dark": "Sombre",
|
||||
"system": "Système"
|
||||
},
|
||||
"notifications": {
|
||||
"settingsSaved": "Paramètres enregistrés avec succès",
|
||||
"settingsSaveFailed": "Échec de l'enregistrement des paramètres",
|
||||
"invalidWebhook": "URL de webhook invalide"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Paramètres du Profil",
|
||||
"description": "Mettez à jour votre nom d'affichage et votre photo de profil.",
|
||||
"photoUpdated": "Photo Mise à Jour",
|
||||
"photoUpdatedMessage": "Votre photo de profil a été mise à jour avec succès !",
|
||||
"uploadFailed": "Échec du Téléchargement",
|
||||
"uploadFailedMessage": "Échec du téléchargement de votre photo. Veuillez réessayer.",
|
||||
"profileUpdated": "Profil Mis à Jour",
|
||||
"profileUpdatedMessage": "Vos informations de profil ont été enregistrées avec succès !",
|
||||
"updateFailed": "Échec de la Mise à Jour",
|
||||
"updateFailedMessage": "Impossible de mettre à jour votre profil. Veuillez vérifier votre connexion et réessayer.",
|
||||
"changePhoto": "Changer la Photo",
|
||||
"displayName": "Nom d'Affichage",
|
||||
"displayNamePlaceholder": "Entrez votre nom d'affichage",
|
||||
"timezone": "Fuseau Horaire",
|
||||
"timezonePlaceholder": "Sélectionnez votre fuseau horaire",
|
||||
"save": "Enregistrer",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Paramètres",
|
||||
"subtitle": "Personnalisez votre expérience et gérez les préférences de votre compte",
|
||||
"upgrade": {
|
||||
"title": "Passer à Plus",
|
||||
"description": "Débloquez des fonctionnalités puissantes pour améliorer votre productivité",
|
||||
"button": "Mettre à Niveau Maintenant",
|
||||
"features": {
|
||||
"richText": "Descriptions en texte enrichi",
|
||||
"notifications": "Notifications de tâches",
|
||||
"apiIntegrations": "Intégrations API",
|
||||
"advancedAutomation": "Automatisation avancée"
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"profile": {
|
||||
"title": "Paramètres du Profil",
|
||||
"description": "Mettez à jour vos informations de profil, photo, nom d'affichage et préférences de fuseau horaire."
|
||||
},
|
||||
"circle": {
|
||||
"title": "Paramètres du Cercle",
|
||||
"description": "Gérez votre cercle, invitez des membres et gérez les demandes d'adhésion."
|
||||
},
|
||||
"account": {
|
||||
"title": "Paramètres du Compte",
|
||||
"description": "Gérez votre abonnement, changez le mot de passe et les options de suppression de compte."
|
||||
},
|
||||
"subaccounts": {
|
||||
"title": "Comptes Gérés",
|
||||
"description": "Créez et gérez des sous-comptes pour vous connecter et accomplir les tâches assignées."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notifications",
|
||||
"description": "Configurez les notifications push, les alertes par e-mail et les cibles de notification pour les tâches."
|
||||
},
|
||||
"mfa": {
|
||||
"title": "Authentification Multi-Facteurs",
|
||||
"description": "Ajoutez une couche de sécurité supplémentaire avec MFA en utilisant des applications d'authentification."
|
||||
},
|
||||
"apitokens": {
|
||||
"title": "Jetons API",
|
||||
"description": "Générez et gérez des jetons d'accès pour les intégrations tierces et l'accès à l'API."
|
||||
},
|
||||
"storage": {
|
||||
"title": "Paramètres de Stockage",
|
||||
"description": "Sauvegardez et restaurez vos données, gérez le stockage local et les préférences de synchronisation."
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "Personnalisation du Panneau Latéral",
|
||||
"description": "Personnalisez la disposition et la visibilité des cartes dans l'interface du panneau latéral."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Préférences de Thème",
|
||||
"description": "Choisissez votre thème préféré et configurez les paramètres de mode sombre/clair."
|
||||
},
|
||||
"localization": {
|
||||
"title": "Localisation",
|
||||
"description": "Personnalisez la langue, le format de date, le format de l'heure et les préférences régionales."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Paramètres Avancés",
|
||||
"description": "Configurez les webhooks, les mises à jour en temps réel et d'autres fonctionnalités avancées pour améliorer la productivité."
|
||||
},
|
||||
"developer": {
|
||||
"title": "Paramètres Développeur",
|
||||
"description": "Consultez les informations techniques sur les jetons d'authentification, les connexions SSE et les données de débogage."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
public/locales/ja/chores.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"title": "タスク",
|
||||
"myChores": "自分のタスク",
|
||||
"allChores": "すべてのタスク",
|
||||
"addChore": "タスクを追加",
|
||||
"editChore": "タスクを編集",
|
||||
"deleteChore": "タスクを削除",
|
||||
"completeChore": "タスクを完了",
|
||||
"dueDate": "期限",
|
||||
"assignedTo": "担当者",
|
||||
"priority": "優先度",
|
||||
"status": "ステータス",
|
||||
"description": "説明",
|
||||
"choreView": {
|
||||
"assignment": "担当",
|
||||
"assigned": "担当者",
|
||||
"last": "前回",
|
||||
"schedule": "スケジュール",
|
||||
"due": "期限",
|
||||
"statistics": "統計",
|
||||
"completed": "完了",
|
||||
"times": "回",
|
||||
"details": "詳細",
|
||||
"createdBy": "作成者",
|
||||
"na": "該当なし",
|
||||
"taskCompleted": "タスク完了",
|
||||
"taskCompletedMessage": "タスクを完了としてマークしました",
|
||||
"taskCompletionUndone": "タスクの完了を取り消しました。",
|
||||
"taskSkipUndone": "タスクのスキップを取り消しました。",
|
||||
"undoSuccessful": "取り消し成功",
|
||||
"undoFailed": "取り消し失敗",
|
||||
"undoFailedMessage": "操作を取り消せませんでした。もう一度お試しください。",
|
||||
"resetTimer": "タイマーをリセット",
|
||||
"resetTimerConfirmation": "本当にタイマーをリセットしますか?このタスクを開始してからのすべての時間記録が削除されます。",
|
||||
"clearAllTimeRecords": "すべての時間記録を削除",
|
||||
"clearAllTimeConfirmation": "このタスクのすべてのタイマーを完全に削除し、「未開始」の状態に戻します。",
|
||||
"descriptionTitle": "説明",
|
||||
"description": "説明 :",
|
||||
"previousNote": "前回のメモ",
|
||||
"previousNoteLabel": "前回のメモ:",
|
||||
"subtasksLabel": "サブタスク :",
|
||||
"taskActions": "タスク操作",
|
||||
"addNote": "メモを追加",
|
||||
"additionalNotes": "追加メモ:",
|
||||
"notePlaceholder": "完了についてのメモを追加...",
|
||||
"setCustomCompletionTime": "完了時刻をカスタム指定",
|
||||
"skipTask": "タスクをスキップ",
|
||||
"skipTaskConfirmation": "本当にこのタスクをスキップしますか?",
|
||||
"markComplete": "完了としてマーク",
|
||||
"markAsDone": "完了にする",
|
||||
"edit": "編集",
|
||||
"archive": "アーカイブ",
|
||||
"unarchive": "アーカイブ解除",
|
||||
"viewHistory": "履歴を表示",
|
||||
"history": "履歴",
|
||||
"startTimer": "タイマー開始",
|
||||
"start": "開始",
|
||||
"pauseTimer": "タイマー一時停止",
|
||||
"approve": "承認",
|
||||
"reject": "却下",
|
||||
"pendingApproval": "承認待ち",
|
||||
"undo": "取り消し",
|
||||
"skip": "スキップ",
|
||||
"cancel": "キャンセル",
|
||||
"noPriority": "優先度なし",
|
||||
"subtasks": "サブタスク",
|
||||
"noDescription": "説明はありません",
|
||||
"timer": {
|
||||
"active": "タイマー動作中",
|
||||
"paused": "タイマー一時停止中",
|
||||
"reset": "タイマーをリセット",
|
||||
"delete": "セッションを削除"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
public/locales/ja/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"save": "保存",
|
||||
"cancel": "キャンセル",
|
||||
"delete": "削除",
|
||||
"edit": "編集",
|
||||
"close": "閉じる",
|
||||
"confirm": "確認",
|
||||
"loading": "読み込み中...",
|
||||
"error": "エラー",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"refresh": "更新",
|
||||
"copy": "コピー",
|
||||
"copied": "コピーしました!",
|
||||
"settings": "設定",
|
||||
"yes": "はい",
|
||||
"no": "いいえ",
|
||||
"back": "戻る",
|
||||
"backToCalendar": "カレンダーに戻る",
|
||||
"logout": "ログアウト",
|
||||
"version": "バージョン",
|
||||
"navigation": {
|
||||
"allTasks": "すべてのタスク",
|
||||
"archived": "アーカイブ済み",
|
||||
"things": "Things",
|
||||
"labels": "ラベル",
|
||||
"projects": "プロジェクト",
|
||||
"filters": "フィルター",
|
||||
"activities": "アクティビティ",
|
||||
"points": "ポイント",
|
||||
"settings": "設定"
|
||||
}
|
||||
}
|
||||
174
public/locales/ja/settings.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"title": "設定",
|
||||
"circleSettings": {
|
||||
"title": "サークル設定",
|
||||
"description": "アカウントは、サークルを作成または参加すると自動的にそのサークルに接続されます。下記のユニークなサークルコードまたはリンクを共有することで、友達を簡単に招待できます。誰かがサークルへの参加をリクエストした際は、下に通知が表示されます。サークルから脱退したい場合は、「サークルを脱退」ボタンを押してください。",
|
||||
"circleCode": "サークルコード",
|
||||
"copyCode": "コードをコピー",
|
||||
"copyLink": "リンクをコピー",
|
||||
"codeCopied": "サークルコードをコピーしました!",
|
||||
"linkCopied": "サークルリンクをコピーしました!",
|
||||
"joinCircle": "サークルに参加",
|
||||
"joinCirclePlaceholder": "サークルコードを入力",
|
||||
"join": "参加",
|
||||
"leave": "サークルを脱退",
|
||||
"leaveConfirmTitle": "サークルを脱退",
|
||||
"leaveConfirmMessage": "本当にこのサークルから脱退しますか?",
|
||||
"circleMembers": "サークルメンバー",
|
||||
"circleMemberRequests": "サークル参加リクエスト",
|
||||
"admin": "管理者",
|
||||
"member": "メンバー",
|
||||
"pending": "保留中",
|
||||
"accept": "承認",
|
||||
"reject": "拒否",
|
||||
"makeAdmin": "管理者にする",
|
||||
"makeMember": "メンバーにする",
|
||||
"remove": "削除",
|
||||
"webhookURL": "Webhook URL",
|
||||
"webhookDescription": "サークルイベントの通知を受け取るための Webhook URL を入力してください",
|
||||
"webhookPlaceholder": "https://your-webhook-url.com"
|
||||
},
|
||||
"accountSettings": {
|
||||
"title": "アカウント設定",
|
||||
"subscription": "サブスクリプション",
|
||||
"subscriptionStatus": "現在のプラン",
|
||||
"free": "無料",
|
||||
"plus": "Plus",
|
||||
"upgrade": "アップグレード",
|
||||
"cancel": "キャンセル",
|
||||
"changePassword": "パスワードを変更",
|
||||
"password": "パスワード",
|
||||
"dangerZone": "危険な操作",
|
||||
"dangerZoneDescription": "アカウントを削除すると元に戻すことはできません。十分にご確認ください。",
|
||||
"deleteAccount": "アカウントを削除"
|
||||
},
|
||||
"localization": {
|
||||
"title": "地域設定",
|
||||
"description": "言語、日付形式、地域の設定をカスタマイズします。",
|
||||
"language": "言語",
|
||||
"languageDescription": "使用する言語を選択してください",
|
||||
"dateFormat": "日付形式",
|
||||
"dateFormatDescription": "アプリケーション全体で日付をどのように表示するかを選択します",
|
||||
"timeFormat": "時刻形式",
|
||||
"timeFormatDescription": "12時間表記または24時間表記を選択します",
|
||||
"12hour": "12時間 (AM/PM)",
|
||||
"24hour": "24時間",
|
||||
"firstDayOfWeek": "週の開始曜日",
|
||||
"firstDayOfWeekDescription": "週の最初の曜日を選択します",
|
||||
"sunday": "日曜日",
|
||||
"monday": "月曜日",
|
||||
"saturday": "土曜日",
|
||||
"formats": {
|
||||
"mdy": "MM/DD/YYYY (米国)",
|
||||
"dmy": "DD/MM/YYYY (欧州)",
|
||||
"ymd": "YYYY-MM-DD (ISO)",
|
||||
"long": "長い形式 (例: January 1, 2024)",
|
||||
"short": "短い形式 (例: Jan 1, 2024)"
|
||||
}
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "サイドパネルのカスタマイズ",
|
||||
"description": "サイドパネル内のカードのレイアウトと表示をカスタマイズします。この機能はタブレットやデスクトップなど大画面のデバイスでのみ利用できます。"
|
||||
},
|
||||
"theme": {
|
||||
"title": "テーマ設定",
|
||||
"description": "サイトの見た目を選択します。単一のテーマを選ぶか、システム設定に同期して昼夜のテーマを自動で切り替えることができます。",
|
||||
"themeMode": "テーマモード",
|
||||
"light": "ライト",
|
||||
"dark": "ダーク",
|
||||
"system": "システム"
|
||||
},
|
||||
"notifications": {
|
||||
"settingsSaved": "設定を保存しました",
|
||||
"settingsSaveFailed": "設定の保存に失敗しました",
|
||||
"invalidWebhook": "Webhook URL が無効です"
|
||||
},
|
||||
"profile": {
|
||||
"title": "プロフィール設定",
|
||||
"description": "表示名とプロフィール写真を更新します。",
|
||||
"photoUpdated": "写真を更新しました",
|
||||
"photoUpdatedMessage": "プロフィール写真が正常に更新されました!",
|
||||
"uploadFailed": "アップロード失敗",
|
||||
"uploadFailedMessage": "写真のアップロードに失敗しました。もう一度お試しください。",
|
||||
"profileUpdated": "プロフィールを更新しました",
|
||||
"profileUpdatedMessage": "プロフィール情報が正常に保存されました!",
|
||||
"updateFailed": "更新失敗",
|
||||
"updateFailedMessage": "プロフィールを更新できませんでした。接続を確認してから再度お試しください。",
|
||||
"changePhoto": "写真を変更",
|
||||
"displayName": "表示名",
|
||||
"displayNamePlaceholder": "表示名を入力してください",
|
||||
"timezone": "タイムゾーン",
|
||||
"timezonePlaceholder": "タイムゾーンを選択してください",
|
||||
"save": "保存",
|
||||
"cancel": "キャンセル"
|
||||
},
|
||||
"overview": {
|
||||
"title": "設定",
|
||||
"subtitle": "体験をカスタマイズし、アカウント設定を管理します",
|
||||
"upgrade": {
|
||||
"title": "Plus へアップグレード",
|
||||
"description": "強力な機能を解放して、生産性を向上させましょう",
|
||||
"button": "今すぐアップグレード",
|
||||
"features": {
|
||||
"richText": "リッチテキストの説明",
|
||||
"notifications": "タスク通知",
|
||||
"apiIntegrations": "API 連携",
|
||||
"advancedAutomation": "高度な自動化"
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"profile": {
|
||||
"title": "プロフィール設定",
|
||||
"description": "プロフィール情報、写真、表示名、タイムゾーンを更新します。"
|
||||
},
|
||||
"circle": {
|
||||
"title": "サークル設定",
|
||||
"description": "サークルの管理、メンバーの招待、参加リクエストへの対応を行います。"
|
||||
},
|
||||
"account": {
|
||||
"title": "アカウント設定",
|
||||
"description": "サブスクリプションの管理、パスワード変更、アカウント削除を行います。"
|
||||
},
|
||||
"subaccounts": {
|
||||
"title": "管理アカウント",
|
||||
"description": "サブアカウントを作成・管理し、ログインや割り当てられたタスクの完了を行えるようにします。"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "通知",
|
||||
"description": "プッシュ通知、メール通知、タスクの通知先を設定します。"
|
||||
},
|
||||
"mfa": {
|
||||
"title": "多要素認証",
|
||||
"description": "認証アプリを使った MFA でセキュリティをさらに強化します。"
|
||||
},
|
||||
"apitokens": {
|
||||
"title": "API トークン",
|
||||
"description": "サードパーティ連携や API アクセス用のトークンを生成・管理します。"
|
||||
},
|
||||
"storage": {
|
||||
"title": "ストレージ設定",
|
||||
"description": "データのバックアップと復元、ローカルストレージと同期の設定を管理します。"
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "サイドパネルのカスタマイズ",
|
||||
"description": "サイドパネルのカードのレイアウトと表示をカスタマイズします。"
|
||||
},
|
||||
"theme": {
|
||||
"title": "テーマ設定",
|
||||
"description": "好みのテーマを選択し、ダーク/ライトモードの設定を行います。"
|
||||
},
|
||||
"localization": {
|
||||
"title": "地域設定",
|
||||
"description": "言語、日付形式、時刻形式、地域の設定をカスタマイズします。"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "詳細設定",
|
||||
"description": "Webhook、リアルタイム更新、その他の高度な機能を設定して生産性を高めます。"
|
||||
},
|
||||
"developer": {
|
||||
"title": "開発者設定",
|
||||
"description": "認証トークン、SSE 接続、デバッグデータなどの技術情報を確認できます。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
public/locales/nl/chores.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"title": "Taken",
|
||||
"myChores": "Mijn Taken",
|
||||
"allChores": "Alle Taken",
|
||||
"addChore": "Taak Toevoegen",
|
||||
"editChore": "Taak Bewerken",
|
||||
"deleteChore": "Taak Verwijderen",
|
||||
"completeChore": "Taak Voltooien",
|
||||
"dueDate": "Vervaldatum",
|
||||
"assignedTo": "Toegewezen aan",
|
||||
"priority": "Prioriteit",
|
||||
"status": "Status",
|
||||
"description": "Beschrijving",
|
||||
"choreView": {
|
||||
"assignment": "Toewijzing",
|
||||
"assigned": "Toegewezen",
|
||||
"last": "Laatste",
|
||||
"schedule": "Schema",
|
||||
"due": "Vervaldatum",
|
||||
"statistics": "Statistieken",
|
||||
"completed": "Voltooid",
|
||||
"times": "keer",
|
||||
"details": "Details",
|
||||
"createdBy": "Aangemaakt door",
|
||||
"na": "N/B",
|
||||
"taskCompleted": "Taak Voltooid",
|
||||
"taskCompletedMessage": "Uw taak is gemarkeerd als voltooid",
|
||||
"taskCompletionUndone": "De voltooiing van de taak is ongedaan gemaakt.",
|
||||
"taskSkipUndone": "Het overslaan van de taak is ongedaan gemaakt.",
|
||||
"undoSuccessful": "Ongedaan Maken Geslaagd",
|
||||
"undoFailed": "Ongedaan Maken Mislukt",
|
||||
"undoFailedMessage": "Kan de actie niet ongedaan maken. Probeer het opnieuw.",
|
||||
"resetTimer": "Timer Resetten",
|
||||
"resetTimerConfirmation": "Weet u zeker dat u de timer wilt resetten? Dit wist alle tijdregistraties sinds u de taak bent begonnen.",
|
||||
"clearAllTimeRecords": "Alle Tijdregistraties Wissen",
|
||||
"clearAllTimeConfirmation": "Dit verwijdert permanent alle timers voor deze taak en zet hem terug naar \"niet gestart\".",
|
||||
"descriptionTitle": "Beschrijving",
|
||||
"description": "Beschrijving :",
|
||||
"previousNote": "Vorige Notitie",
|
||||
"previousNoteLabel": "Vorige notitie:",
|
||||
"subtasksLabel": "Subtaken :",
|
||||
"taskActions": "Taakacties",
|
||||
"addNote": "Een notitie toevoegen",
|
||||
"additionalNotes": "Aanvullende Notities:",
|
||||
"notePlaceholder": "Voeg een notitie toe over de voltooiing...",
|
||||
"setCustomCompletionTime": "Aangepaste voltooitijd instellen",
|
||||
"skipTask": "Taak Overslaan",
|
||||
"skipTaskConfirmation": "Weet u zeker dat u deze taak wilt overslaan?",
|
||||
"markComplete": "Markeren als Voltooid",
|
||||
"markAsDone": "Markeren als gedaan",
|
||||
"edit": "Bewerken",
|
||||
"archive": "Archiveren",
|
||||
"unarchive": "Dearchiveren",
|
||||
"viewHistory": "Geschiedenis Bekijken",
|
||||
"history": "Geschiedenis",
|
||||
"startTimer": "Timer Starten",
|
||||
"start": "Starten",
|
||||
"pauseTimer": "Timer Pauzeren",
|
||||
"approve": "Goedkeuren",
|
||||
"reject": "Afwijzen",
|
||||
"pendingApproval": "In Afwachting van Goedkeuring",
|
||||
"undo": "Ongedaan Maken",
|
||||
"skip": "Overslaan",
|
||||
"cancel": "Annuleren",
|
||||
"noPriority": "Geen Prioriteit",
|
||||
"subtasks": "Subtaken",
|
||||
"noDescription": "Geen beschrijving beschikbaar",
|
||||
"timer": {
|
||||
"active": "Timer Actief",
|
||||
"paused": "Timer Gepauzeerd",
|
||||
"reset": "Timer Resetten",
|
||||
"delete": "Sessie Verwijderen"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
public/locales/nl/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"save": "Opslaan",
|
||||
"cancel": "Annuleren",
|
||||
"delete": "Verwijderen",
|
||||
"edit": "Bewerken",
|
||||
"close": "Sluiten",
|
||||
"confirm": "Bevestigen",
|
||||
"loading": "Laden...",
|
||||
"error": "Fout",
|
||||
"success": "Succes",
|
||||
"warning": "Waarschuwing",
|
||||
"refresh": "Vernieuwen",
|
||||
"copy": "Kopiëren",
|
||||
"copied": "Gekopieerd!",
|
||||
"settings": "Instellingen",
|
||||
"yes": "Ja",
|
||||
"no": "Nee",
|
||||
"back": "Terug",
|
||||
"backToCalendar": "Terug naar Kalender",
|
||||
"logout": "Uitloggen",
|
||||
"version": "Versie",
|
||||
"navigation": {
|
||||
"allTasks": "Alle Taken",
|
||||
"archived": "Gearchiveerd",
|
||||
"things": "Dingen",
|
||||
"labels": "Labels",
|
||||
"projects": "Projecten",
|
||||
"filters": "Filters",
|
||||
"activities": "Activiteiten",
|
||||
"points": "Punten",
|
||||
"settings": "Instellingen"
|
||||
}
|
||||
}
|
||||
174
public/locales/nl/settings.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"title": "Instellingen",
|
||||
"circleSettings": {
|
||||
"title": "Cirkelinstellingen",
|
||||
"description": "Uw account wordt automatisch verbonden met een Cirkel wanneer u er een aanmaakt of lid wordt. Nodig eenvoudig vrienden uit door de unieke Cirkelcode of onderstaande link te delen.",
|
||||
"circleCode": "Cirkelcode",
|
||||
"copyCode": "Code Kopiëren",
|
||||
"copyLink": "Link Kopiëren",
|
||||
"codeCopied": "Cirkelcode gekopieerd!",
|
||||
"linkCopied": "Link gekopieerd!",
|
||||
"joinCircle": "Lid Worden van een Cirkel",
|
||||
"joinCirclePlaceholder": "Voer Cirkelcode in",
|
||||
"join": "Lid Worden",
|
||||
"leave": "Cirkel Verlaten",
|
||||
"leaveConfirmTitle": "Cirkel Verlaten",
|
||||
"leaveConfirmMessage": "Weet u zeker dat u deze cirkel wilt verlaten?",
|
||||
"circleMembers": "Cirkelleden",
|
||||
"circleMemberRequests": "Aanvragen voor Cirkellidmaatschap",
|
||||
"admin": "Beheerder",
|
||||
"member": "Lid",
|
||||
"pending": "In behandeling",
|
||||
"accept": "Accepteren",
|
||||
"reject": "Afwijzen",
|
||||
"makeAdmin": "Beheerder Maken",
|
||||
"makeMember": "Lid Maken",
|
||||
"remove": "Verwijderen",
|
||||
"webhookURL": "Webhook URL",
|
||||
"webhookDescription": "Voer een webhook URL in om meldingen te ontvangen voor cirkelgebeurtenissen",
|
||||
"webhookPlaceholder": "https://uw-webhook-url.com"
|
||||
},
|
||||
"accountSettings": {
|
||||
"title": "Accountinstellingen",
|
||||
"subscription": "Abonnement",
|
||||
"subscriptionStatus": "Huidig Plan",
|
||||
"free": "Gratis",
|
||||
"plus": "Plus",
|
||||
"upgrade": "Upgraden",
|
||||
"cancel": "Annuleren",
|
||||
"changePassword": "Wachtwoord Wijzigen",
|
||||
"password": "Wachtwoord",
|
||||
"dangerZone": "Gevarenzone",
|
||||
"dangerZoneDescription": "Zodra u uw account verwijdert, is er geen weg terug. Wees zeker van uw beslissing.",
|
||||
"deleteAccount": "Account Verwijderen"
|
||||
},
|
||||
"localization": {
|
||||
"title": "Lokalisatie",
|
||||
"description": "Pas de taal, datumnotatie en regionale voorkeuren voor uw account aan.",
|
||||
"language": "Taal",
|
||||
"languageDescription": "Selecteer uw voorkeurstaal",
|
||||
"dateFormat": "Datumnotatie",
|
||||
"dateFormatDescription": "Kies hoe datums in de applicatie worden weergegeven",
|
||||
"timeFormat": "Tijdnotatie",
|
||||
"timeFormatDescription": "Selecteer 12-uurs of 24-uurs tijdnotatie",
|
||||
"12hour": "12-uurs (AM/PM)",
|
||||
"24hour": "24-uurs",
|
||||
"firstDayOfWeek": "Eerste Dag van de Week",
|
||||
"firstDayOfWeekDescription": "Selecteer welke dag uw week begint",
|
||||
"sunday": "Zondag",
|
||||
"monday": "Maandag",
|
||||
"saturday": "Zaterdag",
|
||||
"formats": {
|
||||
"mdy": "MM/DD/JJJJ (VS)",
|
||||
"dmy": "DD/MM/JJJJ (Europa)",
|
||||
"ymd": "JJJJ-MM-DD (ISO)",
|
||||
"long": "Lang formaat (bijv. 1 januari 2024)",
|
||||
"short": "Kort formaat (bijv. 1 jan 2024)"
|
||||
}
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "Zijpaneel Aanpassen",
|
||||
"description": "Pas de indeling en zichtbaarheid van kaarten in het zijpaneel aan. Deze sectie is alleen beschikbaar op grote schermapparaten zoals tablets en desktops."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Themavoorkeuren",
|
||||
"description": "Kies hoe de site er voor u uitziet. Selecteer een enkel thema of synchroniseer met uw systeem en schakel automatisch tussen dag- en nachtthema's.",
|
||||
"themeMode": "Thamamodus",
|
||||
"light": "Licht",
|
||||
"dark": "Donker",
|
||||
"system": "Systeem"
|
||||
},
|
||||
"notifications": {
|
||||
"settingsSaved": "Instellingen succesvol opgeslagen",
|
||||
"settingsSaveFailed": "Opslaan van instellingen mislukt",
|
||||
"invalidWebhook": "Ongeldige webhook URL"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profielinstellingen",
|
||||
"description": "Werk uw weergavenaam en profielfoto bij.",
|
||||
"photoUpdated": "Foto Bijgewerkt",
|
||||
"photoUpdatedMessage": "Uw profielfoto is succesvol bijgewerkt!",
|
||||
"uploadFailed": "Uploaden Mislukt",
|
||||
"uploadFailedMessage": "Het uploaden van uw foto is mislukt. Probeer het opnieuw.",
|
||||
"profileUpdated": "Profiel Bijgewerkt",
|
||||
"profileUpdatedMessage": "Uw profielgegevens zijn succesvol opgeslagen!",
|
||||
"updateFailed": "Bijwerken Mislukt",
|
||||
"updateFailedMessage": "Kan uw profiel niet bijwerken. Controleer uw verbinding en probeer het opnieuw.",
|
||||
"changePhoto": "Foto Wijzigen",
|
||||
"displayName": "Weergavenaam",
|
||||
"displayNamePlaceholder": "Voer uw weergavenaam in",
|
||||
"timezone": "Tijdzone",
|
||||
"timezonePlaceholder": "Selecteer uw tijdzone",
|
||||
"save": "Opslaan",
|
||||
"cancel": "Annuleren"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Instellingen",
|
||||
"subtitle": "Pas uw ervaring aan en beheer uw accountvoorkeuren",
|
||||
"upgrade": {
|
||||
"title": "Upgraden naar Plus",
|
||||
"description": "Ontgrendel krachtige functies om uw productiviteit te verbeteren",
|
||||
"button": "Nu Upgraden",
|
||||
"features": {
|
||||
"richText": "Rijke tekstbeschrijvingen",
|
||||
"notifications": "Taakmeldingen",
|
||||
"apiIntegrations": "API-integraties",
|
||||
"advancedAutomation": "Geavanceerde automatisering"
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"profile": {
|
||||
"title": "Profielinstellingen",
|
||||
"description": "Werk uw profielgegevens, foto, weergavenaam en tijdzonevoorkeuren bij."
|
||||
},
|
||||
"circle": {
|
||||
"title": "Cirkelinstellingen",
|
||||
"description": "Beheer uw cirkel, nodig leden uit en behandel lidmaatschapsverzoeken."
|
||||
},
|
||||
"account": {
|
||||
"title": "Accountinstellingen",
|
||||
"description": "Beheer uw abonnement, wijzig het wachtwoord en opties voor accountverwijdering."
|
||||
},
|
||||
"subaccounts": {
|
||||
"title": "Beheerde Accounts",
|
||||
"description": "Maak subaccounts aan en beheer ze om in te loggen en toegewezen taken te voltooien."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Meldingen",
|
||||
"description": "Configureer pushmeldingen, e-mailwaarschuwingen en meldingsdoelen voor taken."
|
||||
},
|
||||
"mfa": {
|
||||
"title": "Multi-Factor Authenticatie",
|
||||
"description": "Voeg een extra beveiligingslaag toe met MFA via authenticatie-apps."
|
||||
},
|
||||
"apitokens": {
|
||||
"title": "API-tokens",
|
||||
"description": "Genereer en beheer toegangstokens voor integraties van derden en API-toegang."
|
||||
},
|
||||
"storage": {
|
||||
"title": "Opslaginstellingen",
|
||||
"description": "Maak een back-up van uw gegevens en herstel ze, beheer lokale opslag en synchronisatievoorkeuren."
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "Zijpaneel Aanpassen",
|
||||
"description": "Pas de indeling en zichtbaarheid van kaarten in de zijpaneelinterface aan."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Themavoorkeuren",
|
||||
"description": "Kies uw voorkeursthema en configureer de instellingen voor donker/licht modus."
|
||||
},
|
||||
"localization": {
|
||||
"title": "Lokalisatie",
|
||||
"description": "Pas de taal, datumnotatie, tijdnotatie en regionale voorkeuren aan."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Geavanceerde Instellingen",
|
||||
"description": "Configureer webhooks, real-time updates en andere geavanceerde functies voor verbeterde productiviteit."
|
||||
},
|
||||
"developer": {
|
||||
"title": "Ontwikkelaarsinstellingen",
|
||||
"description": "Bekijk technische informatie over authenticatietokens, SSE-verbindingen en foutopsporingsgegevens."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
public/locales/pt/chores.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"title": "Tarefas",
|
||||
"myChores": "Minhas tarefas",
|
||||
"allChores": "Todas as tarefas",
|
||||
"addChore": "Adicionar tarefa",
|
||||
"editChore": "Editar tarefa",
|
||||
"deleteChore": "Excluir tarefa",
|
||||
"completeChore": "Concluir tarefa",
|
||||
"dueDate": "Data de vencimento",
|
||||
"assignedTo": "Atribuída a",
|
||||
"priority": "Prioridade",
|
||||
"status": "Status",
|
||||
"description": "Descrição",
|
||||
"choreView": {
|
||||
"assignment": "Atribuição",
|
||||
"assigned": "Atribuída",
|
||||
"last": "Última",
|
||||
"schedule": "Agendamento",
|
||||
"due": "Vence em",
|
||||
"statistics": "Estatísticas",
|
||||
"completed": "Concluída",
|
||||
"times": "vezes",
|
||||
"details": "Detalhes",
|
||||
"createdBy": "Criada por",
|
||||
"na": "N/D",
|
||||
"taskCompleted": "Tarefa concluída",
|
||||
"taskCompletedMessage": "Sua tarefa foi marcada como concluída",
|
||||
"taskCompletionUndone": "A conclusão da tarefa foi desfeita.",
|
||||
"taskSkipUndone": "O pulo da tarefa foi desfeito.",
|
||||
"undoSuccessful": "Desfeito com sucesso",
|
||||
"undoFailed": "Falha ao desfazer",
|
||||
"undoFailedMessage": "Não foi possível desfazer a ação. Tente novamente.",
|
||||
"resetTimer": "Reiniciar cronômetro",
|
||||
"resetTimerConfirmation": "Tem certeza de que deseja reiniciar o cronômetro? Isso apagará todos os registros de tempo desde o início da tarefa.",
|
||||
"clearAllTimeRecords": "Apagar todos os registros de tempo",
|
||||
"clearAllTimeConfirmation": "Isso apagará permanentemente todos os cronômetros desta tarefa e a marcará como \"não iniciada\".",
|
||||
"descriptionTitle": "Descrição",
|
||||
"description": "Descrição :",
|
||||
"previousNote": "Nota anterior",
|
||||
"previousNoteLabel": "Nota anterior:",
|
||||
"subtasksLabel": "Subtarefas :",
|
||||
"taskActions": "Ações da tarefa",
|
||||
"addNote": "Adicionar uma nota",
|
||||
"additionalNotes": "Notas adicionais:",
|
||||
"notePlaceholder": "Adicione uma nota sobre a conclusão...",
|
||||
"setCustomCompletionTime": "Definir horário de conclusão personalizado",
|
||||
"skipTask": "Pular tarefa",
|
||||
"skipTaskConfirmation": "Tem certeza de que deseja pular esta tarefa?",
|
||||
"markComplete": "Marcar como concluída",
|
||||
"markAsDone": "Marcar como concluída",
|
||||
"edit": "Editar",
|
||||
"archive": "Arquivar",
|
||||
"unarchive": "Desarquivar",
|
||||
"viewHistory": "Ver histórico",
|
||||
"history": "Histórico",
|
||||
"startTimer": "Iniciar cronômetro",
|
||||
"start": "Iniciar",
|
||||
"pauseTimer": "Pausar cronômetro",
|
||||
"approve": "Aprovar",
|
||||
"reject": "Rejeitar",
|
||||
"pendingApproval": "Aguardando aprovação",
|
||||
"undo": "Desfazer",
|
||||
"skip": "Pular",
|
||||
"cancel": "Cancelar",
|
||||
"noPriority": "Sem prioridade",
|
||||
"subtasks": "Subtarefas",
|
||||
"noDescription": "Nenhuma descrição disponível",
|
||||
"timer": {
|
||||
"active": "Cronômetro ativo",
|
||||
"paused": "Cronômetro pausado",
|
||||
"reset": "Reiniciar cronômetro",
|
||||
"delete": "Excluir sessão"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
public/locales/pt/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"save": "Salvar",
|
||||
"cancel": "Cancelar",
|
||||
"delete": "Excluir",
|
||||
"edit": "Editar",
|
||||
"close": "Fechar",
|
||||
"confirm": "Confirmar",
|
||||
"loading": "Carregando...",
|
||||
"error": "Erro",
|
||||
"success": "Sucesso",
|
||||
"warning": "Aviso",
|
||||
"refresh": "Atualizar",
|
||||
"copy": "Copiar",
|
||||
"copied": "Copiado!",
|
||||
"settings": "Configurações",
|
||||
"yes": "Sim",
|
||||
"no": "Não",
|
||||
"back": "Voltar",
|
||||
"backToCalendar": "Voltar ao calendário",
|
||||
"logout": "Sair",
|
||||
"version": "Versão",
|
||||
"navigation": {
|
||||
"allTasks": "Todas as tarefas",
|
||||
"archived": "Arquivadas",
|
||||
"things": "Itens",
|
||||
"labels": "Etiquetas",
|
||||
"projects": "Projetos",
|
||||
"filters": "Filtros",
|
||||
"activities": "Atividades",
|
||||
"points": "Pontos",
|
||||
"settings": "Configurações"
|
||||
}
|
||||
}
|
||||
174
public/locales/pt/settings.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"title": "Configurações",
|
||||
"circleSettings": {
|
||||
"title": "Configurações do círculo",
|
||||
"description": "Sua conta é conectada automaticamente a um círculo quando você cria ou entra em um. Convide amigos facilmente compartilhando o código ou link exclusivo do círculo abaixo. Você receberá uma notificação abaixo quando alguém solicitar entrar no seu círculo. Se quiser sair, basta clicar no botão \"Sair do círculo\".",
|
||||
"circleCode": "Código do círculo",
|
||||
"copyCode": "Copiar código",
|
||||
"copyLink": "Copiar link",
|
||||
"codeCopied": "Código do círculo copiado!",
|
||||
"linkCopied": "Link do círculo copiado!",
|
||||
"joinCircle": "Entrar em um círculo",
|
||||
"joinCirclePlaceholder": "Digite o código do círculo",
|
||||
"join": "Entrar",
|
||||
"leave": "Sair do círculo",
|
||||
"leaveConfirmTitle": "Sair do círculo",
|
||||
"leaveConfirmMessage": "Tem certeza de que deseja sair deste círculo?",
|
||||
"circleMembers": "Membros do círculo",
|
||||
"circleMemberRequests": "Solicitações de entrada no círculo",
|
||||
"admin": "Administrador",
|
||||
"member": "Membro",
|
||||
"pending": "Pendente",
|
||||
"accept": "Aceitar",
|
||||
"reject": "Rejeitar",
|
||||
"makeAdmin": "Tornar administrador",
|
||||
"makeMember": "Tornar membro",
|
||||
"remove": "Remover",
|
||||
"webhookURL": "URL do webhook",
|
||||
"webhookDescription": "Digite uma URL de webhook para receber notificações de eventos do círculo",
|
||||
"webhookPlaceholder": "https://sua-url-de-webhook.com"
|
||||
},
|
||||
"accountSettings": {
|
||||
"title": "Configurações da conta",
|
||||
"subscription": "Assinatura",
|
||||
"subscriptionStatus": "Plano atual",
|
||||
"free": "Gratuito",
|
||||
"plus": "Plus",
|
||||
"upgrade": "Fazer upgrade",
|
||||
"cancel": "Cancelar",
|
||||
"changePassword": "Alterar senha",
|
||||
"password": "Senha",
|
||||
"dangerZone": "Zona de perigo",
|
||||
"dangerZoneDescription": "Após excluir sua conta, não há como voltar atrás. Tenha certeza antes de confirmar.",
|
||||
"deleteAccount": "Excluir conta"
|
||||
},
|
||||
"localization": {
|
||||
"title": "Localização",
|
||||
"description": "Personalize idioma, formato de data e preferências regionais da sua conta.",
|
||||
"language": "Idioma",
|
||||
"languageDescription": "Selecione seu idioma preferido",
|
||||
"dateFormat": "Formato de data",
|
||||
"dateFormatDescription": "Escolha como as datas devem ser exibidas no aplicativo",
|
||||
"timeFormat": "Formato de hora",
|
||||
"timeFormatDescription": "Selecione o formato de 12 ou 24 horas",
|
||||
"12hour": "12 horas (AM/PM)",
|
||||
"24hour": "24 horas",
|
||||
"firstDayOfWeek": "Primeiro dia da semana",
|
||||
"firstDayOfWeekDescription": "Selecione com qual dia sua semana começa",
|
||||
"sunday": "Domingo",
|
||||
"monday": "Segunda-feira",
|
||||
"saturday": "Sábado",
|
||||
"formats": {
|
||||
"mdy": "MM/DD/AAAA (EUA)",
|
||||
"dmy": "DD/MM/AAAA (Europa)",
|
||||
"ymd": "AAAA-MM-DD (ISO)",
|
||||
"long": "Formato longo (ex.: 1 de janeiro de 2024)",
|
||||
"short": "Formato curto (ex.: 1 de jan de 2024)"
|
||||
}
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "Personalização do painel lateral",
|
||||
"description": "Personalize o layout e a visibilidade dos cartões no painel lateral. Esta seção está disponível apenas em dispositivos com telas grandes, como tablets e computadores."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Preferências de tema",
|
||||
"description": "Escolha a aparência do site. Selecione um único tema ou sincronize com o sistema para alternar automaticamente entre os temas claro e escuro.",
|
||||
"themeMode": "Modo do tema",
|
||||
"light": "Claro",
|
||||
"dark": "Escuro",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"notifications": {
|
||||
"settingsSaved": "Configurações salvas com sucesso",
|
||||
"settingsSaveFailed": "Falha ao salvar as configurações",
|
||||
"invalidWebhook": "URL de webhook inválida"
|
||||
},
|
||||
"profile": {
|
||||
"title": "Configurações do perfil",
|
||||
"description": "Atualize seu nome de exibição e foto de perfil.",
|
||||
"photoUpdated": "Foto atualizada",
|
||||
"photoUpdatedMessage": "Sua foto de perfil foi atualizada com sucesso!",
|
||||
"uploadFailed": "Falha no envio",
|
||||
"uploadFailedMessage": "Falha ao enviar sua foto. Tente novamente.",
|
||||
"profileUpdated": "Perfil atualizado",
|
||||
"profileUpdatedMessage": "As informações do seu perfil foram salvas com sucesso!",
|
||||
"updateFailed": "Falha na atualização",
|
||||
"updateFailedMessage": "Não foi possível atualizar seu perfil. Verifique sua conexão e tente novamente.",
|
||||
"changePhoto": "Alterar foto",
|
||||
"displayName": "Nome de exibição",
|
||||
"displayNamePlaceholder": "Digite seu nome de exibição",
|
||||
"timezone": "Fuso horário",
|
||||
"timezonePlaceholder": "Selecione seu fuso horário",
|
||||
"save": "Salvar",
|
||||
"cancel": "Cancelar"
|
||||
},
|
||||
"overview": {
|
||||
"title": "Configurações",
|
||||
"subtitle": "Personalize sua experiência e gerencie as preferências da sua conta",
|
||||
"upgrade": {
|
||||
"title": "Faça upgrade para o Plus",
|
||||
"description": "Desbloqueie recursos poderosos para aumentar sua produtividade",
|
||||
"button": "Fazer upgrade agora",
|
||||
"features": {
|
||||
"richText": "Descrições com texto formatado",
|
||||
"notifications": "Notificações de tarefas",
|
||||
"apiIntegrations": "Integrações de API",
|
||||
"advancedAutomation": "Automação avançada"
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"profile": {
|
||||
"title": "Configurações do perfil",
|
||||
"description": "Atualize as informações do seu perfil, foto, nome de exibição e preferências de fuso horário."
|
||||
},
|
||||
"circle": {
|
||||
"title": "Configurações do círculo",
|
||||
"description": "Gerencie seu círculo, convide membros e cuide das solicitações de entrada."
|
||||
},
|
||||
"account": {
|
||||
"title": "Configurações da conta",
|
||||
"description": "Gerencie sua assinatura, altere a senha e as opções de exclusão de conta."
|
||||
},
|
||||
"subaccounts": {
|
||||
"title": "Contas gerenciadas",
|
||||
"description": "Crie e gerencie subcontas para fazer login e concluir tarefas atribuídas."
|
||||
},
|
||||
"notifications": {
|
||||
"title": "Notificações",
|
||||
"description": "Configure notificações push, alertas por e-mail e destinos de notificação para tarefas."
|
||||
},
|
||||
"mfa": {
|
||||
"title": "Autenticação multifator",
|
||||
"description": "Adicione uma camada extra de segurança com MFA usando aplicativos autenticadores."
|
||||
},
|
||||
"apitokens": {
|
||||
"title": "Tokens de API",
|
||||
"description": "Gere e gerencie tokens de acesso para integrações de terceiros e acesso à API."
|
||||
},
|
||||
"storage": {
|
||||
"title": "Configurações de armazenamento",
|
||||
"description": "Faça backup e restauração dos seus dados, gerencie o armazenamento local e as preferências de sincronização."
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "Personalização do painel lateral",
|
||||
"description": "Personalize o layout e a visibilidade dos cartões na interface do painel lateral."
|
||||
},
|
||||
"theme": {
|
||||
"title": "Preferências de tema",
|
||||
"description": "Escolha seu tema preferido e configure o modo claro/escuro."
|
||||
},
|
||||
"localization": {
|
||||
"title": "Localização",
|
||||
"description": "Personalize idioma, formato de data, formato de hora e preferências regionais."
|
||||
},
|
||||
"advanced": {
|
||||
"title": "Configurações avançadas",
|
||||
"description": "Configure webhooks, atualizações em tempo real e outros recursos avançados para mais produtividade."
|
||||
},
|
||||
"developer": {
|
||||
"title": "Configurações de desenvolvedor",
|
||||
"description": "Veja informações técnicas sobre tokens de autenticação, conexões SSE e dados de depuração."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
public/locales/zh-CN/chores.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"title": "家务",
|
||||
"myChores": "我的家务",
|
||||
"allChores": "所有家务",
|
||||
"addChore": "添加家务",
|
||||
"editChore": "编辑家务",
|
||||
"deleteChore": "删除家务",
|
||||
"completeChore": "完成任务",
|
||||
"dueDate": "截止日期",
|
||||
"assignedTo": "分配给",
|
||||
"priority": "优先级",
|
||||
"status": "状态",
|
||||
"description": "描述",
|
||||
"choreView": {
|
||||
"assignment": "分配",
|
||||
"assigned": "已分配",
|
||||
"last": "上次",
|
||||
"schedule": "日程",
|
||||
"due": "截止",
|
||||
"statistics": "统计",
|
||||
"completed": "已完成",
|
||||
"times": "次",
|
||||
"details": "详情",
|
||||
"createdBy": "创建者",
|
||||
"na": "无",
|
||||
"taskCompleted": "任务已完成",
|
||||
"taskCompletedMessage": "您的任务已标记为完成",
|
||||
"taskCompletionUndone": "任务完成已撤销。",
|
||||
"taskSkipUndone": "任务跳过已撤销。",
|
||||
"undoSuccessful": "撤销成功",
|
||||
"undoFailed": "撤销失败",
|
||||
"undoFailedMessage": "无法撤销该操作。请重试。",
|
||||
"resetTimer": "重置计时器",
|
||||
"resetTimerConfirmation": "您确定要重置计时器吗?这将清除自您开始任务以来的所有时间记录。",
|
||||
"clearAllTimeRecords": "清除所有时间记录",
|
||||
"clearAllTimeConfirmation": "这将永久删除此任务的所有计时器,并将其重置为“未开始”。",
|
||||
"descriptionTitle": "描述",
|
||||
"description": "描述:",
|
||||
"previousNote": "上一条备注",
|
||||
"previousNoteLabel": "上一条备注:",
|
||||
"subtasksLabel": "子任务:",
|
||||
"taskActions": "任务操作",
|
||||
"addNote": "添加备注",
|
||||
"additionalNotes": "附加备注:",
|
||||
"notePlaceholder": "添加关于完成的备注...",
|
||||
"setCustomCompletionTime": "设置自定义完成时间",
|
||||
"skipTask": "跳过任务",
|
||||
"skipTaskConfirmation": "您确定要跳过此任务吗?",
|
||||
"markComplete": "标记完成",
|
||||
"markAsDone": "标记为已完成",
|
||||
"edit": "编辑",
|
||||
"archive": "归档",
|
||||
"unarchive": "取消归档",
|
||||
"viewHistory": "查看历史",
|
||||
"history": "历史",
|
||||
"startTimer": "开始计时",
|
||||
"start": "开始",
|
||||
"pauseTimer": "暂停计时",
|
||||
"approve": "批准",
|
||||
"reject": "拒绝",
|
||||
"pendingApproval": "待批准",
|
||||
"undo": "撤销",
|
||||
"skip": "跳过",
|
||||
"cancel": "取消",
|
||||
"noPriority": "无优先级",
|
||||
"subtasks": "子任务",
|
||||
"noDescription": "暂无描述",
|
||||
"timer": {
|
||||
"active": "计时中",
|
||||
"paused": "已暂停",
|
||||
"reset": "重置计时器",
|
||||
"delete": "删除会话"
|
||||
}
|
||||
}
|
||||
}
|
||||
33
public/locales/zh-CN/common.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"close": "关闭",
|
||||
"confirm": "确认",
|
||||
"loading": "加载中...",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"refresh": "刷新",
|
||||
"copy": "复制",
|
||||
"copied": "已复制!",
|
||||
"settings": "设置",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"back": "返回",
|
||||
"backToCalendar": "返回日历",
|
||||
"logout": "退出登录",
|
||||
"version": "版本",
|
||||
"navigation": {
|
||||
"allTasks": "所有任务",
|
||||
"archived": "已归档",
|
||||
"things": "事项",
|
||||
"labels": "标签",
|
||||
"projects": "项目",
|
||||
"filters": "筛选",
|
||||
"activities": "活动",
|
||||
"points": "积分",
|
||||
"settings": "设置"
|
||||
}
|
||||
}
|
||||
174
public/locales/zh-CN/settings.json
Normal file
@@ -0,0 +1,174 @@
|
||||
{
|
||||
"title": "设置",
|
||||
"circleSettings": {
|
||||
"title": "圈子设置",
|
||||
"description": "当您创建或加入圈子时,您的账户会自动连接到该圈子。通过分享下方的唯一圈子代码或链接,即可轻松邀请好友。当有人请求加入您的圈子时,您会在下方收到通知。如果您想离开,只需点击“离开圈子”按钮。",
|
||||
"circleCode": "圈子代码",
|
||||
"copyCode": "复制代码",
|
||||
"copyLink": "复制链接",
|
||||
"codeCopied": "圈子代码已复制!",
|
||||
"linkCopied": "圈子链接已复制!",
|
||||
"joinCircle": "加入圈子",
|
||||
"joinCirclePlaceholder": "输入圈子代码",
|
||||
"join": "加入",
|
||||
"leave": "离开圈子",
|
||||
"leaveConfirmTitle": "离开圈子",
|
||||
"leaveConfirmMessage": "您确定要离开此圈子吗?",
|
||||
"circleMembers": "圈子成员",
|
||||
"circleMemberRequests": "圈子成员申请",
|
||||
"admin": "管理员",
|
||||
"member": "成员",
|
||||
"pending": "待处理",
|
||||
"accept": "接受",
|
||||
"reject": "拒绝",
|
||||
"makeAdmin": "设为管理员",
|
||||
"makeMember": "设为成员",
|
||||
"remove": "移除",
|
||||
"webhookURL": "Webhook URL",
|
||||
"webhookDescription": "输入 Webhook URL 以接收圈子事件通知",
|
||||
"webhookPlaceholder": "https://your-webhook-url.com"
|
||||
},
|
||||
"accountSettings": {
|
||||
"title": "账户设置",
|
||||
"subscription": "订阅",
|
||||
"subscriptionStatus": "当前方案",
|
||||
"free": "免费版",
|
||||
"plus": "Plus 版",
|
||||
"upgrade": "升级",
|
||||
"cancel": "取消",
|
||||
"changePassword": "修改密码",
|
||||
"password": "密码",
|
||||
"dangerZone": "危险区域",
|
||||
"dangerZoneDescription": "一旦删除账户,将无法恢复。请谨慎操作。",
|
||||
"deleteAccount": "删除账户"
|
||||
},
|
||||
"localization": {
|
||||
"title": "本地化",
|
||||
"description": "自定义您账户的语言、日期格式和区域偏好。",
|
||||
"language": "语言",
|
||||
"languageDescription": "选择您的首选语言",
|
||||
"dateFormat": "日期格式",
|
||||
"dateFormatDescription": "选择整个应用中日期的显示方式",
|
||||
"timeFormat": "时间格式",
|
||||
"timeFormatDescription": "选择 12 小时制或 24 小时制",
|
||||
"12hour": "12 小时制 (AM/PM)",
|
||||
"24hour": "24 小时制",
|
||||
"firstDayOfWeek": "每周起始日",
|
||||
"firstDayOfWeekDescription": "选择每周的第一天",
|
||||
"sunday": "星期日",
|
||||
"monday": "星期一",
|
||||
"saturday": "星期六",
|
||||
"formats": {
|
||||
"mdy": "MM/DD/YYYY (美国)",
|
||||
"dmy": "DD/MM/YYYY (欧洲)",
|
||||
"ymd": "YYYY-MM-DD (ISO)",
|
||||
"long": "长格式 (例如:2024年1月1日)",
|
||||
"short": "短格式 (例如:2024年1月1日)"
|
||||
}
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "侧边栏自定义",
|
||||
"description": "自定义侧边栏中卡片的布局和可见性。此部分仅适用于平板电脑和台式机等大屏设备。"
|
||||
},
|
||||
"theme": {
|
||||
"title": "主题偏好",
|
||||
"description": "选择网站的显示外观。您可以选择单一主题,或与系统同步,自动切换日间和夜间主题。",
|
||||
"themeMode": "主题模式",
|
||||
"light": "浅色",
|
||||
"dark": "深色",
|
||||
"system": "跟随系统"
|
||||
},
|
||||
"notifications": {
|
||||
"settingsSaved": "设置保存成功",
|
||||
"settingsSaveFailed": "设置保存失败",
|
||||
"invalidWebhook": "无效的 Webhook URL"
|
||||
},
|
||||
"profile": {
|
||||
"title": "个人资料设置",
|
||||
"description": "更新您的显示名称和个人头像。",
|
||||
"photoUpdated": "头像已更新",
|
||||
"photoUpdatedMessage": "您的个人头像已成功更新!",
|
||||
"uploadFailed": "上传失败",
|
||||
"uploadFailedMessage": "头像上传失败。请重试。",
|
||||
"profileUpdated": "资料已更新",
|
||||
"profileUpdatedMessage": "您的个人资料已成功保存!",
|
||||
"updateFailed": "更新失败",
|
||||
"updateFailedMessage": "无法更新您的个人资料。请检查网络连接后重试。",
|
||||
"changePhoto": "更换头像",
|
||||
"displayName": "显示名称",
|
||||
"displayNamePlaceholder": "输入您的显示名称",
|
||||
"timezone": "时区",
|
||||
"timezonePlaceholder": "选择您的时区",
|
||||
"save": "保存",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"overview": {
|
||||
"title": "设置",
|
||||
"subtitle": "自定义您的体验并管理账户偏好",
|
||||
"upgrade": {
|
||||
"title": "升级到 Plus",
|
||||
"description": "解锁强大功能,提升您的效率",
|
||||
"button": "立即升级",
|
||||
"features": {
|
||||
"richText": "富文本描述",
|
||||
"notifications": "任务通知",
|
||||
"apiIntegrations": "API 集成",
|
||||
"advancedAutomation": "高级自动化"
|
||||
}
|
||||
},
|
||||
"sections": {
|
||||
"profile": {
|
||||
"title": "个人资料设置",
|
||||
"description": "更新您的个人资料信息、头像、显示名称和时区偏好。"
|
||||
},
|
||||
"circle": {
|
||||
"title": "圈子设置",
|
||||
"description": "管理您的圈子、邀请成员并处理加入申请。"
|
||||
},
|
||||
"account": {
|
||||
"title": "账户设置",
|
||||
"description": "管理订阅、修改密码及账户删除选项。"
|
||||
},
|
||||
"subaccounts": {
|
||||
"title": "托管账户",
|
||||
"description": "创建和管理子账户,以便登录并完成分配的任务。"
|
||||
},
|
||||
"notifications": {
|
||||
"title": "通知",
|
||||
"description": "配置推送通知、邮件提醒及任务通知目标。"
|
||||
},
|
||||
"mfa": {
|
||||
"title": "多因素认证",
|
||||
"description": "使用身份验证器应用启用 MFA,增加一层安全保障。"
|
||||
},
|
||||
"apitokens": {
|
||||
"title": "API 令牌",
|
||||
"description": "生成和管理用于第三方集成及 API 访问的访问令牌。"
|
||||
},
|
||||
"storage": {
|
||||
"title": "存储设置",
|
||||
"description": "备份和恢复数据,管理本地存储及同步偏好。"
|
||||
},
|
||||
"sidepanel": {
|
||||
"title": "侧边栏自定义",
|
||||
"description": "自定义侧边栏界面中卡片的布局和可见性。"
|
||||
},
|
||||
"theme": {
|
||||
"title": "主题偏好",
|
||||
"description": "选择您喜欢的主题并配置深色/浅色模式设置。"
|
||||
},
|
||||
"localization": {
|
||||
"title": "本地化",
|
||||
"description": "自定义语言、日期格式、时间格式及区域偏好。"
|
||||
},
|
||||
"advanced": {
|
||||
"title": "高级设置",
|
||||
"description": "配置 Webhook、实时更新及其他高级功能以提升效率。"
|
||||
},
|
||||
"developer": {
|
||||
"title": "开发者设置",
|
||||
"description": "查看有关身份验证令牌、SSE 连接及调试数据的技术信息。"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
scripts/pull-secrets.sh
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
BW_SERVER="${BW_SERVER:-https://bitwarden.com}"
|
||||
|
||||
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
echo "→ Connecting to Vaultwarden at $BW_SERVER"
|
||||
CURRENT_SERVER=$(bw status | jq -r '.serverUrl // empty')
|
||||
|
||||
if [ "$CURRENT_SERVER" != "$BW_SERVER" ]; then
|
||||
bw logout || true
|
||||
bw config server "$BW_SERVER"
|
||||
fi
|
||||
|
||||
if [ -z "${BW_SESSION:-}" ]; then
|
||||
BW_LOGIN_STATUS=$(bw status | jq -r '.status')
|
||||
|
||||
if [ "$BW_LOGIN_STATUS" = "unauthenticated" ]; then
|
||||
if [ -n "${BW_CLIENTID:-}" ] && [ -n "${BW_CLIENTSECRET:-}" ]; then
|
||||
bw login --apikey
|
||||
else
|
||||
bw login
|
||||
fi
|
||||
fi
|
||||
|
||||
export BW_SESSION=$(bw unlock --passwordenv BW_PASSWORD --raw)
|
||||
fi
|
||||
|
||||
bw sync --session "$BW_SESSION" > /dev/null
|
||||
|
||||
# ── Helper ────────────────────────────────────────────────────────────────────
|
||||
get_note() {
|
||||
bw get item "$1" --session "$BW_SESSION" | jq -r '.notes'
|
||||
}
|
||||
|
||||
get_password() {
|
||||
bw get item "$1" --session "$BW_SESSION" | jq -r '.login.password // .notes'
|
||||
}
|
||||
|
||||
# ── Android ───────────────────────────────────────────────────────────────────
|
||||
echo "→ Writing android/app/google-services.json"
|
||||
get_note "Donetick Google Services Android" > "$REPO_ROOT/android/app/google-services.json"
|
||||
|
||||
echo "→ Writing android keystore"
|
||||
get_note "Donetick Android Keystore" | base64 --decode > "$REPO_ROOT/android/app/release/donetick.jks"
|
||||
|
||||
KEYSTORE_PASSWORD=$(get_password "Donetick Keystore Password")
|
||||
|
||||
cat > "$REPO_ROOT/android/keystore.properties" <<EOF
|
||||
storeFile=release/donetick.jks
|
||||
storePassword=$KEYSTORE_PASSWORD
|
||||
keyAlias=key0
|
||||
keyPassword=$KEYSTORE_PASSWORD
|
||||
EOF
|
||||
|
||||
echo "→ Writing android/play-service-account.json"
|
||||
get_note "Donetick Google Play Service Account" > "$REPO_ROOT/android/play-service-account.json"
|
||||
|
||||
# ── iOS ───────────────────────────────────────────────────────────────────────
|
||||
echo "→ Writing ios/App/App/GoogleService-Info.plist"
|
||||
get_note "Donetick Google Services iOS" > "$REPO_ROOT/ios/App/App/GoogleService-Info.plist"
|
||||
|
||||
echo "→ Writing App Store Connect key"
|
||||
get_note "Donetick App Store Connect Key" | base64 --decode > "$REPO_ROOT/ios/AuthKey_84F695CDQ3.p8"
|
||||
|
||||
# ── Env ───────────────────────────────────────────────────────────────────────
|
||||
echo "→ Writing .env.production"
|
||||
get_note "Donetick Env Production" > "$REPO_ROOT/.env.production"
|
||||
|
||||
echo "✓ All secrets pulled successfully"
|
||||
69
scripts/push-secrets.sh
Executable file
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
# One-time script to upload local secrets into Vaultwarden.
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
# BW_SERVER="${BW_SERVER:-https://www.bitwareden.com}"
|
||||
|
||||
# ── Auth ──────────────────────────────────────────────────────────────────────
|
||||
# bw config server "$BW_SERVER"
|
||||
# bw login
|
||||
export BW_SESSION=$(bw unlock --passwordenv BW_PASSWORD --raw)
|
||||
|
||||
# ── Helper ────────────────────────────────────────────────────────────────────
|
||||
upsert_secure_note() {
|
||||
local name="$1"
|
||||
local content="$2"
|
||||
local existing_id
|
||||
existing_id=$(bw list items --session "$BW_SESSION" | jq -r --arg n "$name" '.[] | select(.name == $n) | .id' | head -1)
|
||||
|
||||
if [[ -n "$existing_id" ]]; then
|
||||
bw get item "$existing_id" --session "$BW_SESSION" \
|
||||
| jq --arg c "$content" '.notes = $c' \
|
||||
| bw encode \
|
||||
| bw edit item "$existing_id" --session "$BW_SESSION" > /dev/null
|
||||
echo " ✓ Updated: $name"
|
||||
else
|
||||
bw get template item --session "$BW_SESSION" \
|
||||
| jq --arg n "$name" --arg c "$content" \
|
||||
'.name = $n | .type = 2 | .secureNote = {"type":0} | .notes = $c' \
|
||||
| bw encode \
|
||||
| bw create item --session "$BW_SESSION" > /dev/null
|
||||
echo " ✓ Created: $name"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Upload ────────────────────────────────────────────────────────────────────
|
||||
echo "→ Uploading Android google-services.json"
|
||||
upsert_secure_note \
|
||||
"Donetick Google Services Android" \
|
||||
"$(cat "$REPO_ROOT/android/app/google-services.json")"
|
||||
|
||||
echo "→ Uploading Fastline google-services.json"
|
||||
upsert_secure_note \
|
||||
"Donetick Google Play Service Account" \
|
||||
"$(cat "$REPO_ROOT/donetick-5f910-5688a280a65a--fastline.json")"
|
||||
|
||||
echo "→ Uploading Android keystore (base64)"
|
||||
upsert_secure_note \
|
||||
"Donetick Android Keystore" \
|
||||
"$(base64 < /Users/mohamad-macbook-air/donetick-android-ley)"
|
||||
|
||||
echo "→ Uploading iOS GoogleService-Info.plist"
|
||||
upsert_secure_note \
|
||||
"Donetick Google Services iOS" \
|
||||
"$(cat "$REPO_ROOT/ios/App/App/GoogleService-Info.plist")"
|
||||
|
||||
echo "→ Uploading App Store Connect key (base64)"
|
||||
upsert_secure_note \
|
||||
"Donetick App Store Connect Key" \
|
||||
"$(base64 < /Users/mohamad-macbook-air/Downloads/AuthKey_84F695CDQ3.p8)"
|
||||
|
||||
echo "→ Uploading .env.production"
|
||||
upsert_secure_note \
|
||||
"Donetick Env Production" \
|
||||
"$(cat "$REPO_ROOT/.env.production")"
|
||||
|
||||
echo ""
|
||||
echo "✓ All secrets uploaded. Verify in Vaultwarden, then you can safely delete local copies outside the repo."
|
||||
echo " NOTE: 'Donetick Keystore Password' should already exist — if not, create it manually as a Login item."
|
||||
98
scripts/release.sh
Executable file
@@ -0,0 +1,98 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
PLATFORM="both"
|
||||
BUMP="patch"
|
||||
SKIP_BUMP=false
|
||||
UPLOAD=false
|
||||
ANDROID_TRACK="internal"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [--android|--ios] [--bump patch|minor|major] [--skip-bump] [--upload] [--track TRACK]
|
||||
|
||||
Builds signed release artifacts for Android (.aab) and/or iOS (.ipa).
|
||||
|
||||
--android Build Android only
|
||||
--ios Build iOS only
|
||||
--bump TYPE Version bump type before building (default: patch)
|
||||
--skip-bump Don't bump the version, build with the current one
|
||||
--upload Also upload: Android to Play Store, iOS to TestFlight
|
||||
--track TRACK Play Store track for --upload (default: internal)
|
||||
-h, --help Show this help
|
||||
|
||||
Requires:
|
||||
- scripts/pull-secrets.sh to have been run (keystore, google-services, App Store Connect key, Play service account)
|
||||
- ASC_ISSUER_ID env var set (App Store Connect API key issuer id) for iOS builds/uploads
|
||||
- bundle install (fastlane) run at least once
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--android) PLATFORM="android"; shift ;;
|
||||
--ios) PLATFORM="ios"; shift ;;
|
||||
--bump) BUMP="$2"; shift 2 ;;
|
||||
--skip-bump) SKIP_BUMP=true; shift ;;
|
||||
--upload) UPLOAD=true; shift ;;
|
||||
--track) ANDROID_TRACK="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [ -f "$REPO_ROOT/.env.production" ]; then
|
||||
set -a
|
||||
# shellcheck disable=SC1091
|
||||
source "$REPO_ROOT/.env.production"
|
||||
set +a
|
||||
fi
|
||||
|
||||
if [ "$SKIP_BUMP" = false ]; then
|
||||
echo "→ Bumping version ($BUMP)"
|
||||
node bump-version.js "$BUMP"
|
||||
fi
|
||||
|
||||
echo "→ Building web assets"
|
||||
npm run build
|
||||
|
||||
echo "→ Syncing Capacitor"
|
||||
npx cap sync
|
||||
|
||||
if [ -f Gemfile ] && command -v bundle >/dev/null 2>&1; then
|
||||
RUN="bundle exec fastlane"
|
||||
else
|
||||
RUN="fastlane"
|
||||
fi
|
||||
|
||||
if [ "$PLATFORM" = "android" ] || [ "$PLATFORM" = "both" ]; then
|
||||
echo "→ Building signed Android release (.aab)"
|
||||
$RUN android release
|
||||
|
||||
if [ "$UPLOAD" = true ]; then
|
||||
echo "→ Uploading Android release to Play Store ($ANDROID_TRACK track)"
|
||||
$RUN android upload track:"$ANDROID_TRACK"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$PLATFORM" = "ios" ] || [ "$PLATFORM" = "both" ]; then
|
||||
echo "→ Building signed iOS release (.ipa)"
|
||||
$RUN ios release
|
||||
|
||||
if [ "$UPLOAD" = true ]; then
|
||||
echo "→ Uploading iOS release to TestFlight"
|
||||
$RUN ios upload
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✓ Release build complete"
|
||||
[ -f android/app/build/outputs/bundle/release/app-release.aab ] && echo " Android: android/app/build/outputs/bundle/release/app-release.aab"
|
||||
[ -f build/ios/Donetick.ipa ] && echo " iOS: build/ios/Donetick.ipa"
|
||||
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo " git add . && git commit -m \"Release \$(node -p \"require('./package.json').version\")\""
|
||||
echo " git tag v\$(node -p \"require('./package.json').version\") && git push origin develop --tags"
|
||||
165
src/App.jsx
@@ -1,17 +1,23 @@
|
||||
import NavBar from '@/views/components/NavBar'
|
||||
import { Button, Snackbar, Typography, useColorScheme } from '@mui/joy'
|
||||
import Tracker from '@openreplay/tracker'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Outlet } from 'react-router-dom'
|
||||
import { Button, Typography, useColorScheme } from '@mui/joy'
|
||||
import { useCallback, useEffect } from 'react'
|
||||
import { Outlet, useNavigate } from 'react-router-dom'
|
||||
import { useRegisterSW } from 'virtual:pwa-register/react'
|
||||
import { registerCapacitorListeners } from './CapacitorListener'
|
||||
import { UserContext } from './contexts/UserContext'
|
||||
import { AuthenticationProvider } from './service/AuthenticationService'
|
||||
import { ErrorProvider } from './service/ErrorProvider'
|
||||
import { GetUserProfile } from './utils/Fetcher'
|
||||
import { apiManager, isTokenValid } from './utils/TokenManager'
|
||||
import PageTransition from './components/animations/PageTransition'
|
||||
import { ImpersonateUserProvider } from './contexts/ImpersonateUserContext'
|
||||
import { AuthProvider } from './hooks/useAuth.jsx'
|
||||
|
||||
import useStatusBar from './hooks/useStatusBar'
|
||||
import { useResource } from './queries/ResourceQueries'
|
||||
import './styles/safe-area.css'
|
||||
|
||||
import SSEProvider from './contexts/SSEContext'
|
||||
import { useNotification } from './service/NotificationProvider'
|
||||
|
||||
import { useSyncOnReconnect } from './hooks/useSyncOnReconnect'
|
||||
import NetworkBanner from './views/components/NetworkBanner'
|
||||
|
||||
const add = className => {
|
||||
document.getElementById('root').classList.add(className)
|
||||
}
|
||||
@@ -19,24 +25,23 @@ const add = className => {
|
||||
const remove = className => {
|
||||
document.getElementById('root').classList.remove(className)
|
||||
}
|
||||
|
||||
// TODO: Update the interval to at 60 minutes
|
||||
const intervalMS = 5 * 60 * 1000 // 5 minutes
|
||||
|
||||
function App() {
|
||||
startApiManager()
|
||||
startOpenReplay()
|
||||
const queryClient = new QueryClient()
|
||||
const { mode, systemMode } = useColorScheme()
|
||||
const [userProfile, setUserProfile] = useState(null)
|
||||
const [showUpdateSnackbar, setShowUpdateSnackbar] = useState(true)
|
||||
const AppContent = () => {
|
||||
const { showNotification } = useNotification()
|
||||
useSyncOnReconnect()
|
||||
|
||||
// Initialize status bar with theme-aware configuration
|
||||
useStatusBar()
|
||||
|
||||
const {
|
||||
offlineReady: [offlineReady, setOfflineReady],
|
||||
offlineReady: [offlineReady, setOfflineReady], // eslint-disable-line no-unused-vars
|
||||
needRefresh: [needRefresh, setNeedRefresh],
|
||||
updateServiceWorker,
|
||||
} = useRegisterSW({
|
||||
onRegistered(r) {
|
||||
// eslint-disable-next-line prefer-template
|
||||
console.log('SW Registered: ' + r)
|
||||
r &&
|
||||
setInterval(() => {
|
||||
@@ -47,12 +52,57 @@ function App() {
|
||||
console.log('SW registration error', error)
|
||||
},
|
||||
})
|
||||
const close = () => {
|
||||
setOfflineReady(false)
|
||||
setNeedRefresh(false)
|
||||
}
|
||||
|
||||
const setThemeClass = () => {
|
||||
useEffect(() => {
|
||||
if (needRefresh) {
|
||||
showNotification({
|
||||
type: 'custom',
|
||||
component: (
|
||||
<div>
|
||||
<Typography level='body-md'>
|
||||
A new version is now available. Click on reload button to update.
|
||||
</Typography>
|
||||
<Button
|
||||
color='secondary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
updateServiceWorker(true)
|
||||
setNeedRefresh(false)
|
||||
}}
|
||||
sx={{ ml: 2 }}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
snackbarProps: {
|
||||
autoHideDuration: null, // Persistent until user action
|
||||
},
|
||||
})
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [needRefresh])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ImpersonateUserProvider>
|
||||
<NavBar />
|
||||
<PageTransition>
|
||||
<Outlet />
|
||||
</PageTransition>
|
||||
</ImpersonateUserProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function App() {
|
||||
const resource = useResource() // eslint-disable-line no-unused-vars
|
||||
const { mode, systemMode } = useColorScheme()
|
||||
const navigate = useNavigate()
|
||||
|
||||
// startOpenReplay()
|
||||
|
||||
const setThemeClass = useCallback(() => {
|
||||
const value = JSON.parse(localStorage.getItem('themeMode')) || mode
|
||||
|
||||
if (value === 'system') {
|
||||
@@ -67,68 +117,27 @@ function App() {
|
||||
}
|
||||
|
||||
return remove('dark')
|
||||
}
|
||||
const getUserProfile = () => {
|
||||
GetUserProfile()
|
||||
.then(res => {
|
||||
res.json().then(data => {
|
||||
setUserProfile(data.res)
|
||||
})
|
||||
})
|
||||
.catch(error => {})
|
||||
}
|
||||
}, [mode, systemMode])
|
||||
|
||||
useEffect(() => {
|
||||
setThemeClass()
|
||||
}, [mode, systemMode])
|
||||
}, [setThemeClass])
|
||||
|
||||
useEffect(() => {
|
||||
registerCapacitorListeners()
|
||||
if (isTokenValid()) {
|
||||
if (!userProfile) getUserProfile()
|
||||
}
|
||||
}, [])
|
||||
registerCapacitorListeners(navigate)
|
||||
}, [navigate])
|
||||
|
||||
return (
|
||||
<div className='min-h-screen'>
|
||||
<div>
|
||||
<NetworkBanner />
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthenticationProvider />
|
||||
<ErrorProvider>
|
||||
<UserContext.Provider value={{ userProfile, setUserProfile }}>
|
||||
<NavBar />
|
||||
<Outlet />
|
||||
</UserContext.Provider>
|
||||
</ErrorProvider>
|
||||
{needRefresh && (
|
||||
<Snackbar open={showUpdateSnackbar}>
|
||||
<Typography level='body-md'>
|
||||
A new version is now available.Click on reload button to update.
|
||||
</Typography>
|
||||
<Button
|
||||
color='secondary'
|
||||
size='small'
|
||||
onClick={() => {
|
||||
updateServiceWorker(true)
|
||||
setShowUpdateSnackbar(false)
|
||||
}}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</Snackbar>
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
|
||||
<AuthProvider>
|
||||
<SSEProvider>
|
||||
<AppContent />
|
||||
</SSEProvider>
|
||||
</AuthProvider>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const startOpenReplay = () => {
|
||||
if (!import.meta.env.VITE_OPENREPLAY_PROJECT_KEY) return
|
||||
const tracker = new Tracker({
|
||||
projectKey: import.meta.env.VITE_OPENREPLAY_PROJECT_KEY,
|
||||
})
|
||||
tracker.start()
|
||||
}
|
||||
export default App
|
||||
|
||||
const startApiManager = () => {
|
||||
apiManager.init()
|
||||
}
|
||||
|
||||
@@ -1,78 +1,290 @@
|
||||
import { LocalNotifications } from '@capacitor/local-notifications';
|
||||
import { App as mobileApp } from '@capacitor/app';
|
||||
import { App as mobileApp } from '@capacitor/app'
|
||||
import { Browser } from '@capacitor/browser'
|
||||
import { Capacitor } from '@capacitor/core'
|
||||
import { Device } from '@capacitor/device'
|
||||
import { LocalNotifications } from '@capacitor/local-notifications'
|
||||
import { Preferences } from '@capacitor/preferences'
|
||||
import { PushNotifications } from '@capacitor/push-notifications'
|
||||
import { PutNotificationTarget } from './utils/Fetcher';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import { focusManager } from '@tanstack/react-query'
|
||||
import { RegisterDeviceToken } from './utils/Fetcher'
|
||||
|
||||
// React Router navigate(), injected by <App /> once the router is mounted.
|
||||
// Using client-side navigation (instead of window.location.href) avoids a full
|
||||
// document reload, which on a cold NFC launch re-boots the app from an
|
||||
// unauthenticated state (iOS "server/connection" error) and re-triggers the
|
||||
// sticky getLaunchUrl() (Android "back reloads the same page").
|
||||
let navigateFn = null
|
||||
const setNavigate = fn => {
|
||||
navigateFn = fn
|
||||
}
|
||||
|
||||
// Navigate client-side when the router is available, otherwise fall back to a
|
||||
// hard navigation (should only happen if a deep link arrives before mount).
|
||||
const routerNavigate = (path, { seedHome = false } = {}) => {
|
||||
if (navigateFn) {
|
||||
// On a cold deep-link launch there is no real screen behind the target, so
|
||||
// seed the chore list as the back target before pushing the chore view.
|
||||
if (seedHome && window.location.pathname === '/') {
|
||||
navigateFn('/chores', { replace: true })
|
||||
}
|
||||
navigateFn(path)
|
||||
} else {
|
||||
window.location.href = path
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to a chore from a deep link / notification tap. Shared by NFC,
|
||||
// local and push notifications so they all get consistent cold-start handling.
|
||||
const navigateToChore = (choreId, { autoComplete, isColdStart } = {}) => {
|
||||
if (choreId == null || choreId === '') return
|
||||
const path = `/chores/${choreId}${autoComplete ? '?auto_complete=' + autoComplete : ''}`
|
||||
|
||||
// If we're already on the target page, skip (avoids redundant navigation).
|
||||
if (window.location.pathname + window.location.search === path) return
|
||||
|
||||
console.log('[deeplink] navigating to', path)
|
||||
routerNavigate(path, { seedHome: isColdStart })
|
||||
}
|
||||
|
||||
// NFC chore deep link: donetick://chores/123?auto_complete=true
|
||||
const handleNFCChoreDeepLink = (url, isColdStart) => {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
// donetick://chores/123 → host='chores', pathname='/123'
|
||||
navigateToChore(urlObj.pathname.slice(1), {
|
||||
autoComplete: urlObj.searchParams.get('auto_complete'),
|
||||
isColdStart,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('[NFC] Error handling chore deep link:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUrlOpen = (url, isColdStart = false) => {
|
||||
console.log('[NFC] handleUrlOpen:', url)
|
||||
if (url.startsWith('donetick://chores/')) {
|
||||
handleNFCChoreDeepLink(url, isColdStart)
|
||||
} else if (url.startsWith('donetick://auth/')) {
|
||||
handleOAuthDeepLink(url)
|
||||
}
|
||||
}
|
||||
|
||||
// OAuth callback handler for deep links
|
||||
const handleOAuthDeepLink = async url => {
|
||||
console.log('OAuth deep link received:', url)
|
||||
|
||||
try {
|
||||
// Parse the URL to extract code and state
|
||||
const urlObj = new URL(url)
|
||||
const code = urlObj.searchParams.get('code')
|
||||
const state = urlObj.searchParams.get('state')
|
||||
|
||||
if (code && state) {
|
||||
// If we're already on the OAuth handler page with the same code, skip
|
||||
// re-navigating (a stale getLaunchUrl() replay would otherwise re-fire it).
|
||||
const currentCode = new URLSearchParams(window.location.search).get(
|
||||
'code',
|
||||
)
|
||||
if (window.location.pathname === '/auth/oauth2' && currentCode === code) {
|
||||
return
|
||||
}
|
||||
|
||||
// Store the OAuth params for the app to pick up
|
||||
await Preferences.set({
|
||||
key: 'oauth_callback',
|
||||
value: JSON.stringify({ code, state, timestamp: Date.now() }),
|
||||
})
|
||||
|
||||
// Close the browser if it's still open
|
||||
try {
|
||||
await Browser.close()
|
||||
} catch (e) {
|
||||
// Browser might already be closed
|
||||
}
|
||||
|
||||
// Navigate to the OAuth handler page
|
||||
routerNavigate(
|
||||
`/auth/oauth2?code=${encodeURIComponent(code)}&state=${encodeURIComponent(state)}`,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error handling OAuth deep link:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const localNotificationListenerRegistration = () => {
|
||||
LocalNotifications.addListener('localNotificationReceived', (notification) => {
|
||||
console.log('Notification received', notification);
|
||||
});
|
||||
LocalNotifications.addListener('localNotificationActionPerformed', (event) => {
|
||||
|
||||
console.log('Notification action performed', event);
|
||||
if (event.actionId === 'tap') {
|
||||
console.log('Notification opened, navigate to chore', event.notification.extra.choreId);
|
||||
window.location.href = `/chores/${event.notification.extra.choreId}`
|
||||
}
|
||||
});
|
||||
}
|
||||
const pushNotificationListenerRegistration = () => {
|
||||
PushNotifications.register();
|
||||
PushNotifications.addListener('registration', (token) => {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
const type = Capacitor.getPlatform() === 'android' ? 1 : 2; // 1 for android, 2 for ios
|
||||
PutNotificationTarget(type, token.value).then((response) => {
|
||||
console.log('Notification target updated', response);
|
||||
}
|
||||
).catch((error) => {
|
||||
console.error('Error updating notification target', error);
|
||||
}
|
||||
);
|
||||
LocalNotifications.addListener('localNotificationReceived', notification => {
|
||||
console.log('Notification received', notification)
|
||||
})
|
||||
LocalNotifications.addListener('localNotificationActionPerformed', event => {
|
||||
console.log('Notification action performed', event)
|
||||
if (event.actionId === 'tap') {
|
||||
console.log(
|
||||
'Notification opened, navigate to chore',
|
||||
event.notification.extra.choreId,
|
||||
)
|
||||
navigateToChore(event.notification.extra.choreId, { isColdStart: true })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TODO save the token in preferences and only send it if it has changed:
|
||||
console.log('Push registration success, token: ' + token.value);
|
||||
}
|
||||
const registerTokenIfNeeded = async (token, deviceInfo, deviceId, platform) => {
|
||||
try {
|
||||
const result = await RegisterDeviceToken(
|
||||
token.value,
|
||||
deviceId.identifier,
|
||||
platform,
|
||||
deviceInfo.appVersion,
|
||||
deviceInfo.model,
|
||||
)
|
||||
|
||||
if (!result) return
|
||||
|
||||
if (result.ok) {
|
||||
await Preferences.set({
|
||||
key: 'deviceRegistration',
|
||||
value: JSON.stringify({
|
||||
token: token.value,
|
||||
deviceId: deviceId.identifier,
|
||||
platform,
|
||||
appVersion: deviceInfo.appVersion,
|
||||
registeredAt: Date.now(),
|
||||
}),
|
||||
})
|
||||
console.log('Device token registered successfully')
|
||||
window.dispatchEvent(new CustomEvent('deviceTokenRegistered'))
|
||||
} else {
|
||||
console.error('Device registration failed:', result.status)
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('deviceTokenRegistrationFailed', {
|
||||
detail: {
|
||||
status: result.status,
|
||||
error: await result.text().catch(() => 'Unknown error'),
|
||||
},
|
||||
}),
|
||||
)
|
||||
}
|
||||
);
|
||||
PushNotifications.addListener('registrationError', (error) => {
|
||||
console.error('Error on registration: ' + JSON.stringify(error));
|
||||
} catch (error) {
|
||||
console.error('Error registering device token:', error)
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('deviceTokenRegistrationFailed', {
|
||||
detail: { status: 0, error: error?.message ?? 'Unknown error' },
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const pushNotificationListenerRegistration = async () => {
|
||||
// Check and request permissions for Android 13+
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
let permStatus = await PushNotifications.checkPermissions()
|
||||
|
||||
if (permStatus.receive === 'prompt') {
|
||||
permStatus = await PushNotifications.requestPermissions()
|
||||
}
|
||||
);
|
||||
PushNotifications.addListener('pushNotificationActionPerformed', fcmEvent => {
|
||||
|
||||
if(fcmEvent.actionId === 'tap') {
|
||||
if (fcmEvent.notification.data.type === 'chore_due') {
|
||||
window.location.href = `/chores/${fcmEvent.notification.data.choreId}`
|
||||
}
|
||||
else {
|
||||
window.location.href = `/my/chores`
|
||||
}
|
||||
|
||||
if (permStatus.receive !== 'granted') {
|
||||
console.warn('Push notification permission not granted')
|
||||
return
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
await PushNotifications.register()
|
||||
|
||||
PushNotifications.addListener('registration', async token => {
|
||||
if (Capacitor.isNativePlatform()) {
|
||||
try {
|
||||
const deviceInfo = await Device.getInfo()
|
||||
const deviceId = await Device.getId()
|
||||
|
||||
|
||||
const registerCapacitorListeners = () => {
|
||||
if(!Capacitor.isNativePlatform()) {
|
||||
console.log('Not a native platform, skipping registration of native listeners');
|
||||
return
|
||||
const platform =
|
||||
Capacitor.getPlatform() === 'android' ? 'android' : 'ios'
|
||||
|
||||
await registerTokenIfNeeded(token, deviceInfo, deviceId, platform)
|
||||
} catch (error) {
|
||||
console.error('Error registering device token', error)
|
||||
}
|
||||
}
|
||||
localNotificationListenerRegistration();
|
||||
pushNotificationListenerRegistration();
|
||||
mobileApp.addListener('backButton', ({ canGoBack }) => {
|
||||
if (canGoBack) {
|
||||
window.history.back();
|
||||
} else {
|
||||
mobileApp.exitApp();
|
||||
}
|
||||
});
|
||||
|
||||
})
|
||||
|
||||
PushNotifications.addListener('registrationError', error => {
|
||||
console.error('Error on registration: ' + JSON.stringify(error))
|
||||
})
|
||||
|
||||
PushNotifications.addListener('pushNotificationReceived', notification => {
|
||||
console.log('Push notification received: ', notification)
|
||||
})
|
||||
|
||||
PushNotifications.addListener('pushNotificationActionPerformed', fcmEvent => {
|
||||
if (fcmEvent.actionId === 'tap') {
|
||||
if (
|
||||
fcmEvent.notification.data.type === 'chore_due' ||
|
||||
fcmEvent.notification.data.type === 'nudge'
|
||||
) {
|
||||
navigateToChore(fcmEvent.notification.data.choreId, {
|
||||
isColdStart: true,
|
||||
})
|
||||
} else {
|
||||
routerNavigate('/chores')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let launchUrlHandled = false
|
||||
let listenersRegistered = false
|
||||
|
||||
const registerCapacitorListeners = navigate => {
|
||||
if (navigate) setNavigate(navigate)
|
||||
|
||||
if (!Capacitor.isNativePlatform()) {
|
||||
console.log(
|
||||
'Not a native platform, skipping registration of native listeners',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
export { registerCapacitorListeners }
|
||||
// registerCapacitorListeners runs from a React effect and may fire more than
|
||||
// once; only wire up the native listeners a single time.
|
||||
if (listenersRegistered) return
|
||||
listenersRegistered = true
|
||||
|
||||
localNotificationListenerRegistration()
|
||||
|
||||
// Cold-start: app was launched by tapping an NFC tag (or other deep link).
|
||||
// getLaunchUrl() is sticky and keeps returning the launch URL across reloads,
|
||||
// so consume it exactly once to avoid re-triggering navigation on every boot.
|
||||
mobileApp.getLaunchUrl().then(result => {
|
||||
if (result?.url && !launchUrlHandled) {
|
||||
launchUrlHandled = true
|
||||
console.log('[NFC] getLaunchUrl:', result.url)
|
||||
handleUrlOpen(result.url, true /* isColdStart */)
|
||||
}
|
||||
})
|
||||
|
||||
// Foreground / singleTask resume: app was already running when the tag was tapped
|
||||
mobileApp.addListener('appUrlOpen', event => {
|
||||
console.log('[NFC] appUrlOpen:', event.url)
|
||||
handleUrlOpen(event.url, false /* isColdStart */)
|
||||
})
|
||||
|
||||
mobileApp.addListener('appStateChange', ({ isActive }) => {
|
||||
focusManager.setFocused(isActive)
|
||||
})
|
||||
|
||||
mobileApp.addListener('backButton', ({ canGoBack }) => {
|
||||
if (canGoBack) {
|
||||
window.history.back()
|
||||
} else if (window.location.pathname !== '/') {
|
||||
// No history (e.g. app launched directly to a chore via NFC) — go home
|
||||
window.location.href = '/'
|
||||
} else {
|
||||
mobileApp.exitApp()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
registerCapacitorListeners,
|
||||
pushNotificationListenerRegistration as registerPushNotifications,
|
||||
}
|
||||
|
||||
BIN
src/assets/ipad_dashbard_calendar.png
Normal file
|
After Width: | Height: | Size: 419 KiB |
BIN
src/assets/screenshot-my-chore-dark.png
Normal file
|
After Width: | Height: | Size: 145 KiB |
|
Before Width: | Height: | Size: 406 KiB After Width: | Height: | Size: 147 KiB |
773
src/components/NotificationTemplate.jsx
Normal file
@@ -0,0 +1,773 @@
|
||||
import { Save } from '@mui/icons-material'
|
||||
import AddIcon from '@mui/icons-material/Add'
|
||||
import DeleteIcon from '@mui/icons-material/Delete'
|
||||
import InfoIcon from '@mui/icons-material/Info'
|
||||
import NotificationsIcon from '@mui/icons-material/Notifications'
|
||||
import Alert from '@mui/joy/Alert'
|
||||
import Badge from '@mui/joy/Badge'
|
||||
import Box from '@mui/joy/Box'
|
||||
import Button from '@mui/joy/Button'
|
||||
import IconButton from '@mui/joy/IconButton'
|
||||
import Input from '@mui/joy/Input'
|
||||
import Option from '@mui/joy/Option'
|
||||
import Select from '@mui/joy/Select'
|
||||
import Typography from '@mui/joy/Typography'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { NOTIFICATION_TYPE, TASK_COLOR } from '../utils/Colors'
|
||||
import { TIME_UNITS } from '../utils/DurationUtils'
|
||||
|
||||
const timeUnits = TIME_UNITS
|
||||
|
||||
const timingOptions = [
|
||||
{ label: 'Before', value: 'before' },
|
||||
{ label: 'Due', value: 'ondue' },
|
||||
{ label: 'After', value: 'after' },
|
||||
]
|
||||
|
||||
function getRelativeLabel(notification) {
|
||||
const { value, unit } = notification
|
||||
const numericValue = Number(value)
|
||||
if (numericValue === 0) {
|
||||
return 'On due date'
|
||||
}
|
||||
const unitName = unit === 'm' ? 'minutes' : unit === 'h' ? 'hours' : 'days'
|
||||
const absValue = Math.abs(numericValue)
|
||||
return `${absValue} ${unitName} ${numericValue < 0 ? 'before' : 'after'} due`
|
||||
}
|
||||
|
||||
// Helper functions to convert between internal value and UI representation
|
||||
function getUIRepresentation(notification) {
|
||||
const numericValue = Number(notification.value)
|
||||
if (numericValue === 0) {
|
||||
return { timing: 'ondue', displayValue: 0, unit: notification.unit }
|
||||
} else if (numericValue < 0) {
|
||||
return {
|
||||
timing: 'before',
|
||||
displayValue: Math.abs(numericValue),
|
||||
unit: notification.unit,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
timing: 'after',
|
||||
displayValue: numericValue,
|
||||
unit: notification.unit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getInternalValue(timing, displayValue) {
|
||||
if (timing === 'ondue') return 0
|
||||
if (timing === 'before') return -Math.abs(displayValue)
|
||||
return Math.abs(displayValue) // 'after'
|
||||
}
|
||||
|
||||
const NotificationTemplate = ({
|
||||
maxNotifications = 5,
|
||||
onChange,
|
||||
value,
|
||||
showTimeline = true,
|
||||
}) => {
|
||||
const [notifications, setNotifications] = useState(
|
||||
value?.templates ||
|
||||
JSON.parse(localStorage.getItem('defaultNotificationTemplate')) ||
|
||||
[],
|
||||
)
|
||||
|
||||
const notificationsRef = useRef(notifications)
|
||||
const [draftValues, setDraftValues] = useState({})
|
||||
|
||||
const [error, setError] = useState(null)
|
||||
const [showSaveDefault, setShowSaveDefault] = useState(false)
|
||||
// Create a map of notification indices for timeline display
|
||||
const [notificationIndexMap, setNotificationIndexMap] = useState({})
|
||||
|
||||
const updateNotificationIndices = useCallback(() => {
|
||||
// Convert notifications to minutes for proper chronological sorting
|
||||
const convertToMinutes = (value, unit) => {
|
||||
const numericValue = Number(value)
|
||||
if (numericValue === 0) return 0
|
||||
let minutes = Math.abs(numericValue)
|
||||
if (unit === 'h') minutes *= 60
|
||||
if (unit === 'd') minutes *= 24 * 60
|
||||
return numericValue < 0 ? -minutes : minutes
|
||||
}
|
||||
|
||||
// Sort notifications for consistent ordering by actual time duration
|
||||
const sorted = [...notifications].sort((a, b) => {
|
||||
const aMinutes = convertToMinutes(a.value, a.unit)
|
||||
const bMinutes = convertToMinutes(b.value, b.unit)
|
||||
return aMinutes - bMinutes
|
||||
})
|
||||
|
||||
const indexMap = {}
|
||||
// Map original array indices to their chronological position numbers
|
||||
notifications.forEach((originalNotification, originalIdx) => {
|
||||
const chronologicalPosition = sorted.findIndex(
|
||||
sortedNotification =>
|
||||
Number(sortedNotification.value) ===
|
||||
Number(originalNotification.value) &&
|
||||
sortedNotification.unit === originalNotification.unit,
|
||||
)
|
||||
indexMap[originalIdx] = chronologicalPosition + 1
|
||||
})
|
||||
|
||||
setNotificationIndexMap(indexMap)
|
||||
}, [notifications])
|
||||
|
||||
// Sort notifications and update the index mapping
|
||||
useEffect(() => {
|
||||
updateNotificationIndices()
|
||||
}, [updateNotificationIndices])
|
||||
|
||||
// Notify parent component of changes including the template name
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
onChange({ notifications })
|
||||
}
|
||||
}, [notifications, onChange])
|
||||
|
||||
// Validates if a notification configuration already exists
|
||||
const isDuplicate = (notification, currentIdx = -1, list = notifications) => {
|
||||
return list.some((n, idx) => {
|
||||
if (idx === currentIdx) return false
|
||||
|
||||
return (
|
||||
Number(n.value) === Number(notification.value) &&
|
||||
n.unit === notification.unit
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const getSmartSuggestion = type => {
|
||||
let suggestions = []
|
||||
if (type === 'reminder' || type === 'before') {
|
||||
suggestions = [
|
||||
{ value: -1, unit: 'd' },
|
||||
{ value: -3, unit: 'h' },
|
||||
{ value: -30, unit: 'm' },
|
||||
]
|
||||
} else if (type === 'followup' || type === 'after') {
|
||||
suggestions = [
|
||||
{ value: 1, unit: 'd' },
|
||||
{ value: 3, unit: 'd' },
|
||||
{ value: 7, unit: 'd' },
|
||||
]
|
||||
}
|
||||
return suggestions.find(
|
||||
suggestion => !isDuplicate(suggestion, -1, notificationsRef.current),
|
||||
)
|
||||
}
|
||||
|
||||
const handleChange = (idx, field, value) => {
|
||||
const currentNotification = notifications[idx]
|
||||
const uiRep = getUIRepresentation(currentNotification)
|
||||
|
||||
let updatedUIRep = { ...uiRep }
|
||||
let updatedNotification = { ...currentNotification }
|
||||
|
||||
// Update the UI representation based on the field being changed
|
||||
if (field === 'timing') {
|
||||
updatedUIRep.timing = value
|
||||
// Reset display value when switching to "On Due"
|
||||
if (value === 'ondue') {
|
||||
updatedUIRep.displayValue = 0
|
||||
} else if (Number(currentNotification.value) === 0) {
|
||||
const suggestion = getSmartSuggestion(value)
|
||||
if (suggestion) {
|
||||
updatedUIRep.displayValue = Math.abs(suggestion.value)
|
||||
updatedUIRep.unit = suggestion.unit
|
||||
} else {
|
||||
updatedUIRep.displayValue = 1
|
||||
updatedUIRep.unit = 'h'
|
||||
}
|
||||
}
|
||||
} else if (field === 'displayValue') {
|
||||
updatedUIRep.displayValue = Math.max(0, Number(value))
|
||||
} else if (field === 'unit') {
|
||||
updatedUIRep.unit = value
|
||||
updatedNotification.unit = value
|
||||
}
|
||||
|
||||
// Convert back to internal representation
|
||||
const newInternalValue = getInternalValue(
|
||||
updatedUIRep.timing,
|
||||
updatedUIRep.displayValue,
|
||||
)
|
||||
updatedNotification = {
|
||||
...updatedNotification,
|
||||
value: newInternalValue,
|
||||
unit: updatedUIRep.unit,
|
||||
}
|
||||
|
||||
const updated = notifications.map((n, i) =>
|
||||
i === idx ? updatedNotification : n,
|
||||
)
|
||||
setNotifications(updated)
|
||||
notificationsRef.current = updated
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const handleBlur = idx => {
|
||||
const currentList = notificationsRef.current
|
||||
const currentNotification = currentList[idx]
|
||||
|
||||
if (!currentNotification) return
|
||||
|
||||
if (isDuplicate(currentNotification, idx, currentList)) {
|
||||
setError(
|
||||
'This notification setting already exists. Please use a different timing.',
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const addSmartNotification = type => {
|
||||
if (notifications.length >= maxNotifications) return
|
||||
setShowSaveDefault(true)
|
||||
let newNotification
|
||||
|
||||
if (type === 'due') {
|
||||
if (notificationsRef.current.some(n => Number(n.value) === 0)) {
|
||||
setError('Only one "Due Alert" notification is allowed.')
|
||||
return
|
||||
}
|
||||
newNotification = { value: 0, unit: 'm' }
|
||||
} else {
|
||||
newNotification = getSmartSuggestion(type)
|
||||
if (!newNotification) {
|
||||
setError(`All common ${type} times are already configured.`)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new notification to the end (don't sort, keep form order)
|
||||
const updatedNotifications = [...notifications, newNotification]
|
||||
|
||||
setNotifications(updatedNotifications)
|
||||
notificationsRef.current = updatedNotifications
|
||||
setError(null)
|
||||
}
|
||||
|
||||
const removeNotification = idx => {
|
||||
const updated = notifications.filter((_, i) => i !== idx)
|
||||
setNotifications(updated)
|
||||
notificationsRef.current = updated
|
||||
|
||||
setDraftValues(prev => {
|
||||
const next = { ...prev }
|
||||
delete next[idx]
|
||||
return next
|
||||
})
|
||||
|
||||
onChange && onChange(updated)
|
||||
setShowSaveDefault(true)
|
||||
}
|
||||
|
||||
const renderTimeline = () => {
|
||||
// Convert notifications to minutes for proper chronological sorting
|
||||
const convertToMinutes = (value, unit) => {
|
||||
const numericValue = Number(value)
|
||||
if (numericValue === 0) return 0
|
||||
let minutes = Math.abs(numericValue)
|
||||
if (unit === 'h') minutes *= 60
|
||||
if (unit === 'd') minutes *= 24 * 60
|
||||
return numericValue < 0 ? -minutes : minutes
|
||||
}
|
||||
|
||||
// Sort notifications chronologically by actual time (in minutes)
|
||||
const sorted = [...notifications].sort((a, b) => {
|
||||
const aMinutes = convertToMinutes(a.value, a.unit)
|
||||
const bMinutes = convertToMinutes(b.value, b.unit)
|
||||
return aMinutes - bMinutes
|
||||
})
|
||||
|
||||
// Get min and max notification times in minutes for dynamic scaling
|
||||
const minutesValues = sorted.map(n => convertToMinutes(n.value, n.unit))
|
||||
const minBefore = Math.min(0, ...minutesValues) // Default to 0 if no "before" notifications
|
||||
const maxAfter = Math.max(0, ...minutesValues) // Default to 0 if no "after" notifications
|
||||
|
||||
const getPositionPercent = (value, unit) => {
|
||||
const minutes = convertToMinutes(value, unit)
|
||||
|
||||
// Due date is always at center (50%)
|
||||
if (minutes === 0) return 50
|
||||
|
||||
// For notifications before due date (negative values)
|
||||
if (minutes < 0) {
|
||||
if (minBefore === 0) return 30 // Default position if no before notifications
|
||||
// Scale between 10% (furthest left) and 45% (closest to due)
|
||||
return 45 - (Math.abs(minutes) / Math.abs(minBefore)) * 35
|
||||
}
|
||||
|
||||
// For notifications after due date (positive values)
|
||||
if (maxAfter === 0) return 70 // Default position if no after notifications
|
||||
// Scale between 55% (closest to due) and 90% (furthest right)
|
||||
return 55 + (minutes / maxAfter) * 35
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ mt: 3, mb: 2 }}>
|
||||
<Typography level={'body-md'} sx={{ mb: 1 }}>
|
||||
Notification Timeline
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
height: 90,
|
||||
bgcolor: 'background.surface',
|
||||
borderRadius: 'md',
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.outlinedBorder',
|
||||
p: 2,
|
||||
transition: 'height 0.3s ease',
|
||||
// '&:hover': {
|
||||
// height: 130,
|
||||
// },
|
||||
}}
|
||||
>
|
||||
{/* Timeline line */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
height: 3,
|
||||
bgcolor: 'neutral.outlinedBorder',
|
||||
mt: 2,
|
||||
}}
|
||||
>
|
||||
{/* Due date marker */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: '50%',
|
||||
height: 16,
|
||||
width: 3,
|
||||
bgcolor: 'warning.500',
|
||||
top: -8,
|
||||
transform: 'translateX(-50%)',
|
||||
borderRadius: 'sm',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level={'body-xs'}
|
||||
sx={{
|
||||
mt: 4,
|
||||
fontWeight: 'md',
|
||||
color: 'warning.700',
|
||||
fontSize: '0.6rem',
|
||||
}}
|
||||
>
|
||||
Due Date
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Notification markers */}
|
||||
{sorted.map((n, i) => {
|
||||
// Calculate position based on actual time duration
|
||||
const percent = getPositionPercent(n.value, n.unit)
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={i}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
left: `${percent}%`,
|
||||
transform: 'translateX(-50%)',
|
||||
color:
|
||||
convertToMinutes(n.value, n.unit) < 0
|
||||
? TASK_COLOR.SCHEDULED
|
||||
: convertToMinutes(n.value, n.unit) === 0
|
||||
? TASK_COLOR.TODAY
|
||||
: TASK_COLOR.LATE,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
transition: 'all 0.3s ease',
|
||||
cursor: 'pointer',
|
||||
opacity: 0.85,
|
||||
'&:hover': {
|
||||
opacity: 1,
|
||||
transform: 'translateX(-50%) scale(1.1)',
|
||||
zIndex: 10,
|
||||
},
|
||||
}}
|
||||
title={getRelativeLabel(n)}
|
||||
>
|
||||
<Badge
|
||||
badgeContent={
|
||||
notificationIndexMap[
|
||||
notifications.findIndex(
|
||||
original =>
|
||||
Number(original.value) === Number(n.value) &&
|
||||
original.unit === n.unit,
|
||||
)
|
||||
] || i + 1
|
||||
}
|
||||
size={'sm'}
|
||||
variant={'solid'}
|
||||
sx={{
|
||||
'--Badge-paddingX': '4px',
|
||||
'--Badge-minHeight': '16px',
|
||||
'--Badge-fontSize': '0.65rem',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
'& .MuiBadge-badge': {
|
||||
background:
|
||||
convertToMinutes(n.value, n.unit) < 0
|
||||
? NOTIFICATION_TYPE.PREDUE
|
||||
: convertToMinutes(n.value, n.unit) === 0
|
||||
? NOTIFICATION_TYPE.DUE_DATE
|
||||
: NOTIFICATION_TYPE.POSTDUE,
|
||||
color: 'white',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<NotificationsIcon
|
||||
fontSize={'small'}
|
||||
sx={{
|
||||
height: 18,
|
||||
width: 18,
|
||||
}}
|
||||
/>
|
||||
</Badge>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Box>
|
||||
{error && (
|
||||
<Alert
|
||||
variant='soft'
|
||||
color='danger'
|
||||
sx={{ mb: 2 }}
|
||||
startDecorator={<InfoIcon />}
|
||||
>
|
||||
{error}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{notifications
|
||||
.map((n, idx) => ({ notification: n, originalIndex: idx }))
|
||||
.sort((a, b) => {
|
||||
const aBadgeNumber = notificationIndexMap[a.originalIndex] || 0
|
||||
const bBadgeNumber = notificationIndexMap[b.originalIndex] || 0
|
||||
return aBadgeNumber - bBadgeNumber
|
||||
})
|
||||
.map(({ notification: n, originalIndex: idx }) => {
|
||||
// Get ordered badge number from timeline sorting
|
||||
const badgeNumber = notificationIndexMap[idx]
|
||||
const uiRep = getUIRepresentation(n)
|
||||
|
||||
// Check if an "On Due" notification exists anywhere else in the list
|
||||
const hasOnDueElsewhere = notificationsRef.current.some(
|
||||
(notif, i) => i !== idx && Number(notif.value) === 0,
|
||||
)
|
||||
|
||||
const getNotificationColors = value => {
|
||||
if (Number(value) < 0) {
|
||||
return {
|
||||
bgColor: NOTIFICATION_TYPE.PREDUE,
|
||||
lightBg: `${NOTIFICATION_TYPE.PREDUE}20`,
|
||||
borderColor: `${NOTIFICATION_TYPE.PREDUE}40`,
|
||||
textColor: NOTIFICATION_TYPE.PREDUE,
|
||||
}
|
||||
} else if (Number(value) === 0) {
|
||||
return {
|
||||
bgColor: NOTIFICATION_TYPE.DUE_DATE,
|
||||
lightBg: `${NOTIFICATION_TYPE.DUE_DATE}20`,
|
||||
borderColor: `${NOTIFICATION_TYPE.DUE_DATE}40`,
|
||||
textColor: NOTIFICATION_TYPE.DUE_DATE,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
bgColor: NOTIFICATION_TYPE.POSTDUE,
|
||||
lightBg: `${NOTIFICATION_TYPE.POSTDUE}20`,
|
||||
borderColor: `${NOTIFICATION_TYPE.POSTDUE}40`,
|
||||
textColor: NOTIFICATION_TYPE.POSTDUE,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const colors = getNotificationColors(n.value)
|
||||
|
||||
return (
|
||||
<Box key={idx} sx={{ position: 'relative' }}>
|
||||
<Badge
|
||||
badgeContent={badgeNumber}
|
||||
size={'sm'}
|
||||
sx={{
|
||||
'--Badge-minHeight': '16px',
|
||||
'--Badge-fontSize': '0.7rem',
|
||||
'--Badge-paddingX': '5px',
|
||||
top: 10,
|
||||
left: 10,
|
||||
'& .MuiBadge-badge': {
|
||||
background: colors.bgColor,
|
||||
color: 'white',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
mb: 1.5,
|
||||
p: 2,
|
||||
borderRadius: 8,
|
||||
border: '1px solid',
|
||||
borderColor: 'neutral.outlinedBorder',
|
||||
background: 'background.surface',
|
||||
transition: 'all 0.2s ease',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1.5,
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
minWidth: 0,
|
||||
flex: '1',
|
||||
display: { xs: 'none', md: 'flex' }, // Show only on md and up
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
color: 'text.primary',
|
||||
fontSize: 14,
|
||||
}}
|
||||
>
|
||||
{getRelativeLabel(n)}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
value={uiRep.timing}
|
||||
onChange={(_, value) => handleChange(idx, 'timing', value)}
|
||||
onBlur={() => handleBlur(idx)}
|
||||
sx={{ minWidth: 80 }}
|
||||
size={'sm'}
|
||||
>
|
||||
{timingOptions.map(opt => (
|
||||
<Option
|
||||
key={opt.value}
|
||||
value={opt.value}
|
||||
disabled={opt.value === 'ondue' && hasOnDueElsewhere}
|
||||
>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<Input
|
||||
type={'number'}
|
||||
min={0}
|
||||
value={
|
||||
draftValues[idx] !== undefined
|
||||
? draftValues[idx]
|
||||
: uiRep.displayValue
|
||||
}
|
||||
disabled={uiRep.timing === 'ondue'}
|
||||
onChange={e => {
|
||||
const val = e.target.value
|
||||
if (val.includes('-')) return
|
||||
|
||||
setDraftValues(prev => ({ ...prev, [idx]: val }))
|
||||
}}
|
||||
onKeyDown={e => {
|
||||
if (['-', 'e', '+', '.'].includes(e.key)) {
|
||||
e.preventDefault()
|
||||
}
|
||||
}}
|
||||
onBlur={e => {
|
||||
let val = e.target.value
|
||||
|
||||
const numericVal = Number(val)
|
||||
val =
|
||||
numericVal <= 0
|
||||
? hasOnDueElsewhere
|
||||
? 1
|
||||
: 0
|
||||
: numericVal
|
||||
|
||||
handleChange(idx, 'displayValue', val)
|
||||
setDraftValues(prev => {
|
||||
const next = { ...prev }
|
||||
delete next[idx]
|
||||
return next
|
||||
})
|
||||
handleBlur(idx)
|
||||
}}
|
||||
sx={{
|
||||
width: 60,
|
||||
opacity: uiRep.timing === 'ondue' ? 0.6 : 1,
|
||||
}}
|
||||
size={'sm'}
|
||||
placeholder='0'
|
||||
/>
|
||||
<Select
|
||||
value={n.unit}
|
||||
disabled={uiRep.timing === 'ondue'}
|
||||
onChange={(_, value) => handleChange(idx, 'unit', value)}
|
||||
onBlur={() => handleBlur(idx)}
|
||||
sx={{
|
||||
minWidth: 70,
|
||||
opacity: uiRep.timing === 'ondue' ? 0.6 : 1,
|
||||
}}
|
||||
size={'sm'}
|
||||
>
|
||||
{timeUnits.map(opt => (
|
||||
<Option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</Option>
|
||||
))}
|
||||
</Select>
|
||||
<IconButton
|
||||
onClick={() => removeNotification(idx)}
|
||||
disabled={notifications.length === 1}
|
||||
color={'danger'}
|
||||
size={'sm'}
|
||||
variant={'soft'}
|
||||
sx={{
|
||||
transition: 'all 0.2s ease',
|
||||
'&:hover': {
|
||||
transform: 'scale(1.1)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DeleteIcon fontSize={'small'} />
|
||||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
})}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: 1.5,
|
||||
mt: 1,
|
||||
mb: 2,
|
||||
flexWrap: 'wrap',
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => addSmartNotification('reminder')}
|
||||
disabled={notifications.length >= maxNotifications}
|
||||
startDecorator={<AddIcon />}
|
||||
size={'sm'}
|
||||
variant={'outlined'}
|
||||
sx={{
|
||||
borderRadius: 6,
|
||||
fontWeight: 500,
|
||||
borderColor: `${TASK_COLOR.SCHEDULED}60`,
|
||||
color: TASK_COLOR.SCHEDULED,
|
||||
'&:hover': {
|
||||
borderColor: TASK_COLOR.SCHEDULED,
|
||||
background: `${TASK_COLOR.SCHEDULED}10`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Reminder
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => addSmartNotification('due')}
|
||||
disabled={
|
||||
notifications.length >= maxNotifications ||
|
||||
notifications.some(n => Number(n.value) === 0)
|
||||
}
|
||||
startDecorator={<AddIcon />}
|
||||
size={'sm'}
|
||||
variant={'outlined'}
|
||||
sx={{
|
||||
borderRadius: 6,
|
||||
fontWeight: 500,
|
||||
borderColor: `${NOTIFICATION_TYPE.DUE_DATE}60`,
|
||||
color: NOTIFICATION_TYPE.DUE_DATE,
|
||||
'&:hover': {
|
||||
borderColor: NOTIFICATION_TYPE.DUE_DATE,
|
||||
background: `${NOTIFICATION_TYPE.DUE_DATE}10`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Due Alert
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => addSmartNotification('followup')}
|
||||
disabled={notifications.length >= maxNotifications}
|
||||
startDecorator={<AddIcon />}
|
||||
size={'sm'}
|
||||
variant={'outlined'}
|
||||
sx={{
|
||||
borderRadius: 6,
|
||||
fontWeight: 500,
|
||||
borderColor: `${NOTIFICATION_TYPE.POSTDUE}60`,
|
||||
color: NOTIFICATION_TYPE.POSTDUE,
|
||||
'&:hover': {
|
||||
borderColor: NOTIFICATION_TYPE.POSTDUE,
|
||||
background: `${NOTIFICATION_TYPE.POSTDUE}10`,
|
||||
},
|
||||
}}
|
||||
>
|
||||
Follow-up
|
||||
</Button>
|
||||
</Box>
|
||||
{showSaveDefault && (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 1,
|
||||
display: 'flex',
|
||||
justifyContent: 'start',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant='outlined'
|
||||
size='sm'
|
||||
color='neutral'
|
||||
startDecorator={<Save />}
|
||||
sx={{
|
||||
borderRadius: 6,
|
||||
fontWeight: 500,
|
||||
'&:hover': {
|
||||
background: 'neutral.softHoverBg',
|
||||
},
|
||||
}}
|
||||
onClick={() => {
|
||||
localStorage.setItem(
|
||||
'defaultNotificationTemplate',
|
||||
JSON.stringify(notifications),
|
||||
)
|
||||
setShowSaveDefault(false)
|
||||
}}
|
||||
>
|
||||
Remember for Future Tasks
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{showTimeline && renderTimeline()}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default NotificationTemplate
|
||||
192
src/components/RealTimeSettings.jsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { Sync, SyncDisabled } from '@mui/icons-material'
|
||||
import { Box, Card, Chip, FormHelperText, Switch, Typography } from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { useSSEContext } from '../hooks/useSSEContext'
|
||||
import { useUserProfile } from '../queries/UserQueries'
|
||||
import { isPlusAccount } from '../utils/Helpers'
|
||||
import SSEConnectionStatus from './SSEConnectionStatus'
|
||||
|
||||
const REALTIME_TYPES = {
|
||||
DISABLED: 'disabled',
|
||||
SSE: 'sse',
|
||||
}
|
||||
|
||||
const RealTimeSettings = () => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
|
||||
// SSE context
|
||||
const sseContext = useSSEContext()
|
||||
|
||||
// Get current realtime type from localStorage
|
||||
const getCurrentRealtimeType = () => {
|
||||
const sseEnabled = localStorage.getItem('sse_enabled') === 'true'
|
||||
return sseEnabled ? REALTIME_TYPES.SSE : REALTIME_TYPES.DISABLED
|
||||
}
|
||||
|
||||
const [realtimeType, setRealtimeType] = useState(getCurrentRealtimeType())
|
||||
|
||||
const handleRealtimeTypeChange = (event, newValue) => {
|
||||
if (!isPlusAccount(userProfile)) {
|
||||
return // Don't allow changes for non-Plus users
|
||||
}
|
||||
|
||||
setRealtimeType(newValue)
|
||||
|
||||
// Update localStorage and toggle connections
|
||||
switch (newValue) {
|
||||
case REALTIME_TYPES.DISABLED:
|
||||
localStorage.setItem('sse_enabled', 'false')
|
||||
sseContext.disconnect()
|
||||
break
|
||||
case REALTIME_TYPES.SSE:
|
||||
localStorage.setItem('sse_enabled', 'true')
|
||||
sseContext.connect()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const getCurrentContext = () => {
|
||||
switch (realtimeType) {
|
||||
case REALTIME_TYPES.SSE:
|
||||
return sseContext
|
||||
default:
|
||||
return {
|
||||
isConnected: false,
|
||||
isConnecting: false,
|
||||
error: null,
|
||||
getConnectionStatus: () => 'disabled',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const context = getCurrentContext()
|
||||
|
||||
const getStatusDescription = () => {
|
||||
if (!isPlusAccount(userProfile)) {
|
||||
return 'Real-time updates are not available in the Basic plan. Upgrade to Plus to receive instant notifications when tasks are updated.'
|
||||
}
|
||||
|
||||
if (realtimeType === REALTIME_TYPES.DISABLED) {
|
||||
return 'Real-time updates are disabled. Enable them to see live changes when you or other circle members complete, skip, or modify tasks.'
|
||||
}
|
||||
|
||||
if (context.isConnected) {
|
||||
return "Real-time updates are working. You'll see live changes when you or other circle members complete, skip, or modify tasks."
|
||||
}
|
||||
|
||||
if (context.isConnecting) {
|
||||
return 'Connecting to real-time updates...'
|
||||
}
|
||||
|
||||
if (context.error) {
|
||||
return `Real-time updates are enabled but not working: ${context.error}`
|
||||
}
|
||||
|
||||
return 'Real-time updates are enabled but not currently connected.'
|
||||
}
|
||||
|
||||
const getConnectionStatusComponent = () => {
|
||||
switch (realtimeType) {
|
||||
case REALTIME_TYPES.SSE:
|
||||
return <SSEConnectionStatus variant='chip' />
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Card sx={{ mt: 2, p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'flex-start', gap: 2, mb: 2 }}>
|
||||
<Switch
|
||||
checked={realtimeType !== REALTIME_TYPES.DISABLED}
|
||||
onChange={e => {
|
||||
handleRealtimeTypeChange(
|
||||
null,
|
||||
e.target.checked ? REALTIME_TYPES.SSE : REALTIME_TYPES.DISABLED,
|
||||
)
|
||||
}}
|
||||
color={
|
||||
realtimeType !== REALTIME_TYPES.DISABLED ? 'success' : 'neutral'
|
||||
}
|
||||
disabled={!isPlusAccount(userProfile)}
|
||||
inputProps={{ 'aria-label': 'Enable Real-time Updates' }}
|
||||
/>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
mb: 0.5,
|
||||
}}
|
||||
>
|
||||
<Typography level='title-md'>
|
||||
Real-time Updates
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Chip variant='soft' color='warning' sx={{ ml: 1 }}>
|
||||
Plus Feature
|
||||
</Chip>
|
||||
)}
|
||||
</Typography>
|
||||
|
||||
{realtimeType !== REALTIME_TYPES.DISABLED &&
|
||||
isPlusAccount(userProfile) ? (
|
||||
<Sync color={context.isConnected ? 'success' : 'disabled'} />
|
||||
) : (
|
||||
<SyncDisabled color='disabled' />
|
||||
)}
|
||||
</Box>
|
||||
<Typography level='body-sm' color='neutral'>
|
||||
Get instant notifications when tasks are updated
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* <FormControl orientation='horizontal' sx={{ mb: 2 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<FormLabel>Real-time Connection Type</FormLabel>
|
||||
<FormHelperText sx={{ mt: 0 }}>
|
||||
Choose how to receive real-time updates
|
||||
</FormHelperText>
|
||||
</Box>
|
||||
<Select
|
||||
value={realtimeType}
|
||||
onChange={handleRealtimeTypeChange}
|
||||
disabled={!isPlusAccount(userProfile)}
|
||||
sx={{ minWidth: 140 }}
|
||||
>
|
||||
<Option value={REALTIME_TYPES.DISABLED}>Disabled</Option>
|
||||
<Option value={REALTIME_TYPES.WEBSOCKET}>WebSocket</Option>
|
||||
<Option value={REALTIME_TYPES.SSE}>SSE</Option>
|
||||
</Select>
|
||||
</FormControl> */}
|
||||
|
||||
<FormHelperText sx={{ mb: 2 }}>{getStatusDescription()}</FormHelperText>
|
||||
|
||||
{realtimeType !== REALTIME_TYPES.DISABLED &&
|
||||
isPlusAccount(userProfile) && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
|
||||
<Typography level='body-xs' color='neutral'>
|
||||
Status:
|
||||
</Typography>
|
||||
{getConnectionStatusComponent()}
|
||||
{context.error && (
|
||||
<Typography level='body-xs' color='danger'>
|
||||
{context.error}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
|
||||
Real-time updates are not available in the Basic plan. Upgrade to Plus
|
||||
to receive instant notifications when you or other circle members
|
||||
complete, skip, or modify tasks.
|
||||
</Typography>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default RealTimeSettings
|
||||
106
src/components/SSEConnectionStatus.jsx
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Circle, SignalWifi4Bar, SignalWifiOff } from '@mui/icons-material'
|
||||
import { Box, Chip, Tooltip, Typography } from '@mui/joy'
|
||||
import { useSSEContext } from '../hooks/useSSEContext'
|
||||
|
||||
const SSEConnectionStatus = ({
|
||||
variant = 'minimal',
|
||||
showError = false,
|
||||
sx = {},
|
||||
}) => {
|
||||
const { isConnected, isConnecting, error, getConnectionStatus } =
|
||||
useSSEContext()
|
||||
|
||||
const getStatusColor = () => {
|
||||
if (isConnected) return 'success'
|
||||
if (isConnecting) return 'warning'
|
||||
return 'danger'
|
||||
}
|
||||
|
||||
const getStatusIcon = () => {
|
||||
if (isConnected) return <SignalWifi4Bar />
|
||||
if (isConnecting) return <Circle />
|
||||
return <SignalWifiOff />
|
||||
}
|
||||
|
||||
const getStatusText = () => {
|
||||
if (isConnected) return 'Connected'
|
||||
if (isConnecting) return 'Connecting...'
|
||||
return 'Disconnected'
|
||||
}
|
||||
|
||||
const getTooltipText = () => {
|
||||
const status = getConnectionStatus()
|
||||
if (error) return `Real-time updates (SSE): ${status} - ${error}`
|
||||
if (!isConnected && !isConnecting) {
|
||||
return `Real-time updates (SSE): ${status} - Join a circle to enable real-time updates`
|
||||
}
|
||||
return `Real-time updates (SSE): ${status}`
|
||||
}
|
||||
|
||||
if (variant === 'minimal') {
|
||||
return (
|
||||
<Tooltip title={getTooltipText()} size='sm'>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<Circle
|
||||
sx={{
|
||||
fontSize: 8,
|
||||
color:
|
||||
getStatusColor() === 'success'
|
||||
? 'success.main'
|
||||
: getStatusColor() === 'warning'
|
||||
? 'warning.main'
|
||||
: 'danger.main',
|
||||
}}
|
||||
/>
|
||||
{showError && error && (
|
||||
<Typography level='body-xs' color='danger'>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
if (variant === 'chip') {
|
||||
return (
|
||||
<Tooltip title={getTooltipText()} size='sm'>
|
||||
<Chip
|
||||
color={getStatusColor()}
|
||||
size='sm'
|
||||
variant='soft'
|
||||
startDecorator={getStatusIcon()}
|
||||
sx={sx}
|
||||
>
|
||||
{getStatusText()}
|
||||
</Chip>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
// Full variant
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1, ...sx }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
{getStatusIcon()}
|
||||
<Typography level='body-sm' color={getStatusColor()}>
|
||||
{getStatusText()}
|
||||
</Typography>
|
||||
</Box>
|
||||
{showError && error && (
|
||||
<Typography level='body-xs' color='danger'>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default SSEConnectionStatus
|
||||
149
src/components/SSESettings.jsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Sync, SyncDisabled } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
Chip,
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
FormLabel,
|
||||
Switch,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useSSEContext } from '../hooks/useSSEContext'
|
||||
import { useUserProfile } from '../queries/UserQueries'
|
||||
import { isPlusAccount } from '../utils/Helpers'
|
||||
import SSEConnectionStatus from './SSEConnectionStatus'
|
||||
|
||||
const SSESettings = () => {
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const {
|
||||
isConnected,
|
||||
isConnecting,
|
||||
error,
|
||||
getConnectionStatus,
|
||||
toggleSSEEnabled,
|
||||
isSSEEnabled,
|
||||
} = useSSEContext()
|
||||
|
||||
const handleToggle = () => {
|
||||
console.log('=== TOGGLE CLICKED ===')
|
||||
if (!isPlusAccount(userProfile)) {
|
||||
console.log('Not a Plus account, returning early')
|
||||
return // Don't allow toggle for non-Plus users
|
||||
}
|
||||
const currentlyEnabled = isSSEEnabled()
|
||||
console.log('SSE Settings - Toggle clicked:', {
|
||||
currentlyEnabled,
|
||||
newState: !currentlyEnabled,
|
||||
userProfile,
|
||||
isPlusAccount: isPlusAccount(userProfile),
|
||||
})
|
||||
toggleSSEEnabled(!currentlyEnabled)
|
||||
}
|
||||
|
||||
const getStatusDescription = () => {
|
||||
if (!isPlusAccount(userProfile)) {
|
||||
return 'Real-time updates (SSE) are not available in the Basic plan. Upgrade to Plus to receive instant notifications when chores are updated.'
|
||||
}
|
||||
|
||||
if (!isSSEEnabled()) {
|
||||
return 'Real-time updates (SSE) are disabled. Enable to see live changes when you or other circle members complete, skip, or modify chores.'
|
||||
}
|
||||
|
||||
if (isConnected) {
|
||||
return "Real-time updates (SSE) are working. You'll see live changes when you or other circle members complete, skip, or modify chores."
|
||||
}
|
||||
|
||||
if (isConnecting) {
|
||||
return 'Connecting to real-time updates (SSE)...'
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return `Real-time updates (SSE) are enabled but not working: ${error}`
|
||||
}
|
||||
|
||||
return 'Real-time updates (SSE) are enabled but not currently connected.'
|
||||
}
|
||||
|
||||
return (
|
||||
<Card sx={{ mt: 2, p: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, mb: 2 }}>
|
||||
{isSSEEnabled() && isPlusAccount(userProfile) ? (
|
||||
<Sync color={isConnected ? 'success' : 'disabled'} />
|
||||
) : (
|
||||
<SyncDisabled color='disabled' />
|
||||
)}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography level='title-md'>
|
||||
Real-time Updates (SSE)
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Chip variant='soft' color='warning' sx={{ ml: 1 }}>
|
||||
Plus Feature
|
||||
</Chip>
|
||||
)}
|
||||
</Typography>
|
||||
<Typography level='body-sm' color='neutral'>
|
||||
Get instant notifications via Server-Sent Events
|
||||
</Typography>
|
||||
</Box>
|
||||
{isSSEEnabled() && isPlusAccount(userProfile) && (
|
||||
<SSEConnectionStatus variant='chip' />
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<FormControl orientation='horizontal' sx={{ mb: 2 }}>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<FormLabel>Enable Real-time Updates (SSE)</FormLabel>
|
||||
<FormHelperText sx={{ mt: 0 }}>
|
||||
{getStatusDescription()}
|
||||
</FormHelperText>
|
||||
</Box>
|
||||
<Switch
|
||||
checked={isSSEEnabled() && isPlusAccount(userProfile)}
|
||||
onChange={handleToggle}
|
||||
disabled={!isPlusAccount(userProfile)}
|
||||
color={
|
||||
isSSEEnabled() && isPlusAccount(userProfile) ? 'success' : 'neutral'
|
||||
}
|
||||
variant='solid'
|
||||
endDecorator={
|
||||
isSSEEnabled() && isPlusAccount(userProfile) ? 'On' : 'Off'
|
||||
}
|
||||
slotProps={{ endDecorator: { sx: { minWidth: 24 } } }}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
{isSSEEnabled() && isPlusAccount(userProfile) && (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 1 }}>
|
||||
<Typography level='body-xs' color='neutral'>
|
||||
Status:
|
||||
</Typography>
|
||||
<Chip
|
||||
size='sm'
|
||||
variant='soft'
|
||||
color={
|
||||
isConnected ? 'success' : isConnecting ? 'warning' : 'danger'
|
||||
}
|
||||
>
|
||||
{getConnectionStatus()}
|
||||
</Chip>
|
||||
{error && (
|
||||
<Typography level='body-xs' color='danger'>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{!isPlusAccount(userProfile) && (
|
||||
<Typography level='body-sm' color='warning' sx={{ mt: 1 }}>
|
||||
Real-time updates (SSE) are not available in the Basic plan. Upgrade
|
||||
to Plus to receive instant notifications when you or other circle
|
||||
members complete, skip, or modify chores.
|
||||
</Typography>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
export default SSESettings
|
||||
259
src/components/SubscriptionModal.jsx
Normal file
@@ -0,0 +1,259 @@
|
||||
import { Check, Star } from '@mui/icons-material'
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Card,
|
||||
Chip,
|
||||
Divider,
|
||||
Modal,
|
||||
ModalDialog,
|
||||
Radio,
|
||||
Typography,
|
||||
} from '@mui/joy'
|
||||
import { useState } from 'react'
|
||||
import { useNotification } from '../service/NotificationProvider'
|
||||
import { GetSubscriptionSession } from '../utils/Fetcher'
|
||||
|
||||
const SubscriptionModal = ({ open, onClose }) => {
|
||||
const [selectedPlan, setSelectedPlan] = useState('yearly')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const { showError } = useNotification()
|
||||
|
||||
const plans = {
|
||||
yearly: {
|
||||
price: '$39.00',
|
||||
period: 'year',
|
||||
total: '$39.00/year',
|
||||
// savings: 'Save $20.88',
|
||||
// popular: true,
|
||||
},
|
||||
// monthly: {
|
||||
// price: '$4.99',
|
||||
// period: 'month',
|
||||
// total: '$4.99/month',
|
||||
// savings: null,
|
||||
// },
|
||||
}
|
||||
|
||||
const features = [
|
||||
'Task notifications and reminders',
|
||||
'Rich text descriptions with images uploads',
|
||||
'Thing-based task triggers',
|
||||
'API tokens for integrations',
|
||||
'Image uploads in descriptions',
|
||||
'Advanced task automation',
|
||||
// 'Unlimited task history',
|
||||
// 'Unlimited things history',
|
||||
]
|
||||
|
||||
const handleSubscribe = async () => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
// Call the backend with the selected plan
|
||||
const response = await GetSubscriptionSession()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to create subscription session')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Redirect to Stripe
|
||||
if (data.sessionURL) {
|
||||
window.location.href = data.sessionURL
|
||||
} else {
|
||||
throw new Error('No session URL received')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Subscription error:', error)
|
||||
showError({
|
||||
title: 'Subscription Error',
|
||||
message: 'Failed to start subscription process. Please try again.',
|
||||
})
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal open={open} onClose={onClose}>
|
||||
<ModalDialog
|
||||
layout='center'
|
||||
sx={{
|
||||
width: 600,
|
||||
maxWidth: '95vw',
|
||||
maxHeight: '95vh',
|
||||
overflow: 'auto',
|
||||
p: 0,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: 4 }}>
|
||||
{/* Header */}
|
||||
<Box sx={{ textAlign: 'center', mb: 4 }}>
|
||||
<Typography level='h3' sx={{ mb: 1 }}>
|
||||
Upgrade to Plus
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Features List */}
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<Typography level='title-lg' sx={{ mb: 2 }}>
|
||||
What's included:
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1.5 }}>
|
||||
{features.map((feature, index) => (
|
||||
<Box
|
||||
key={index}
|
||||
sx={{ display: 'flex', alignItems: 'center', gap: 2 }}
|
||||
>
|
||||
<Check color='success' sx={{ fontSize: 20 }} />
|
||||
<Typography level='body-md'>{feature}</Typography>
|
||||
</Box>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
<Divider sx={{ my: 3 }} />
|
||||
|
||||
{/* Plan Selection */}
|
||||
<Box
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 1.2, mb: 4 }}
|
||||
>
|
||||
{Object.entries(plans).map(([key, plan]) => (
|
||||
<Card
|
||||
key={key}
|
||||
color={selectedPlan === key ? 'primary' : 'neutral'}
|
||||
onClick={() => setSelectedPlan(key)}
|
||||
sx={{
|
||||
width: '100%',
|
||||
minHeight: 48,
|
||||
maxHeight: 64,
|
||||
cursor: 'pointer',
|
||||
transition: 'all 0.2s',
|
||||
mb: 0.2,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
px: 2.5,
|
||||
py: 1.2,
|
||||
position: 'relative',
|
||||
overflow: 'visible',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2,
|
||||
justifyContent: 'flex-start',
|
||||
width: '100%',
|
||||
}}
|
||||
>
|
||||
<Radio
|
||||
checked={selectedPlan === key}
|
||||
onChange={() => setSelectedPlan(key)}
|
||||
value={key}
|
||||
name='subscription-plan'
|
||||
color='primary'
|
||||
sx={{ mr: 1 }}
|
||||
/>
|
||||
<Typography level='body-md' sx={{ fontWeight: 600 }}>
|
||||
{key.charAt(0).toUpperCase() + key.slice(1)}
|
||||
</Typography>
|
||||
<Typography level='body-sm' sx={{ fontWeight: 500, ml: 1 }}>
|
||||
{plan.price}
|
||||
<span style={{ color: '#888', fontWeight: 400 }}>
|
||||
{' '}
|
||||
/ {plan.period}
|
||||
</span>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
position: 'absolute',
|
||||
right: 16,
|
||||
top: -18,
|
||||
}}
|
||||
>
|
||||
{plan.popular && (
|
||||
<Chip
|
||||
variant='solid'
|
||||
color='warning'
|
||||
size='sm'
|
||||
startDecorator={<Star />}
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: 12,
|
||||
px: 1,
|
||||
py: 0.1,
|
||||
boxShadow: 2,
|
||||
mt: 0.8,
|
||||
}}
|
||||
>
|
||||
Most Popular
|
||||
</Chip>
|
||||
)}
|
||||
{plan.savings && (
|
||||
<Chip
|
||||
variant='soft'
|
||||
color='success'
|
||||
size='sm'
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
fontSize: 12,
|
||||
px: 1,
|
||||
py: 0.1,
|
||||
boxShadow: 2,
|
||||
mt: 0.8,
|
||||
}}
|
||||
>
|
||||
{plan.savings}
|
||||
</Chip>
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<Box
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 1.2, mt: 2 }}
|
||||
>
|
||||
<Button
|
||||
variant='solid'
|
||||
color='primary'
|
||||
onClick={handleSubscribe}
|
||||
loading={isLoading}
|
||||
fullWidth
|
||||
size='lg'
|
||||
sx={{ mb: 1 }}
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
<Button
|
||||
variant='plain'
|
||||
onClick={onClose}
|
||||
disabled={isLoading}
|
||||
fullWidth
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
{/* Footer */}
|
||||
<Typography
|
||||
level='body-xs'
|
||||
color='neutral'
|
||||
sx={{ textAlign: 'center', mt: 3 }}
|
||||
>
|
||||
Cancel anytime. No hidden fees. Secure payment powered by Stripe.
|
||||
</Typography>
|
||||
</Box>
|
||||
</ModalDialog>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default SubscriptionModal
|
||||
539
src/components/UserProfileAvatar.jsx
Normal file
@@ -0,0 +1,539 @@
|
||||
import {
|
||||
AdminPanelSettings,
|
||||
DarkModeOutlined,
|
||||
GroupAdd,
|
||||
LightModeOutlined,
|
||||
Logout,
|
||||
Person,
|
||||
Settings,
|
||||
SwapHoriz,
|
||||
Tune,
|
||||
WorkspacePremium,
|
||||
} from '@mui/icons-material'
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
Divider,
|
||||
Dropdown,
|
||||
ListItemContent,
|
||||
ListItemDecorator,
|
||||
Menu,
|
||||
MenuButton,
|
||||
MenuItem,
|
||||
Sheet,
|
||||
Typography,
|
||||
useColorScheme,
|
||||
} from '@mui/joy'
|
||||
import { useMediaQuery } from '@mui/material'
|
||||
import moment from 'moment'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useImpersonateUser } from '../contexts/ImpersonateUserContext'
|
||||
import useStickyState from '../hooks/useStickyState'
|
||||
import { useCircleMembers, useUserProfile } from '../queries/UserQueries'
|
||||
import { apiClient } from '../utils/ApiClient'
|
||||
import { isPlusAccount, resolvePhotoURL } from '../utils/Helpers'
|
||||
import UserModal from '../views/Modals/Inputs/UserModal'
|
||||
import SubscriptionModal from './SubscriptionModal'
|
||||
|
||||
const UserProfileAvatar = () => {
|
||||
const navigate = useNavigate()
|
||||
const { mode, setMode } = useColorScheme()
|
||||
const { data: userProfile } = useUserProfile()
|
||||
const {
|
||||
isImpersonating,
|
||||
startImpersonation,
|
||||
stopImpersonation,
|
||||
canImpersonate,
|
||||
getEffectiveUser,
|
||||
} = useImpersonateUser()
|
||||
const { data: circleMembersData } = useCircleMembers()
|
||||
const [isModalOpen, setIsModalOpen] = useState(false)
|
||||
const [isSubscriptionModalOpen, setIsSubscriptionModalOpen] = useState(false)
|
||||
const [themeMode, setThemeMode] = useStickyState(mode, 'themeMode')
|
||||
const isLargeScreen = useMediaQuery(theme => theme.breakpoints.up('lg'))
|
||||
|
||||
if (!userProfile) return null
|
||||
|
||||
const currentUser = getEffectiveUser(userProfile)
|
||||
const isAdmin = canImpersonate(userProfile, circleMembersData?.res)
|
||||
const isPlusUser = isPlusAccount(userProfile)
|
||||
|
||||
const getSubscriptionStatus = () => {
|
||||
if (!userProfile) return 'Free'
|
||||
|
||||
if (userProfile.subscription === 'active') {
|
||||
return 'Plus'
|
||||
}
|
||||
|
||||
if (
|
||||
userProfile.subscription === 'cancelled' &&
|
||||
moment().isBefore(userProfile.expiration)
|
||||
) {
|
||||
return 'Plus (expires soon)'
|
||||
}
|
||||
|
||||
return 'Free'
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
apiClient.handleLogout()
|
||||
}
|
||||
|
||||
const handleSupportEmail = () => {
|
||||
window.location.href = 'mailto:support@donetick.com'
|
||||
}
|
||||
|
||||
const isDarkMode = themeMode === 'dark'
|
||||
|
||||
const handleThemeToggle = () => {
|
||||
const newThemeMode = isDarkMode ? 'light' : 'dark'
|
||||
setThemeMode(newThemeMode)
|
||||
setMode(newThemeMode)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown>
|
||||
<MenuButton
|
||||
variant='plain'
|
||||
sx={{
|
||||
p: 0,
|
||||
border: 'none',
|
||||
backgroundColor: 'transparent',
|
||||
borderRadius: '50%',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
||||
// transform: 'scale(1.05)',
|
||||
// transition: 'all 0.2s ease',
|
||||
},
|
||||
'&:active': {
|
||||
// transform: 'scale(0.95)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
{isImpersonating ? (
|
||||
<Box sx={{ position: 'relative' }}>
|
||||
<Avatar
|
||||
src={resolvePhotoURL(currentUser?.image)}
|
||||
alt={currentUser?.displayName || currentUser?.name}
|
||||
size='md'
|
||||
sx={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
border: '2px solid var(--joy-palette-background-surface)',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
/>
|
||||
<Avatar
|
||||
src={resolvePhotoURL(userProfile?.image || userProfile?.avatar)}
|
||||
alt={userProfile?.displayName || userProfile?.name}
|
||||
size='sm'
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
bottom: -2,
|
||||
left: -2,
|
||||
width: 18,
|
||||
height: 18,
|
||||
border: '2px solid var(--joy-palette-background-surface)',
|
||||
backgroundColor: 'var(--joy-palette-background-surface)',
|
||||
boxShadow: '0 1px 4px rgba(0,0,0,0.2)',
|
||||
}}
|
||||
/>
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: -2,
|
||||
right: -2,
|
||||
width: 14,
|
||||
height: 14,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'var(--joy-palette-primary-500)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
border: '1px solid var(--joy-palette-background-surface)',
|
||||
boxShadow: '0 1px 2px rgba(0,0,0,0.2)',
|
||||
}}
|
||||
>
|
||||
<SwapHoriz sx={{ fontSize: 8, color: 'white' }} />
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
<Avatar
|
||||
src={resolvePhotoURL(currentUser?.image || currentUser?.avatar)}
|
||||
alt={currentUser?.displayName || currentUser?.name}
|
||||
size='md'
|
||||
sx={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
border: '2px solid var(--joy-palette-background-surface)',
|
||||
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</MenuButton>
|
||||
<Menu
|
||||
placement='bottom-end'
|
||||
sx={{
|
||||
minWidth: 280,
|
||||
p: 1,
|
||||
'--List-gap': '4px',
|
||||
boxShadow: 'var(--joy-shadow-lg)',
|
||||
border: '1px solid var(--joy-palette-divider)',
|
||||
borderRadius: 'var(--joy-radius-md)',
|
||||
}}
|
||||
>
|
||||
<Sheet sx={{ p: 2, borderRadius: 'var(--joy-radius-sm)', mb: 1 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<Avatar
|
||||
src={resolvePhotoURL(currentUser?.image || currentUser?.avatar)}
|
||||
alt={currentUser?.displayName || currentUser?.name}
|
||||
size='lg'
|
||||
sx={{
|
||||
width: 48,
|
||||
height: 48,
|
||||
border: '2px solid var(--joy-palette-background-surface)',
|
||||
}}
|
||||
/>
|
||||
<Box sx={{ flex: 1, minWidth: 0 }}>
|
||||
<Typography
|
||||
level='title-md'
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
color: 'var(--joy-palette-text-primary)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
mb: 0.25,
|
||||
}}
|
||||
>
|
||||
{currentUser?.displayName || currentUser?.name}
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5 }}>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
color: 'var(--joy-palette-text-tertiary)',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
whiteSpace: 'nowrap',
|
||||
flex: 1,
|
||||
}}
|
||||
>
|
||||
{currentUser?.email}
|
||||
</Typography>
|
||||
{isPlusUser && (
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
color: 'var(--joy-palette-warning-600)',
|
||||
fontWeight: 500,
|
||||
fontSize: '11px',
|
||||
}}
|
||||
>
|
||||
{getSubscriptionStatus()}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
{isImpersonating && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 0.5,
|
||||
mt: 0.5,
|
||||
px: 1,
|
||||
py: 0.25,
|
||||
backgroundColor: 'var(--joy-palette-primary-softBg)',
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
width: 'fit-content',
|
||||
}}
|
||||
>
|
||||
<SwapHoriz
|
||||
sx={{
|
||||
fontSize: 12,
|
||||
color: 'var(--joy-palette-primary-600)',
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{
|
||||
color: 'var(--joy-palette-primary-600)',
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Impersonating
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Sheet>
|
||||
|
||||
{isAdmin && (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator
|
||||
sx={{ color: 'var(--joy-palette-primary-500)' }}
|
||||
>
|
||||
<AdminPanelSettings />
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
||||
{isImpersonating ? 'Switch User' : 'Impersonate User'}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
||||
>
|
||||
Act as another user
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
|
||||
{isImpersonating && (
|
||||
<MenuItem
|
||||
onClick={() => stopImpersonation()}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator
|
||||
sx={{ color: 'var(--joy-palette-success-500)' }}
|
||||
>
|
||||
<Person />
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
||||
Stop Impersonating
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
||||
>
|
||||
Return to your account
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<Divider sx={{ my: 1 }} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
onClick={() => navigate('/settings')}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator sx={{ color: 'var(--joy-palette-neutral-500)' }}>
|
||||
<Settings />
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
||||
Settings
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
||||
>
|
||||
Account & preferences
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
|
||||
<MenuItem
|
||||
onClick={() => navigate('/settings/circle')}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator sx={{ color: 'var(--joy-palette-primary-500)' }}>
|
||||
<GroupAdd />
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
||||
Invite People
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
||||
>
|
||||
Add members to your circle
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
{isLargeScreen && (
|
||||
<MenuItem
|
||||
onClick={() => navigate('/settings/detailed#sidepanel')}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator
|
||||
sx={{ color: 'var(--joy-palette-neutral-500)' }}
|
||||
>
|
||||
<Tune />
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
||||
Side Panel Settings
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
||||
>
|
||||
Customize layout & cards
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
<MenuItem
|
||||
onClick={handleThemeToggle}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator sx={{ color: 'var(--joy-palette-neutral-500)' }}>
|
||||
{isDarkMode ? <LightModeOutlined /> : <DarkModeOutlined />}
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
||||
{isDarkMode ? 'Switch to Light' : 'Switch to Dark'}
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
||||
>
|
||||
Toggle theme appearance
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
|
||||
{!isPlusUser && (
|
||||
<MenuItem
|
||||
onClick={() => navigate('/settings/account')}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-warning-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator
|
||||
sx={{ color: 'var(--joy-palette-warning-500)' }}
|
||||
>
|
||||
<WorkspacePremium />
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
Upgrade to Plus
|
||||
</Typography>
|
||||
<Typography level='body-xs'>Unlock premium features</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
)}
|
||||
|
||||
{/* <MenuItem
|
||||
onClick={handleSupportEmail}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-neutral-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator sx={{ color: 'var(--joy-palette-info-500)' }}>
|
||||
<Email />
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Typography level='body-sm' sx={{ fontWeight: 500 }}>
|
||||
Support
|
||||
</Typography>
|
||||
<Typography
|
||||
level='body-xs'
|
||||
sx={{ color: 'var(--joy-palette-text-tertiary)' }}
|
||||
>
|
||||
support@donetick.com
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem> */}
|
||||
|
||||
<Divider sx={{ my: 1 }} />
|
||||
|
||||
<MenuItem
|
||||
onClick={handleLogout}
|
||||
sx={{
|
||||
borderRadius: 'var(--joy-radius-sm)',
|
||||
'&:hover': {
|
||||
backgroundColor: 'var(--joy-palette-danger-softHoverBg)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ListItemDecorator sx={{ color: 'var(--joy-palette-danger-500)' }}>
|
||||
<Logout />
|
||||
</ListItemDecorator>
|
||||
<ListItemContent>
|
||||
<Typography
|
||||
level='body-sm'
|
||||
sx={{ fontWeight: 500, color: 'var(--joy-palette-danger-500)' }}
|
||||
>
|
||||
Logout
|
||||
</Typography>
|
||||
</ListItemContent>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</Dropdown>
|
||||
|
||||
<UserModal
|
||||
isOpen={isModalOpen}
|
||||
performers={circleMembersData?.res}
|
||||
onSelect={user => {
|
||||
startImpersonation(user, userProfile)
|
||||
setIsModalOpen(false)
|
||||
}}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>
|
||||
|
||||
<SubscriptionModal
|
||||
open={isSubscriptionModalOpen}
|
||||
onClose={() => setIsSubscriptionModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default UserProfileAvatar
|
||||
88
src/components/animations/AnimatedList.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from 'react'
|
||||
import { Box } from '@mui/joy'
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group'
|
||||
import { useStaggeredAnimation, useReducedMotion } from '../../hooks/useAnimations'
|
||||
import './PageTransition.css'
|
||||
|
||||
const AnimatedList = ({
|
||||
children,
|
||||
staggerDelay = 50,
|
||||
animationType = 'stagger', // 'stagger', 'fade', 'slide'
|
||||
direction = 'up', // 'up', 'down', 'left', 'right'
|
||||
renderItem,
|
||||
keyExtractor,
|
||||
items,
|
||||
...boxProps
|
||||
}) => {
|
||||
// Handle both children and items patterns
|
||||
let childrenArray
|
||||
if (items && renderItem) {
|
||||
childrenArray = items.map((item, index) =>
|
||||
React.cloneElement(renderItem(item, index), {
|
||||
key: keyExtractor ? keyExtractor(item, index) : index
|
||||
})
|
||||
)
|
||||
} else {
|
||||
childrenArray = React.Children.toArray(children)
|
||||
}
|
||||
|
||||
const visibleItems = useStaggeredAnimation(childrenArray.length, staggerDelay)
|
||||
const prefersReducedMotion = useReducedMotion()
|
||||
|
||||
// If user prefers reduced motion, render without animations
|
||||
if (prefersReducedMotion) {
|
||||
return (
|
||||
<Box {...boxProps}>
|
||||
{items && renderItem ? childrenArray : children}
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const getAnimationClass = () => {
|
||||
switch (animationType) {
|
||||
case 'fade':
|
||||
return 'fade'
|
||||
case 'slide':
|
||||
return direction === 'up' ? 'slide-up' : 'page'
|
||||
case 'stagger':
|
||||
default:
|
||||
return 'stagger'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Box {...boxProps}>
|
||||
<TransitionGroup component={null}>
|
||||
{childrenArray.map((child, index) => {
|
||||
const isVisible = visibleItems.has(index)
|
||||
|
||||
return (
|
||||
<CSSTransition
|
||||
key={child.key || index}
|
||||
in={isVisible}
|
||||
timeout={{
|
||||
enter: 300,
|
||||
exit: 200,
|
||||
}}
|
||||
classNames={getAnimationClass()}
|
||||
unmountOnExit={false}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
opacity: isVisible ? 1 : 0,
|
||||
transform: isVisible ? 'none' : 'translateY(20px)',
|
||||
transition: 'opacity 0.3s ease, transform 0.3s ease',
|
||||
transitionDelay: `${index * staggerDelay}ms`,
|
||||
}}
|
||||
>
|
||||
{child}
|
||||
</Box>
|
||||
</CSSTransition>
|
||||
)
|
||||
})}
|
||||
</TransitionGroup>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default AnimatedList
|
||||
105
src/components/animations/LoadingScreen.jsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { Box, CircularProgress, Typography } from '@mui/joy'
|
||||
import { styled } from '@mui/joy/styles'
|
||||
|
||||
// Styled components with keyframe animations
|
||||
const LoadingContainer = styled(Box)({
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
backgroundColor: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(8px)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
zIndex: 9999,
|
||||
animation: 'fadeIn 0.3s ease-out',
|
||||
'@keyframes fadeIn': {
|
||||
from: {
|
||||
opacity: 0,
|
||||
},
|
||||
to: {
|
||||
opacity: 1,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const LoadingContent = styled(Box)({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '24px',
|
||||
animation: 'slideUp 0.5s ease-out 0.2s both',
|
||||
'@keyframes slideUp': {
|
||||
from: {
|
||||
opacity: 0,
|
||||
transform: 'translateY(20px)',
|
||||
},
|
||||
to: {
|
||||
opacity: 1,
|
||||
transform: 'translateY(0)',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const PulsingText = styled(Typography)({
|
||||
animation: 'pulse 2s ease-in-out infinite',
|
||||
'@keyframes pulse': {
|
||||
'0%, 100%': {
|
||||
opacity: 1,
|
||||
},
|
||||
'50%': {
|
||||
opacity: 0.5,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const LogoContainer = styled(Box)({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
marginBottom: '24px',
|
||||
})
|
||||
|
||||
const LoadingScreen = ({
|
||||
message = 'Loading...',
|
||||
showLogo = true,
|
||||
size = 'lg',
|
||||
}) => {
|
||||
return (
|
||||
<LoadingContainer>
|
||||
<LoadingContent>
|
||||
{showLogo && (
|
||||
<LogoContainer>
|
||||
<Typography
|
||||
level='h1'
|
||||
sx={{
|
||||
fontSize: { xs: '2rem', sm: '2.5rem' },
|
||||
fontWeight: 700,
|
||||
color: 'primary.500',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
Done
|
||||
<span style={{ color: '#06b6d4' }}>tick</span>
|
||||
</Typography>
|
||||
</LogoContainer>
|
||||
)}
|
||||
|
||||
<CircularProgress
|
||||
size={size}
|
||||
sx={{
|
||||
'--CircularProgress-size': size === 'lg' ? '60px' : '40px',
|
||||
mb: 2,
|
||||
}}
|
||||
/>
|
||||
|
||||
<PulsingText level='body-md'>{message}</PulsingText>
|
||||
</LoadingContent>
|
||||
</LoadingContainer>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoadingScreen
|
||||
248
src/components/animations/PageTransition.css
Normal file
@@ -0,0 +1,248 @@
|
||||
/* Page Wrapper */
|
||||
.page-wrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* View Transitions API - Native browser page transitions */
|
||||
/* These run automatically when the browser supports view transitions */
|
||||
|
||||
/* Forward navigation (going deeper) */
|
||||
::view-transition-old(root):only-child {
|
||||
animation: slide-out-left 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
::view-transition-new(root):only-child {
|
||||
animation: slide-in-right 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Back navigation (going up) */
|
||||
html[data-transition='back']::view-transition-old(root):only-child {
|
||||
animation: slide-out-right 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
html[data-transition='back']::view-transition-new(root):only-child {
|
||||
animation: slide-in-left 250ms cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* Fade for auth pages */
|
||||
html[data-transition='fade']::view-transition-old(root):only-child,
|
||||
html[data-transition='fade']::view-transition-new(root):only-child {
|
||||
animation: fade 200ms ease-in-out;
|
||||
}
|
||||
|
||||
/* Animation keyframes */
|
||||
@keyframes slide-out-left {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(-30%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-right {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-out-right {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slide-in-left {
|
||||
from {
|
||||
transform: translateX(-30%);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Modal/overlay animations (still useful for non-route transitions) */
|
||||
.modal-enter {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
}
|
||||
|
||||
.modal-enter-active {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
transition:
|
||||
opacity 250ms ease-out,
|
||||
transform 250ms ease-out;
|
||||
}
|
||||
|
||||
.modal-exit {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
|
||||
.modal-exit-active {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
transition:
|
||||
opacity 200ms ease-in,
|
||||
transform 200ms ease-in;
|
||||
}
|
||||
|
||||
/* Slide up animation for bottom sheets/panels */
|
||||
.slide-up-enter {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
}
|
||||
|
||||
.slide-up-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition:
|
||||
opacity 300ms ease-out,
|
||||
transform 300ms ease-out;
|
||||
}
|
||||
|
||||
.slide-up-exit {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.slide-up-exit-active {
|
||||
opacity: 0;
|
||||
transform: translateY(100%);
|
||||
transition:
|
||||
opacity 250ms ease-in,
|
||||
transform 250ms ease-in;
|
||||
}
|
||||
|
||||
/* Loading spinner animation */
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* Skeleton loading animations */
|
||||
@keyframes skeleton-loading {
|
||||
0% {
|
||||
background-position: -200px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: calc(200px + 100%) 0;
|
||||
}
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
|
||||
background-size: 200px 100%;
|
||||
animation: skeleton-loading 1.5s infinite;
|
||||
}
|
||||
|
||||
/* Micro-interactions */
|
||||
.interactive-element {
|
||||
transition: all 0.2s ease-out;
|
||||
transform: translateZ(0); /* Enable GPU acceleration */
|
||||
}
|
||||
|
||||
.interactive-element:hover {
|
||||
transform: translateY(-2px) translateZ(0);
|
||||
}
|
||||
|
||||
.interactive-element:active {
|
||||
transform: translateY(0) translateZ(0);
|
||||
transition: all 0.1s ease-out;
|
||||
}
|
||||
|
||||
/* Stagger animation for lists */
|
||||
.stagger-enter {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
|
||||
.stagger-enter-active {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
transition:
|
||||
opacity 300ms ease-out,
|
||||
transform 300ms ease-out;
|
||||
}
|
||||
|
||||
/* Add animation delays for staggered effects */
|
||||
.stagger-enter-active:nth-child(1) {
|
||||
transition-delay: 0ms;
|
||||
}
|
||||
.stagger-enter-active:nth-child(2) {
|
||||
transition-delay: 50ms;
|
||||
}
|
||||
.stagger-enter-active:nth-child(3) {
|
||||
transition-delay: 100ms;
|
||||
}
|
||||
.stagger-enter-active:nth-child(4) {
|
||||
transition-delay: 150ms;
|
||||
}
|
||||
.stagger-enter-active:nth-child(5) {
|
||||
transition-delay: 200ms;
|
||||
}
|
||||
.stagger-enter-active:nth-child(6) {
|
||||
transition-delay: 250ms;
|
||||
}
|
||||
.stagger-enter-active:nth-child(7) {
|
||||
transition-delay: 300ms;
|
||||
}
|
||||
.stagger-enter-active:nth-child(8) {
|
||||
transition-delay: 350ms;
|
||||
}
|
||||
|
||||
/* Reduced motion for accessibility */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
/* Disable View Transitions for users who prefer reduced motion */
|
||||
::view-transition-old(root),
|
||||
::view-transition-new(root) {
|
||||
animation: none !important;
|
||||
}
|
||||
|
||||
.modal-enter,
|
||||
.modal-enter-active,
|
||||
.modal-exit,
|
||||
.modal-exit-active,
|
||||
.interactive-element,
|
||||
.stagger-enter,
|
||||
.stagger-enter-active {
|
||||
transition: none !important;
|
||||
animation: none !important;
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
130
src/components/animations/PageTransition.jsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import { useLayoutEffect, useRef, useState } from 'react'
|
||||
import { flushSync } from 'react-dom'
|
||||
import { useLocation } from 'react-router-dom'
|
||||
import './PageTransition.css'
|
||||
|
||||
// Route hierarchy for determining navigation direction
|
||||
const routeHierarchy = {
|
||||
'/': 0,
|
||||
'/my/chores': 0,
|
||||
'/chores': 1,
|
||||
'/chores/create': 2,
|
||||
'/settings': 1,
|
||||
'/things': 1,
|
||||
'/activities': 1,
|
||||
'/points': 1,
|
||||
'/labels': 1,
|
||||
'/projects': 1,
|
||||
'/login': 0,
|
||||
'/signup': 1,
|
||||
'/landing': 0,
|
||||
'/archived': 1,
|
||||
}
|
||||
|
||||
const getRouteLevel = pathname => {
|
||||
// Check for exact matches first
|
||||
if (routeHierarchy[pathname] !== undefined) {
|
||||
return routeHierarchy[pathname]
|
||||
}
|
||||
|
||||
// Check for dynamic routes (e.g., /chores/123/edit)
|
||||
if (pathname.includes('/chores/') && pathname.includes('/edit')) {
|
||||
return 3
|
||||
}
|
||||
if (pathname.includes('/chores/') && pathname.includes('/history')) {
|
||||
return 3
|
||||
}
|
||||
if (pathname.includes('/chores/') && pathname.includes('/timer')) {
|
||||
return 3
|
||||
}
|
||||
if (
|
||||
pathname.includes('/chores/') &&
|
||||
!pathname.includes('/edit') &&
|
||||
!pathname.includes('/history') &&
|
||||
!pathname.includes('/timer')
|
||||
) {
|
||||
return 2
|
||||
}
|
||||
if (pathname.includes('/things/')) {
|
||||
return 2
|
||||
}
|
||||
if (pathname.includes('/settings/')) {
|
||||
return 2
|
||||
}
|
||||
|
||||
// Default level
|
||||
return 1
|
||||
}
|
||||
|
||||
const PageTransition = ({ children }) => {
|
||||
const location = useLocation()
|
||||
const prevLevel = useRef(0)
|
||||
const [displayLocation, setDisplayLocation] = useState(location)
|
||||
const isFirstRender = useRef(true)
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// Skip transition on first render
|
||||
if (isFirstRender.current) {
|
||||
isFirstRender.current = false
|
||||
setDisplayLocation(location)
|
||||
prevLevel.current = getRouteLevel(location.pathname)
|
||||
return
|
||||
}
|
||||
|
||||
// Don't transition if location hasn't actually changed
|
||||
if (location.pathname === displayLocation.pathname) {
|
||||
return
|
||||
}
|
||||
|
||||
const currentLevel = getRouteLevel(location.pathname)
|
||||
const previousLevel = prevLevel.current
|
||||
|
||||
// Determine navigation direction
|
||||
const isBack = currentLevel < previousLevel
|
||||
const isFade =
|
||||
location.pathname.includes('/login') ||
|
||||
location.pathname.includes('/signup') ||
|
||||
location.pathname.includes('/landing') ||
|
||||
location.pathname.includes('/auth/')
|
||||
|
||||
// Apply transition type as data attribute for CSS
|
||||
document.documentElement.dataset.transition = isFade
|
||||
? 'fade'
|
||||
: isBack
|
||||
? 'back'
|
||||
: 'forward'
|
||||
|
||||
// Trigger View Transition if supported
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(() => {
|
||||
// flushSync forces React to update the DOM synchronously
|
||||
// This ensures the View Transition captures the actual DOM change
|
||||
flushSync(() => {
|
||||
setDisplayLocation(location)
|
||||
})
|
||||
// Scroll to top after DOM update
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'instant' })
|
||||
})
|
||||
} else {
|
||||
// Fallback for browsers without View Transitions API
|
||||
setDisplayLocation(location)
|
||||
window.scrollTo({ top: 0, left: 0, behavior: 'instant' })
|
||||
}
|
||||
|
||||
// Update refs
|
||||
prevLevel.current = currentLevel
|
||||
}, [location, displayLocation.pathname])
|
||||
|
||||
return (
|
||||
<div
|
||||
className='page-wrapper'
|
||||
style={{
|
||||
paddingBottom: `var(--safe-area-inset-bottom, 0px)`,
|
||||
}}
|
||||
>
|
||||
{displayLocation.pathname === location.pathname ? children : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PageTransition
|
||||
138
src/components/animations/SkeletonLoader.jsx
Normal file
@@ -0,0 +1,138 @@
|
||||
import { Box, Skeleton } from '@mui/joy'
|
||||
|
||||
const SkeletonLoader = ({
|
||||
type = 'card',
|
||||
count = 1,
|
||||
height = 100,
|
||||
width = '100%',
|
||||
variant = 'rectangular',
|
||||
...props
|
||||
}) => {
|
||||
const renderSkeleton = () => {
|
||||
switch (type) {
|
||||
case 'card':
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 'md',
|
||||
}}
|
||||
>
|
||||
<Skeleton variant='text' height={24} width='60%' sx={{ mb: 1 }} />
|
||||
<Skeleton
|
||||
variant='text'
|
||||
height={16}
|
||||
width='100%'
|
||||
sx={{ mb: 0.5 }}
|
||||
/>
|
||||
<Skeleton variant='text' height={16} width='80%' sx={{ mb: 2 }} />
|
||||
<Box sx={{ display: 'flex', gap: 1 }}>
|
||||
<Skeleton variant='circular' width={32} height={32} />
|
||||
<Skeleton variant='rectangular' height={32} width={80} />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'list':
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 2, p: 1.5 }}>
|
||||
<Skeleton variant='circular' width={40} height={40} />
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Skeleton
|
||||
variant='text'
|
||||
height={20}
|
||||
width='70%'
|
||||
sx={{ mb: 0.5 }}
|
||||
/>
|
||||
<Skeleton variant='text' height={16} width='50%' />
|
||||
</Box>
|
||||
<Skeleton variant='rectangular' width={60} height={24} />
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'chore':
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
p: 2,
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
borderRadius: 'md',
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Skeleton variant='text' height={24} width='50%' />
|
||||
<Skeleton variant='circular' width={24} height={24} />
|
||||
</Box>
|
||||
<Skeleton variant='text' height={16} width='80%' sx={{ mb: 1 }} />
|
||||
<Box sx={{ display: 'flex', gap: 1, mb: 2 }}>
|
||||
<Skeleton variant='rectangular' height={20} width={60} />
|
||||
<Skeleton variant='rectangular' height={20} width={40} />
|
||||
</Box>
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
}}
|
||||
>
|
||||
<Skeleton variant='circular' width={32} height={32} />
|
||||
<Skeleton variant='text' height={16} width='30%' />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
||||
case 'profile':
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
p: 3,
|
||||
}}
|
||||
>
|
||||
<Skeleton
|
||||
variant='circular'
|
||||
width={80}
|
||||
height={80}
|
||||
sx={{ mb: 2 }}
|
||||
/>
|
||||
<Skeleton variant='text' height={24} width={150} sx={{ mb: 1 }} />
|
||||
<Skeleton variant='text' height={16} width={100} />
|
||||
</Box>
|
||||
)
|
||||
|
||||
default:
|
||||
return (
|
||||
<Skeleton
|
||||
variant={variant}
|
||||
height={height}
|
||||
width={width}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{Array.from({ length: count }, (_, index) => (
|
||||
<Box key={index} sx={{ mb: type === 'list' ? 0 : 2 }}>
|
||||
{renderSkeleton()}
|
||||
</Box>
|
||||
))}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SkeletonLoader
|
||||
107
src/components/animations/SmoothButton.jsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { Button } from '@mui/joy'
|
||||
import { styled } from '@mui/joy/styles'
|
||||
|
||||
const AnimatedButton = styled(Button)(({ theme }) => ({
|
||||
transition: 'all 0.2s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transform: 'translateZ(0)', // Enable GPU acceleration
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
|
||||
'&:hover:not(:disabled)': {
|
||||
transform: 'translateY(-2px) translateZ(0)',
|
||||
boxShadow: theme.shadow.lg,
|
||||
},
|
||||
|
||||
'&:active:not(:disabled)': {
|
||||
transform: 'translateY(0) translateZ(0)',
|
||||
transition: 'all 0.1s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
},
|
||||
|
||||
'&:focus-visible': {
|
||||
outline: '2px solid',
|
||||
outlineColor: theme.palette.primary[500],
|
||||
outlineOffset: '2px',
|
||||
},
|
||||
|
||||
// Ripple effect
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
width: '0',
|
||||
height: '0',
|
||||
borderRadius: '50%',
|
||||
background: 'currentColor',
|
||||
opacity: 0.1,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
transition: 'width 0.6s, height 0.6s',
|
||||
},
|
||||
|
||||
'&:active::before': {
|
||||
width: '300px',
|
||||
height: '300px',
|
||||
},
|
||||
|
||||
// Loading state
|
||||
'&[data-loading="true"]': {
|
||||
pointerEvents: 'none',
|
||||
position: 'relative',
|
||||
|
||||
'& > *': {
|
||||
opacity: 0.6,
|
||||
},
|
||||
|
||||
'&::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
width: '16px',
|
||||
height: '16px',
|
||||
border: '2px solid currentColor',
|
||||
borderTop: '2px solid transparent',
|
||||
borderRadius: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
animation: 'spin 1s linear infinite',
|
||||
},
|
||||
},
|
||||
|
||||
// Reduced motion support
|
||||
'@media (prefers-reduced-motion: reduce)': {
|
||||
transition: 'none',
|
||||
transform: 'none !important',
|
||||
|
||||
'&:hover:not(:disabled)': {
|
||||
transform: 'none',
|
||||
},
|
||||
|
||||
'&:active:not(:disabled)': {
|
||||
transform: 'none',
|
||||
},
|
||||
|
||||
'&::before': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const SmoothButton = ({ children, loading = false, onClick, ...props }) => {
|
||||
const handleClick = event => {
|
||||
if (loading) return
|
||||
onClick?.(event)
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatedButton
|
||||
{...props}
|
||||
onClick={handleClick}
|
||||
data-loading={loading}
|
||||
disabled={props.disabled || loading}
|
||||
>
|
||||
{children}
|
||||
</AnimatedButton>
|
||||
)
|
||||
}
|
||||
|
||||
export default SmoothButton
|
||||
88
src/components/animations/SmoothCard.jsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import React from 'react'
|
||||
import { Card } from '@mui/joy'
|
||||
import { styled } from '@mui/joy/styles'
|
||||
|
||||
const AnimatedCard = styled(Card)(({ theme }) => ({
|
||||
transition: 'all 0.3s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
transform: 'translateZ(0)', // Enable GPU acceleration
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
|
||||
'&:hover': {
|
||||
transform: 'translateY(-4px) translateZ(0)',
|
||||
boxShadow: theme.shadow.lg,
|
||||
},
|
||||
|
||||
'&:active': {
|
||||
transform: 'translateY(-2px) translateZ(0)',
|
||||
transition: 'all 0.1s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
},
|
||||
|
||||
// Subtle background animation on hover
|
||||
'&::before': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: 'linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent)',
|
||||
opacity: 0,
|
||||
transition: 'opacity 0.3s ease',
|
||||
pointerEvents: 'none',
|
||||
borderRadius: 'inherit',
|
||||
},
|
||||
|
||||
'&:hover::before': {
|
||||
opacity: 1,
|
||||
},
|
||||
|
||||
// Focus states for accessibility
|
||||
'&:focus-visible': {
|
||||
outline: '2px solid',
|
||||
outlineColor: theme.palette.primary[500],
|
||||
outlineOffset: '2px',
|
||||
},
|
||||
|
||||
// Reduced motion support
|
||||
'@media (prefers-reduced-motion: reduce)': {
|
||||
transition: 'none',
|
||||
transform: 'none !important',
|
||||
|
||||
'&:hover': {
|
||||
transform: 'none',
|
||||
boxShadow: theme.shadow.md, // Still provide visual feedback
|
||||
},
|
||||
|
||||
'&:active': {
|
||||
transform: 'none',
|
||||
},
|
||||
|
||||
'&::before': {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const SmoothCard = ({
|
||||
children,
|
||||
onClick,
|
||||
animationDisabled = false,
|
||||
...props
|
||||
}) => {
|
||||
if (animationDisabled) {
|
||||
return (
|
||||
<Card {...props} onClick={onClick}>
|
||||
{children}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<AnimatedCard {...props} onClick={onClick}>
|
||||
{children}
|
||||
</AnimatedCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default SmoothCard
|
||||
54
src/components/animations/StaggeredList.jsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Box } from '@mui/joy'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { CSSTransition, TransitionGroup } from 'react-transition-group'
|
||||
import './PageTransition.css'
|
||||
|
||||
const StaggeredList = ({
|
||||
children,
|
||||
staggerDelay = 50,
|
||||
initialDelay = 0,
|
||||
animate = true,
|
||||
}) => {
|
||||
const [isVisible, setIsVisible] = useState(!animate)
|
||||
|
||||
useEffect(() => {
|
||||
if (animate) {
|
||||
const timer = setTimeout(() => {
|
||||
setIsVisible(true)
|
||||
}, initialDelay)
|
||||
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [animate, initialDelay])
|
||||
|
||||
if (!animate) {
|
||||
return <Box>{children}</Box>
|
||||
}
|
||||
|
||||
const childrenArray = React.Children.toArray(children)
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<TransitionGroup component={null}>
|
||||
{isVisible &&
|
||||
childrenArray.map((child, index) => (
|
||||
<CSSTransition
|
||||
key={child.key || index}
|
||||
classNames='stagger'
|
||||
timeout={{
|
||||
enter: 300 + index * staggerDelay,
|
||||
exit: 200,
|
||||
}}
|
||||
style={{
|
||||
transitionDelay: `${index * staggerDelay}ms`,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ mb: 1 }}>{child}</Box>
|
||||
</CSSTransition>
|
||||
))}
|
||||
</TransitionGroup>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default StaggeredList
|
||||
9
src/components/animations/index.js
Normal file
@@ -0,0 +1,9 @@
|
||||
// Animation Components
|
||||
export { default as AnimatedList } from './AnimatedList'
|
||||
export { default as PageTransition } from './PageTransition'
|
||||
export { default as SmoothButton } from './SmoothButton'
|
||||
export { default as SmoothCard } from './SmoothCard'
|
||||
export { default as StaggeredList } from './StaggeredList'
|
||||
|
||||
// Animation Styles
|
||||
import './PageTransition.css'
|
||||