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 @@