mirror of
https://github.com/PretendoNetwork/website.git
synced 2026-09-30 04:26:36 -05:00
feat: account editing on web
This commit is contained in:
@@ -62,3 +62,7 @@ There are no fully required configuration variables, the app can runs minimally
|
||||
| | `PN_WEBSITE_PUBLIC_HCAPTCHA_SITE_KEY` | HCaptcha site key | - |
|
||||
| | | | |
|
||||
| Discourse SSO | `PN_WEBSITE_DISCOURSE_SSO_SECRET` | Discourse SSO secret | - |
|
||||
|
||||
TODO:
|
||||
|
||||
account settings error handling
|
||||
9
package-lock.json
generated
9
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@discordjs/rest": "^2.6.3",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@internationalized/date": "^3.12.3",
|
||||
"@nuxt/content": "^3.4.0",
|
||||
"@nuxt/eslint": "^1.3.0",
|
||||
"@nuxt/fonts": "^0.14.0",
|
||||
@@ -18,7 +19,7 @@
|
||||
"@nuxtjs/i18n": "^10.6.0",
|
||||
"@pinia/nuxt": "^1.0.1",
|
||||
"@pretendonetwork/error-codes": "^1.2.2",
|
||||
"@pretendonetwork/grpc": "^2.5.4",
|
||||
"@pretendonetwork/grpc": "^2.5.7",
|
||||
"@pretendonetwork/mii-js": "^1.0.11",
|
||||
"@vueuse/core": "^14.4.0",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
@@ -5346,9 +5347,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@pretendonetwork/grpc": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.5.4.tgz",
|
||||
"integrity": "sha512-spjg6sOSP8z+9T9vqR9RM5guJb7UHYQlFBfb6p6WxpaYHsXlmVIRPeQlR7NTkgpNzARV/4QVpqerBbtLDvN4oA==",
|
||||
"version": "2.5.7",
|
||||
"resolved": "https://registry.npmjs.org/@pretendonetwork/grpc/-/grpc-2.5.7.tgz",
|
||||
"integrity": "sha512-HmWyBxm/Om6S5k+PYLxVU/MaRcSUEV0NAfKccy4i02ZJTZccCJRRKiSBl3WfXSYE+TJNJhSoURGK56E1i8CXqA==",
|
||||
"license": "AGPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@bufbuild/protobuf": "^2.2.2",
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"dependencies": {
|
||||
"@discordjs/rest": "^2.6.3",
|
||||
"@hcaptcha/vue3-hcaptcha": "^1.3.0",
|
||||
"@internationalized/date": "^3.12.3",
|
||||
"@nuxt/content": "^3.4.0",
|
||||
"@nuxt/eslint": "^1.3.0",
|
||||
"@nuxt/fonts": "^0.14.0",
|
||||
@@ -24,7 +25,7 @@
|
||||
"@nuxtjs/i18n": "^10.6.0",
|
||||
"@pinia/nuxt": "^1.0.1",
|
||||
"@pretendonetwork/error-codes": "^1.2.2",
|
||||
"@pretendonetwork/grpc": "^2.5.4",
|
||||
"@pretendonetwork/grpc": "^2.5.7",
|
||||
"@pretendonetwork/mii-js": "^1.0.11",
|
||||
"@vueuse/core": "^14.4.0",
|
||||
"better-sqlite3": "^12.11.1",
|
||||
|
||||
@@ -3,17 +3,14 @@ import { AccountUpdateSchema } from '~~/shared/api-types';
|
||||
export default defineEventHandler(async (event): Promise<void> => {
|
||||
const body = await readZodBody(event, AccountUpdateSchema);
|
||||
const auth = enforceLoggedIn(event);
|
||||
const apiFetch = useHttpApi(event, auth.token);
|
||||
const grpc = useApiGrpcWithToken(event, auth.token);
|
||||
|
||||
// There's no equivalent GRPC endpoint to use, so we're using the HTTP api
|
||||
await apiFetch('/v1/user', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
mii: body.mii,
|
||||
environment: body.environment
|
||||
}),
|
||||
headers: {
|
||||
'Content-type': 'application/json'
|
||||
}
|
||||
await grpc.updateUserData({
|
||||
gender: body.gender,
|
||||
birthday: body.birthday,
|
||||
region: body.region,
|
||||
timezone: body.timezone,
|
||||
mii: body.mii,
|
||||
serverAccessLevel: body.serverAccessLevel
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ export default defineEventHandler(async (event): Promise<GetApiAuthMe> => {
|
||||
birthday: data.birthday,
|
||||
gender: data.gender,
|
||||
country: data.country,
|
||||
region: data.region,
|
||||
timezone: data.timezone,
|
||||
emailAddress: data.emailAddress,
|
||||
serverAccessLevel: data.serverAccessLevel as any,
|
||||
|
||||
@@ -53,6 +53,6 @@ export function useLegacyApiGrpcWithToken(event: H3Event, token: string): Client
|
||||
return getGrpcClient(event, APIDefinition, token);
|
||||
}
|
||||
|
||||
export function useApiGrpcWithToken(event: H3Event, token: string): Client<APIDefinition> {
|
||||
return getGrpcClient(event, APIDefinition, token);
|
||||
export function useApiGrpcWithToken(event: H3Event, token: string): Client<ApiServiceDefinition> {
|
||||
return getGrpcClient(event, ApiServiceDefinition, token);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ export type GetApiAuthMe = {
|
||||
birthday: string;
|
||||
gender: string;
|
||||
country: string;
|
||||
region: number;
|
||||
timezone: string;
|
||||
emailAddress: string;
|
||||
serverAccessLevel: 'dev' | 'test' | 'prod';
|
||||
@@ -107,12 +108,12 @@ export const RegisterSchema = z.object({
|
||||
export type ApiAuthRegisterRequest = z.infer<typeof RegisterSchema>;
|
||||
|
||||
export const AccountUpdateSchema = z.object({
|
||||
mii: z.object({
|
||||
name: z.string(),
|
||||
primary: z.enum(['Y', 'N']),
|
||||
data: z.string()
|
||||
}).optional(),
|
||||
environment: z.enum(['prod', 'test', 'dev']).optional()
|
||||
birthday: z.string().optional(),
|
||||
mii: z.string().optional(),
|
||||
serverAccessLevel: z.enum(['prod', 'test', 'dev']).optional(),
|
||||
gender: z.enum(['F', 'M']).optional(),
|
||||
region: z.number().optional(),
|
||||
timezone: z.string().optional()
|
||||
});
|
||||
export type ApiAccountUpdateRequest = z.infer<typeof AccountUpdateSchema>;
|
||||
|
||||
|
||||
@@ -138,7 +138,7 @@ div.modal-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
background: rgba(0, 0, 0, 0.75);
|
||||
|
||||
z-index: 100;
|
||||
}
|
||||
@@ -146,11 +146,12 @@ div.modal-wrapper.hidden {
|
||||
display: none;
|
||||
}
|
||||
div.modal {
|
||||
background: var(--bg-shade-3);
|
||||
background: var(--bg-shade-1);
|
||||
padding: 48px;
|
||||
border-radius: 8px;
|
||||
text-align: left;
|
||||
width: min(660px, 90%);
|
||||
max-width: min(660px, 90%);
|
||||
width: 100vw;
|
||||
box-sizing: border-box;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
43687
src/assets/json/regions.json
Normal file
43687
src/assets/json/regions.json
Normal file
File diff suppressed because it is too large
Load Diff
14121
src/assets/json/timezones.json
Normal file
14121
src/assets/json/timezones.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -223,7 +223,8 @@
|
||||
"nickname": "Nickname",
|
||||
"birthDate": "Birth date",
|
||||
"gender": "Gender",
|
||||
"country": "Country/region",
|
||||
"country": "Country",
|
||||
"region": "Region",
|
||||
"timezone": "Timezone",
|
||||
"serverEnv": "Server environment",
|
||||
"production": "Production",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -285,7 +285,7 @@ const { isLoading: isSaving, execute } = useAsync({
|
||||
return apiFetch('/api/account/update', {
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
mii: { name: mii.value.miiName, primary: 'Y', data: mii.value.encode().toString('base64') }
|
||||
mii: mii.value.encode().toString('base64')
|
||||
} satisfies ApiAccountUpdateRequest
|
||||
});
|
||||
},
|
||||
|
||||
155
src/utils/localizeConsole.ts
Normal file
155
src/utils/localizeConsole.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import regions from '@/assets/json/regions.json';
|
||||
import timezones from '@/assets/json/timezones.json';
|
||||
|
||||
export function webLocaleToConsoleLocale(lo: string): 'japanese' | 'french' | 'german' | 'italian' | 'spanish' | 'korean' | 'dutch' | 'portuguese' | 'russian' | 'chinese_traditional' | 'chinese_simple' | 'english' {
|
||||
const convObj = {
|
||||
'ja': 'japanese',
|
||||
'fr': 'french',
|
||||
'de': 'german',
|
||||
'it': 'italian',
|
||||
'es': 'spanish',
|
||||
'ko': 'korean',
|
||||
'nl': 'dutch',
|
||||
'pt': 'portuguese',
|
||||
'ru': 'russian',
|
||||
'zh-Hant': 'chinese_traditional',
|
||||
'zh-Hans': 'chinese_simple'
|
||||
};
|
||||
|
||||
const l = lo.split('-');
|
||||
|
||||
return convObj[l[0]] || convObj[lo] || 'english';
|
||||
}
|
||||
|
||||
export function webLocaleToTimezoneLocale(lo: string) {
|
||||
const l = lo.split('-');
|
||||
|
||||
switch (l[0]) {
|
||||
case 'ja':
|
||||
case 'fr':
|
||||
case 'de':
|
||||
case 'it':
|
||||
case 'es':
|
||||
case 'nl':
|
||||
case 'pt':
|
||||
case 'ru': {
|
||||
return l[0];
|
||||
}
|
||||
default: {
|
||||
return 'en';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function regionIdToCountryObject(region: number) {
|
||||
const countryId = (region >>> 24) & 0xFF;
|
||||
return regions.find(r => r.id === countryId);
|
||||
}
|
||||
|
||||
export function regionIdToObject(region: number) {
|
||||
return regions.flatMap(c => c.regions).find(r => r.id === region);
|
||||
}
|
||||
|
||||
export function regionIdToLocalizedNames(localeCode: string, region: number | undefined) {
|
||||
if (!localeCode || !region) {
|
||||
return;
|
||||
}
|
||||
|
||||
const l = webLocaleToConsoleLocale(localeCode);
|
||||
|
||||
const lCountryName = regionIdToCountryObject(region)?.translations[l];
|
||||
const lRegionName = regionIdToObject(region)?.translations[l];
|
||||
|
||||
return {
|
||||
country: lCountryName,
|
||||
region: lRegionName
|
||||
};
|
||||
}
|
||||
|
||||
export function getLocalizedRegionTimezones(localeCode: string, region: number | undefined) {
|
||||
if (!localeCode || !region) {
|
||||
return;
|
||||
}
|
||||
|
||||
const country = regionIdToCountryObject(region);
|
||||
const l = webLocaleToTimezoneLocale(localeCode);
|
||||
|
||||
const countryTimezones = timezones[country.iso_code];
|
||||
|
||||
return countryTimezones[l] || countryTimezones.en || countryTimezones.ja;
|
||||
}
|
||||
|
||||
export function getLocalizedTimezoneString(localeCode: string | undefined, region: number | undefined, timezone: string | undefined) {
|
||||
if (!localeCode || !region || !timezone) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tList = getLocalizedRegionTimezones(localeCode, region);
|
||||
|
||||
let localizedTz = '';
|
||||
|
||||
const tzObj = tList.find((v) => {
|
||||
return (v.area === timezone);
|
||||
});
|
||||
|
||||
if (tzObj) {
|
||||
localizedTz = `${timezone?.replaceAll('_', ' ')} - ${tzObj.name}`;
|
||||
}
|
||||
|
||||
return localizedTz;
|
||||
}
|
||||
|
||||
export function getLocalizedCountryList(localeCode: string) {
|
||||
const l = webLocaleToConsoleLocale(localeCode);
|
||||
|
||||
const lCountryList: {
|
||||
code: string;
|
||||
id: number;
|
||||
name: string;
|
||||
}[] = [];
|
||||
|
||||
regions.forEach(
|
||||
(r) => {
|
||||
lCountryList.push({
|
||||
code: r.iso_code,
|
||||
id: r.id,
|
||||
name: r.translations[l] || r.translations.english || r.name || r.translations.japanese
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
lCountryList.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return lCountryList;
|
||||
}
|
||||
|
||||
export function getLocalizedRegionList(localeCode: string, region: number | undefined) {
|
||||
if (!localeCode || !region) {
|
||||
return;
|
||||
}
|
||||
|
||||
const l = webLocaleToConsoleLocale(localeCode);
|
||||
const c = regionIdToCountryObject(region);
|
||||
|
||||
const lRegionList: {
|
||||
id: number;
|
||||
name: string;
|
||||
}[] = [];
|
||||
|
||||
c?.regions.forEach(
|
||||
(r) => {
|
||||
lRegionList.push({
|
||||
id: r.id,
|
||||
name: r.translations[l] || r.translations.english || r.name || r.translations.japanese
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
lRegionList.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
|
||||
return lRegionList;
|
||||
}
|
||||
Reference in New Issue
Block a user