From aee62522e97a0e9645c6f15ab9c6ce2ccd83c143 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Sat, 13 Dec 2025 21:24:09 -0800 Subject: [PATCH] redesign profile header with gamertag + gbf profile link --- src/assets/icons/sword.svg | 6 + src/lib/api/adapters/user.adapter.ts | 83 ++++- src/lib/api/resources/users.ts | 12 +- .../components/profile/ProfileHeader.svelte | 334 +++++++++++++----- src/lib/types/UserCookie.d.ts | 2 + src/lib/types/api/entities.ts | 1 + 6 files changed, 346 insertions(+), 92 deletions(-) create mode 100644 src/assets/icons/sword.svg diff --git a/src/assets/icons/sword.svg b/src/assets/icons/sword.svg new file mode 100644 index 00000000..c03e4258 --- /dev/null +++ b/src/assets/icons/sword.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/lib/api/adapters/user.adapter.ts b/src/lib/api/adapters/user.adapter.ts index b396b49d..f30adb55 100644 --- a/src/lib/api/adapters/user.adapter.ts +++ b/src/lib/api/adapters/user.adapter.ts @@ -3,6 +3,32 @@ import type { Party } from '$lib/types/api/party' import type { RequestOptions } from './types' import { DEFAULT_ADAPTER_CONFIG } from './config' +/** + * API response for user data (already camelCased by BaseAdapter.transformResponse) + * Note: BaseAdapter automatically transforms snake_case to camelCase, + * so we receive granblueId, showGamertag, etc. + */ +interface ApiUserResponse { + id: string + username: string + language: string + private: boolean + gender: number + theme: string + role: number + granblueId?: number | string | null // API returns number, transformed to camelCase + showGamertag?: boolean // transformed from show_gamertag + gamertag?: string + avatar: { + picture: string + element: string + } +} + +/** + * Transformed user info (camelCase for frontend) + * Uses our preferred naming convention (showCrewGamertag, crewGamertag) + */ export interface UserInfo { id: string username: string @@ -11,6 +37,9 @@ export interface UserInfo { gender: number theme: string role: number + granblueId?: string + showCrewGamertag?: boolean + crewGamertag?: string avatar: { picture: string element: string @@ -30,6 +59,29 @@ export interface UserProfileResponse { perPage?: number } +/** + * Transform API user response to frontend UserInfo format + * Renames API fields to our preferred naming convention + */ +function transformUserResponse(apiUser: ApiUserResponse): UserInfo { + return { + id: apiUser.id, + username: apiUser.username, + language: apiUser.language, + private: apiUser.private, + gender: apiUser.gender, + theme: apiUser.theme, + role: apiUser.role, + // granblueId comes as number from API, convert to string + granblueId: apiUser.granblueId != null ? String(apiUser.granblueId) : undefined, + // Rename showGamertag to showCrewGamertag + showCrewGamertag: apiUser.showGamertag, + // Rename gamertag to crewGamertag + crewGamertag: apiUser.gamertag, + avatar: apiUser.avatar + } +} + /** * Adapter for user-related API operations */ @@ -38,7 +90,11 @@ export class UserAdapter extends BaseAdapter { * Get user information */ async getInfo(username: string, options?: RequestOptions): Promise { - return this.request(`/users/info/${encodeURIComponent(username)}`, options) + const result = await this.request( + `/users/info/${encodeURIComponent(username)}`, + options + ) + return transformUserResponse(result) } /** @@ -47,14 +103,26 @@ export class UserAdapter extends BaseAdapter { async getProfile(username: string, page = 1): Promise { const params = page > 1 ? { page } : undefined const response = await this.request<{ - profile: UserProfile - meta?: { count?: number; total_pages?: number; totalPages?: number; per_page?: number; perPage?: number } + profile: ApiUserResponse & { parties?: Party[] } + meta?: { + count?: number + total_pages?: number + totalPages?: number + per_page?: number + perPage?: number + } }>(`/users/${encodeURIComponent(username)}`, { params }) const items = Array.isArray(response.profile?.parties) ? response.profile.parties : [] + // Transform API response to frontend format + const user: UserProfile = { + ...transformUserResponse(response.profile), + parties: items + } + const result: UserProfileResponse = { - user: response.profile, + user, items, page } @@ -150,7 +218,7 @@ export class UserAdapter extends BaseAdapter { */ async updateProfile(updates: Partial): Promise { // Wrap updates in 'user' key as required by Rails backend - const result = await this.request('/users/me', { + const result = await this.request('/users/me', { method: 'PUT', body: JSON.stringify({ user: updates }) }) @@ -158,14 +226,15 @@ export class UserAdapter extends BaseAdapter { // Clear cache for current user after update this.clearCache('/users/me') - return result + return transformUserResponse(result) } /** * Get current user */ async getCurrentUser(): Promise { - return this.request('/users/me') + const result = await this.request('/users/me') + return transformUserResponse(result) } } diff --git a/src/lib/api/resources/users.ts b/src/lib/api/resources/users.ts index 38269b24..d7cb8b4b 100644 --- a/src/lib/api/resources/users.ts +++ b/src/lib/api/resources/users.ts @@ -6,6 +6,8 @@ export interface UserUpdateParams { gender?: number | undefined language?: string | undefined theme?: string | undefined + granblueId?: string | undefined + showCrewGamertag?: boolean | undefined } export interface UserResponse { @@ -19,6 +21,8 @@ export interface UserResponse { language: string theme: string role: number + granblueId?: string + showCrewGamertag?: boolean } export const users = { @@ -33,6 +37,8 @@ export const users = { gender?: number | undefined language?: string | undefined theme?: string | undefined + granblue_id?: string | undefined + show_gamertag?: boolean | undefined } = {} if (params.picture !== undefined) updates.picture = params.picture @@ -40,6 +46,8 @@ export const users = { if (params.gender !== undefined) updates.gender = params.gender if (params.language !== undefined) updates.language = params.language if (params.theme !== undefined) updates.theme = params.theme + if (params.granblueId !== undefined) updates.granblue_id = params.granblueId + if (params.showCrewGamertag !== undefined) updates.show_gamertag = params.showCrewGamertag const result = await userAdapter.updateProfile(updates) return { @@ -49,7 +57,9 @@ export const users = { gender: result.gender, language: result.language, theme: result.theme, - role: result.role + role: result.role, + granblueId: result.granblueId, + showCrewGamertag: result.showCrewGamertag } } } diff --git a/src/lib/components/profile/ProfileHeader.svelte b/src/lib/components/profile/ProfileHeader.svelte index 89e18c99..498c6df1 100644 --- a/src/lib/components/profile/ProfileHeader.svelte +++ b/src/lib/components/profile/ProfileHeader.svelte @@ -2,6 +2,7 @@ import { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar' import { DropdownMenu } from 'bits-ui' import Icon from '$lib/components/Icon.svelte' + import Tooltip from '$lib/components/ui/Tooltip.svelte' import DropdownItem from '$lib/components/ui/dropdown/DropdownItem.svelte' import InviteUserModal from '$lib/components/crew/InviteUserModal.svelte' import type { CrewRole } from '$lib/types/api/crew' @@ -13,6 +14,14 @@ title?: string activeTab: 'teams' | 'favorites' | 'collection' isOwner?: boolean + /** User's selected element for theming */ + element?: string + /** User's Granblue Fantasy ID for profile link */ + granblueId?: string + /** Whether to show crew gamertag */ + showCrewGamertag?: boolean + /** The crew's gamertag to display */ + crewGamertag?: string /** Current user's crew role (null if not in a crew) */ viewerCrewRole?: CrewRole | null /** Current user's crew ID */ @@ -28,11 +37,20 @@ title, activeTab, isOwner = false, + element = 'null', + granblueId, + showCrewGamertag = false, + crewGamertag, viewerCrewRole = null, viewerCrewId = null, targetUserHasCrew = false }: Props = $props() + // GBF profile URL + const gbfProfileUrl = $derived( + granblueId ? `https://game.granbluefantasy.jp/#profile/${granblueId}` : null + ) + const avatarSrc = $derived(getAvatarSrc(avatarPicture)) const avatarSrcSet = $derived(getAvatarSrcSet(avatarPicture)) const displayTitle = $derived(title || username) @@ -54,98 +72,123 @@
- {#if avatarPicture} - {`Avatar - {:else} - - {/if} -
-

{displayTitle}

- +
+
+

{displayTitle}

+ {#if showCrewGamertag && crewGamertag} + {crewGamertag} + {/if} +
+
+
+ +
+ {#if gbfProfileUrl} + + + + + + {/if} + + {#if showMenu} + + + + + + + + {#if canInvite} + + + + {/if} + + + + {/if} +
- {#if showMenu} -
- - - - - - - - {#if canInvite} - - - - {/if} - - - -
- {/if} +
{#if canInvite && userId && viewerCrewId} - + {/if}