redesign profile header with gamertag + gbf profile link
This commit is contained in:
parent
14819f0b73
commit
aee62522e9
6 changed files with 346 additions and 92 deletions
6
src/assets/icons/sword.svg
Normal file
6
src/assets/icons/sword.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<polyline points="14.5 17.5 3 6 3 3 6 3 17.5 14.5"/>
|
||||||
|
<line x1="13" y1="19" x2="19" y2="13"/>
|
||||||
|
<line x1="16" y1="16" x2="20" y2="20"/>
|
||||||
|
<line x1="19" y1="21" x2="21" y2="19"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 353 B |
|
|
@ -3,6 +3,32 @@ import type { Party } from '$lib/types/api/party'
|
||||||
import type { RequestOptions } from './types'
|
import type { RequestOptions } from './types'
|
||||||
import { DEFAULT_ADAPTER_CONFIG } from './config'
|
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 {
|
export interface UserInfo {
|
||||||
id: string
|
id: string
|
||||||
username: string
|
username: string
|
||||||
|
|
@ -11,6 +37,9 @@ export interface UserInfo {
|
||||||
gender: number
|
gender: number
|
||||||
theme: string
|
theme: string
|
||||||
role: number
|
role: number
|
||||||
|
granblueId?: string
|
||||||
|
showCrewGamertag?: boolean
|
||||||
|
crewGamertag?: string
|
||||||
avatar: {
|
avatar: {
|
||||||
picture: string
|
picture: string
|
||||||
element: string
|
element: string
|
||||||
|
|
@ -30,6 +59,29 @@ export interface UserProfileResponse {
|
||||||
perPage?: number
|
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
|
* Adapter for user-related API operations
|
||||||
*/
|
*/
|
||||||
|
|
@ -38,7 +90,11 @@ export class UserAdapter extends BaseAdapter {
|
||||||
* Get user information
|
* Get user information
|
||||||
*/
|
*/
|
||||||
async getInfo(username: string, options?: RequestOptions): Promise<UserInfo> {
|
async getInfo(username: string, options?: RequestOptions): Promise<UserInfo> {
|
||||||
return this.request<UserInfo>(`/users/info/${encodeURIComponent(username)}`, options)
|
const result = await this.request<ApiUserResponse>(
|
||||||
|
`/users/info/${encodeURIComponent(username)}`,
|
||||||
|
options
|
||||||
|
)
|
||||||
|
return transformUserResponse(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -47,14 +103,26 @@ export class UserAdapter extends BaseAdapter {
|
||||||
async getProfile(username: string, page = 1): Promise<UserProfileResponse> {
|
async getProfile(username: string, page = 1): Promise<UserProfileResponse> {
|
||||||
const params = page > 1 ? { page } : undefined
|
const params = page > 1 ? { page } : undefined
|
||||||
const response = await this.request<{
|
const response = await this.request<{
|
||||||
profile: UserProfile
|
profile: ApiUserResponse & { parties?: Party[] }
|
||||||
meta?: { count?: number; total_pages?: number; totalPages?: number; per_page?: number; perPage?: number }
|
meta?: {
|
||||||
|
count?: number
|
||||||
|
total_pages?: number
|
||||||
|
totalPages?: number
|
||||||
|
per_page?: number
|
||||||
|
perPage?: number
|
||||||
|
}
|
||||||
}>(`/users/${encodeURIComponent(username)}`, { params })
|
}>(`/users/${encodeURIComponent(username)}`, { params })
|
||||||
|
|
||||||
const items = Array.isArray(response.profile?.parties) ? response.profile.parties : []
|
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 = {
|
const result: UserProfileResponse = {
|
||||||
user: response.profile,
|
user,
|
||||||
items,
|
items,
|
||||||
page
|
page
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +218,7 @@ export class UserAdapter extends BaseAdapter {
|
||||||
*/
|
*/
|
||||||
async updateProfile(updates: Partial<UserInfo>): Promise<UserInfo> {
|
async updateProfile(updates: Partial<UserInfo>): Promise<UserInfo> {
|
||||||
// Wrap updates in 'user' key as required by Rails backend
|
// Wrap updates in 'user' key as required by Rails backend
|
||||||
const result = await this.request<UserInfo>('/users/me', {
|
const result = await this.request<ApiUserResponse>('/users/me', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
body: JSON.stringify({ user: updates })
|
body: JSON.stringify({ user: updates })
|
||||||
})
|
})
|
||||||
|
|
@ -158,14 +226,15 @@ export class UserAdapter extends BaseAdapter {
|
||||||
// Clear cache for current user after update
|
// Clear cache for current user after update
|
||||||
this.clearCache('/users/me')
|
this.clearCache('/users/me')
|
||||||
|
|
||||||
return result
|
return transformUserResponse(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get current user
|
* Get current user
|
||||||
*/
|
*/
|
||||||
async getCurrentUser(): Promise<UserInfo> {
|
async getCurrentUser(): Promise<UserInfo> {
|
||||||
return this.request<UserInfo>('/users/me')
|
const result = await this.request<ApiUserResponse>('/users/me')
|
||||||
|
return transformUserResponse(result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ export interface UserUpdateParams {
|
||||||
gender?: number | undefined
|
gender?: number | undefined
|
||||||
language?: string | undefined
|
language?: string | undefined
|
||||||
theme?: string | undefined
|
theme?: string | undefined
|
||||||
|
granblueId?: string | undefined
|
||||||
|
showCrewGamertag?: boolean | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UserResponse {
|
export interface UserResponse {
|
||||||
|
|
@ -19,6 +21,8 @@ export interface UserResponse {
|
||||||
language: string
|
language: string
|
||||||
theme: string
|
theme: string
|
||||||
role: number
|
role: number
|
||||||
|
granblueId?: string
|
||||||
|
showCrewGamertag?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const users = {
|
export const users = {
|
||||||
|
|
@ -33,6 +37,8 @@ export const users = {
|
||||||
gender?: number | undefined
|
gender?: number | undefined
|
||||||
language?: string | undefined
|
language?: string | undefined
|
||||||
theme?: string | undefined
|
theme?: string | undefined
|
||||||
|
granblue_id?: string | undefined
|
||||||
|
show_gamertag?: boolean | undefined
|
||||||
} = {}
|
} = {}
|
||||||
|
|
||||||
if (params.picture !== undefined) updates.picture = params.picture
|
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.gender !== undefined) updates.gender = params.gender
|
||||||
if (params.language !== undefined) updates.language = params.language
|
if (params.language !== undefined) updates.language = params.language
|
||||||
if (params.theme !== undefined) updates.theme = params.theme
|
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)
|
const result = await userAdapter.updateProfile(updates)
|
||||||
return {
|
return {
|
||||||
|
|
@ -49,7 +57,9 @@ export const users = {
|
||||||
gender: result.gender,
|
gender: result.gender,
|
||||||
language: result.language,
|
language: result.language,
|
||||||
theme: result.theme,
|
theme: result.theme,
|
||||||
role: result.role
|
role: result.role,
|
||||||
|
granblueId: result.granblueId,
|
||||||
|
showCrewGamertag: result.showCrewGamertag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar'
|
import { getAvatarSrc, getAvatarSrcSet } from '$lib/utils/avatar'
|
||||||
import { DropdownMenu } from 'bits-ui'
|
import { DropdownMenu } from 'bits-ui'
|
||||||
import Icon from '$lib/components/Icon.svelte'
|
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 DropdownItem from '$lib/components/ui/dropdown/DropdownItem.svelte'
|
||||||
import InviteUserModal from '$lib/components/crew/InviteUserModal.svelte'
|
import InviteUserModal from '$lib/components/crew/InviteUserModal.svelte'
|
||||||
import type { CrewRole } from '$lib/types/api/crew'
|
import type { CrewRole } from '$lib/types/api/crew'
|
||||||
|
|
@ -13,6 +14,14 @@
|
||||||
title?: string
|
title?: string
|
||||||
activeTab: 'teams' | 'favorites' | 'collection'
|
activeTab: 'teams' | 'favorites' | 'collection'
|
||||||
isOwner?: boolean
|
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) */
|
/** Current user's crew role (null if not in a crew) */
|
||||||
viewerCrewRole?: CrewRole | null
|
viewerCrewRole?: CrewRole | null
|
||||||
/** Current user's crew ID */
|
/** Current user's crew ID */
|
||||||
|
|
@ -28,11 +37,20 @@
|
||||||
title,
|
title,
|
||||||
activeTab,
|
activeTab,
|
||||||
isOwner = false,
|
isOwner = false,
|
||||||
|
element = 'null',
|
||||||
|
granblueId,
|
||||||
|
showCrewGamertag = false,
|
||||||
|
crewGamertag,
|
||||||
viewerCrewRole = null,
|
viewerCrewRole = null,
|
||||||
viewerCrewId = null,
|
viewerCrewId = null,
|
||||||
targetUserHasCrew = false
|
targetUserHasCrew = false
|
||||||
}: Props = $props()
|
}: Props = $props()
|
||||||
|
|
||||||
|
// GBF profile URL
|
||||||
|
const gbfProfileUrl = $derived(
|
||||||
|
granblueId ? `https://game.granbluefantasy.jp/#profile/${granblueId}` : null
|
||||||
|
)
|
||||||
|
|
||||||
const avatarSrc = $derived(getAvatarSrc(avatarPicture))
|
const avatarSrc = $derived(getAvatarSrc(avatarPicture))
|
||||||
const avatarSrcSet = $derived(getAvatarSrcSet(avatarPicture))
|
const avatarSrcSet = $derived(getAvatarSrcSet(avatarPicture))
|
||||||
const displayTitle = $derived(title || username)
|
const displayTitle = $derived(title || username)
|
||||||
|
|
@ -54,98 +72,123 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="header">
|
<header class="header">
|
||||||
{#if avatarPicture}
|
<div class="header-top">
|
||||||
<img
|
<div class="profile-info">
|
||||||
class="avatar"
|
{#if avatarPicture}
|
||||||
alt={`Avatar of ${username}`}
|
<img
|
||||||
src={avatarSrc}
|
class="avatar"
|
||||||
srcset={avatarSrcSet}
|
alt={`Avatar of ${username}`}
|
||||||
width="64"
|
src={avatarSrc}
|
||||||
height="64"
|
srcset={avatarSrcSet}
|
||||||
/>
|
width="56"
|
||||||
{:else}
|
height="56"
|
||||||
<div class="avatar" aria-hidden="true"></div>
|
/>
|
||||||
{/if}
|
{:else}
|
||||||
<div class="header-content">
|
<div class="avatar" aria-hidden="true"></div>
|
||||||
<h1>{displayTitle}</h1>
|
|
||||||
<nav class="tabs" aria-label="Profile sections">
|
|
||||||
<a
|
|
||||||
class:active={activeTab === 'teams' || activeTab === 'favorites'}
|
|
||||||
href="/{username}"
|
|
||||||
data-sveltekit-preload-data="hover"
|
|
||||||
>
|
|
||||||
Teams
|
|
||||||
</a>
|
|
||||||
{#if isOwner}
|
|
||||||
<a
|
|
||||||
class:active={activeTab === 'favorites'}
|
|
||||||
href="/{username}?tab=favorites"
|
|
||||||
data-sveltekit-preload-data="hover"
|
|
||||||
>
|
|
||||||
Favorites
|
|
||||||
</a>
|
|
||||||
{/if}
|
{/if}
|
||||||
<a
|
<div class="name-section">
|
||||||
class:active={activeTab === 'collection'}
|
<div class="name-row">
|
||||||
href="/{username}/collection/characters"
|
<h1>{displayTitle}</h1>
|
||||||
data-sveltekit-preload-data="hover"
|
{#if showCrewGamertag && crewGamertag}
|
||||||
>
|
<span class="gamertag-pill" data-element={element}>{crewGamertag}</span>
|
||||||
Collection
|
{/if}
|
||||||
</a>
|
</div>
|
||||||
</nav>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="header-actions">
|
||||||
|
{#if gbfProfileUrl}
|
||||||
|
<Tooltip content="In-game profile">
|
||||||
|
<a
|
||||||
|
href={gbfProfileUrl}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="gbf-profile-link"
|
||||||
|
>
|
||||||
|
<Icon name="sword" size={24} />
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if showMenu}
|
||||||
|
<DropdownMenu.Root>
|
||||||
|
<DropdownMenu.Trigger class="menu-trigger">
|
||||||
|
<Icon name="ellipsis" size={16} />
|
||||||
|
</DropdownMenu.Trigger>
|
||||||
|
|
||||||
|
<DropdownMenu.Portal>
|
||||||
|
<DropdownMenu.Content class="dropdown-content" sideOffset={5} align="end">
|
||||||
|
{#if canInvite}
|
||||||
|
<DropdownItem>
|
||||||
|
<button onclick={() => (inviteModalOpen = true)}>
|
||||||
|
<Icon name="user-plus" size={14} />
|
||||||
|
<span>Invite to Crew</span>
|
||||||
|
</button>
|
||||||
|
</DropdownItem>
|
||||||
|
{/if}
|
||||||
|
</DropdownMenu.Content>
|
||||||
|
</DropdownMenu.Portal>
|
||||||
|
</DropdownMenu.Root>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if showMenu}
|
<nav class="tabs" aria-label="Profile sections" data-element={element}>
|
||||||
<div class="header-actions">
|
<a class:active={activeTab === 'teams'} href="/{username}" data-sveltekit-preload-data="hover">
|
||||||
<DropdownMenu.Root>
|
Teams
|
||||||
<DropdownMenu.Trigger class="menu-trigger">
|
</a>
|
||||||
<Icon name="ellipsis" size={16} />
|
{#if isOwner}
|
||||||
</DropdownMenu.Trigger>
|
<a
|
||||||
|
class:active={activeTab === 'favorites'}
|
||||||
<DropdownMenu.Portal>
|
href="/{username}/favorites"
|
||||||
<DropdownMenu.Content class="dropdown-content" sideOffset={5} align="end">
|
data-sveltekit-preload-data="hover"
|
||||||
{#if canInvite}
|
>
|
||||||
<DropdownItem>
|
Favorites
|
||||||
<button onclick={() => (inviteModalOpen = true)}>
|
</a>
|
||||||
<Icon name="user-plus" size={14} />
|
{/if}
|
||||||
<span>Invite to Crew</span>
|
<a
|
||||||
</button>
|
class:active={activeTab === 'collection'}
|
||||||
</DropdownItem>
|
href="/{username}/collection/characters"
|
||||||
{/if}
|
data-sveltekit-preload-data="hover"
|
||||||
</DropdownMenu.Content>
|
>
|
||||||
</DropdownMenu.Portal>
|
Collection
|
||||||
</DropdownMenu.Root>
|
</a>
|
||||||
</div>
|
</nav>
|
||||||
{/if}
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{#if canInvite && userId && viewerCrewId}
|
{#if canInvite && userId && viewerCrewId}
|
||||||
<InviteUserModal
|
<InviteUserModal bind:open={inviteModalOpen} {userId} {username} crewId={viewerCrewId} />
|
||||||
bind:open={inviteModalOpen}
|
|
||||||
{userId}
|
|
||||||
{username}
|
|
||||||
crewId={viewerCrewId}
|
|
||||||
/>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
@use '$src/themes/spacing' as *;
|
@use '$src/themes/spacing' as *;
|
||||||
@use '$src/themes/colors' as *;
|
@use '$src/themes/colors' as *;
|
||||||
@use '$src/themes/layout' as *;
|
@use '$src/themes/layout' as *;
|
||||||
|
@use '$src/themes/typography' as *;
|
||||||
|
|
||||||
.header {
|
.header {
|
||||||
background: var(--card-bg);
|
background: var(--card-bg);
|
||||||
border-radius: $card-corner;
|
border-radius: $card-corner;
|
||||||
padding: $unit-2x;
|
margin-bottom: $unit-2x;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: $unit-2x $unit-3x;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: $unit-2x;
|
gap: $unit-2x;
|
||||||
margin-bottom: $unit-2x;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatar {
|
||||||
width: 64px;
|
width: 56px;
|
||||||
height: 64px;
|
height: 56px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: $grey-80;
|
background: $grey-80;
|
||||||
border: 1px solid $grey-75;
|
border: 1px solid $grey-75;
|
||||||
|
|
@ -153,38 +196,159 @@
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-content {
|
.name-section {
|
||||||
flex: 1;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: $unit-half;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin: 0 0 $unit-half;
|
margin: 0;
|
||||||
font-size: 24px;
|
font-size: 20px;
|
||||||
|
font-weight: $medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
.gamertag-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 2px $unit;
|
||||||
|
border-radius: $full-corner;
|
||||||
|
font-size: $font-small;
|
||||||
|
font-weight: $medium;
|
||||||
|
|
||||||
|
// Element-based pill colors
|
||||||
|
&[data-element='wind'] {
|
||||||
|
background: $wind-bg-20;
|
||||||
|
color: $wind-text-20;
|
||||||
|
}
|
||||||
|
&[data-element='fire'] {
|
||||||
|
background: $fire-bg-20;
|
||||||
|
color: $fire-text-20;
|
||||||
|
}
|
||||||
|
&[data-element='water'] {
|
||||||
|
background: $water-bg-20;
|
||||||
|
color: $water-text-20;
|
||||||
|
}
|
||||||
|
&[data-element='earth'] {
|
||||||
|
background: $earth-bg-20;
|
||||||
|
color: $earth-text-20;
|
||||||
|
}
|
||||||
|
&[data-element='light'] {
|
||||||
|
background: $light-bg-20;
|
||||||
|
color: $light-text-20;
|
||||||
|
}
|
||||||
|
&[data-element='dark'] {
|
||||||
|
background: $dark-bg-20;
|
||||||
|
color: $dark-text-20;
|
||||||
|
}
|
||||||
|
&[data-element='null'],
|
||||||
|
&:not([data-element]) {
|
||||||
|
background: $grey-90;
|
||||||
|
color: $grey-30;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.gbf-profile-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: $card-corner;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
background-color 0.15s ease,
|
||||||
|
color 0.15s ease;
|
||||||
|
|
||||||
|
:global(svg) {
|
||||||
|
stroke-width: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--button-contained-bg-hover, $grey-90);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--focus-ring);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: $unit-2x;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs a {
|
.tabs a {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: $unit-2x $unit;
|
||||||
|
border-top: 1px solid var(--border-color, rgba(0, 0, 0, 0.08));
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
padding-bottom: 2px;
|
font-size: $font-small;
|
||||||
border-bottom: 2px solid transparent;
|
font-weight: $medium;
|
||||||
|
transition:
|
||||||
|
color 0.15s ease,
|
||||||
|
background-color 0.15s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
|
||||||
&.active {
|
|
||||||
border-color: var(--accent-color, #3366ff);
|
|
||||||
color: var(--accent-color, #3366ff);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Element-based active tab colors
|
||||||
|
.tabs[data-element='wind'] a.active {
|
||||||
|
color: var(--wind-nav-selected-text);
|
||||||
|
background: var(--wind-nav-selected-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs[data-element='fire'] a.active {
|
||||||
|
color: var(--fire-nav-selected-text);
|
||||||
|
background: var(--fire-nav-selected-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs[data-element='water'] a.active {
|
||||||
|
color: var(--water-nav-selected-text);
|
||||||
|
background: var(--water-nav-selected-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs[data-element='earth'] a.active {
|
||||||
|
color: var(--earth-nav-selected-text);
|
||||||
|
background: var(--earth-nav-selected-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs[data-element='light'] a.active {
|
||||||
|
color: var(--light-nav-selected-text);
|
||||||
|
background: var(--light-nav-selected-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs[data-element='dark'] a.active {
|
||||||
|
color: var(--dark-nav-selected-text);
|
||||||
|
background: var(--dark-nav-selected-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs[data-element='null'] a.active,
|
||||||
|
.tabs:not([data-element]) a.active {
|
||||||
|
color: var(--null-nav-selected-text);
|
||||||
|
background: var(--null-nav-selected-bg);
|
||||||
|
}
|
||||||
|
|
||||||
.header-actions {
|
.header-actions {
|
||||||
margin-left: auto;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: $unit-half;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -199,7 +363,9 @@
|
||||||
border: none;
|
border: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
transition: background-color 0.15s ease, color 0.15s ease;
|
transition:
|
||||||
|
background-color 0.15s ease,
|
||||||
|
color 0.15s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--button-contained-bg-hover, $grey-90);
|
background: var(--button-contained-bg-hover, $grey-90);
|
||||||
|
|
|
||||||
2
src/lib/types/UserCookie.d.ts
vendored
2
src/lib/types/UserCookie.d.ts
vendored
|
|
@ -5,4 +5,6 @@ export interface UserCookie {
|
||||||
gender: number
|
gender: number
|
||||||
theme: string
|
theme: string
|
||||||
bahamut?: boolean
|
bahamut?: boolean
|
||||||
|
granblueId?: string
|
||||||
|
showCrewGamertag?: boolean
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,7 @@ export interface Summon {
|
||||||
}
|
}
|
||||||
transcendenceHp?: number
|
transcendenceHp?: number
|
||||||
transcendenceAtk?: number
|
transcendenceAtk?: number
|
||||||
|
series?: number
|
||||||
// Database/admin fields
|
// Database/admin fields
|
||||||
releaseDate?: string
|
releaseDate?: string
|
||||||
flbDate?: string
|
flbDate?: string
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue