Added UserBox page

This commit is contained in:
alexay7
2024-06-05 18:57:48 +02:00
parent c5d81afdf6
commit f4bb1101bf
7 changed files with 1134 additions and 203 deletions

View File

@@ -11,5 +11,7 @@ export const DISCORD_INVITE = 'https://discord.gg/FNgveqFF7s'
// UI
export const FADE_OUT = { duration: 200 }
export const FADE_IN = { delay: 400 }
export const DEFAULT_PFP = "/assets/imgs/no_profile.png"
export const DEFAULT_PFP = '/assets/imgs/no_profile.png'
// USERBOX_ASSETS
export const HAS_USERBOX_ASSETS = true

View File

@@ -118,5 +118,68 @@ export type AllMusic = { [key: string]: MusicMeta }
export interface GameOption {
key: string
value: any
type: "Boolean"
type: 'Boolean'
}
export interface UserBox {
userName:string,
level:number,
exp:string,
point:number,
totalPoint:number,
playerRating:number,
highestRating:number,
nameplateId:number,
frameId:number,
characterId:number,
trophyId:number,
totalMapNum:number,
totalHiScore: number,
totalBasicHighScore:number,
totalAdvancedHighScore:number,
totalExpertHighScore:number,
totalMasterHighScore:number,
totalUltimaHighScore:number,
friendCount:number,
firstPlayDate:Date,
lastPlayDate:Date,
courseClass:number,
overPowerPoint:number,
overPowerRate:number,
mapIconId:number,
voiceId:number,
avatarWear: number,
avatarHead: number,
avatarFace: number,
avatarSkin: number,
avatarItem: number,
avatarFront: number,
avatarBack: number,
}
// Assign a number to each kind of user box item with an enum
export enum UserBoxItemKind {
nameplate = 1,
frame = 2,
trophy = 3,
mapicon = 8,
sysvoice = 9,
avatar = 11,
}
// Define type only with the keys
export type UserBoxItemKindStr = keyof typeof UserBoxItemKind;
type ChangePlateReq = {kind:'plate', nameplateId:number}
type ChangeFrameReq = {kind:'frame', frameId:number}
type ChangeTrophyReq = {kind:'trophy',trophyId:number}
type ChangeMapIconReq = {kind:'mapicon',mapiconid:number}
type ChangeVoiceReq = {kind:'sysvoice',voiceId:number}
type ChangeAvatarReq = {
kind:'avatar',
accId:number,
category:number
}
export type ChangeUserBoxReq = {aimeId:string} & (ChangePlateReq | ChangeFrameReq | ChangeTrophyReq | ChangeMapIconReq | ChangeVoiceReq | ChangeAvatarReq);

View File

