fix: chuni: errors from null userbox values when items not in database

This commit is contained in:
sk1982 2024-04-22 00:28:52 -04:00
parent 61892d004e
commit 140520fc5e
4 changed files with 35 additions and 32 deletions

View File

@ -58,14 +58,17 @@ export async function getUserData(user: { id: number }) {
[`${name}.name as ${name}Name`, `${name}.iconPath as ${name}Icon`, [`${name}.name as ${name}Name`, `${name}.iconPath as ${name}Icon`,
`${name}.texturePath as ${name}Texture`] as const) `${name}.texturePath as ${name}Texture`] as const)
]) ])
.where(({ and, eb, selectFrom }) => and([ .where(({ and, eb, selectFrom, or }) => and([
eb('p.user', '=', user.id), eb('p.user', '=', user.id),
eb('p.version', '=', eb('p.version', '=',
selectFrom('chuni_static_music') selectFrom('chuni_static_music')
.select(({ fn }) => fn.max('version').as('latest')) .select(({ fn }) => fn.max('version').as('latest'))
), ),
...avatarNames.map(name => eb(`${name}.version`, '=', selectFrom('chuni_static_music') ...avatarNames.map(name => or([
.select(({ fn }) => fn.max('version').as('latest')))) eb(`${name}.version`, '=', selectFrom('chuni_static_avatar')
.select(({ fn }) => fn.max('version').as('latest'))),
eb(`${name}.version`, 'is', null)
]))
])) ]))
.executeTakeFirst(); .executeTakeFirst();

View File

@ -43,7 +43,7 @@ const ITEM_KEYS: Record<keyof UserboxItems, keyof NonNullable<ChuniUserData>> =
const AVATAR_KEYS = ['avatarWear', 'avatarHead', 'avatarFace', 'avatarSkin', 'avatarItem', 'avatarFront', 'avatarBack'] as const; const AVATAR_KEYS = ['avatarWear', 'avatarHead', 'avatarFace', 'avatarSkin', 'avatarItem', 'avatarFront', 'avatarBack'] as const;
type RequiredUserbox = NonNullable<UserboxItems>; type RequiredUserbox = NonNullable<UserboxItems>;
type EquippedItem = { [K in keyof RequiredUserbox]: RequiredUserbox[K][number] }; type EquippedItem = { [K in keyof RequiredUserbox]: RequiredUserbox[K][number] | null };
type SavedItem = { [K in keyof RequiredUserbox]: boolean } & { username: boolean }; type SavedItem = { [K in keyof RequiredUserbox]: boolean } & { username: boolean };
export const ChuniUserbox = ({ profile, userboxItems }: ChuniUserboxProps) => { export const ChuniUserbox = ({ profile, userboxItems }: ChuniUserboxProps) => {
@ -92,7 +92,7 @@ export const ChuniUserbox = ({ profile, userboxItems }: ChuniUserboxProps) => {
const update: Partial<ProfileUpdate> = Object.fromEntries((Object.entries(equipped) as Entries<typeof equipped>) const update: Partial<ProfileUpdate> = Object.fromEntries((Object.entries(equipped) as Entries<typeof equipped>)
.filter(([k]) => items.includes(k as any)) .filter(([k]) => items.includes(k as any))
.map(([k, v]) => [ITEM_KEYS[k], v.id])); .map(([k, v]) => [ITEM_KEYS[k], v?.id]));
if ((items as string[]).includes('username')) if ((items as string[]).includes('username'))
update.userName = username; update.userName = username;
@ -129,13 +129,13 @@ export const ChuniUserbox = ({ profile, userboxItems }: ChuniUserboxProps) => {
selectedKeys={selectedLine} onSelectionChange={s => { selectedKeys={selectedLine} onSelectionChange={s => {
if (typeof s === 'string' || !s.size) return; if (typeof s === 'string' || !s.size) return;
setSelectedLine(s as any); setSelectedLine(s as any);
play(getAudioUrl(`chuni/system-voice/${selectingVoice?.cuePath ?? equipped.systemVoice.cuePath}_${[...s][0]}`)) play(getAudioUrl(`chuni/system-voice/${selectingVoice?.cuePath ?? equipped.systemVoice?.cuePath}_${[...s][0]}`))
}}> }}>
{CHUNI_VOICE_LINES.map(([line, id]) => <SelectItem key={id}>{line}</SelectItem>)} {CHUNI_VOICE_LINES.map(([line, id]) => <SelectItem key={id}>{line}</SelectItem>)}
</Select> </Select>
<Button isIconOnly color="primary" className="p-1.5 h-full" radius="none" size="lg" isDisabled={!playPreviews} <Button isIconOnly color="primary" className="p-1.5 h-full" radius="none" size="lg" isDisabled={!playPreviews}
onPress={() => playingVoice ? stop() : onPress={() => playingVoice ? stop() :
play(getAudioUrl(`chuni/system-voice/${selectingVoice?.cuePath ?? equipped.systemVoice.cuePath}_${[...selectedLine][0]}`))}> play(getAudioUrl(`chuni/system-voice/${selectingVoice?.cuePath ?? equipped.systemVoice?.cuePath}_${[...selectedLine][0]}`))}>
{playingVoice ? <StopIcon /> : <PlayIcon />} {playingVoice ? <StopIcon /> : <PlayIcon />}
</Button> </Button>
</div>); </div>);
@ -171,14 +171,14 @@ export const ChuniUserbox = ({ profile, userboxItems }: ChuniUserboxProps) => {
<ChuniNameplate profile={profile ? { <ChuniNameplate profile={profile ? {
...profile, ...profile,
userName: username, userName: username,
nameplateName: equipped.namePlate.name, nameplateName: equipped.namePlate?.name,
nameplateImage: equipped.namePlate.imagePath, nameplateImage: equipped.namePlate?.imagePath,
trophyName: equipped.trophy.name, trophyName: equipped.trophy?.name,
trophyRareType: equipped.trophy.rareType trophyRareType: equipped.trophy?.rareType
} : null} className="w-full" /> } : null} className="w-full" />
</div> </div>
<ChuniNameModal username={profile?.userName!} isOpen={editingUsername} <ChuniNameModal username={profile?.userName ?? ''} isOpen={editingUsername}
onClose={() => setEditingUsername(false)} onClose={() => setEditingUsername(false)}
setUsername={u => { setUsername={u => {
setSaved(s => ({ ...s, username: false })); setSaved(s => ({ ...s, username: false }));
@ -225,12 +225,12 @@ export const ChuniUserbox = ({ profile, userboxItems }: ChuniUserboxProps) => {
<div className="flex flex-col sm:flex-row h-full w-full items-center sm:pl-3 sm:pr-6 sm:pb-2"> <div className="flex flex-col sm:flex-row h-full w-full items-center sm:pl-3 sm:pr-6 sm:pb-2">
<div className="w-full max-w-96"> <div className="w-full max-w-96">
<ChuniAvatar className="w-full sm:w-auto sm:h-96" <ChuniAvatar className="w-full sm:w-auto sm:h-96"
wear={equipped.avatarWear.texturePath} wear={equipped.avatarWear?.texturePath}
head={equipped.avatarHead.texturePath} head={equipped.avatarHead?.texturePath}
face={equipped.avatarFace.texturePath} face={equipped.avatarFace?.texturePath}
skin={equipped.avatarSkin.texturePath} skin={equipped.avatarSkin?.texturePath}
item={equipped.avatarItem.texturePath} item={equipped.avatarItem?.texturePath}
back={equipped.avatarBack.texturePath}/> back={equipped.avatarBack?.texturePath}/>
</div> </div>
<div className="grid grid-cols-2 w-full px-2 sm:px-0 sm:flex flex-col gap-1.5 sm:ml-3 flex-grow"> <div className="grid grid-cols-2 w-full px-2 sm:px-0 sm:flex flex-col gap-1.5 sm:ml-3 flex-grow">
{(['avatarHead', 'avatarFace', 'avatarWear', 'avatarSkin', 'avatarItem', 'avatarBack'] as const).map(k => ((k !== 'avatarSkin' || userboxItems.avatarSkin.length > 1) && <SelectModalButton {(['avatarHead', 'avatarFace', 'avatarWear', 'avatarSkin', 'avatarItem', 'avatarBack'] as const).map(k => ((k !== 'avatarSkin' || userboxItems.avatarSkin.length > 1) && <SelectModalButton
@ -267,8 +267,8 @@ export const ChuniUserbox = ({ profile, userboxItems }: ChuniUserboxProps) => {
<div className="flex w-full flex-col sm:flex-row items-center px-2 sm:px-4 sm:pb-4 h-full"> <div className="flex w-full flex-col sm:flex-row items-center px-2 sm:px-4 sm:pb-4 h-full">
<div className="flex flex-col"> <div className="flex flex-col">
<Image className="w-80 max-w-full" width={320} height={204} priority <Image className="w-80 max-w-full" width={320} height={204} priority
alt={equipped.systemVoice.name ?? ''} src={getImageUrl(`chuni/system-voice-icon/${equipped.systemVoice.imagePath}`)} /> alt={equipped.systemVoice?.name ?? ''} src={getImageUrl(`chuni/system-voice-icon/${equipped.systemVoice?.imagePath}`)} />
<span className="text-center">{ equipped.systemVoice.name }</span> <span className="text-center">{ equipped.systemVoice?.name }</span>
</div> </div>
<div className="flex flex-col flex-grow w-fullmt-3 sm:-mt-5 sm:w-auto gap-2 w-full"> <div className="flex flex-col flex-grow w-fullmt-3 sm:-mt-5 sm:w-auto gap-2 w-full">
@ -324,8 +324,8 @@ export const ChuniUserbox = ({ profile, userboxItems }: ChuniUserboxProps) => {
<Image className="w-52 max-w-full -mt-2" width={208} height={208} priority <Image className="w-52 max-w-full -mt-2" width={208} height={208} priority
alt={equipped.mapIcon.name ?? ''} src={getImageUrl(`chuni/map-icon/${equipped.mapIcon.imagePath}`)} /> alt={equipped.mapIcon?.name ?? ''} src={getImageUrl(`chuni/map-icon/${equipped.mapIcon?.imagePath}`)} />
<span className="text-center mb-2">{ equipped.mapIcon.name }</span> <span className="text-center mb-2">{ equipped.mapIcon?.name }</span>
<div className="px-2 w-full flex justify-center"> <div className="px-2 w-full flex justify-center">
<SelectModalButton onSelected={i => i && equipItem('mapIcon', i)} selectedItem={equipped.mapIcon} <SelectModalButton onSelected={i => i && equipItem('mapIcon', i)} selectedItem={equipped.mapIcon}
displayMode="grid" modalSize="full" rowSize={210} colSize={175} items={userboxItems.mapIcon} gap={6} displayMode="grid" modalSize="full" rowSize={210} colSize={175} items={userboxItems.mapIcon} gap={6}

View File

@ -2,12 +2,12 @@ import { getImageUrl } from '@/helpers/assets';
import Image from 'next/image'; import Image from 'next/image';
export type ChuniAvatarProps = { export type ChuniAvatarProps = {
wear: string | null, wear: string | null | undefined,
head: string | null, head: string | null | undefined,
face: string | null, face: string | null | undefined,
skin: string | null, skin: string | null | undefined,
item: string | null, item: string | null | undefined,
back: string | null, back: string | null | undefined,
className?: string className?: string
}; };

View File

@ -15,7 +15,7 @@ export type Profile = PickNullable<ChuniUserData, (typeof CHUNI_NAMEPLATE_PROFIL
export type ChuniNameplateProps = { export type ChuniNameplateProps = {
className?: string, className?: string,
profile: Profile, profile: Partial<Profile>,
}; };
export const ChuniNameplate = ({ className, profile }: ChuniNameplateProps) => { export const ChuniNameplate = ({ className, profile }: ChuniNameplateProps) => {
@ -36,7 +36,7 @@ export const ChuniNameplate = ({ className, profile }: ChuniNameplateProps) => {
</div> </div>
</div> </div>
</div> </div>
<ChuniTrophy rarity={profile.trophyRareType} name={profile.trophyName} /> <ChuniTrophy rarity={profile?.trophyRareType ?? 0} name={profile?.trophyName ?? ''} />
<div className="w-[99%] h-[52%] flex"> <div className="w-[99%] h-[52%] flex">
<div className="w-full m-[0.25%]"> <div className="w-full m-[0.25%]">
<div className="h-full w-full bg-gray-400 px-[2%] @container-size flex flex-col text-black font-semibold text-nowrap overflow-hidden"> <div className="h-full w-full bg-gray-400 px-[2%] @container-size flex flex-col text-black font-semibold text-nowrap overflow-hidden">
@ -65,8 +65,8 @@ export const ChuniNameplate = ({ className, profile }: ChuniNameplateProps) => {
</div> </div>
</div> </div>
<Image className="ml-auto aspect-square h-full w-auto bg-gray-200 border-2 border-black" alt="Character" width={135} height={135} src={profile.characterId !== null ? getImageUrl( <Image className="ml-auto aspect-square h-full w-auto bg-gray-200 border-2 border-black" alt="Character" width={135} height={135} src={profile.characterId !== null ? getImageUrl(
`chuni/character/CHU_UI_Character_${Math.floor(profile.characterId / 10).toString() `chuni/character/CHU_UI_Character_${Math.floor(profile.characterId! / 10).toString()
.padStart(4, '0')}_${(profile.characterId % 10).toString().padStart(2, '0')}_02`) : ''}/> .padStart(4, '0')}_${(profile.characterId! % 10).toString().padStart(2, '0')}_02`) : ''}/>
</div> </div>
</div> </div>
<Image width={576} height={228} priority <Image width={576} height={228} priority