diff --git a/.docker/squash-images.fish b/.docker/squash-images.fish new file mode 100755 index 00000000..856e8de0 --- /dev/null +++ b/.docker/squash-images.fish @@ -0,0 +1,17 @@ +#!/usr/bin/fish + +# Shitty image opt script +# Delete once we have proper stuff + +source (which env_parallel.fish) + +set files (find . -name '*.png' -print0 | string split0) + +echo Will process (count $files) images. + +function process + oxipng -o max --zopfli --zi 50 --ng --strip all $argv + ect -9 -strip --allfilters-b $argv +end + +env_parallel --env process process ::: $files \ No newline at end of file diff --git a/apps/juxtaposition-ui/package.json b/apps/juxtaposition-ui/package.json index 66c3e485..0506cd43 100644 --- a/apps/juxtaposition-ui/package.json +++ b/apps/juxtaposition-ui/package.json @@ -32,6 +32,7 @@ "express-session": "^1.19.0", "express-subdomain": "^1.0.6", "hashmap": "^2.4.0", + "i18next": "^25.8.13", "luxon": "^3.7.2", "method-override": "^3.0.0", "moment": "^2.30.1", @@ -48,13 +49,14 @@ "prom-client": "^15.1.3", "react": "^19.1.1", "react-dom": "^19.2.4", + "react-i18next": "^16.5.4", "redis": "^5.10.0", "tsx": "^4.21.0", "zod": "^4.3.6" }, "devDependencies": { "@pretendonetwork/cave-types": "^1.0.2", - "@pretendonetwork/eslint-config": "^0.1.3", + "@pretendonetwork/eslint-config": "^0.1.4", "@pretendonetwork/wiiu-browser-types": "^1.0.0", "@pretendonetwork/wiiu-dialog-types": "^1.0.0", "@pretendonetwork/wiiu-error-viewer-types": "^1.0.0", @@ -73,9 +75,9 @@ "esbuild-fix-imports-plugin": "^1.0.23", "esbuild-plugin-copy": "^2.1.1", "esbuild-raw-plugin": "^0.3.1", - "eslint": "^9.39.2", - "globals": "^17.3.0", - "nodemon": "^3.1.11", + "eslint": "^9.39.3", + "globals": "^17.4.0", + "nodemon": "^3.1.14", "npm-run-all": "^4.1.5", "tsup": "^8.5.1", "typescript": "^5.9.3" diff --git a/apps/juxtaposition-ui/src/translations/ar.json b/apps/juxtaposition-ui/src/assets/locales/ar.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/ar.json rename to apps/juxtaposition-ui/src/assets/locales/ar.json diff --git a/apps/juxtaposition-ui/src/translations/ast.json b/apps/juxtaposition-ui/src/assets/locales/ast.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/ast.json rename to apps/juxtaposition-ui/src/assets/locales/ast.json diff --git a/apps/juxtaposition-ui/src/translations/be.json b/apps/juxtaposition-ui/src/assets/locales/be.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/be.json rename to apps/juxtaposition-ui/src/assets/locales/be.json diff --git a/apps/juxtaposition-ui/src/translations/ca.json b/apps/juxtaposition-ui/src/assets/locales/ca.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/ca.json rename to apps/juxtaposition-ui/src/assets/locales/ca.json diff --git a/apps/juxtaposition-ui/src/translations/cs.json b/apps/juxtaposition-ui/src/assets/locales/cs.json similarity index 95% rename from apps/juxtaposition-ui/src/translations/cs.json rename to apps/juxtaposition-ui/src/assets/locales/cs.json index 2f67fe3a..74da60e8 100644 --- a/apps/juxtaposition-ui/src/translations/cs.json +++ b/apps/juxtaposition-ui/src/assets/locales/cs.json @@ -86,7 +86,7 @@ "sixth": "Nezveřejňujte osobní informace — Vaše i jiných", "first": "Následující pravidla jsou důležitými pokyny, které z Juxtu dělají zábavným a příjemným místem pro každého. Pravidla Juxtu obsahují detailní informace, přečtěte si je, prosím, pozorně.", "eighth": "Dejte pozor na spoilery", - "seventh": "Pamatujte, že znát někoho na Juxtu není to samé, jako je znát v reálném světě. Nikdy na Juxtu nesdílejte e-mailovou adresu, bydliště, jméno pracoviště, školy nebo jiné osobní údaje o Vás nebo jiných lidech. Dále také nepříjímejte pozvánky ke schůzkám v reálném světě. Juxt je online komunita a neměla by být používána k plánování schůzek v reálném světě.", + "seventh": "Pamatujte, že znát někoho na Juxtu není to samé jako znát někoho v reálném světě. Nikdy na Juxtu nesdílejte e-mailovou adresu, bydliště, kde pracujete, kam chodíte do školy nebo jiné osobní údaje o Vás nebo jiných lidech. Dále také nepříjímejte pozvánky ke schůzkám v reálném světě. Juxt je online komunita a neměla by být používána k plánování schůzek v reálném světě.", "twelfth": "Hrál(a) jste tuto hru?" }, "beta_text": { @@ -106,7 +106,7 @@ "ready": "Jste připraven(a) začít s Juxtem", "guest_text": "Váš účet v tuto chvíli nemůžeme ověřit, což nejspíš znamená, že jste přihlášen(a) účtem Nintendo Network. Stále můžete prohlížet Juxt, ale nebudte moct Yeah!ovat příspěvky nebo přidávat své vlastní, dokud se nepřihlásíte s účtem Pretendo Network. Pro více informací nahlédněte do Discord serveru.", "experience": "Zkušenost s hrami", - "analytics": "Analytics", + "analytics": "Analytické údaje", "rules": "Pravidla Juxtu", "welcome_text": "Juxt je herní komunita, která propojuje lidi z celého světa prostřednictvím Mii postav. Použijte Juxt pro sdílení Vašich herních zážitků a poznejte lidi z celého světa.", "analytics_text": "Služba Juxtaposition používá Cloudflare Web Analytics, aby sledovala, jak ji lidé používají. Tato data zahrnují, ale nejsou omezena na: čas přístupu, navštívené stránky a čas strávený na síti. Tato data nejsou spojena s Vaší identitou a nejsou používána Cloudflare ani námi pro účely reklamy.", diff --git a/apps/juxtaposition-ui/src/translations/cy.json b/apps/juxtaposition-ui/src/assets/locales/cy.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/cy.json rename to apps/juxtaposition-ui/src/assets/locales/cy.json diff --git a/apps/juxtaposition-ui/src/translations/da.json b/apps/juxtaposition-ui/src/assets/locales/da.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/da.json rename to apps/juxtaposition-ui/src/assets/locales/da.json diff --git a/apps/juxtaposition-ui/src/translations/de.json b/apps/juxtaposition-ui/src/assets/locales/de.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/de.json rename to apps/juxtaposition-ui/src/assets/locales/de.json diff --git a/apps/juxtaposition-ui/src/translations/el.json b/apps/juxtaposition-ui/src/assets/locales/el.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/el.json rename to apps/juxtaposition-ui/src/assets/locales/el.json diff --git a/apps/juxtaposition-ui/src/translations/en.json b/apps/juxtaposition-ui/src/assets/locales/en.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/en.json rename to apps/juxtaposition-ui/src/assets/locales/en.json diff --git a/apps/juxtaposition-ui/src/translations/en@uwu.json b/apps/juxtaposition-ui/src/assets/locales/en@uwu.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/en@uwu.json rename to apps/juxtaposition-ui/src/assets/locales/en@uwu.json diff --git a/apps/juxtaposition-ui/src/translations/eo.json b/apps/juxtaposition-ui/src/assets/locales/eo.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/eo.json rename to apps/juxtaposition-ui/src/assets/locales/eo.json diff --git a/apps/juxtaposition-ui/src/translations/es.json b/apps/juxtaposition-ui/src/assets/locales/es.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/es.json rename to apps/juxtaposition-ui/src/assets/locales/es.json diff --git a/apps/juxtaposition-ui/src/assets/locales/eu.json b/apps/juxtaposition-ui/src/assets/locales/eu.json new file mode 100644 index 00000000..fc26d88a --- /dev/null +++ b/apps/juxtaposition-ui/src/assets/locales/eu.json @@ -0,0 +1,126 @@ +{ + "language": "Euskara", + "global": { + "user_page": "Erabiltzailearen orria", + "activity_feed": "Jarduera", + "communities": "Komunitateak", + "messages": "Mezuak", + "notifications": "Jakinarazpenak", + "go_back": "Atzera joan", + "back": "Atzera", + "save": "Gorde", + "more": "Argitarapen gehiago erakutsi", + "no_posts": "Ez dago argitarapenik", + "private": "Pribatua", + "close": "Itxi", + "exit": "Irten", + "next": "Hurrengoa", + "yeahs": "Datseginak" + }, + "all_communities": { + "text": "Komunitate denak", + "announcements": "Iragarpenak", + "ann_string": "Hemen klik egin notizia berriak ikusteko!", + "popular_places": "Toki ospetsuak", + "new_communities": "Komunitate berriak", + "show_all": "Dena erakutsi", + "search": "Komunitateak bilatu..." + }, + "community": { + "follow": "Komunitatea Jarraitu", + "following": "Jarraitzen", + "followers": "Jarraitzaileak", + "posts": "Argitalpenak", + "tags": "Etiketak", + "recent": "Publikazio berriak", + "popular": "Argitaralpen ospetsuak", + "verified": "Egiaztatutako argitaralpenak", + "new": "Argitalpen berriak" + }, + "user_page": { + "country": "Herrialdea", + "birthday": "Urtebetetzea", + "game_experience": "Joko mailak", + "friends": "Lagunak", + "following": "Jarraitzen", + "followers": "Jarraitzaileak", + "follow_user": "", + "no_following": "", + "befriend": "", + "posts": "", + "following_user": "", + "pending": "", + "unfriend": "", + "no_followers": "", + "no_friends": "" + }, + "setup": { + "rules_text": { + "eleventh": "", + "tenth": "", + "third": "", + "thirteenth": "", + "fifth": "", + "ninth": "", + "fourth": "", + "second": "", + "sixth": "", + "first": "", + "eighth": "", + "seventh": "", + "twelfth": "" + }, + "beta_text": { + "third": "", + "second": "", + "first": "" + }, + "experience_text": { + "expert": "", + "info": "", + "intermediate": "", + "beginner": "" + }, + "guest_button": "", + "analytics": "", + "done_button": "", + "guest": "", + "ready": "", + "guest_text": "", + "experience": "", + "rules": "", + "welcome_text": "", + "analytics_text": "", + "info": "", + "info_text": "", + "ready_text": "", + "done": "", + "beta": "", + "welcome": "" + }, + "notifications": { + "none": "", + "new_follower": "" + }, + "new_post": { + "swearing": "", + "text_hint": "", + "new_post_text": "", + "post_to": "" + }, + "user_settings": { + "show_game": "", + "show_comment": "", + "show_birthday": "", + "profile_settings": "", + "profile_comment": "", + "swearing": "", + "show_country": "" + }, + "activity_feed": { + "empty": "" + }, + "messages": { + "coming_soon": "" + } +} diff --git a/apps/juxtaposition-ui/src/translations/fi.json b/apps/juxtaposition-ui/src/assets/locales/fi.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/fi.json rename to apps/juxtaposition-ui/src/assets/locales/fi.json diff --git a/apps/juxtaposition-ui/src/translations/fr.json b/apps/juxtaposition-ui/src/assets/locales/fr.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/fr.json rename to apps/juxtaposition-ui/src/assets/locales/fr.json diff --git a/apps/juxtaposition-ui/src/translations/fr_CA.json b/apps/juxtaposition-ui/src/assets/locales/fr_CA.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/fr_CA.json rename to apps/juxtaposition-ui/src/assets/locales/fr_CA.json diff --git a/apps/juxtaposition-ui/src/translations/ga.json b/apps/juxtaposition-ui/src/assets/locales/ga.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/ga.json rename to apps/juxtaposition-ui/src/assets/locales/ga.json diff --git a/apps/juxtaposition-ui/src/translations/gl.json b/apps/juxtaposition-ui/src/assets/locales/gl.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/gl.json rename to apps/juxtaposition-ui/src/assets/locales/gl.json diff --git a/apps/juxtaposition-ui/src/translations/he.json b/apps/juxtaposition-ui/src/assets/locales/he.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/he.json rename to apps/juxtaposition-ui/src/assets/locales/he.json diff --git a/apps/juxtaposition-ui/src/translations/hr.json b/apps/juxtaposition-ui/src/assets/locales/hr.json similarity index 98% rename from apps/juxtaposition-ui/src/translations/hr.json rename to apps/juxtaposition-ui/src/assets/locales/hr.json index a7664b1b..058e1b18 100644 --- a/apps/juxtaposition-ui/src/translations/hr.json +++ b/apps/juxtaposition-ui/src/assets/locales/hr.json @@ -73,7 +73,7 @@ "text_hint": "Dodirni ovdje za izradu objave." }, "messages": { - "coming_soon": "Poruke još ne radi. Navrati kasnije!" + "coming_soon": "Nema poruka" }, "setup": { "welcome": "Dobro došli u Juxtaposition!", @@ -108,7 +108,7 @@ "intermediate": "Napredni", "expert": "Profesionalac" }, - "analytics": "Analytics", + "analytics": "Analitika", "analytics_text": "Juxtaposition koristi Cloudflare Web Analytics kako bi pratio način na koji korisnici koriste našu uslugu. Prikupljeni podaci uključuju, ali nisu ograničeni na vrijeme posjete, posjećene stranice i provedeno vrijeme na web stranici. Ovi podaci nisu na nikoji način s tobom povezani i mi niti Cloudflare ih ne koristimo za oglase.", "ready": "Sve je spremno za početak korištenja Juxta", "ready_text": "Najprije pogledaj neke zajednice i vidi što ljudi iz cijelog svijeta objavljuju. Iskoristi ovu priliku da upoznaš Juxt. Možda ćeš otkriti nešto novo!", diff --git a/apps/juxtaposition-ui/src/translations/hu.json b/apps/juxtaposition-ui/src/assets/locales/hu.json similarity index 99% rename from apps/juxtaposition-ui/src/translations/hu.json rename to apps/juxtaposition-ui/src/assets/locales/hu.json index 4a855452..7e11d2ca 100644 --- a/apps/juxtaposition-ui/src/translations/hu.json +++ b/apps/juxtaposition-ui/src/assets/locales/hu.json @@ -108,7 +108,7 @@ "intermediate": "Középszint", "expert": "Profi" }, - "analytics": "Analytics", + "analytics": "Analitika", "ready": "Készen állsz használni a Juxt-ot", "ready_text": "Először nézz meg néhány közösséget, és nézd meg, miről posztolnak az emberek a világ minden tájáról. Használd ki az alkalmat, hogy megismerkedj a Juxttal. Útközben új felfedezéseket tehetsz!", "done": "Jó szórakozást a Juxt-ban!", diff --git a/apps/juxtaposition-ui/src/translations/id.json b/apps/juxtaposition-ui/src/assets/locales/id.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/id.json rename to apps/juxtaposition-ui/src/assets/locales/id.json diff --git a/apps/juxtaposition-ui/src/translations/it.json b/apps/juxtaposition-ui/src/assets/locales/it.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/it.json rename to apps/juxtaposition-ui/src/assets/locales/it.json diff --git a/apps/juxtaposition-ui/src/translations/ja.json b/apps/juxtaposition-ui/src/assets/locales/ja.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/ja.json rename to apps/juxtaposition-ui/src/assets/locales/ja.json diff --git a/apps/juxtaposition-ui/src/translations/kk.json b/apps/juxtaposition-ui/src/assets/locales/kk.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/kk.json rename to apps/juxtaposition-ui/src/assets/locales/kk.json diff --git a/apps/juxtaposition-ui/src/translations/ko.json b/apps/juxtaposition-ui/src/assets/locales/ko.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/ko.json rename to apps/juxtaposition-ui/src/assets/locales/ko.json diff --git a/apps/juxtaposition-ui/src/translations/lt.json b/apps/juxtaposition-ui/src/assets/locales/lt.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/lt.json rename to apps/juxtaposition-ui/src/assets/locales/lt.json diff --git a/apps/juxtaposition-ui/src/translations/lv.json b/apps/juxtaposition-ui/src/assets/locales/lv.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/lv.json rename to apps/juxtaposition-ui/src/assets/locales/lv.json diff --git a/apps/juxtaposition-ui/src/translations/nl.json b/apps/juxtaposition-ui/src/assets/locales/nl.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/nl.json rename to apps/juxtaposition-ui/src/assets/locales/nl.json diff --git a/apps/juxtaposition-ui/src/translations/pl.json b/apps/juxtaposition-ui/src/assets/locales/pl.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/pl.json rename to apps/juxtaposition-ui/src/assets/locales/pl.json diff --git a/apps/juxtaposition-ui/src/translations/pt.json b/apps/juxtaposition-ui/src/assets/locales/pt.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/pt.json rename to apps/juxtaposition-ui/src/assets/locales/pt.json diff --git a/apps/juxtaposition-ui/src/translations/pt_PT.json b/apps/juxtaposition-ui/src/assets/locales/pt_PT.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/pt_PT.json rename to apps/juxtaposition-ui/src/assets/locales/pt_PT.json diff --git a/apps/juxtaposition-ui/src/translations/pt_br.json b/apps/juxtaposition-ui/src/assets/locales/pt_br.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/pt_br.json rename to apps/juxtaposition-ui/src/assets/locales/pt_br.json diff --git a/apps/juxtaposition-ui/src/translations/ro.json b/apps/juxtaposition-ui/src/assets/locales/ro.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/ro.json rename to apps/juxtaposition-ui/src/assets/locales/ro.json diff --git a/apps/juxtaposition-ui/src/translations/ru.json b/apps/juxtaposition-ui/src/assets/locales/ru.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/ru.json rename to apps/juxtaposition-ui/src/assets/locales/ru.json diff --git a/apps/juxtaposition-ui/src/translations/sk.json b/apps/juxtaposition-ui/src/assets/locales/sk.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/sk.json rename to apps/juxtaposition-ui/src/assets/locales/sk.json diff --git a/apps/juxtaposition-ui/src/translations/sr.json b/apps/juxtaposition-ui/src/assets/locales/sr.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/sr.json rename to apps/juxtaposition-ui/src/assets/locales/sr.json diff --git a/apps/juxtaposition-ui/src/translations/sv.json b/apps/juxtaposition-ui/src/assets/locales/sv.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/sv.json rename to apps/juxtaposition-ui/src/assets/locales/sv.json diff --git a/apps/juxtaposition-ui/src/translations/ta.json b/apps/juxtaposition-ui/src/assets/locales/ta.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/ta.json rename to apps/juxtaposition-ui/src/assets/locales/ta.json diff --git a/apps/juxtaposition-ui/src/assets/locales/tok.json b/apps/juxtaposition-ui/src/assets/locales/tok.json new file mode 100644 index 00000000..c76e52d4 --- /dev/null +++ b/apps/juxtaposition-ui/src/assets/locales/tok.json @@ -0,0 +1,126 @@ +{ + "setup": { + "rules_text": { + "eleventh": "", + "tenth": "", + "third": "", + "thirteenth": "", + "fifth": "", + "ninth": "", + "fourth": "", + "second": "", + "sixth": "", + "first": "", + "eighth": "", + "seventh": "", + "twelfth": "" + }, + "beta_text": { + "third": "", + "second": "", + "first": "" + }, + "experience_text": { + "expert": "", + "info": "", + "intermediate": "", + "beginner": "" + }, + "guest_button": "", + "analytics": "", + "done_button": "", + "guest": "", + "ready": "", + "guest_text": "", + "experience": "", + "rules": "", + "welcome_text": "", + "analytics_text": "", + "info": "", + "info_text": "", + "ready_text": "", + "done": "", + "beta": "", + "welcome": "" + }, + "global": { + "communities": "", + "more": "", + "next": "", + "yeahs": "", + "messages": "", + "activity_feed": "", + "exit": "", + "back": "", + "notifications": "", + "save": "", + "no_posts": "", + "private": "", + "close": "", + "user_page": "", + "go_back": "" + }, + "notifications": { + "none": "", + "new_follower": "" + }, + "user_page": { + "follow_user": "", + "game_experience": "", + "no_following": "", + "befriend": "", + "posts": "", + "friends": "", + "following_user": "", + "pending": "", + "followers": "", + "unfriend": "", + "no_followers": "", + "no_friends": "", + "birthday": "", + "country": "", + "following": "" + }, + "new_post": { + "swearing": "", + "text_hint": "", + "new_post_text": "", + "post_to": "" + }, + "community": { + "new": "", + "posts": "", + "tags": "", + "followers": "", + "following": "", + "follow": "", + "verified": "", + "recent": "", + "popular": "" + }, + "user_settings": { + "show_game": "", + "show_comment": "", + "show_birthday": "", + "profile_settings": "", + "profile_comment": "", + "swearing": "", + "show_country": "" + }, + "all_communities": { + "show_all": "", + "announcements": "", + "search": "", + "text": "", + "ann_string": "", + "new_communities": "", + "popular_places": "" + }, + "language": "", + "activity_feed": { + "empty": "" + }, + "messages": { + "coming_soon": "" + } +} diff --git a/apps/juxtaposition-ui/src/translations/tr.json b/apps/juxtaposition-ui/src/assets/locales/tr.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/tr.json rename to apps/juxtaposition-ui/src/assets/locales/tr.json diff --git a/apps/juxtaposition-ui/src/translations/uk.json b/apps/juxtaposition-ui/src/assets/locales/uk.json similarity index 100% rename from apps/juxtaposition-ui/src/translations/uk.json rename to apps/juxtaposition-ui/src/assets/locales/uk.json diff --git a/apps/juxtaposition-ui/src/translations/zh.json b/apps/juxtaposition-ui/src/assets/locales/zh.json similarity index 99% rename from apps/juxtaposition-ui/src/translations/zh.json rename to apps/juxtaposition-ui/src/assets/locales/zh.json index cd185986..27a0d251 100644 --- a/apps/juxtaposition-ui/src/translations/zh.json +++ b/apps/juxtaposition-ui/src/assets/locales/zh.json @@ -1,5 +1,5 @@ { - "language": "中文(简体)", + "language": "简体中文", "global": { "user_page": "用户页面", "activity_feed": "活动提要", @@ -38,7 +38,7 @@ "new": "新建帖子" }, "user_page": { - "country": "国家", + "country": "国家/地区", "birthday": "生日", "game_experience": "游戏经验", "posts": "帖子", diff --git a/apps/juxtaposition-ui/src/translations/zh_Hant.json b/apps/juxtaposition-ui/src/assets/locales/zh_Hant.json similarity index 55% rename from apps/juxtaposition-ui/src/translations/zh_Hant.json rename to apps/juxtaposition-ui/src/assets/locales/zh_Hant.json index ca5d7bd8..83d87936 100644 --- a/apps/juxtaposition-ui/src/translations/zh_Hant.json +++ b/apps/juxtaposition-ui/src/assets/locales/zh_Hant.json @@ -4,40 +4,40 @@ "user_page": "個人檔案", "exit": "離開", "activity_feed": "動態消息", - "messages": "聊天訊息", - "notifications": "通知中心", + "messages": "訊息", + "notifications": "通知", "communities": "社群", - "go_back": "返回上層", - "yeahs": "我贊同", + "go_back": "返回", + "yeahs": "讚同", "back": "返回", - "more": "載入更多投稿", + "more": "載入更多貼文", "private": "私人", "next": "繼續", - "no_posts": "查無投稿", + "no_posts": "沒有貼文", "close": "關閉", "save": "儲存" }, "all_communities": { "text": "所有社群", "announcements": "公告", - "ann_string": "點選此處查看近期公告!", + "ann_string": "點觸此處查看最近公告!", "popular_places": "熱門社群", "new_communities": "新設社群", "show_all": "顯示所有", "search": "搜尋社群……" }, "activity_feed": { - "empty": "這裡空空如也。先去跟隨其他人吧!" + "empty": "這裡空空如也。先去追蹤其他人吧!" }, "messages": { - "coming_soon": "聊天功能尚未準備就緒。敬請期待!" + "coming_soon": "沒有訊息" }, "setup": { "welcome": "歡迎來到 Juxtaposition!", "beta": "《Beta 免責聲明》", "beta_text": { - "first": "您即將使用之 Juxt 首個公開測試 Beta 版本,即知曉許多功能仍未開發完全,且可能隨時變動。", - "second": "您應知曉 Beta 期間之資料庫數據,日後可能遭到擦除。", + "first": "歡迎來到 Pretendo Network 公開測試 Beta 版!雖然與第一個公開測試 Beta 版相比已經發生了許多變化,Juxt 仍處於開發階段,隨時都可能發生變化。", + "second": "這可能包括在 Beta 測試階段結束時或測試期間完全清除資料庫。", "third": "本網站及其軟體與附屬內容,均基於「現況性」與「可提供性」為開展。Pretendo Network 絕不保證(無論其明示抑或暗示)本網站及其軟體與附屬內容之合用性或可用性。" }, "info": "什麼是 Juxtaposition?", @@ -45,18 +45,18 @@ "rules": "《Juxt 社群守則》", "rules_text": { "first": "為了讓 Juxt 的大家都能享受樂趣,敬請詳閱《Juxt 社群守則》。以下將羅列數條為務必遵守的規範。", - "second": "投稿應全球皆宜", + "second": "貼文可在世界各地查看", "fourth": "人們應彼此共好", - "fifth": "為了讓 Juxt 的大家享受到樂趣,請您多對其他使用者關愛與包容。不投稿不適當或冒犯性的內容,和我們一同保持 Juxt 的優質氛圍。", - "sixth": "切勿投稿您或他人之個人資料", - "eighth": "切勿投稿劇透內容", + "fifth": "為了讓 Juxt 的大家享受到樂趣,請您多對其他使用者關愛與包容。不要發布不適當或冒犯性的內容,和我們一同保持 Juxt 的優質氛圍。", + "sixth": "切勿發布您或其他人的個人資料", + "eighth": "切勿發布劇透內容", "tenth": "違反《守則》懲處", "eleventh": "我們的目標是讓 Juxt 的大家都能享受樂趣。如果有人違反《Juxt 社群守則》,我們會採取適當行動,封鎖違規使用者或整臺遊戲機。", "twelfth": "玩過遊戲了嗎?", - "thirteenth": "如果您在有玩過的遊戲上投稿,其內文將會附有一個圖示表明您曾經玩過。", - "third": "Juxt 匯聚了全世界人們對諸多遊戲的觀點。請您謹記,在遊戲社群投稿時既要真誠表達自己,也要考慮到他人感受,因為所有人都能看見您的投稿。投稿前應三思而後行。Juxt 服務範圍也包括網際網路,即使是不使用 Juxt 的人也能看見您的投稿。此外,您在好友投稿上的回應不僅是好友能看見,而是所有人。敬請留意。", + "thirteenth": "如果您在有玩過的遊戲上發布貼文,其內文將會附有一個圖示表明您曾經玩過。", + "third": "Juxt 匯聚了全世界人們對諸多遊戲的觀點。請您謹記,在遊戲社群發布貼文時既要真誠表達自己,也要考慮到他人感受,因為所有人都能看見您的貼文。發布貼文前應三思而後行。Juxt 服務範圍也包括網際網路,即使是不使用 Juxt 的人也能看見您的貼文。此外,您在好友貼文上已發布的回應不僅是好友能看見,而是所有人。敬請留意。", "seventh": "請您謹記,從 Juxt 認識某人不代表也從現實生活認識了他。切勿在 Juxt 共享您或他人的個人資料,包括電子郵件地址、工作場域、學校名稱等等。如果您在 Juxt 上收到某個人的線下見面邀請,請不要接受。Juxt 只是一個線上社群平臺,不應用於安排線下聚會。", - "ninth": "有些人來到 Juxt 找尋遊戲的技巧和訣竅,但也有些人只想自己發掘遊戲裡的各種隱藏要素。將遊戲隱藏要素或劇情透露的投稿,稱作\"劇透。\" 如果要投稿劇透內容,請確保該文已被勾選為劇透投稿。如此一來,不想被劇透的人就不會看到您的投稿。" + "ninth": "有些人來到 Juxt 找尋遊戲的技巧和訣竅,但也有些人只想自己發掘遊戲裡的各種隱藏要素。將遊戲隱藏要素或劇情透露的貼文,稱作\"劇透。\" 如果要發布劇透內容,請確認此貼文已被勾選為劇透貼文。如此一來,不想被劇透的人就不會看到您的貼文。" }, "experience": "遊戲經驗", "experience_text": { @@ -65,7 +65,7 @@ "intermediate": "休閒業餘", "expert": "技藝高超" }, - "welcome_text": "Juxt 旨在讓世界各地的人們以 Mii 化身串聯遊戲社群。使用 Juxt,向全世界分享您獨到的遊戲經歷。", + "welcome_text": "Juxt 旨在透過 Mii 角色將世界各地的人們與遊戲社群聯繫起來。使用 Juxt 分享你的遊戲體驗,認識來自世界各地的人。", "guest": "訪客模式", "analytics": "Analytics(分析)", "ready": "開始使用 Juxt", @@ -74,53 +74,53 @@ "guest_button": "我明白了", "analytics_text": "Juxtaposition 使用 Cloudflare Web Analytics(分析)來追蹤使用者存取服務的情況。收集的資料包括但不限於拜訪時間、拜訪的頁面以及停留頁面的時間。這些資料不會以任何方式與您關連,也不會被我們或 Cloudflare 用於分析廣告投放。", "ready_text": "先到社群頁面,觀摩來自全世界的投稿作品吧。還能趁機熟悉 Juxt 的介面。您也可能就此發現新世界!", - "guest_text": "目前無法驗證您的帳號(可能是因為您使用了 Nintendo Network 帳號)。您可以繼續瀏覽 Juxt,但在登入 Pretendo Network 前,將無法進行「我贊同!」或投稿內容。詳情請前往 Discord 伺服器瞭解。" + "guest_text": "我們目前無法驗證您的帳號(可能是因為您使用了 Nintendo Network 帳號)。您可以繼續瀏覽 Juxt,但在登入 Pretendo Network 前,將無法進行「讚同!」或發布貼文。如要查看詳情,請前往 Discord 伺服器瞭解。" }, "community": { - "recent": "近期投稿", - "popular": "熱門投稿", - "verified": "名人投稿", - "new": "投稿", - "follow": "跟隨社群", - "following": "跟隨中", - "followers": "跟隨者", - "posts": "投稿", + "recent": "最近貼文", + "popular": "熱門貼文", + "verified": "已驗證貼文", + "new": "新增貼文", + "follow": "追蹤社群", + "following": "追蹤中", + "followers": "追蹤者", + "posts": "貼文", "tags": "標籤" }, "user_page": { "game_experience": "遊戲經驗", - "posts": "投稿", + "posts": "貼文", "friends": "好友", - "following": "跟隨中", - "followers": "跟隨者", - "follow_user": "跟隨", + "following": "追蹤中", + "followers": "追蹤者", + "follow_user": "追蹤", "befriend": "成為好友", - "pending": "待回應", - "unfriend": "取消好友", - "no_friends": "查無好友", - "country": "居住地", + "pending": "待處理", + "unfriend": "刪除好友", + "no_friends": "沒有好友", + "country": "國家/地區", "birthday": "生日", - "following_user": "跟隨中", - "no_following": "查無跟隨了哪些人", - "no_followers": "查無跟隨者" + "following_user": "追蹤中", + "no_following": "沒有追蹤任何人", + "no_followers": "沒有追蹤者" }, "user_settings": { - "show_country": "在個人檔案顯示居住地", + "show_country": "在個人檔案顯示國家/地區", "show_birthday": "在個人檔案顯示生日", "show_game": "在個人檔案顯示遊戲經驗", "show_comment": "在個人檔案顯示自我介紹", "profile_comment": "自我介紹", "profile_settings": "個人檔案設定", - "swearing": "自我介紹不得包含露骨內容。" + "swearing": "自我介紹不能包含露骨內容。" }, "notifications": { - "none": "查無通知。", - "new_follower": "成為了您的跟隨者!" + "none": "沒有通知。", + "new_follower": "已開始追蹤你!" }, "new_post": { - "swearing": "投稿不得包含露骨內容。", - "new_post_text": "新投稿", - "post_to": "投稿至", - "text_hint": "點選此處進行投稿。" + "swearing": "貼文不能包含露骨內容。", + "new_post_text": "新增貼文", + "post_to": "發佈至", + "text_hint": "點觸此處新增貼文。" } } diff --git a/apps/juxtaposition-ui/src/i18n.ts b/apps/juxtaposition-ui/src/i18n.ts new file mode 100644 index 00000000..39e55427 --- /dev/null +++ b/apps/juxtaposition-ui/src/i18n.ts @@ -0,0 +1,55 @@ +import fs from 'fs/promises'; +import path from 'path'; +import i18next from 'i18next'; +import { initReactI18next } from 'react-i18next'; +import { langsFolder } from '@/util'; +import type en from '@/assets/locales/en.json'; +import type { ParamPack } from '@/types/common/param-pack'; + +const entries = await fs.readdir(langsFolder, { withFileTypes: true }); +const langFiles = entries + .filter(v => v.isFile() && v.name.endsWith('.json')) + .map(v => ({ filePath: path.join(v.parentPath, v.name), lang: path.basename(v.name, path.extname(v.name)).toUpperCase() })); +const loadedFiles = await Promise.all(langFiles.map(v => fs.readFile(v.filePath, 'utf8'))); +const finalObject = Object.fromEntries(loadedFiles.map((v, i) => [langFiles[i].lang, { ns: JSON.parse(v) }])); + +export const resources: Record = { + ...finalObject +}; + +const fallbackLang = 'EN'; +export function getLanguage(paramPack?: ParamPack | null): string { + if (!paramPack) { + return fallbackLang; + } + + // Not currently possible to get any other languages, maybe we can add an in-app selector later? + const languageIdMap: Record = { + 0: 'JA', + 1: 'EN', + 2: 'FR', + 3: 'DE', + 4: 'IT', + 5: 'ES', + 6: 'ZH', + 7: 'KO', + 8: 'NL', + 9: 'PT', + 10: 'RU', + 11: 'ZH' + }; + + return languageIdMap[paramPack.language_id] ?? fallbackLang; +} + +export const createI18n = (lang: string): any => i18next + .use(initReactI18next) + .init({ + lng: lang, + resources, + fallbackLng: fallbackLang, + defaultNS: 'ns', + interpolation: { + escapeValue: false // JSX already safes from xss + } + }); diff --git a/apps/juxtaposition-ui/src/middleware/checkBan.tsx b/apps/juxtaposition-ui/src/middleware/checkBan.tsx index 2d72dd30..bfca7cbd 100644 --- a/apps/juxtaposition-ui/src/middleware/checkBan.tsx +++ b/apps/juxtaposition-ui/src/middleware/checkBan.tsx @@ -3,7 +3,6 @@ import { database as db } from '@/database'; import { config } from '@/config'; import { humanDate, humanFromNow } from '@/util'; import { WebLoginView } from '@/services/juxt-web/views/web/loginView'; -import { buildContext } from '@/services/juxt-web/views/context'; import { CtrFatalErrorView } from '@/services/juxt-web/views/ctr/errorView'; import { PortalFatalErrorView } from '@/services/juxt-web/views/portal/errorView'; import type { RequestHandler } from 'express'; @@ -48,7 +47,7 @@ export const checkBan: RequestHandler = async (request, response, next) => { const banMessage = 'No access. Must be tester or dev'; const banCode = 5989999; return response.jsxForDirectory({ - web: , + web: , portal: , ctr: }); @@ -83,7 +82,7 @@ export const checkBan: RequestHandler = async (request, response, next) => { banMessage += `\n\nIf you have any questions, please contact the moderators on the Pretendo Network Forum (https://preten.do/ban-appeal/).`; return response.jsxForDirectory({ - web: , + web: , portal: , ctr: }); diff --git a/apps/juxtaposition-ui/src/middleware/consoleAuth.tsx b/apps/juxtaposition-ui/src/middleware/consoleAuth.tsx index e3326661..c87b5512 100644 --- a/apps/juxtaposition-ui/src/middleware/consoleAuth.tsx +++ b/apps/juxtaposition-ui/src/middleware/consoleAuth.tsx @@ -1,8 +1,9 @@ import { config } from '@/config'; +import { getLanguage } from '@/i18n'; import { logger } from '@/logger'; import { CtrFatalErrorView } from '@/services/juxt-web/views/ctr/errorView'; import { PortalFatalErrorView } from '@/services/juxt-web/views/portal/errorView'; -import { decodeParamPack, getPIDFromServiceToken, getUserAccountData, getUserDataFromToken, processLanguage } from '@/util'; +import { decodeParamPack, getPIDFromServiceToken, getUserAccountData, getUserDataFromToken } from '@/util'; import type { RequestHandler, Response } from 'express'; function renderAuthError(res: Response, code: number, message: string): void { @@ -79,7 +80,7 @@ export const consoleAuth: RequestHandler = async (request, response, next) => { } response.locals.uaIsConsole = uaIsConsole; - response.locals.lang = processLanguage(request.paramPackData); + response.locals.lang = getLanguage(request.paramPackData); response.locals.pid = request.pid; return next(); }; diff --git a/apps/juxtaposition-ui/src/middleware/detectVersion.ts b/apps/juxtaposition-ui/src/middleware/detectVersion.ts index 7dbfb139..fa0f8fe4 100644 --- a/apps/juxtaposition-ui/src/middleware/detectVersion.ts +++ b/apps/juxtaposition-ui/src/middleware/detectVersion.ts @@ -1,11 +1,11 @@ -import { processLanguage } from '@/util'; +import { getLanguage } from '@/i18n'; import type { Request, RequestHandler } from 'express'; export const detectVersion: RequestHandler = async (request, response, next) => { // Check the domain and set the directory if (includes(request, 'juxt')) { request.directory = 'web'; - response.locals.lang = processLanguage(); + response.locals.lang = getLanguage(); } else { request.directory = includes(request, 'portal') ? 'portal' : 'ctr'; } diff --git a/apps/juxtaposition-ui/src/middleware/discovery.tsx b/apps/juxtaposition-ui/src/middleware/discovery.tsx index 9919b5c6..97b7354b 100644 --- a/apps/juxtaposition-ui/src/middleware/discovery.tsx +++ b/apps/juxtaposition-ui/src/middleware/discovery.tsx @@ -1,7 +1,6 @@ import { database as db } from '@/database'; import { config } from '@/config'; import { WebLoginView } from '@/services/juxt-web/views/web/loginView'; -import { buildContext } from '@/services/juxt-web/views/context'; import { PortalFatalErrorView } from '@/services/juxt-web/views/portal/errorView'; import { CtrFatalErrorView } from '@/services/juxt-web/views/ctr/errorView'; import type { RequestHandler } from 'express'; @@ -24,7 +23,7 @@ export const checkDiscovery: RequestHandler = async (request, response, next) => break; } return response.jsxForDirectory({ - web: , + web: , portal: , ctr: }); diff --git a/apps/juxtaposition-ui/src/middleware/jsx.tsx b/apps/juxtaposition-ui/src/middleware/jsx.tsx index a8470b15..dddb0026 100644 --- a/apps/juxtaposition-ui/src/middleware/jsx.tsx +++ b/apps/juxtaposition-ui/src/middleware/jsx.tsx @@ -1,10 +1,12 @@ import { renderToStaticMarkup } from 'react-dom/server'; -import { buildContext } from '@/services/juxt-web/views/context'; import { WebErrorView } from '@/services/juxt-web/views/web/errorView'; import { CtrErrorView } from '@/services/juxt-web/views/ctr/errorView'; import { PortalErrorView } from '@/services/juxt-web/views/portal/errorView'; -import type { ReactElement } from 'react'; +import { buildContext, RenderContext } from '@/services/juxt-web/views/common/components/RenderContext'; +import { LangProvider } from '@/services/juxt-web/views/common/components/LangProvider'; +import type { ReactElement, ReactNode } from 'react'; import type { RequestHandler } from 'express'; +import type { RenderContextContent } from '@/services/juxt-web/views/common/components/RenderContext'; import type { ErrorViewProps } from '@/services/juxt-web/views/web/errorView'; const htmlDoctype = ''; @@ -15,13 +17,22 @@ export function renderJsx(el: ReactElement): string { return htmlWithEvents; } +export function ContextProviders(props: { ctx: RenderContextContent; children?: ReactNode }): ReactNode { + return ( + + {props.children} + + ); +} /** * Render JSX as static markup. Only static! No state or event handlers are supported. */ export const jsxRenderer: RequestHandler = (request, response, next) => { response.jsx = (el, addDoctype): typeof response => { + const finalEl = {el}; + const prefix = (addDoctype ?? true) ? htmlDoctype + '\n' : ''; - response.send(prefix + renderJsx(el)); + response.send(prefix + renderJsx(finalEl)); return response; }; @@ -48,7 +59,6 @@ export const jsxRenderer: RequestHandler = (request, response, next) => { response.renderError = (opt): typeof response => { const props: ErrorViewProps = { - ctx: buildContext(response), requestId: request.id, code: opt.code, message: opt.message diff --git a/apps/juxtaposition-ui/src/middleware/webAuth.ts b/apps/juxtaposition-ui/src/middleware/webAuth.ts index f41ebb73..6c3fc1c0 100644 --- a/apps/juxtaposition-ui/src/middleware/webAuth.ts +++ b/apps/juxtaposition-ui/src/middleware/webAuth.ts @@ -1,6 +1,7 @@ import { config } from '@/config'; +import { getLanguage } from '@/i18n'; import { logger } from '@/logger'; -import { getUserAccountData, getUserDataFromToken, processLanguage } from '@/util'; +import { getUserAccountData, getUserDataFromToken } from '@/util'; import type { RequestHandler, Request, Response } from 'express'; const cookieDomain = config.http.cookieDomain; @@ -24,7 +25,7 @@ export const webAuth: RequestHandler = async (request, response, next) => { response.clearCookie('refresh_token', { domain: cookieDomain, path: '/' }); response.clearCookie('token_type', { domain: cookieDomain, path: '/' }); if (request.path === '/login') { - response.locals.lang = processLanguage(); + response.locals.lang = getLanguage(); request.tokens = {}; request.paramPackData = null; return next(); diff --git a/apps/juxtaposition-ui/src/services/juxt-web/index.ts b/apps/juxtaposition-ui/src/services/juxt-web/index.ts index 4d9e5490..634da1f0 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/index.ts +++ b/apps/juxtaposition-ui/src/services/juxt-web/index.ts @@ -15,9 +15,8 @@ const webRouter = express.Router(); // We want to check which domain we're running on before we fetch any files, // but we don't care about discovery until we're making it to the consoles themselves router.use(detectVersion); -router.use('/', routes.WEB_FILES); -router.use('/robots.txt', routes.ROBOTS); -router.use('/web', routes.PWA); +router.use(routes.STATIC); +router.use(routes.ENTRYPOINT); router.use(checkDiscovery); // Create subdomains diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/admin/admin.tsx b/apps/juxtaposition-ui/src/services/juxt-web/routes/admin/admin.tsx index 1f34135b..ccd219cb 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/admin/admin.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/admin/admin.tsx @@ -13,7 +13,6 @@ import { humanDate, createLogEntry, getReasonMap, getUserAccountData, newNotific import { getUserMetrics } from '@/metrics'; import { parseReq } from '@/services/juxt-web/routes/routeUtils'; import { WebUserListView } from '@/services/juxt-web/views/web/admin/userListView'; -import { buildContext } from '@/services/juxt-web/views/context'; import { WebReportListView } from '@/services/juxt-web/views/web/admin/reportListView'; import { WebManageCommunityView } from '@/services/juxt-web/views/web/admin/manageCommunityView'; import { WebNewCommunityView } from '@/services/juxt-web/views/web/admin/newCommunityView'; @@ -62,7 +61,7 @@ adminRouter.get('/posts', async function (req, res) { }); res.jsxForDirectory({ - web: + web: }); }); @@ -123,7 +122,7 @@ adminRouter.get('/accounts', async function (req, res) { const userMetrics = await getUserMetrics(); res.jsxForDirectory({ - web: + web: }); }); @@ -172,7 +171,7 @@ adminRouter.get('/accounts/:pid', async function (req, res) { res.jsxForDirectory({ web: ( + web: }); }); @@ -408,7 +407,7 @@ adminRouter.get('/communities/new', async function (req, res) { } res.jsxForDirectory({ - web: + web: }); }); @@ -535,7 +534,7 @@ adminRouter.get('/communities/:community_id', async function (req, res) { } res.jsxForDirectory({ - web: + web: }); }); diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/communities.tsx b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/communities.tsx index 27cd8574..1a4f6a70 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/communities.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/communities.tsx @@ -8,7 +8,6 @@ import { POST } from '@/models/post'; import { redisGet, redisRemove, redisSet } from '@/redisCache'; import { parseReq } from '@/services/juxt-web/routes/routeUtils'; import { WebCommunityListView, WebCommunityOverviewView } from '@/services/juxt-web/views/web/communityListView'; -import { buildContext } from '@/services/juxt-web/views/context'; import { PortalCommunityListView, PortalCommunityOverviewView } from '@/services/juxt-web/views/portal/communityListView'; import { CtrCommunityListView, CtrCommunityOverviewView } from '@/services/juxt-web/views/ctr/communityListView'; import { PortalSubCommunityView } from '@/services/juxt-web/views/portal/subCommunityView'; @@ -20,6 +19,8 @@ import { WebPostListView } from '@/services/juxt-web/views/web/postList'; import { PortalPostListView } from '@/services/juxt-web/views/portal/postList'; import { CtrPostListView } from '@/services/juxt-web/views/ctr/postList'; import { zodFallback } from '@/util'; +import { CtrNewPostPage } from '@/services/juxt-web/views/ctr/newPostView'; +import { PortalNewPostPage } from '@/services/juxt-web/views/portal/newPostView'; import type { InferSchemaType } from 'mongoose'; import type { PostListViewProps } from '@/services/juxt-web/views/web/postList'; import type { CommunityViewProps } from '@/services/juxt-web/views/web/communityView'; @@ -33,7 +34,6 @@ communitiesRouter.get('/', async function (req, res) { const communityStats = await getCommunityStats(); const props: CommunityOverviewViewProps = { - ctx: buildContext(res), newCommunities: communityStats.new, popularCommunities: communityStats.popular }; @@ -48,7 +48,6 @@ communitiesRouter.get('/all', async function (req, res) { const communities = await database.getCommunities(90); const props: CommunityListViewProps = { - ctx: buildContext(res), communities }; res.jsxForDirectory({ @@ -107,7 +106,6 @@ communitiesRouter.get('/:communityID/related', async function (req, res) { } const props: SubCommunityViewProps = { - ctx: buildContext(res), community, subcommunities: children }; @@ -117,6 +115,30 @@ communitiesRouter.get('/:communityID/related', async function (req, res) { }); }); +communitiesRouter.get('/:communityID/create', async function (req, res) { + const { params } = parseReq(req, { + params: z.object({ + communityID: z.string() + }) + }); + + const community = await database.getCommunityByID(params.communityID); + if (!community) { + return res.sendStatus(404); + } + + const props = { + id: community.olive_community_id, + name: community.name, + url: `/posts/new`, + show: 'post' + }; + res.jsxForDirectory({ + ctr: , + portal: + }); +}); + communitiesRouter.get('/:communityID/:type', async function (req, res) { const { query, params, auth } = parseReq(req, { params: z.object({ @@ -174,7 +196,6 @@ communitiesRouter.get('/:communityID/:type', async function (req, res) { const numPosts = await database.getTotalPostsByCommunity(community); const postListProps: PostListViewProps = { - ctx: buildContext(res), nextLink: `/titles/${params.communityID}/${params.type}/more?offset=${posts.length}&pjax=true`, posts, userContent @@ -189,7 +210,6 @@ communitiesRouter.get('/:communityID/:type', async function (req, res) { } const props: CommunityViewProps = { - ctx: buildContext(res), feedType: type, community, hasSubCommunities: subCommunities.length > 0, @@ -247,7 +267,6 @@ communitiesRouter.get('/:communityID/:type/more', async function (req, res) { } const postListProps: PostListViewProps = { - ctx: buildContext(res), nextLink: `/titles/${params.communityID}/${params.type}/more?offset=${offset + posts.length}&pjax=true`, posts, userContent diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/entrypoint.ts b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/entrypoint.ts new file mode 100644 index 00000000..b9466ff6 --- /dev/null +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/entrypoint.ts @@ -0,0 +1,6 @@ +import express from 'express'; +export const entrypointRouter = express.Router(); + +entrypointRouter.get('/', function (req, res) { + res.redirect('/titles/show'); +}); diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/feed.tsx b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/feed.tsx index f32b201a..ce910bda 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/feed.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/feed.tsx @@ -5,7 +5,6 @@ import { POST } from '@/models/post'; import { config } from '@/config'; import { parseReq } from '@/services/juxt-web/routes/routeUtils'; import { WebGlobalFeedView, WebPersonalFeedView } from '@/services/juxt-web/views/web/feed'; -import { buildContext } from '@/services/juxt-web/views/context'; import { WebPostListView } from '@/services/juxt-web/views/web/postList'; import { CtrGlobalFeedView, CtrPersonalFeedView } from '@/services/juxt-web/views/ctr/feed'; import { CtrPostListView } from '@/services/juxt-web/views/ctr/postList'; @@ -30,17 +29,16 @@ feedRouter.get('/', async function (req, res) { if (query.pjax) { return res.jsxForDirectory({ - web: , - portal: , - ctr: + web: , + portal: , + ctr: }); } - const title = res.locals.lang.global.activity_feed; return res.jsxForDirectory({ - web: , - portal: , - ctr: + web: , + portal: , + ctr: }); }); @@ -64,17 +62,16 @@ feedRouter.get('/all', async function (req, res) { if (query.pjax) { return res.jsxForDirectory({ - web: , - portal: , - ctr: + web: , + portal: , + ctr: }); } - const title = res.locals.lang.global.activity_feed; return res.jsxForDirectory({ - web: , - portal: , - ctr: + web: , + portal: , + ctr: }); }); @@ -98,9 +95,9 @@ feedRouter.get('/more', async function (req, res) { } return res.jsxForDirectory({ - web: , - portal: , - ctr: + web: , + portal: , + ctr: }); }); @@ -129,8 +126,8 @@ feedRouter.get('/all/more', async function (req, res) { } return res.jsxForDirectory({ - web: , - portal: , - ctr: + web: , + portal: , + ctr: }); }); diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/messages.tsx b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/messages.tsx index a47a2549..0b0a7a05 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/messages.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/messages.tsx @@ -1,12 +1,12 @@ import crypto from 'crypto'; import express from 'express'; import { Snowflake as snowflake } from 'node-snowflake'; +import { z } from 'zod'; import { config } from '@/config'; import { database } from '@/database'; import { uploadPainting, uploadScreenshot } from '@/images'; import { CONVERSATION } from '@/models/conversation'; import { POST } from '@/models/post'; -import { buildContext } from '@/services/juxt-web/views/context'; import { CtrMessagesView } from '@/services/juxt-web/views/ctr/messages'; import { CtrMessageThreadView } from '@/services/juxt-web/views/ctr/messageThread'; import { PortalMessagesView } from '@/services/juxt-web/views/portal/messages'; @@ -15,6 +15,8 @@ import { WebMessagesView } from '@/services/juxt-web/views/web/messages'; import { WebMessageThreadView } from '@/services/juxt-web/views/web/messageThread'; import { getInvalidPostRegex, getUserAccountData, getUserFriendPIDs } from '@/util'; import { parseReq } from '@/services/juxt-web/routes/routeUtils'; +import { CtrNewPostPage } from '@/services/juxt-web/views/ctr/newPostView'; +import { PortalNewPostPage } from '@/services/juxt-web/views/portal/newPostView'; import type { PaintingUrls, ScreenshotUrls } from '@/images'; export const messagesRouter = express.Router(); @@ -23,9 +25,9 @@ messagesRouter.get('/', async function (req, res) { const { auth } = parseReq(req); const conversations = await database.getConversations(auth().pid); res.jsxForDirectory({ - web: , - portal: , - ctr: , + web: , + portal: , + ctr: , disableDoctypeFor: ['ctr'] }); }); @@ -259,9 +261,37 @@ messagesRouter.get('/:message_id', async function (req, res) { await conversation.markAsRead(authCtx.pid); res.jsxForDirectory({ - web: , - portal: , - ctr: + web: , + portal: , + ctr: + }); +}); + +messagesRouter.get('/:message_id/create', async function (req, res) { + const { params, auth } = parseReq(req, { + params: z.object({ + message_id: z.string() + }) + }); + + const conversation = await database.getConversationByID(params.message_id); + if (!conversation || conversation.users.length < 2) { + return res.sendStatus(404); + } + + // Get the conversation member who *isn't* us + const partner = conversation.users[0].pid !== auth().pid ? conversation.users[0] : conversation.users[1]; + + const props = { + id: conversation.id, + pid: partner.pid, + messagePid: partner.pid, + url: `/friend_messages/new`, + show: 'message-page' + }; + res.jsxForDirectory({ + ctr: , + portal: }); }); diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/notifications.tsx b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/notifications.tsx index 511c000d..bf927d01 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/notifications.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/notifications.tsx @@ -4,7 +4,6 @@ import { database } from '@/database'; import { getUserFriendRequestsIncoming } from '@/util'; import { parseReq } from '@/services/juxt-web/routes/routeUtils'; import { WebNotificationListView, WebNotificationWrapperView } from '@/services/juxt-web/views/web/notificationListView'; -import { buildContext } from '@/services/juxt-web/views/context'; import { PortalNotificationListView, PortalNotificationWrapperView } from '@/services/juxt-web/views/portal/notificationListView'; import { CtrNotificationListView, CtrNotificationWrapperView } from '@/services/juxt-web/views/ctr/notificationListView'; import { PortalFriendRequestListView } from '@/services/juxt-web/views/portal/friendRequestListView'; @@ -27,7 +26,6 @@ notificationRouter.get('/my_news', async function (req, res) { } const props: NotificationListViewProps = { - ctx: buildContext(res), notifications }; @@ -41,17 +39,17 @@ notificationRouter.get('/my_news', async function (req, res) { res.jsxForDirectory({ web: ( - + ), portal: ( - + ), ctr: ( - + ) @@ -70,7 +68,6 @@ notificationRouter.get('/friend_requests', async function (req, res) { const validRequests = allRequests.filter(request => new Date(Number(request.expires) * 1000) > new Date(now.getTime() - 29 * 24 * 60 * 60 * 1000)); const props: FriendRequestListViewProps = { - ctx: buildContext(res), requests: validRequests }; @@ -83,12 +80,12 @@ notificationRouter.get('/friend_requests', async function (req, res) { res.jsxForDirectory({ portal: ( - + ), ctr: ( - + ) diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/posts.tsx b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/posts.tsx index 895955a8..831d9356 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/posts.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/posts.tsx @@ -14,10 +14,13 @@ import { redisRemove } from '@/redisCache'; import { createLogEntry, getInvalidPostRegex, getUserAccountData } from '@/util'; import { addEmpathyById, removeEmpathyById } from '@/api/empathy'; import { parseReq } from '@/services/juxt-web/routes/routeUtils'; -import { buildContext } from '@/services/juxt-web/views/context'; import { WebPostPageView } from '@/services/juxt-web/views/web/postPageView'; import { CtrPostPageView } from '@/services/juxt-web/views/ctr/postPageView'; import { PortalPostPageView } from '@/services/juxt-web/views/portal/postPageView'; +import { CtrNewPostPage } from '@/services/juxt-web/views/ctr/newPostView'; +import { PortalNewPostPage } from '@/services/juxt-web/views/portal/newPostView'; +import { PortalReportPostPage } from '@/services/juxt-web/views/portal/reportPostView'; +import { CtrReportPostPage } from '@/services/juxt-web/views/ctr/reportPostView'; import type { Request, Response } from 'express'; import type { InferSchemaType } from 'mongoose'; import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc'; @@ -180,7 +183,6 @@ postsRouter.get('/:post_id', async function (req, res) { ) && userSettings?.account_status === 0; const props: PostPageViewProps = { - ctx: buildContext(res), community, post, postPNID, @@ -250,6 +252,51 @@ postsRouter.post('/:post_id/new', postLimit, upload.none(), async function (req, await newPost(req, res); }); +postsRouter.get('/:post_id/create', async function (req, res) { + const { params, auth } = parseReq(req, { + params: z.object({ + post_id: z.string() + }) + }); + + const parent = await getPostById(auth().tokens, params.post_id); + if (!parent) { + return res.sendStatus(404); + } + + const props = { + id: parent.community_id, + pid: parent.pid, + url: `/posts/${parent.id}/new`, + show: 'post' + }; + res.jsxForDirectory({ + ctr: , + portal: + }); +}); + +postsRouter.get('/:post_id/report', async function (req, res) { + const { params, auth } = parseReq(req, { + params: z.object({ + post_id: z.string() + }) + }); + + const post = await getPostById(auth().tokens, params.post_id); + if (!post) { + return res.redirect('/404'); + } + + const props = { + id: params.post_id + }; + return res.jsxForDirectory({ + ctr: , + portal: + }); +}); + postsRouter.post('/:post_id/report', upload.none(), async function (req, res) { const { body, auth } = parseReq(req, { body: z.object({ @@ -341,6 +388,10 @@ async function newPost(req: Request, res: Response): Promise { return; } else { community = await database.getCommunityByID(parentPost.community_id); + if (parentPost.removed) { + res.sendStatus(400); + return; + } } } if (params.post_id && (body.body === '' && body.painting === '' && body.screenshot === '')) { diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/show.tsx b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/show.tsx index 638cc1a8..02d85a54 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/show.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/show.tsx @@ -3,7 +3,6 @@ import { z } from 'zod'; import { database } from '@/database'; import { createUser, setName } from '@/util'; import { parseReq } from '@/services/juxt-web/routes/routeUtils'; -import { buildContext } from '@/services/juxt-web/views/context'; import { WebFirstRunView } from '@/services/juxt-web/views/web/firstRunView'; import { PortalFirstRunView } from '@/services/juxt-web/views/portal/firstRunView'; import { CtrFirstRunView } from '@/services/juxt-web/views/ctr/firstRunView'; @@ -22,9 +21,9 @@ showRouter.get('/', async function (req, res) { const content = await database.getUserContent(req.pid); if (!user || !content) { return res.jsxForDirectory({ - web: , - portal: , - ctr: + web: , + portal: , + ctr: }); } @@ -51,9 +50,9 @@ showRouter.get('/', async function (req, res) { showRouter.get('/first', async function (req, res) { return res.jsxForDirectory({ - web: , - portal: , - ctr: + web: , + portal: , + ctr: }); }); diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/static.ts b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/static.ts new file mode 100644 index 00000000..c5feb40a --- /dev/null +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/static.ts @@ -0,0 +1,25 @@ +import path from 'path'; +import express from 'express'; +import { distFolder } from '@/util'; +export const staticRouter = express.Router(); + +// Normal asset files +const webFilesRoot = path.join(distFolder, 'webfiles'); +const assetsStaticRouter = express.Router(); +assetsStaticRouter.use(express.static(webFilesRoot)); +assetsStaticRouter.use((req, res, _next) => { + res.sendStatus(404); // 404 for only /assets +}); +staticRouter.use('/assets', assetsStaticRouter); + +// Global files, served on the root of the domain +const webFilesGlobalRoot = path.join(distFolder, 'webfiles', 'global'); +staticRouter.use('/', express.static(webFilesGlobalRoot)); + +// Some notification database entries use hardcoded paths to images, will need a migration. +// Once that has been updated, this route can be removed +staticRouter.use('/images', (req, res, next) => { + const directory = req.directory ?? 'web'; + const fileRoot = path.join(distFolder, 'webfiles', directory, 'images'); + return express.static(fileRoot)(req, res, next); +}); diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/topics.tsx b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/topics.tsx index 992ac950..a1ceb79f 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/topics.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/topics.tsx @@ -4,7 +4,6 @@ import { config } from '@/config'; import { database } from '@/database'; import { POST } from '@/models/post'; import { parseReq } from '@/services/juxt-web/routes/routeUtils'; -import { buildContext } from '@/services/juxt-web/views/context'; import { WebPostListView } from '@/services/juxt-web/views/web/postList'; import { CtrPostListView } from '@/services/juxt-web/views/ctr/postList'; import { PortalPostListView } from '@/services/juxt-web/views/portal/postList'; @@ -33,17 +32,17 @@ topicsRouter.get('/', async function (req, res) { if (query.pjax) { return res.jsxForDirectory({ - web: , - portal: , - ctr: + web: , + portal: , + ctr: }); } const title = query.topic_tag; return res.jsxForDirectory({ - web: , - portal: , - ctr: + web: , + portal: , + ctr: }); }); @@ -70,8 +69,8 @@ topicsRouter.get('/more', async function (req, res) { } return res.jsxForDirectory({ - web: , - portal: , - ctr: + web: , + portal: , + ctr: }); }); diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/userpage.tsx b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/userpage.tsx index 46ca200a..e1ed9faf 100644 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/console/userpage.tsx +++ b/apps/juxtaposition-ui/src/services/juxt-web/routes/console/userpage.tsx @@ -9,7 +9,6 @@ import { SETTINGS } from '@/models/settings'; import { getCommunityHash, getUserAccountData, getUserFriendPIDs, newNotification } from '@/util'; import { parseReq } from '@/services/juxt-web/routes/routeUtils'; import { WebUserPageView } from '@/services/juxt-web/views/web/userPageView'; -import { buildContext } from '@/services/juxt-web/views/context'; import { WebPostListView } from '@/services/juxt-web/views/web/postList'; import { PortalPostListView } from '@/services/juxt-web/views/portal/postList'; import { CtrPostListView } from '@/services/juxt-web/views/ctr/postList'; @@ -33,7 +32,7 @@ const upload = multer({ dest: 'uploads/' }); const pidParamSchema = z.union([z.literal('me'), z.coerce.number()]); userPageRouter.get('/menu', async function (req, res) { - res.jsx(); + res.jsx(); }); userPageRouter.get('/me', async function (req, res) { @@ -92,7 +91,6 @@ userPageRouter.get('/me/settings', async function (req, res) { } const props: UserSettingsViewProps = { - ctx: buildContext(res), userSettings }; res.jsxForDirectory({ @@ -267,7 +265,6 @@ async function userPage(req: Request, res: Response, userID: number): Promise v.pid !== 0), communities: communities .filter(v => v !== '0') // UserContent had a wrong default of [0], which means it needs to be filtered out before usage @@ -411,7 +404,6 @@ async function userRelations(req: Request, res: Response, userID: number): Promi }); } const props: UserPageViewProps = { - ctx: buildContext(res), baseLink: link, friendPids: friends, isOnline: userSettings.last_active ? isDateInRange(userSettings.last_active, 10) : false, @@ -457,7 +449,6 @@ async function morePosts(req: Request, res: Response, userID: number): Promise); + return res.jsx(); }); loginRouter.post('/', async (req, res) => { @@ -32,14 +31,14 @@ loginRouter.post('/', async (req, res) => { const login = await passwordLogin(username, password).catch((e) => { switch (e.details) { case 'INVALID_ARGUMENT: User not found': - res.jsx(); + res.jsx(); break; case 'INVALID_ARGUMENT: Password is incorrect': - res.jsx(); + res.jsx(); break; default: logger.error(e, `Login error for ${username}`); - res.jsx(); + res.jsx(); break; } }); @@ -49,7 +48,7 @@ loginRouter.post('/', async (req, res) => { const PNID = await getUserDataFromToken(login.accessToken); if (!PNID) { - return res.jsx(); + return res.jsx(); } const discovery = await database.getEndPoint(config.serverEnvironment); diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/web/pwa.ts b/apps/juxtaposition-ui/src/services/juxt-web/routes/web/pwa.ts deleted file mode 100644 index 62500224..00000000 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/web/pwa.ts +++ /dev/null @@ -1,15 +0,0 @@ -import path from 'path'; -import express from 'express'; -import { distFolder } from '@/util'; - -export const pwaRouter = express.Router(); - -pwaRouter.get('/icons/:filename', function (req, res) { - res.set('Content-Type', 'image/png'); - res.sendFile('/images/icons/' + req.params.filename, { root: path.join(distFolder, '/webfiles/web') }); -}); - -pwaRouter.get('/manifest.json', function (req, res) { - res.set('Content-Type', 'text/json'); - res.sendFile('manifest.json', { root: path.join(distFolder, '/webfiles/web') }); -}); diff --git a/apps/juxtaposition-ui/src/services/juxt-web/routes/web/robots.ts b/apps/juxtaposition-ui/src/services/juxt-web/routes/web/robots.ts deleted file mode 100644 index f244a589..00000000 --- a/apps/juxtaposition-ui/src/services/juxt-web/routes/web/robots.ts +++ /dev/null @@ -1,10 +0,0 @@ -import path from 'path'; -import express from 'express'; -import { distFolder } from '@/util'; - -export const robotsRouter = express.Router(); - -robotsRouter.get('/', function (req, res) { - res.set('Content-Type', 'text/css'); - res.sendFile('robots.txt', { root: path.join(distFolder, '/webfiles/web') }); -}); diff --git a/apps/juxtaposition-ui/src/services/juxt-web/views/common.tsx b/apps/juxtaposition-ui/src/services/juxt-web/views/common.tsx deleted file mode 100644 index 5db087d7..00000000 --- a/apps/juxtaposition-ui/src/services/juxt-web/views/common.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import type { ReactNode } from 'react'; - -export function InlineScript(props: { src: string }): ReactNode { - return + + ); return ( - +