@@ -17,13 +17,13 @@ export const EN_REF_USER = {
'UserHome.Version': 'Last Version',
'UserHome.RecentScores': 'Recent Scores',
'UserHome.NoData': 'No data in the past ${days} days',
'UserHome.UnknownSong': "(unknown song)",
'UserHome.UnknownSong': '(unknown song)',
'UserHome.Settings': 'Settings',
'UserHome.NoValidGame': "The user hasn't played any game yet.",
'UserHome.ShowRanksDetails': "Click to show details",
'UserHome.NoValidGame': 'The user hasn\'t played any game yet.',
'UserHome.ShowRanksDetails': 'Click to show details',
'UserHome.RankDetail.Title': 'Achievement Details',
'UserHome.RankDetail.Level': "Level",
'UserHome.B50': "B50",
'UserHome.RankDetail.Level': 'Level',
'UserHome.B50': 'B50',
}
export const EN_REF_Welcome = {
@@ -57,11 +57,11 @@ export const EN_REF_LEADERBOARD = {
}
export const EN_REF_GENERAL = {
'game.mai2': "Mai",
'game.chu3': "Chuni",
'game.ongeki': "Ongeki",
'game.wacca': "Wacca",
'status.error': "Error",
'game.mai2': 'Mai',
'game.chu3': 'Chuni',
'game.ongeki': 'Ongeki',
'game.wacca': 'Wacca',
'status.error': 'Error',
'status.error.hint': 'Something went wrong, please try again later or ',
'status.error.hint.link': 'join our discord for support.',
'status.detail': 'Detail: ${detail}',
@@ -82,39 +82,40 @@ export const EN_REF_HOME = {
'home.join-discord-description': 'Join our Discord server to chat with other players and get help.',
'home.setup': 'Setup Connection',
'home.setup-description': 'If you own a cab or arcade setup, begin setting up the connection.',
'home.linkcard.cards': "Your Cards",
'home.linkcard.description': "Here are the cards you have linked to your account",
'home.linkcard.account-card': "Account Card",
'home.linkcard.registered': "Registered",
'home.linkcard.lastused': "Last used",
'home.linkcard.enter-info': "Please enter the following information",
'home.linkcard.access-code': "The 20-digit access code on the back of your card. (If it doesn't work, please try scanning your card in game and enter the access code shown on screen)",
'home.linkcard.enter-sn1': "Download the NFC Tools app on your phone",
'home.linkcard.enter-sn2': "and scan your card. Then, enter the Serial Number.",
'home.linkcard.link': "Link",
'home.linkcard.data-conflict': "Data Conflict",
'home.linkcard.name': "Name",
'home.linkcard.rating': "Rating",
'home.linkcard.last-login': "Last Login",
'home.linkcard.linked-own': "This card is already linked to your account",
'home.linkcard.linked-another': "This card is already linked to another account",
'home.linkcard.notfound': "Card not found",
'home.linkcard.unlink': "Unlink Card",
'home.linkcard.unlink-notice': "Are you sure you want to unlink this card?",
'home.setup.welcome': "Welcome! If you own an arcade cabinet or game setup, please follow the instructions below to set up the connection with AquaDX.",
'home.setup.blockquote': "We assume that you already have the required files and can run the game (e.g. ROM and segatools) that come with the cabinet or game setup. If not, please contact the seller of your device for the required files, as we will not provide them for copyright reasons.",
'home.setup.get': "Get started",
'home.setup.edit': "Please edit your segatools.ini file and modify the following lines",
'home.setup.test': "Then, after you restart the game, you should be able to connect to AquaDX. Please verify that the network tests are all GOOD in the test menu.",
'home.setup.ask': "If you have any questions, please ask in our",
'home.setup.support': "server",
'home.setup.keychip-tips': "This is your unique keychip, do not share it with anyone",
'home.linkcard.cards': 'Your Cards',
'home.linkcard.description': 'Here are the cards you have linked to your account',
'home.linkcard.account-card': 'Account Card',
'home.linkcard.registered': 'Registered',
'home.linkcard.lastused': 'Last used',
'home.linkcard.enter-info': 'Please enter the following information',
'home.linkcard.access-code': 'The 20-digit access code on the back of your card. (If it doesn\'t work, please try scanning your card in game and enter the access code shown on screen)',
'home.linkcard.enter-sn1': 'Download the NFC Tools app on your phone',
'home.linkcard.enter-sn2': 'and scan your card. Then, enter the Serial Number.',
'home.linkcard.link': 'Link',
'home.linkcard.data-conflict': 'Data Conflict',
'home.linkcard.name': 'Name',
'home.linkcard.rating': 'Rating',
'home.linkcard.last-login': 'Last Login',
'home.linkcard.linked-own': 'This card is already linked to your account',
'home.linkcard.linked-another': 'This card is already linked to another account',
'home.linkcard.notfound': 'Card not found',
'home.linkcard.unlink': 'Unlink Card',
'home.linkcard.unlink-notice': 'Are you sure you want to unlink this card?',
'home.setup.welcome': 'Welcome! If you own an arcade cabinet or game setup, please follow the instructions below to set up the connection with AquaDX.',
'home.setup.blockquote': 'We assume that you already have the required files and can run the game (e.g. ROM and segatools) that come with the cabinet or game setup. If not, please contact the seller of your device for the required files, as we will not provide them for copyright reasons.',
'home.setup.get': 'Get started',
'home.setup.edit': 'Please edit your segatools.ini file and modify the following lines',
'home.setup.test': 'Then, after you restart the game, you should be able to connect to AquaDX. Please verify that the network tests are all GOOD in the test menu.',
'home.setup.ask': 'If you have any questions, please ask in our',
'home.setup.support': 'server',
'home.setup.keychip-tips': 'This is your unique keychip, do not share it with anyone',
}
export const EN_REF_SETTINGS = {
'settings.title': 'Settings',
'settings.tabs.profile': 'Profile',
'settings.tabs.game': 'Game',
'settings.tabs.userbox': 'Userbox',
'settings.fields.unlockMusic.name': 'Unlock All Music',
'settings.fields.unlockMusic.desc': 'Unlock all music and master difficulty in game.',
'settings.fields.unlockChara.name': 'Unlock All Characters',
@@ -140,7 +141,30 @@ export const EN_REF_SETTINGS = {
'settings.profile.unchanged': 'Unchanged',
}
export const EN_REF_USERBOX = {
'userbox.tabs.chusan':'Chuni',
'userbox.tabs.maimai':'Mai (WIP)',
'userbox.tabs.ongeki':'Ongeki (WIP)',
'userbox.nameplate': 'Nameplate',
'userbox.frame': 'Frame',
'userbox.trophy': 'Trophy (Title)',
'userbox.mapicon': 'Map Icon',
'userbox.voice':'System Voice',
'userbox.wear':'Avatar Wear',
'userbox.head':'Avatar Head',
'userbox.face':'Avatar Face',
'userbox.skin':'Avatar Skin',
'userbox.item':'Avatar Item',
'userbox.front':'Avatar Front',
'userbox.back':'Avatar Back',
'userbox.preview.avatar':'Avatar Preview',
'userbox.preview.nameplate':'Nameplate Preview',
'userbox.preview.ui':'Interface Preview',
'userbox.error.noprofile':'No profile was found for this game',
'userbox.error.nodata':'No data was found for this game',
}
export const EN_REF = { ...EN_REF_USER, ...EN_REF_Welcome, ...EN_REF_GENERAL,
...EN_REF_LEADERBOARD, ...EN_REF_HOME, ...EN_REF_SETTINGS }
...EN_REF_LEADERBOARD, ...EN_REF_HOME, ...EN_REF_SETTINGS, ...EN_REF_USERBOX }
export type LocalizedMessages = typeof EN_REF

View File

@@ -1,162 +1,315 @@
import { AQUA_HOST, DATA_HOST } from "./config";
import type {
AllMusic,
Card,
CardSummary,
GenericGameSummary,
GenericRanking,
TrendEntry,
AquaNetUser, GameOption
} from "./generalTypes";
import type { GameName } from "./scoring";
interface RequestInitWithParams extends RequestInit {
params?: { [index: string]: string }
localCache?: boolean
}
/**
* Modify a fetch url
*
* @param input Fetch url input
* @param callback Callback for modification
*/
export function reconstructUrl(input: URL | RequestInfo, callback: (url: URL) => URL | void): RequestInfo | URL {
let u = new URL((input instanceof Request) ? input.url : input);
const result = callback(u)
if (result) u = result
if (input instanceof Request) {
// @ts-ignore
return { url: u, ...input }
}
return u
}
/**
* Fetch with url parameters
*/
export function fetchWithParams(input: URL | RequestInfo, init?: RequestInitWithParams): Promise<Response> {
return fetch(reconstructUrl(input, u => {
u.search = new URLSearchParams(init?.params ?? {}).toString()
}), init)
}
let cache: { [index: string]: any } = {}
export async function post(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
// Add token if exists
const token = localStorage.getItem('token')
if (token && !('token' in params)) params = { ...(params ?? {}), token }
if (init?.localCache) {
const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)]
if (cached) return cached
}
let res = await fetchWithParams(AQUA_HOST + endpoint, {
method: 'POST',
params,
...init
}).catch(e => {
console.error(e)
throw new Error('Network error')
})
if (!res.ok) {
const text = await res.text()
console.error(`${res.status}: ${text}`)
// If 400 invalid token is caught, should invalidate the token and redirect to signin
if (text === 'Invalid token') {
localStorage.removeItem('token')
window.location.href = '/'
}
// Try to parse as json
let json
try {
json = JSON.parse(text)
} catch (e) {
throw new Error(text)
}
if (json.error) throw new Error(json.error)
}
const ret = res.json()
cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret
return ret
}
/**
* aqua.net.UserRegistrar
*
* @param user
*/
async function register(user: { username: string, email: string, password: string, turnstile: string }) {
return await post('/api/v2/user/register', user)
}
async function login(user: { email: string, password: string, turnstile: string }) {
const data = await post('/api/v2/user/login', user)
// Put token into local storage
localStorage.setItem('token', data.token)
}
const isLoggedIn = () => !!localStorage.getItem('token')
const ensureLoggedIn = () => !isLoggedIn() && (window.location.href = '/')
export const USER = {
register,
login,
confirmEmail: (token: string) =>
post('/api/v2/user/confirm-email', { token }),
me: (): Promise<AquaNetUser> => {
ensureLoggedIn()
return post('/api/v2/user/me', {})
},
keychip: (): Promise<string> =>
post('/api/v2/user/keychip', {}).then(it => it.keychip),
setting: (key: string, value: string) =>
post('/api/v2/user/setting', { key: key === 'password' ? 'pwHash' : key, value }),
uploadPfp: (file: File) => {
const formData = new FormData()
formData.append('file', file)
return post('/api/v2/user/upload-pfp', { }, { method: 'POST', body: formData })
},
isLoggedIn,
ensureLoggedIn,
}
export const CARD = {
summary: (cardId: string): Promise<{card: Card, summary: CardSummary}> =>
post('/api/v2/card/summary', { cardId }),
link: (props: { cardId: string, migrate: string }) =>
post('/api/v2/card/link', props),
unlink: (cardId: string) =>
post('/api/v2/card/unlink', { cardId }),
userGames: (username: string): Promise<CardSummary> =>
post('/api/v2/card/user-games', { username }),
}
export const GAME = {
trend: (username: string, game: GameName): Promise<TrendEntry[]> =>
post(`/api/v2/game/${game}/trend`, { username }),
userSummary: (username: string, game: GameName): Promise<GenericGameSummary> =>
post(`/api/v2/game/${game}/user-summary`, { username }),
ranking: (game: GameName): Promise<GenericRanking[]> =>
post(`/api/v2/game/${game}/ranking`, { }),
}
export const DATA = {
allMusic: (game: GameName): Promise<AllMusic> =>
fetch(`${DATA_HOST}/d/${game}/00/all-music.json`).then(it => it.json())
}
export const SETTING = {
get: (): Promise<GameOption[]> =>
post('/api/v2/settings/get', {}),
set: (key: string, value: any) =>
post('/api/v2/settings/set', { key, value: `${value}` }),
}
import { AQUA_HOST, DATA_HOST } from './config'
import type {
AllMusic,
Card,
CardSummary,
GenericGameSummary,
GenericRanking,
TrendEntry,
AquaNetUser, GameOption,
UserBox,
ChangeUserBoxReq,
UserBoxItemKind
} from './generalTypes'
import type { GameName } from './scoring'
interface RequestInitWithParams extends RequestInit {
params?: { [index: string]: string }
localCache?: boolean
}
/**
* Modify a fetch url
*
* @param input Fetch url input
* @param callback Callback for modification
*/
export function reconstructUrl(input: URL | RequestInfo, callback: (url: URL) => URL | void): RequestInfo | URL {
let u = new URL((input instanceof Request) ? input.url : input)
const result = callback(u)
if (result) u = result
if (input instanceof Request) {
// @ts-ignore
return { url: u, ...input }
}
return u
}
/**
* Fetch with url parameters
*/
export function fetchWithParams(input: URL | RequestInfo, init?: RequestInitWithParams): Promise<Response> {
return fetch(reconstructUrl(input, u => {
u.search = new URLSearchParams(init?.params ?? {}).toString()
}), init)
}
const cache: { [index: string]: any } = {}
export async function post(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
// Add token if exists
const token = localStorage.getItem('token')
if (token && !('token' in params)) params = { ...(params ?? {}), token }
if (init?.localCache) {
const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)]
if (cached) return cached
}
const res = await fetchWithParams(AQUA_HOST + endpoint, {
method: 'POST',
params,
...init
}).catch(e => {
console.error(e)
throw new Error('Network error')
})
if (!res.ok) {
const text = await res.text()
console.error(`${res.status}: ${text}`)
// If 400 invalid token is caught, should invalidate the token and redirect to signin
if (text === 'Invalid token') {
localStorage.removeItem('token')
window.location.href = '/'
}
// Try to parse as json
let json
try {
json = JSON.parse(text)
} catch (e) {
throw new Error(text)
}
if (json.error) throw new Error(json.error)
}
const ret = res.json()
cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret
return ret
}
export async function get(endpoint: string, params:any,init?: RequestInitWithParams): Promise<any> {
// Add token if exists
const token = localStorage.getItem('token')
if (init?.localCache) {
const cached = cache[endpoint + JSON.stringify(init)]
if (cached) return cached
}
const res = await fetchWithParams(AQUA_HOST + endpoint, {
method: 'GET',
params,
...init
}).catch(e => {
console.error(e)
throw new Error('Network error')
})
if (!res.ok) {
const text = await res.text()
console.error(`${res.status}: ${text}`)
// If 400 invalid token is caught, should invalidate the token and redirect to signin
if (text === 'Invalid token') {
localStorage.removeItem('token')
window.location.href = '/'
}
// Try to parse as json
let json
try {
json = JSON.parse(text)
} catch (e) {
throw new Error(text)
}
if (json.error) throw new Error(json.error)
}
const ret = res.json()
cache[endpoint + JSON.stringify(init)] = ret
return ret
}
export async function put(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
// Add token if exists
const token = localStorage.getItem('token')
if (token && !('token' in params)) params = { ...(params ?? {}), token }
if (init?.localCache) {
const cached = cache[endpoint + JSON.stringify(params) + JSON.stringify(init)]
if (cached) return cached
}
const res = await fetchWithParams(AQUA_HOST + endpoint, {
method: 'PUT',
body: JSON.stringify(params),
headers:{
'Content-Type':'application/json',
...init?.headers
},
...init
}).catch(e => {
console.error(e)
throw new Error('Network error')
})
if (!res.ok) {
const text = await res.text()
console.error(`${res.status}: ${text}`)
// If 400 invalid token is caught, should invalidate the token and redirect to signin
if (text === 'Invalid token') {
localStorage.removeItem('token')
window.location.href = '/'
}
// Try to parse as json
let json
try {
json = JSON.parse(text)
} catch (e) {
throw new Error(text)
}
if (json.error) throw new Error(json.error)
}
const ret = res.json()
cache[endpoint + JSON.stringify(params) + JSON.stringify(init)] = ret
return ret
}
export async function realPost(endpoint: string, params: any, init?: RequestInitWithParams): Promise<any> {
const res = await fetchWithParams(AQUA_HOST + endpoint, {
method: 'POST',
body: JSON.stringify(params),
headers:{
'Content-Type':'application/json',
...init?.headers
},
...init
}).catch(e => {
console.error(e)
throw new Error('Network error')
})
if (!res.ok) {
const text = await res.text()
console.error(`${res.status}: ${text}`)
// If 400 invalid token is caught, should invalidate the token and redirect to signin
if (text === 'Invalid token') {
localStorage.removeItem('token')
window.location.href = '/'
}
// Try to parse as json
let json
try {
json = JSON.parse(text)
} catch (e) {
throw new Error(text)
}
if (json.error) throw new Error(json.error)
}
return res.json()
}
/**
* aqua.net.UserRegistrar
*
* @param user
*/
async function register(user: { username: string, email: string, password: string, turnstile: string }) {
return await post('/api/v2/user/register', user)
}
async function login(user: { email: string, password: string, turnstile: string }) {
const data = await post('/api/v2/user/login', user)
// Put token into local storage
localStorage.setItem('token', data.token)
}
const isLoggedIn = () => !!localStorage.getItem('token')
const ensureLoggedIn = () => !isLoggedIn() && (window.location.href = '/')
export const USER = {
register,
login,
confirmEmail: (token: string) =>
post('/api/v2/user/confirm-email', { token }),
me: (): Promise<AquaNetUser> => {
ensureLoggedIn()
return post('/api/v2/user/me', {})
},
keychip: (): Promise<string> =>
post('/api/v2/user/keychip', {}).then(it => it.keychip),
setting: (key: string, value: string) =>
post('/api/v2/user/setting', { key: key === 'password' ? 'pwHash' : key, value }),
uploadPfp: (file: File) => {
const formData = new FormData()
formData.append('file', file)
return post('/api/v2/user/upload-pfp', { }, { method: 'POST', body: formData })
},
isLoggedIn,
ensureLoggedIn,
}
export const USERBOX = {
getAimeId:(cardId:string):Promise<{luid:string}|null> =>realPost('/api/sega/aime/getByAccessCode',{ accessCode:cardId }),
getProfile:(aimeId:string):Promise<UserBox> =>get('/api/game/chuni/v2/profile',{ aimeId }),
getUnlockedItems:(aimeId:string, itemId: UserBoxItemKind):Promise<{itemKind:number, itemId:number,stock:number,isValid:boolean}[]> =>
get(`/api/game/chuni/v2/item/${itemId}`,{ aimeId }),
getItemLabels:() => {
const kinds = [ 'nameplate', 'frame', 'trophy', 'mapicon', 'sysvoice', 'avatar' ]
return Promise.all(kinds.map(it =>
get(`/api/game/chuni/v2/data/${it}`,{}).then((res:{id:number,name:string}[]) =>
// Use the id as the key
res.reduce((acc, cur) => ({ ...acc, [cur.id]: cur.name }), {}) as { [index: number]: string }
))).then(([ nameplate, frame, trophy, mapicon, sysvoice, avatar ]) => ({
nameplate, frame, trophy, mapicon, sysvoice, avatar
}))
},
setUserBox:({ kind,...body }:ChangeUserBoxReq) =>
put(`/api/game/chuni/v2/profile/${kind}`, body),
}
export const CARD = {
summary: (cardId: string): Promise<{card: Card, summary: CardSummary}> =>
post('/api/v2/card/summary', { cardId }),
link: (props: { cardId: string, migrate: string }) =>
post('/api/v2/card/link', props),
unlink: (cardId: string) =>
post('/api/v2/card/unlink', { cardId }),
userGames: (username: string): Promise<CardSummary> =>
post('/api/v2/card/user-games', { username }),
}
export const GAME = {
trend: (username: string, game: GameName): Promise<TrendEntry[]> =>
post(`/api/v2/game/${game}/trend`, { username }),
userSummary: (username: string, game: GameName): Promise<GenericGameSummary> =>
post(`/api/v2/game/${game}/user-summary`, { username }),
ranking: (game: GameName): Promise<GenericRanking[]> =>
post(`/api/v2/game/${game}/ranking`, { }),
}
export const DATA = {
allMusic: (game: GameName): Promise<AllMusic> =>
fetch(`${DATA_HOST}/d/${game}/00/all-music.json`).then(it => it.json())
}
export const SETTING = {
get: (): Promise<GameOption[]> =>
post('/api/v2/settings/get', {}),
set: (key: string, value: any) =>
post('/api/v2/settings/set', { key, value: `${value}` }),
}