Merge pull request #1629 from ajnart/ssr/optimize

This commit is contained in:
Thomas Camlong
2023-11-14 20:04:25 +01:00
committed by GitHub
7 changed files with 537 additions and 421 deletions

View File

@@ -1,6 +1,6 @@
{ {
"name": "homarr Top Members Report", "name": "homarr Top Members Report",
"url": "https://crowdin.com/project/homarr", "url": "https://translate.homarr.dev/project/homarr",
"unit": "words", "unit": "words",
"dateRange": { "dateRange": {
"from": "2022-08-25", "from": "2022-08-25",
@@ -8,28 +8,6 @@
}, },
"language": "All", "language": "All",
"data": [ "data": [
{
"user": {
"id": "15492732",
"username": "hillaliy",
"fullName": "Yossi Hillali (hillaliy)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15492732/medium/0bae17b421604892d888e3fc70cf0587.jpeg",
"joined": "2022-10-15 15:18:50"
},
"languages": [
{
"id": "he",
"name": "Hebrew"
}
],
"translated": 5404,
"target": 4717,
"approved": 5437,
"voted": 0,
"positiveVotes": 12,
"negativeVotes": 0,
"winning": 5395
},
{ {
"user": { "user": {
"id": "15491798", "id": "15491798",
@@ -44,13 +22,57 @@
"name": "Danish" "name": "Danish"
} }
], ],
"translated": 5353, "translated": 5893,
"target": 5159, "target": 5686,
"approved": 5371, "approved": 5911,
"voted": 0, "voted": 0,
"positiveVotes": 0, "positiveVotes": 0,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 5353 "winning": 5893
},
{
"user": {
"id": "15492732",
"username": "hillaliy",
"fullName": "Yossi Hillali (hillaliy)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15492732/medium/0bae17b421604892d888e3fc70cf0587.jpeg",
"joined": "2022-10-15 15:18:50"
},
"languages": [
{
"id": "he",
"name": "Hebrew"
}
],
"translated": 5815,
"target": 5068,
"approved": 5848,
"voted": 0,
"positiveVotes": 12,
"negativeVotes": 0,
"winning": 5806
},
{
"user": {
"id": "15554645",
"username": "crendasien",
"fullName": "Nicole (crendasien)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15554645/medium/598ab1d4aaf6b8dccd5ba16be92da7b9.jpeg",
"joined": "2022-11-28 14:18:44"
},
"languages": [
{
"id": "it",
"name": "Italian"
}
],
"translated": 5288,
"target": 5378,
"approved": 5613,
"voted": 0,
"positiveVotes": 11,
"negativeVotes": 0,
"winning": 5285
}, },
{ {
"user": { "user": {
@@ -90,28 +112,6 @@
"negativeVotes": 1, "negativeVotes": 1,
"winning": 5074 "winning": 5074
}, },
{
"user": {
"id": "15554645",
"username": "crendasien",
"fullName": "Nicole (crendasien)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15554645/medium/598ab1d4aaf6b8dccd5ba16be92da7b9.jpeg",
"joined": "2022-11-28 14:18:44"
},
"languages": [
{
"id": "it",
"name": "Italian"
}
],
"translated": 4910,
"target": 5000,
"approved": 5235,
"voted": 0,
"positiveVotes": 11,
"negativeVotes": 0,
"winning": 4907
},
{ {
"user": { "user": {
"id": "12701640", "id": "12701640",
@@ -130,35 +130,13 @@
"name": "Spanish" "name": "Spanish"
} }
], ],
"translated": 4446, "translated": 4822,
"target": 4685, "target": 5078,
"approved": 0, "approved": 0,
"voted": 166, "voted": 166,
"positiveVotes": 24, "positiveVotes": 30,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 963 "winning": 1017
},
{
"user": {
"id": "15674593",
"username": "Marty88",
"fullName": "Marty (Marty88)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15674593/medium/492b1509d52bd2809dea768121217125.jpeg",
"joined": "2023-02-08 16:28:53"
},
"languages": [
{
"id": "sk",
"name": "Slovak"
}
],
"translated": 4302,
"target": 3955,
"approved": 3732,
"voted": 0,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 3726
}, },
{ {
"user": { "user": {
@@ -174,13 +152,13 @@
"name": "German" "name": "German"
} }
], ],
"translated": 4245, "translated": 4652,
"target": 4326, "target": 4751,
"approved": 3964, "approved": 4371,
"voted": 0, "voted": 0,
"positiveVotes": 25, "positiveVotes": 25,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 3685 "winning": 4092
}, },
{ {
"user": { "user": {
@@ -196,8 +174,8 @@
"name": "Swedish" "name": "Swedish"
} }
], ],
"translated": 4142, "translated": 4557,
"target": 3889, "target": 4273,
"approved": 0, "approved": 0,
"voted": 0, "voted": 0,
"positiveVotes": 0, "positiveVotes": 0,
@@ -218,8 +196,74 @@
"name": "Turkish" "name": "Turkish"
} }
], ],
"translated": 3845, "translated": 4384,
"target": 3244, "target": 3701,
"approved": 0,
"voted": 0,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 0
},
{
"user": {
"id": "15674593",
"username": "Marty88",
"fullName": "Marty (Marty88)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15674593/medium/492b1509d52bd2809dea768121217125.jpeg",
"joined": "2023-02-08 16:28:53"
},
"languages": [
{
"id": "sk",
"name": "Slovak"
}
],
"translated": 4347,
"target": 3995,
"approved": 3777,
"voted": 0,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 3771
},
{
"user": {
"id": "15709853",
"username": "RJSkudra",
"fullName": "RJS (RJSkudra)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15709853/medium/c3abf2774913dc4e81fb261d36d7668c.png",
"joined": "2023-04-08 13:07:46"
},
"languages": [
{
"id": "lv",
"name": "Latvian"
}
],
"translated": 4280,
"target": 3758,
"approved": 4195,
"voted": 0,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 4184
},
{
"user": {
"id": "16077170",
"username": "Topbcy",
"fullName": "Turbo (Topbcy)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16077170/medium/d3aed33ea56330338756cfcd89477cfe.jpeg",
"joined": "2023-10-29 07:14:20"
},
"languages": [
{
"id": "zh-TW",
"name": "Chinese Traditional"
}
],
"translated": 4171,
"target": 6555,
"approved": 0, "approved": 0,
"voted": 0, "voted": 0,
"positiveVotes": 0, "positiveVotes": 0,
@@ -240,14 +284,36 @@
"name": "Hungarian" "name": "Hungarian"
} }
], ],
"translated": 3734, "translated": 4135,
"target": 3409, "target": 3788,
"approved": 0, "approved": 0,
"voted": 0, "voted": 0,
"positiveVotes": 0, "positiveVotes": 0,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 0 "winning": 0
}, },
{
"user": {
"id": "15617065",
"username": "somerlev",
"fullName": "somerlev",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15617065/medium/f4b13513e311ec902d90b2f718412c55.jpg",
"joined": "2023-01-01 15:03:01"
},
"languages": [
{
"id": "ru",
"name": "Russian"
}
],
"translated": 3866,
"target": 3432,
"approved": 4640,
"voted": 160,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 3655
},
{ {
"user": { "user": {
"id": "15644717", "id": "15644717",
@@ -262,35 +328,35 @@
"name": "Chinese Simplified" "name": "Chinese Simplified"
} }
], ],
"translated": 3296, "translated": 3836,
"target": 5128, "target": 5983,
"approved": 3666, "approved": 4206,
"voted": 1, "voted": 1,
"positiveVotes": 1, "positiveVotes": 1,
"negativeVotes": 2, "negativeVotes": 2,
"winning": 2873 "winning": 3413
}, },
{ {
"user": { "user": {
"id": "15709853", "id": "15677023",
"username": "RJSkudra", "username": "Spillebulle",
"fullName": "RJS (RJSkudra)", "fullName": "Spillebulle",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15709853/medium/c3abf2774913dc4e81fb261d36d7668c.png", "avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15677023/medium/096cf68fccf4b666954a0a57a974af64_default.png",
"joined": "2023-04-08 13:07:46" "joined": "2023-02-08 02:51:18"
}, },
"languages": [ "languages": [
{ {
"id": "lv", "id": "no",
"name": "Latvian" "name": "Norwegian"
} }
], ],
"translated": 3074, "translated": 3234,
"target": 2734, "target": 3063,
"approved": 2987, "approved": 4451,
"voted": 0, "voted": 0,
"positiveVotes": 0, "positiveVotes": 0,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 2980 "winning": 3225
}, },
{ {
"user": { "user": {
@@ -306,14 +372,40 @@
"name": "Vietnamese" "name": "Vietnamese"
} }
], ],
"translated": 2929, "translated": 3001,
"target": 4087, "target": 4174,
"approved": 4, "approved": 23,
"voted": 0, "voted": 0,
"positiveVotes": 0, "positiveVotes": 0,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 4 "winning": 4
}, },
{
"user": {
"id": "15875457",
"username": "raelyan",
"fullName": "Raelyan (raelyan)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15875457/medium/2f4fda1d1aaa5dcc79b328baf3f03151.jpeg",
"joined": "2023-06-14 12:51:04"
},
"languages": [
{
"id": "gl",
"name": "Galician"
},
{
"id": "es-ES",
"name": "Spanish"
}
],
"translated": 2924,
"target": 3268,
"approved": 3791,
"voted": 5,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 2901
},
{ {
"user": { "user": {
"id": "15428592", "id": "15428592",
@@ -336,54 +428,6 @@
"negativeVotes": 0, "negativeVotes": 0,
"winning": 2681 "winning": 2681
}, },
{
"user": {
"id": "15875457",
"username": "raelyan",
"fullName": "Raelyan (raelyan)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15875457/medium/2f4fda1d1aaa5dcc79b328baf3f03151.jpeg",
"joined": "2023-06-14 12:51:04"
},
"languages": [
{
"id": "gl",
"name": "Galician"
},
{
"id": "es-ES",
"name": "Spanish"
}
],
"translated": 2740,
"target": 3061,
"approved": 3553,
"voted": 5,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 2717
},
{
"user": {
"id": "15617065",
"username": "somerlev",
"fullName": "somerlev",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15617065/medium/f4b13513e311ec902d90b2f718412c55.jpg",
"joined": "2023-01-01 15:03:01"
},
"languages": [
{
"id": "ru",
"name": "Russian"
}
],
"translated": 2688,
"target": 2379,
"approved": 2987,
"voted": 160,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 2557
},
{ {
"user": { "user": {
"id": "15419914", "id": "15419914",
@@ -402,8 +446,8 @@
"name": "German" "name": "German"
} }
], ],
"translated": 2474, "translated": 2607,
"target": 2463, "target": 2595,
"approved": 0, "approved": 0,
"voted": 27, "voted": 27,
"positiveVotes": 0, "positiveVotes": 0,
@@ -412,25 +456,25 @@
}, },
{ {
"user": { "user": {
"id": "15677023", "id": "15865139",
"username": "Spillebulle", "username": "Beardy",
"fullName": "Spillebulle", "fullName": "Beardy",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15677023/medium/096cf68fccf4b666954a0a57a974af64_default.png", "avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15865139/medium/fca6b9d2b3f52e286d1568f52b83b6a0_default.png",
"joined": "2023-02-08 02:51:18" "joined": "2023-06-07 06:24:20"
}, },
"languages": [ "languages": [
{ {
"id": "no", "id": "el",
"name": "Norwegian" "name": "Greek"
} }
], ],
"translated": 2342, "translated": 2386,
"target": 2195, "target": 2567,
"approved": 2342, "approved": 0,
"voted": 0, "voted": 3,
"positiveVotes": 0, "positiveVotes": 0,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 2338 "winning": 0
}, },
{ {
"user": { "user": {
@@ -498,28 +542,6 @@
"negativeVotes": 0, "negativeVotes": 0,
"winning": 0 "winning": 0
}, },
{
"user": {
"id": "15865139",
"username": "Beardy",
"fullName": "Beardy",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15865139/medium/fca6b9d2b3f52e286d1568f52b83b6a0_default.png",
"joined": "2023-06-07 06:24:20"
},
"languages": [
{
"id": "el",
"name": "Greek"
}
],
"translated": 1975,
"target": 2118,
"approved": 0,
"voted": 3,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 0
},
{ {
"user": { "user": {
"id": "15149958", "id": "15149958",
@@ -534,11 +556,11 @@
"name": "French" "name": "French"
} }
], ],
"translated": 1720, "translated": 1753,
"target": 1943, "target": 1978,
"approved": 1103, "approved": 1103,
"voted": 20, "voted": 20,
"positiveVotes": 8, "positiveVotes": 16,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 774 "winning": 774
}, },
@@ -659,6 +681,10 @@
"id": "ru", "id": "ru",
"name": "Russian" "name": "Russian"
}, },
{
"id": "sk",
"name": "Slovak"
},
{ {
"id": "sl", "id": "sl",
"name": "Slovenian" "name": "Slovenian"
@@ -671,6 +697,10 @@
"id": "sv-SE", "id": "sv-SE",
"name": "Swedish" "name": "Swedish"
}, },
{
"id": "tr",
"name": "Turkish"
},
{ {
"id": "uk", "id": "uk",
"name": "Ukrainian" "name": "Ukrainian"
@@ -680,12 +710,12 @@
"name": "Vietnamese" "name": "Vietnamese"
} }
], ],
"translated": 1461, "translated": 1576,
"target": 1547, "target": 1691,
"approved": 1463, "approved": 1463,
"voted": 0, "voted": 0,
"positiveVotes": 189, "positiveVotes": 189,
"negativeVotes": 20, "negativeVotes": 21,
"winning": 1215 "winning": 1215
}, },
{ {
@@ -708,7 +738,7 @@
"voted": 0, "voted": 0,
"positiveVotes": 0, "positiveVotes": 0,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 0 "winning": 351
}, },
{ {
"user": { "user": {
@@ -832,6 +862,32 @@
"negativeVotes": 1, "negativeVotes": 1,
"winning": 0 "winning": 0
}, },
{
"user": {
"id": "15977271",
"username": "tagaishi",
"fullName": "tagaishi",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15977271/medium/eade504c83a5a1ff831c80a538fbdb44_default.png",
"joined": "2023-08-22 07:09:16"
},
"languages": [
{
"id": "zh-CN",
"name": "Chinese Simplified"
},
{
"id": "fr",
"name": "French"
}
],
"translated": 588,
"target": 693,
"approved": 0,
"voted": 2,
"positiveVotes": 2,
"negativeVotes": 0,
"winning": 95
},
{ {
"user": { "user": {
"id": "15925879", "id": "15925879",
@@ -850,7 +906,7 @@
"target": 711, "target": 711,
"approved": 0, "approved": 0,
"voted": 1, "voted": 1,
"positiveVotes": 12, "positiveVotes": 16,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 153 "winning": 153
}, },
@@ -940,7 +996,7 @@
"voted": 0, "voted": 0,
"positiveVotes": 0, "positiveVotes": 0,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 198 "winning": 250
}, },
{ {
"user": { "user": {
@@ -986,6 +1042,28 @@
"negativeVotes": 0, "negativeVotes": 0,
"winning": 0 "winning": 0
}, },
{
"user": {
"id": "15454038",
"username": "sebekmartin",
"fullName": "Martin Sebek (sebekmartin)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15454038/medium/bcfb44598cdfd1d7cd4eb35812538962.jpeg",
"joined": "2023-10-08 09:26:03"
},
"languages": [
{
"id": "cs",
"name": "Czech"
}
],
"translated": 393,
"target": 355,
"approved": 0,
"voted": 0,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 0
},
{ {
"user": { "user": {
"id": "13330448", "id": "13330448",
@@ -1030,32 +1108,6 @@
"negativeVotes": 3, "negativeVotes": 3,
"winning": 119 "winning": 119
}, },
{
"user": {
"id": "15977271",
"username": "tagaishi",
"fullName": "tagaishi",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15977271/medium/eade504c83a5a1ff831c80a538fbdb44_default.png",
"joined": "2023-08-22 07:09:16"
},
"languages": [
{
"id": "zh-CN",
"name": "Chinese Simplified"
},
{
"id": "fr",
"name": "French"
}
],
"translated": 328,
"target": 395,
"approved": 0,
"voted": 2,
"positiveVotes": 2,
"negativeVotes": 0,
"winning": 95
},
{ {
"user": { "user": {
"id": "15685239", "id": "15685239",
@@ -1124,22 +1176,22 @@
}, },
{ {
"user": { "user": {
"id": "14949159", "id": "7795",
"username": "f1refa11", "username": "zielmann",
"fullName": "FireFall (f1refa11)", "fullName": "Luke (zielmann)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14949159/medium/fd2ae63b8eb4462200ba96abf943c1b9.png", "avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png",
"joined": "2023-09-06 14:55:13" "joined": "2023-10-12 09:50:59"
}, },
"languages": [ "languages": [
{ {
"id": "ru", "id": "pl",
"name": "Russian" "name": "Polish"
} }
], ],
"translated": 228, "translated": 266,
"target": 203, "target": 258,
"approved": 0, "approved": 0,
"voted": 0, "voted": 7,
"positiveVotes": 0, "positiveVotes": 0,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 0 "winning": 0
@@ -1158,14 +1210,58 @@
"name": "Chinese Simplified" "name": "Chinese Simplified"
} }
], ],
"translated": 210, "translated": 264,
"target": 339, "target": 429,
"approved": 0, "approved": 0,
"voted": 0, "voted": 0,
"positiveVotes": 4, "positiveVotes": 4,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 126 "winning": 126
}, },
{
"user": {
"id": "16084674",
"username": "ai5d02sb",
"fullName": "ai5d02sb",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16084674/medium/7c8119fe2a5ca71bb15f636916a42b95_default.png",
"joined": "2023-11-02 15:47:09"
},
"languages": [
{
"id": "fr",
"name": "French"
}
],
"translated": 264,
"target": 275,
"approved": 0,
"voted": 12,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 0
},
{
"user": {
"id": "14949159",
"username": "f1refa11",
"fullName": "FireFall (f1refa11)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14949159/medium/fd2ae63b8eb4462200ba96abf943c1b9.png",
"joined": "2023-09-06 14:55:13"
},
"languages": [
{
"id": "ru",
"name": "Russian"
}
],
"translated": 228,
"target": 203,
"approved": 0,
"voted": 0,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 134
},
{ {
"user": { "user": {
"id": "13641407", "id": "13641407",
@@ -1208,7 +1304,7 @@
"voted": 0, "voted": 0,
"positiveVotes": 54, "positiveVotes": 54,
"negativeVotes": 3, "negativeVotes": 3,
"winning": 20 "winning": 17
}, },
{ {
"user": { "user": {
@@ -1232,6 +1328,28 @@
"negativeVotes": 3, "negativeVotes": 3,
"winning": 75 "winning": 75
}, },
{
"user": {
"id": "14934947",
"username": "djismgaming",
"fullName": "Ismael (djismgaming)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/14934947/medium/f5a8570713c34ab0f7d5405d105e2a9a.jpeg",
"joined": "2023-11-12 08:36:15"
},
"languages": [
{
"id": "es-ES",
"name": "Spanish"
}
],
"translated": 164,
"target": 181,
"approved": 0,
"voted": 6,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 0
},
{ {
"user": { "user": {
"id": "12580457", "id": "12580457",
@@ -1922,28 +2040,6 @@
"negativeVotes": 0, "negativeVotes": 0,
"winning": 0 "winning": 0
}, },
{
"user": {
"id": "7795",
"username": "zielmann",
"fullName": "Luke (zielmann)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/7795/medium/ad22b8b8d5eb33e4154d53a454c862fd_default.png",
"joined": "2023-10-12 09:50:59"
},
"languages": [
{
"id": "pl",
"name": "Polish"
}
],
"translated": 4,
"target": 4,
"approved": 0,
"voted": 6,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 0
},
{ {
"user": { "user": {
"id": "15643771", "id": "15643771",
@@ -2672,23 +2768,6 @@
"negativeVotes": 0, "negativeVotes": 0,
"winning": 0 "winning": 0
}, },
{
"user": {
"id": "15454038",
"username": "sebekmartin",
"fullName": "Martin Sebek (sebekmartin)",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/15454038/medium/bcfb44598cdfd1d7cd4eb35812538962.jpeg",
"joined": "2023-10-08 09:26:03"
},
"languages": [],
"translated": 0,
"target": 0,
"approved": 0,
"voted": 0,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 0
},
{ {
"user": { "user": {
"id": "16051620", "id": "16051620",
@@ -2727,6 +2806,23 @@
"positiveVotes": 0, "positiveVotes": 0,
"negativeVotes": 0, "negativeVotes": 0,
"winning": 0 "winning": 0
},
{
"user": {
"id": "16097722",
"username": "explosiveparrot",
"fullName": "explosiveparrot",
"avatarUrl": "https://crowdin-static.downloads.crowdin.com/avatar/16097722/medium/7762f80fc1da63f5b2eb87de9d640324_default.png",
"joined": "2023-11-10 21:23:11"
},
"languages": [],
"translated": 0,
"target": 0,
"approved": 0,
"voted": 0,
"positiveVotes": 0,
"negativeVotes": 0,
"winning": 0
} }
] ]
} }

View File

@@ -44,12 +44,14 @@
}, },
"seeMore": "See more...", "seeMore": "See more...",
"position": { "position": {
"left": "Left", "left": "Left",
"center": "Center", "center": "Center",
"right": "Right" "right": "Right"
}, },
"attributes": { "attributes": {
"width": "Width", "width": "Width",
"height": "Height" "height": "Height"
} },
"public": "Public",
"restricted": "Restricted"
} }

View File

@@ -25,7 +25,6 @@ export default function BoardPage({
type BoardGetServerSideProps = { type BoardGetServerSideProps = {
config: ConfigType; config: ConfigType;
dockerEnabled: boolean;
_nextI18Next?: SSRConfig['_nextI18Next']; _nextI18Next?: SSRConfig['_nextI18Next'];
}; };

View File

@@ -18,19 +18,21 @@ import {
IconDeviceFloppy, IconDeviceFloppy,
IconDotsVertical, IconDotsVertical,
IconFolderFilled, IconFolderFilled,
IconLock,
IconLockOff,
IconPlus, IconPlus,
IconStack, IconStack,
IconStarFilled, IconStarFilled,
IconTrash, IconTrash,
} from '@tabler/icons-react'; } from '@tabler/icons-react';
import { GetServerSideProps } from 'next'; import { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next';
import { useSession } from 'next-auth/react';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import Head from 'next/head'; import Head from 'next/head';
import Link from 'next/link'; import Link from 'next/link';
import { openCreateBoardModal } from '~/components/Manage/Board/create-board.modal'; import { openCreateBoardModal } from '~/components/Manage/Board/create-board.modal';
import { openDeleteBoardModal } from '~/components/Manage/Board/delete-board.modal'; import { openDeleteBoardModal } from '~/components/Manage/Board/delete-board.modal';
import { ManageLayout } from '~/components/layout/Templates/ManageLayout'; import { ManageLayout } from '~/components/layout/Templates/ManageLayout';
import { boardRouter } from '~/server/api/routers/board';
import { getServerAuthSession } from '~/server/auth'; import { getServerAuthSession } from '~/server/auth';
import { sleep } from '~/tools/client/time'; import { sleep } from '~/tools/client/time';
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations'; import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
@@ -38,13 +40,18 @@ import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
import { manageNamespaces } from '~/tools/server/translation-namespaces'; import { manageNamespaces } from '~/tools/server/translation-namespaces';
import { api } from '~/utils/api'; import { api } from '~/utils/api';
const BoardsPage = () => { // Infer return type from the `getServerSideProps` function
const context = api.useContext(); export default function BoardsPage({
const { data: sessionData } = useSession(); boards,
const { data } = api.boards.all.useQuery(); session,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
const { data, refetch } = api.boards.all.useQuery(undefined, {
initialData: boards,
cacheTime: 1000 * 60 * 5, // Cache for 5 minutes
});
const { mutateAsync } = api.user.makeDefaultDashboard.useMutation({ const { mutateAsync } = api.user.makeDefaultDashboard.useMutation({
onSettled: () => { onSettled: () => {
void context.boards.invalidate(); refetch();
}, },
}); });
@@ -62,7 +69,7 @@ const BoardsPage = () => {
<Group position="apart"> <Group position="apart">
<Title mb="xl">{t('pageTitle')}</Title> <Title mb="xl">{t('pageTitle')}</Title>
{sessionData?.user.isAdmin && ( {session?.user.isAdmin && (
<Button <Button
onClick={openCreateBoardModal} onClick={openCreateBoardModal}
leftIcon={<IconPlus size="1rem" />} leftIcon={<IconPlus size="1rem" />}
@@ -73,153 +80,167 @@ const BoardsPage = () => {
)} )}
</Group> </Group>
{data && ( <SimpleGrid
<SimpleGrid cols={3}
cols={3} spacing="lg"
spacing="lg" breakpoints={[
breakpoints={[ { maxWidth: '62rem', cols: 2, spacing: 'lg' },
{ maxWidth: '62rem', cols: 2, spacing: 'lg' }, { maxWidth: '48rem', cols: 1, spacing: 'lg' },
{ maxWidth: '48rem', cols: 1, spacing: 'lg' }, ]}
]} >
> {data.map((board, index) => (
{data.map((board, index) => ( <Card key={index} shadow="sm" padding="lg" radius="md" pos="relative" withBorder>
<Card key={index} shadow="sm" padding="lg" radius="md" pos="relative" withBorder> <LoadingOverlay visible={deletingDashboards.includes(board.name)} />
<LoadingOverlay visible={deletingDashboards.includes(board.name)} />
<Group mb="xl" position="apart" noWrap> <Group mb="xl" position="apart" noWrap>
<Text weight={500} mb="xs"> <Text weight={500} mb="xs">
{board.name} {board.name}
</Text> </Text>
<Group spacing="xs" noWrap> <Group spacing="xs" noWrap>
<Badge leftSection={<IconFolderFilled size=".7rem" />} color="pink" variant="light">
{t('cards.badges.fileSystem')}
</Badge>
<Badge
leftSection={
board.allowGuests ? <IconLock size=".7rem" /> : <IconLockOff size=".7rem" />
}
color="green"
variant="light"
>
{board.allowGuests ? t('common:public') : t('common:restricted')}
</Badge>
{board.isDefaultForUser && (
<Badge <Badge
leftSection={<IconFolderFilled size=".7rem" />} leftSection={<IconStarFilled size=".7rem" />}
color="pink" color="yellow"
variant="light" variant="light"
> >
{t('cards.badges.fileSystem')} {t('cards.badges.default')}
</Badge> </Badge>
{board.isDefaultForUser && ( )}
<Badge </Group>
leftSection={<IconStarFilled size=".7rem" />} </Group>
color="yellow"
variant="light" <Stack spacing={3}>
> <Group position="apart">
{t('cards.badges.default')} <Group spacing="xs">
</Badge> <IconBox opacity={0.7} size="1rem" />
<Text color="dimmed">{t('cards.statistics.apps')}</Text>
</Group>
<Text>{board.countApps}</Text>
</Group>
<Group position="apart">
<Group spacing="xs">
<IconStack opacity={0.7} size="1rem" />
<Text color="dimmed">{t('cards.statistics.widgets')}</Text>
</Group>
<Text>{board.countWidgets}</Text>
</Group>
<Group position="apart">
<Group spacing="xs">
<IconCategory opacity={0.7} size="1rem" />
<Text color="dimmed">{t('cards.statistics.categories')}</Text>
</Group>
<Text>{board.countCategories}</Text>
</Group>
</Stack>
<Group mt="md">
<Button
component={Link}
style={{ flexGrow: 1 }}
variant="default"
color="blue"
radius="md"
href={`/board/${board.name}`}
>
{t('cards.buttons.view')}
</Button>
<Menu width={240} withinPortal position="bottom-end">
<Menu.Target>
<ActionIcon h={34} w={34} variant="default">
<IconDotsVertical size="1rem" />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
icon={<IconDeviceFloppy size="1rem" />}
onClick={async () => {
void mutateAsync({
board: board.name,
});
}}
>
<Text size="sm">{t('cards.menu.setAsDefault')}</Text>
</Menu.Item>
{session?.user.isAdmin && (
<>
<Menu.Divider />
<Menu.Item
onClick={async () => {
openDeleteBoardModal({
boardName: board.name,
onConfirm: async () => {
append(board.name);
// give user feedback, that it's being deleted
await sleep(500);
filter((item, _) => item !== board.name);
},
});
}}
disabled={board.name === 'default'}
icon={<IconTrash size="1rem" />}
color="red"
>
<Text size="sm">{t('cards.menu.delete.label')}</Text>
{board.name === 'default' && (
<Text size="xs">{t('cards.menu.delete.disabled')}</Text>
)}
</Menu.Item>
</>
)} )}
</Group> </Menu.Dropdown>
</Group> </Menu>
</Group>
<Stack spacing={3}> </Card>
<Group position="apart"> ))}
<Group spacing="xs"> </SimpleGrid>
<IconBox opacity={0.7} size="1rem" />
<Text color="dimmed">{t('cards.statistics.apps')}</Text>
</Group>
<Text>{board.countApps}</Text>
</Group>
<Group position="apart">
<Group spacing="xs">
<IconStack opacity={0.7} size="1rem" />
<Text color="dimmed">{t('cards.statistics.widgets')}</Text>
</Group>
<Text>{board.countWidgets}</Text>
</Group>
<Group position="apart">
<Group spacing="xs">
<IconCategory opacity={0.7} size="1rem" />
<Text color="dimmed">{t('cards.statistics.categories')}</Text>
</Group>
<Text>{board.countCategories}</Text>
</Group>
</Stack>
<Group mt="md">
<Button
component={Link}
style={{ flexGrow: 1 }}
variant="default"
color="blue"
radius="md"
href={`/board/${board.name}`}
>
{t('cards.buttons.view')}
</Button>
<Menu width={240} withinPortal position="bottom-end">
<Menu.Target>
<ActionIcon h={34} w={34} variant="default">
<IconDotsVertical size="1rem" />
</ActionIcon>
</Menu.Target>
<Menu.Dropdown>
<Menu.Item
icon={<IconDeviceFloppy size="1rem" />}
onClick={async () => {
void mutateAsync({
board: board.name,
});
}}
>
<Text size="sm">{t('cards.menu.setAsDefault')}</Text>
</Menu.Item>
{sessionData?.user.isAdmin && (
<>
<Menu.Divider />
<Menu.Item
onClick={async () => {
openDeleteBoardModal({
boardName: board.name,
onConfirm: async () => {
append(board.name);
// give user feedback, that it's being deleted
await sleep(500);
filter((item, _) => item !== board.name);
},
});
}}
disabled={board.name === 'default'}
icon={<IconTrash size="1rem" />}
color="red"
>
<Text size="sm">{t('cards.menu.delete.label')}</Text>
{board.name === 'default' && (
<Text size="xs">{t('cards.menu.delete.disabled')}</Text>
)}
</Menu.Item>
</>
)}
</Menu.Dropdown>
</Menu>
</Group>
</Card>
))}
</SimpleGrid>
)}
</ManageLayout> </ManageLayout>
); );
}; }
export const getServerSideProps: GetServerSideProps = async (ctx) => { export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const session = await getServerAuthSession(ctx); const session = await getServerAuthSession({ req: context.req, res: context.res });
const result = checkForSessionOrAskForLogin(
const result = checkForSessionOrAskForLogin(ctx, session, () => true); context,
if (result) { session,
() => session?.user.isAdmin == true
);
if (result !== undefined) {
return result; return result;
} }
const caller = boardRouter.createCaller({
session: session,
cookies: context.req.cookies,
});
const boards = await caller.all();
const translations = await getServerSideTranslations( const translations = await getServerSideTranslations(
manageNamespaces, manageNamespaces,
ctx.locale, context.locale,
ctx.req, context.req,
ctx.res context.res
); );
return { return {
props: { props: {
boards,
session,
...translations, ...translations,
}, },
}; };
}; };
export default BoardsPage;

View File

@@ -25,6 +25,7 @@ export const boardRouter = createTRPCRouter({
return { return {
name: name, name: name,
allowGuests: config.settings.access.allowGuests,
countApps: countApps, countApps: countApps,
countWidgets: config.widgets.length, countWidgets: config.widgets.length,
countCategories: config.categories.length, countCategories: config.categories.length,

View File

@@ -1,8 +1,8 @@
import { import {
GetServerSideProps,
GetServerSidePropsContext, GetServerSidePropsContext,
GetServerSidePropsResult, GetServerSidePropsResult,
PreviewData, PreviewData,
Redirect
} from 'next'; } from 'next';
import { Session } from 'next-auth'; import { Session } from 'next-auth';
@@ -13,13 +13,12 @@ export const checkForSessionOrAskForLogin = (
context: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>, context: GetServerSidePropsContext<ParsedUrlQuery, PreviewData>,
session: Session | null, session: Session | null,
accessCallback: () => boolean accessCallback: () => boolean
): GetServerSidePropsResult<any> | undefined => { ): GetServerSidePropsResult<never> | undefined => {
const permitted = accessCallback(); const permitted = accessCallback();
// user is logged in but does not have the required access // user is logged in but does not have the required access
if (session?.user && !permitted) { if (session?.user && !permitted) {
return { return {
props: {},
redirect: { redirect: {
destination: '/401', destination: '/401',
permanent: false permanent: false
@@ -34,7 +33,6 @@ export const checkForSessionOrAskForLogin = (
// user is logged out and needs to sign in // user is logged out and needs to sign in
return { return {
props: {},
redirect: { redirect: {
destination: `/auth/login?redirectAfterLogin=${context.resolvedUrl}`, destination: `/auth/login?redirectAfterLogin=${context.resolvedUrl}`,
permanent: false, permanent: false,

View File

@@ -157,7 +157,6 @@ describe('[slug] page', () => {
destination: '/auth/login?redirectAfterLogin=/board/my-authentication-board', destination: '/auth/login?redirectAfterLogin=/board/my-authentication-board',
permanent: false, permanent: false,
}, },
props: {},
}); });
expect(serverAuthModule.getServerAuthSession).toHaveBeenCalledOnce(); expect(serverAuthModule.getServerAuthSession).toHaveBeenCalledOnce();
expect(configExistsModule.configExists).toHaveBeenCalledOnce(); expect(configExistsModule.configExists).toHaveBeenCalledOnce();