rework ModalFooter to use action props
- onCancel callback with fixed "Nevermind" label - optional primaryAction object (label, onclick, destructive, disabled) - optional left snippet for custom content
This commit is contained in:
parent
7dae71965a
commit
c54c959522
10 changed files with 333 additions and 223 deletions
|
|
@ -8,11 +8,15 @@
|
||||||
import Select from './ui/Select.svelte'
|
import Select from './ui/Select.svelte'
|
||||||
import Switch from './ui/switch/Switch.svelte'
|
import Switch from './ui/switch/Switch.svelte'
|
||||||
import Button from './ui/Button.svelte'
|
import Button from './ui/Button.svelte'
|
||||||
|
import Input from './ui/Input.svelte'
|
||||||
import { pictureData, type Picture } from '$lib/utils/pictureData'
|
import { pictureData, type Picture } from '$lib/utils/pictureData'
|
||||||
import { users } from '$lib/api/resources/users'
|
import { users } from '$lib/api/resources/users'
|
||||||
import type { UserCookie } from '$lib/types/UserCookie'
|
import type { UserCookie } from '$lib/types/UserCookie'
|
||||||
import { setUserCookie } from '$lib/auth/cookies'
|
import { setUserCookie } from '$lib/auth/cookies'
|
||||||
import { invalidateAll } from '$app/navigation'
|
import { invalidateAll } from '$app/navigation'
|
||||||
|
import { createQuery } from '@tanstack/svelte-query'
|
||||||
|
import { crewQueries } from '$lib/api/queries/crew.queries'
|
||||||
|
import { userAdapter } from '$lib/api/adapters/user.adapter'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean
|
open: boolean
|
||||||
|
|
@ -25,7 +29,7 @@
|
||||||
|
|
||||||
let { open = $bindable(false), onOpenChange, username, userId, user, role }: Props = $props()
|
let { open = $bindable(false), onOpenChange, username, userId, user, role }: Props = $props()
|
||||||
|
|
||||||
// Form state
|
// Form state - fields from cookie (can use immediately)
|
||||||
let picture = $state(user.picture)
|
let picture = $state(user.picture)
|
||||||
let element = $state(user.element)
|
let element = $state(user.element)
|
||||||
let gender = $state(user.gender)
|
let gender = $state(user.gender)
|
||||||
|
|
@ -33,9 +37,48 @@
|
||||||
let theme = $state(user.theme)
|
let theme = $state(user.theme)
|
||||||
let bahamut = $state(user.bahamut ?? false)
|
let bahamut = $state(user.bahamut ?? false)
|
||||||
|
|
||||||
|
// Form state - fields from API (must wait for query to load)
|
||||||
|
// Initialize as empty - will be set by $effect when API data loads
|
||||||
|
let granblueId = $state('')
|
||||||
|
let showCrewGamertag = $state(false)
|
||||||
|
let apiDataLoaded = $state(false)
|
||||||
|
|
||||||
let saving = $state(false)
|
let saving = $state(false)
|
||||||
let error = $state<string | null>(null)
|
let error = $state<string | null>(null)
|
||||||
|
|
||||||
|
// Fetch current user data from API (to get actual show_gamertag value from database)
|
||||||
|
const currentUserQuery = createQuery(() => ({
|
||||||
|
queryKey: ['currentUser', 'settings'],
|
||||||
|
queryFn: () => userAdapter.getCurrentUser(),
|
||||||
|
enabled: open // Only fetch when modal is open
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Fetch current user's crew (for showing gamertag toggle)
|
||||||
|
const myCrewQuery = createQuery(() => ({
|
||||||
|
...crewQueries.myCrew(),
|
||||||
|
enabled: open // Only fetch when modal is open
|
||||||
|
}))
|
||||||
|
|
||||||
|
const isInCrew = $derived(!!myCrewQuery.data)
|
||||||
|
const crewGamertag = $derived(myCrewQuery.data?.gamertag)
|
||||||
|
const isLoadingApiData = $derived(currentUserQuery.isLoading || currentUserQuery.isPending)
|
||||||
|
|
||||||
|
// Update form state when API data loads (to get actual values from database)
|
||||||
|
$effect(() => {
|
||||||
|
if (currentUserQuery.data) {
|
||||||
|
granblueId = currentUserQuery.data.granblueId ?? ''
|
||||||
|
showCrewGamertag = currentUserQuery.data.showCrewGamertag ?? false
|
||||||
|
apiDataLoaded = true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Reset apiDataLoaded when modal closes so fresh data is fetched next time
|
||||||
|
$effect(() => {
|
||||||
|
if (!open) {
|
||||||
|
apiDataLoaded = false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// Get current locale from user settings
|
// Get current locale from user settings
|
||||||
const locale = $derived(user.language as 'en' | 'ja')
|
const locale = $derived(user.language as 'en' | 'ja')
|
||||||
|
|
||||||
|
|
@ -107,7 +150,9 @@
|
||||||
element,
|
element,
|
||||||
gender,
|
gender,
|
||||||
language,
|
language,
|
||||||
theme
|
theme,
|
||||||
|
granblueId: granblueId || undefined,
|
||||||
|
showCrewGamertag
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call API to update user settings
|
// Call API to update user settings
|
||||||
|
|
@ -120,7 +165,9 @@
|
||||||
language: response.language,
|
language: response.language,
|
||||||
gender: response.gender,
|
gender: response.gender,
|
||||||
theme: response.theme,
|
theme: response.theme,
|
||||||
bahamut
|
bahamut,
|
||||||
|
granblueId: response.granblueId,
|
||||||
|
showCrewGamertag: response.showCrewGamertag
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save to cookie (we'll need to handle this server-side)
|
// Save to cookie (we'll need to handle this server-side)
|
||||||
|
|
@ -137,7 +184,7 @@
|
||||||
body: JSON.stringify(updatedUser)
|
body: JSON.stringify(updatedUser)
|
||||||
})
|
})
|
||||||
|
|
||||||
// If language or theme changed, we need a full page reload
|
// If language, theme, or bahamut mode changed, we need a full page reload
|
||||||
if (user.language !== language || user.theme !== theme || user.bahamut !== bahamut) {
|
if (user.language !== language || user.theme !== theme || user.bahamut !== bahamut) {
|
||||||
await invalidateAll()
|
await invalidateAll()
|
||||||
window.location.reload()
|
window.location.reload()
|
||||||
|
|
@ -203,6 +250,39 @@
|
||||||
contained
|
contained
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Granblue ID -->
|
||||||
|
{#if isLoadingApiData}
|
||||||
|
<div class="loading-field">
|
||||||
|
<span class="loading-label">Granblue ID</span>
|
||||||
|
<span class="loading-text">Loading...</span>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Input
|
||||||
|
bind:value={granblueId}
|
||||||
|
label="Granblue ID"
|
||||||
|
placeholder="Enter your Granblue ID"
|
||||||
|
contained
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Show Crew Gamertag (only if in a crew with a gamertag) -->
|
||||||
|
{#if isInCrew && crewGamertag}
|
||||||
|
<div class="inline-switch">
|
||||||
|
<label for="show-gamertag">
|
||||||
|
<span>Show crew tag on profile</span>
|
||||||
|
{#if isLoadingApiData}
|
||||||
|
<span class="loading-text">Loading...</span>
|
||||||
|
{:else}
|
||||||
|
<Switch bind:checked={showCrewGamertag} name="show-gamertag" element={element as 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light' | undefined} />
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
|
<p class="field-hint">Display "{crewGamertag}" next to your name</p>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<hr class="separator" />
|
||||||
|
|
||||||
<!-- Gender Selection -->
|
<!-- Gender Selection -->
|
||||||
<Select
|
<Select
|
||||||
bind:value={gender}
|
bind:value={gender}
|
||||||
|
|
@ -233,12 +313,13 @@
|
||||||
contained
|
contained
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<!-- Admin Mode (only for admins) -->
|
<!-- Bahamut Mode (only for admins) -->
|
||||||
{#if role === 9}
|
{#if role === 9}
|
||||||
|
<hr class="separator" />
|
||||||
<div class="switch-field">
|
<div class="switch-field">
|
||||||
<label for="bahamut-mode">
|
<label for="bahamut-mode">
|
||||||
<span>Admin Mode</span>
|
<span>Bahamut Mode</span>
|
||||||
<Switch bind:checked={bahamut} name="bahamut-mode" />
|
<Switch bind:checked={bahamut} name="bahamut-mode" element={element as 'wind' | 'fire' | 'water' | 'earth' | 'dark' | 'light' | undefined} />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -246,14 +327,15 @@
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
{#snippet children()}
|
onCancel={handleClose}
|
||||||
<Button variant="ghost" onclick={handleClose} disabled={saving}>Cancel</Button>
|
cancelDisabled={saving}
|
||||||
<Button onclick={handleSave} variant="primary" disabled={saving}>
|
primaryAction={{
|
||||||
{saving ? 'Saving...' : 'Save Changes'}
|
label: saving ? 'Saving...' : 'Save Changes',
|
||||||
</Button>
|
onclick: handleSave,
|
||||||
{/snippet}
|
disabled: saving
|
||||||
</ModalFooter>
|
}}
|
||||||
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
@ -283,6 +365,53 @@
|
||||||
gap: spacing.$unit-3x;
|
gap: spacing.$unit-3x;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border-color, rgba(0, 0, 0, 0.08));
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-switch {
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: typography.$font-regular;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-hint {
|
||||||
|
margin: spacing.$unit-half 0 0;
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: spacing.$unit-half;
|
||||||
|
|
||||||
|
.loading-label {
|
||||||
|
font-size: typography.$font-small;
|
||||||
|
font-weight: typography.$medium;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: typography.$font-regular;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-text {
|
||||||
|
font-size: typography.$font-regular;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.picture-section {
|
.picture-section {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: spacing.$unit-3x;
|
gap: spacing.$unit-3x;
|
||||||
|
|
|
||||||
|
|
@ -74,16 +74,21 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
// Build filters for search (using SearchFilters type from search.queries)
|
// Build filters for search (using SearchFilters type from search.queries)
|
||||||
|
// Filter seriesFilters to only numbers for characterSeries (strings are weapon UUIDs)
|
||||||
|
const numericSeriesFilters = $derived(
|
||||||
|
seriesFilters.filter((s): s is number => typeof s === 'number')
|
||||||
|
)
|
||||||
const searchFilters = $derived<SearchFilters>({
|
const searchFilters = $derived<SearchFilters>({
|
||||||
element: elementFilters.length > 0 ? elementFilters : undefined,
|
element: elementFilters.length > 0 ? elementFilters : undefined,
|
||||||
rarity: rarityFilters.length > 0 ? rarityFilters : undefined,
|
rarity: rarityFilters.length > 0 ? rarityFilters : undefined,
|
||||||
season: seasonFilters.length > 0 ? seasonFilters : undefined,
|
season: seasonFilters.length > 0 ? seasonFilters : undefined,
|
||||||
characterSeries: seriesFilters.length > 0 ? seriesFilters : undefined,
|
characterSeries: numericSeriesFilters.length > 0 ? numericSeriesFilters : undefined,
|
||||||
proficiency: proficiencyFilters.length > 0 ? proficiencyFilters : undefined
|
proficiency: proficiencyFilters.length > 0 ? proficiencyFilters : undefined
|
||||||
})
|
})
|
||||||
|
|
||||||
// Search query with infinite scroll - dynamic based on entity type
|
// Search query with infinite scroll - dynamic based on entity type
|
||||||
const searchResults = createInfiniteQuery(() => {
|
// Type assertion needed because queryKeys differ but data shape is the same
|
||||||
|
const getSearchOptions = () => {
|
||||||
const query = searchQuery
|
const query = searchQuery
|
||||||
const filters = searchFilters
|
const filters = searchFilters
|
||||||
|
|
||||||
|
|
@ -102,7 +107,8 @@
|
||||||
enabled: open
|
enabled: open
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
const searchResults = createInfiniteQuery(getSearchOptions as () => ReturnType<typeof searchQueries.weapons>)
|
||||||
|
|
||||||
// Flatten results and deduplicate by ID
|
// Flatten results and deduplicate by ID
|
||||||
const allResults = $derived.by(() => {
|
const allResults = $derived.by(() => {
|
||||||
|
|
@ -445,39 +451,25 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
{#snippet children()}
|
onCancel={() => (open = false)}
|
||||||
<div class="modal-footer">
|
primaryAction={{
|
||||||
<div class="footer-left">
|
label: currentMutation.isPending ? 'Adding...' : 'Add to Collection',
|
||||||
{#if selectedCount > 0}
|
onclick: handleAdd,
|
||||||
<button
|
disabled: selectedCount === 0 || currentMutation.isPending
|
||||||
type="button"
|
}}
|
||||||
class="selected-link"
|
>
|
||||||
class:active={showOnlySelected}
|
{#snippet left()}
|
||||||
onclick={toggleShowSelected}
|
{#if selectedCount > 0}
|
||||||
>
|
<button
|
||||||
{selectedText}
|
type="button"
|
||||||
</button>
|
class="selected-link"
|
||||||
{/if}
|
class:active={showOnlySelected}
|
||||||
</div>
|
onclick={toggleShowSelected}
|
||||||
<div class="footer-right">
|
>
|
||||||
<Button variant="ghost" onclick={() => (open = false)}>
|
{selectedText}
|
||||||
Cancel
|
</button>
|
||||||
</Button>
|
{/if}
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
disabled={selectedCount === 0 || currentMutation.isPending}
|
|
||||||
onclick={handleAdd}
|
|
||||||
>
|
|
||||||
{#if currentMutation.isPending}
|
|
||||||
<Icon name="loader-2" size={16} />
|
|
||||||
Adding...
|
|
||||||
{:else}
|
|
||||||
Add to Collection
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalFooter>
|
</ModalFooter>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
@ -588,22 +580,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-left {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-right {
|
|
||||||
display: flex;
|
|
||||||
gap: $unit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.selected-link {
|
.selected-link {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@
|
||||||
import ModalHeader from '$lib/components/ui/ModalHeader.svelte'
|
import ModalHeader from '$lib/components/ui/ModalHeader.svelte'
|
||||||
import ModalBody from '$lib/components/ui/ModalBody.svelte'
|
import ModalBody from '$lib/components/ui/ModalBody.svelte'
|
||||||
import ModalFooter from '$lib/components/ui/ModalFooter.svelte'
|
import ModalFooter from '$lib/components/ui/ModalFooter.svelte'
|
||||||
import Button from '$lib/components/ui/Button.svelte'
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
open: boolean
|
open: boolean
|
||||||
|
|
@ -83,14 +82,15 @@
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
{#if !success}
|
{#if !success}
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
<Button variant="secondary" onclick={handleCancel} disabled={sendMutation.isPending}>
|
onCancel={handleCancel}
|
||||||
Cancel
|
cancelDisabled={sendMutation.isPending}
|
||||||
</Button>
|
primaryAction={{
|
||||||
<Button variant="primary" onclick={handleSend} disabled={sendMutation.isPending}>
|
label: sendMutation.isPending ? 'Sending...' : 'Send Invitation',
|
||||||
{sendMutation.isPending ? 'Sending...' : 'Send Invitation'}
|
onclick: handleSend,
|
||||||
</Button>
|
disabled: sendMutation.isPending
|
||||||
</ModalFooter>
|
}}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -165,20 +165,16 @@
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
{#if !inviteSuccess}
|
{#if !inviteSuccess}
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
<Button variant="secondary" onclick={handleCancel}>
|
onCancel={handleCancel}
|
||||||
Cancel
|
primaryAction={foundUser
|
||||||
</Button>
|
? {
|
||||||
{#if foundUser}
|
label: sendMutation.isPending ? 'Sending...' : 'Send Invitation',
|
||||||
<Button
|
onclick: handleInvite,
|
||||||
variant="primary"
|
disabled: sendMutation.isPending
|
||||||
onclick={handleInvite}
|
}
|
||||||
disabled={sendMutation.isPending}
|
: undefined}
|
||||||
>
|
/>
|
||||||
{sendMutation.isPending ? 'Sending...' : 'Send Invitation'}
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
</ModalFooter>
|
|
||||||
{/if}
|
{/if}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@
|
||||||
import ModalHeader from '$lib/components/ui/ModalHeader.svelte'
|
import ModalHeader from '$lib/components/ui/ModalHeader.svelte'
|
||||||
import ModalBody from '$lib/components/ui/ModalBody.svelte'
|
import ModalBody from '$lib/components/ui/ModalBody.svelte'
|
||||||
import ModalFooter from '$lib/components/ui/ModalFooter.svelte'
|
import ModalFooter from '$lib/components/ui/ModalFooter.svelte'
|
||||||
import Button from '$lib/components/ui/Button.svelte'
|
|
||||||
import type { ConflictData } from '$lib/types/api/conflict'
|
import type { ConflictData } from '$lib/types/api/conflict'
|
||||||
import type { GridCharacter, GridWeapon } from '$lib/types/api/party'
|
import type { GridCharacter, GridWeapon } from '$lib/types/api/party'
|
||||||
import type { Character, Weapon } from '$lib/types/api/entities'
|
import type { Character, Weapon } from '$lib/types/api/entities'
|
||||||
|
|
@ -233,15 +232,14 @@
|
||||||
{/if}
|
{/if}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
{#snippet children()}
|
onCancel={handleCancel}
|
||||||
<Button variant="ghost" onclick={handleCancel} disabled={isLoading}>
|
cancelDisabled={isLoading}
|
||||||
{m.conflict_cancel()}
|
primaryAction={{
|
||||||
</Button>
|
label: m.conflict_confirm(),
|
||||||
<Button variant="primary" onclick={handleResolve} disabled={isLoading}>
|
onclick: handleResolve,
|
||||||
{m.conflict_confirm()}
|
disabled: isLoading
|
||||||
</Button>
|
}}
|
||||||
{/snippet}
|
/>
|
||||||
</ModalFooter>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -2,16 +2,43 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Snippet } from 'svelte'
|
import type { Snippet } from 'svelte'
|
||||||
|
import Button from './Button.svelte'
|
||||||
|
|
||||||
interface Props {
|
interface PrimaryAction {
|
||||||
children: Snippet
|
label: string
|
||||||
|
onclick: () => void
|
||||||
|
destructive?: boolean
|
||||||
|
disabled?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
let { children }: Props = $props()
|
interface Props {
|
||||||
|
onCancel: () => void
|
||||||
|
cancelDisabled?: boolean
|
||||||
|
primaryAction?: PrimaryAction
|
||||||
|
left?: Snippet
|
||||||
|
}
|
||||||
|
|
||||||
|
let { onCancel, cancelDisabled = false, primaryAction, left }: Props = $props()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
{@render children()}
|
{#if left}
|
||||||
|
<div class="left">
|
||||||
|
{@render left()}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<div class="actions">
|
||||||
|
<Button variant="ghost" onclick={onCancel} disabled={cancelDisabled}>Nevermind</Button>
|
||||||
|
{#if primaryAction}
|
||||||
|
<Button
|
||||||
|
variant={primaryAction.destructive ? 'destructive' : 'primary'}
|
||||||
|
onclick={primaryAction.onclick}
|
||||||
|
disabled={primaryAction.disabled}
|
||||||
|
>
|
||||||
|
{primaryAction.label}
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
@ -22,6 +49,16 @@
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: spacing.$unit-2x;
|
gap: spacing.$unit-2x;
|
||||||
justify-content: flex-end;
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: spacing.$unit;
|
||||||
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -112,6 +112,7 @@
|
||||||
role: 'captain',
|
role: 'captain',
|
||||||
retired: false,
|
retired: false,
|
||||||
retiredAt: null,
|
retiredAt: null,
|
||||||
|
joinedAt: new Date().toISOString(),
|
||||||
createdAt: new Date().toISOString()
|
createdAt: new Date().toISOString()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -237,8 +238,8 @@
|
||||||
<div class="crew-dashboard">
|
<div class="crew-dashboard">
|
||||||
<CrewHeader
|
<CrewHeader
|
||||||
title={crewStore.crew?.name ?? ''}
|
title={crewStore.crew?.name ?? ''}
|
||||||
subtitle={crewStore.crew?.gamertag}
|
subtitle={crewStore.crew?.gamertag ?? undefined}
|
||||||
description={crewStore.crew?.description}
|
description={crewStore.crew?.description ?? undefined}
|
||||||
>
|
>
|
||||||
{#snippet actions()}
|
{#snippet actions()}
|
||||||
{#if crewStore.isOfficer}
|
{#if crewStore.isOfficer}
|
||||||
|
|
@ -292,8 +293,8 @@
|
||||||
<span class="event-dates">
|
<span class="event-dates">
|
||||||
{formatDate(event.startDate)} – {formatDate(event.endDate)}
|
{formatDate(event.startDate)} – {formatDate(event.endDate)}
|
||||||
</span>
|
</span>
|
||||||
<span class="event-status status-{event.status}"
|
<span class="event-status status-{event.status ?? 'unknown'}"
|
||||||
>{formatEventStatus(event.status, event.startDate)}</span
|
>{formatEventStatus(event.status ?? 'unknown', event.startDate)}</span
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -354,20 +355,15 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
{#snippet children()}
|
onCancel={handleCloseModal}
|
||||||
<Button variant="ghost" onclick={handleCloseModal} disabled={createCrewMutation.isPending}>
|
cancelDisabled={createCrewMutation.isPending}
|
||||||
Cancel
|
primaryAction={{
|
||||||
</Button>
|
label: createCrewMutation.isPending ? 'Creating...' : 'Create Crew',
|
||||||
<Button
|
onclick: handleCreateCrew,
|
||||||
onclick={handleCreateCrew}
|
disabled: !canCreate || createCrewMutation.isPending
|
||||||
variant="primary"
|
}}
|
||||||
disabled={!canCreate || createCrewMutation.isPending}
|
/>
|
||||||
>
|
|
||||||
{createCrewMutation.isPending ? 'Creating...' : 'Create Crew'}
|
|
||||||
</Button>
|
|
||||||
{/snippet}
|
|
||||||
</ModalFooter>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
@ -419,24 +415,15 @@
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
{#snippet children()}
|
onCancel={handleCloseSettingsModal}
|
||||||
<Button
|
cancelDisabled={updateCrewMutation.isPending}
|
||||||
variant="ghost"
|
primaryAction={{
|
||||||
onclick={handleCloseSettingsModal}
|
label: updateCrewMutation.isPending ? 'Saving...' : 'Save',
|
||||||
disabled={updateCrewMutation.isPending}
|
onclick: handleUpdateSettings,
|
||||||
>
|
disabled: !canSaveSettings || updateCrewMutation.isPending
|
||||||
Cancel
|
}}
|
||||||
</Button>
|
/>
|
||||||
<Button
|
|
||||||
onclick={handleUpdateSettings}
|
|
||||||
variant="primary"
|
|
||||||
disabled={!canSaveSettings || updateCrewMutation.isPending}
|
|
||||||
>
|
|
||||||
{updateCrewMutation.isPending ? 'Saving...' : 'Save'}
|
|
||||||
</Button>
|
|
||||||
{/snippet}
|
|
||||||
</ModalFooter>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@
|
||||||
// Query for event and participation data
|
// Query for event and participation data
|
||||||
const eventQuery = createQuery(() => ({
|
const eventQuery = createQuery(() => ({
|
||||||
queryKey: ['crew', 'gw', 'event', eventNumber],
|
queryKey: ['crew', 'gw', 'event', eventNumber],
|
||||||
queryFn: () => gwAdapter.getEventWithParticipation(eventNumber),
|
queryFn: () => gwAdapter.getEventWithParticipation(eventNumber ?? ''),
|
||||||
enabled: !!eventNumber && crewStore.isInCrew
|
enabled: !!eventNumber && crewStore.isInCrew
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
|
@ -404,7 +404,7 @@
|
||||||
|
|
||||||
<!-- Add Score Modal -->
|
<!-- Add Score Modal -->
|
||||||
<Dialog bind:open={showScoreModal} onOpenChange={(open) => !open && closeScoreModal()}>
|
<Dialog bind:open={showScoreModal} onOpenChange={(open) => !open && closeScoreModal()}>
|
||||||
<ModalHeader title="Add Score" onClose={closeScoreModal} />
|
<ModalHeader title="Add Score" />
|
||||||
<ModalBody>
|
<ModalBody>
|
||||||
<div class="score-form">
|
<div class="score-form">
|
||||||
<Select
|
<Select
|
||||||
|
|
@ -455,20 +455,17 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
<Button variant="secondary" size="small" onclick={closeScoreModal}>Cancel</Button>
|
onCancel={closeScoreModal}
|
||||||
<Button
|
primaryAction={{
|
||||||
variant="primary"
|
label: isSubmitting ? 'Saving...' : 'Save',
|
||||||
size="small"
|
onclick: handleSubmitScore,
|
||||||
onclick={handleSubmitScore}
|
disabled: isSubmitting ||
|
||||||
disabled={isSubmitting ||
|
|
||||||
!selectedPlayerId ||
|
!selectedPlayerId ||
|
||||||
(!isCumulative && calculatedCumulativeScore === 0) ||
|
(!isCumulative && calculatedCumulativeScore === 0) ||
|
||||||
(isCumulative && !cumulativeScore)}
|
(isCumulative && !cumulativeScore)
|
||||||
>
|
}}
|
||||||
{isSubmitting ? 'Saving...' : 'Save'}
|
/>
|
||||||
</Button>
|
|
||||||
</ModalFooter>
|
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
||||||
|
|
@ -552,23 +552,14 @@
|
||||||
</p>
|
</p>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
{#snippet children()}
|
onCancel={() => (confirmDialogOpen = false)}
|
||||||
<Button variant="ghost" onclick={() => (confirmDialogOpen = false)}>Cancel</Button>
|
primaryAction={{
|
||||||
<Button
|
label: confirmAction === 'remove' ? 'Remove' : confirmAction === 'promote' ? 'Promote' : 'Demote',
|
||||||
variant={confirmAction === 'remove' ? 'destructive' : 'primary'}
|
onclick: handleConfirmAction,
|
||||||
onclick={handleConfirmAction}
|
destructive: confirmAction === 'remove'
|
||||||
>
|
}}
|
||||||
{#if confirmAction === 'remove'}
|
/>
|
||||||
Remove
|
|
||||||
{:else if confirmAction === 'promote'}
|
|
||||||
Promote
|
|
||||||
{:else}
|
|
||||||
Demote
|
|
||||||
{/if}
|
|
||||||
</Button>
|
|
||||||
{/snippet}
|
|
||||||
</ModalFooter>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
@ -623,24 +614,15 @@
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
{#snippet children()}
|
onCancel={() => (phantomDialogOpen = false)}
|
||||||
<Button
|
cancelDisabled={createPhantomMutation.isPending}
|
||||||
variant="ghost"
|
primaryAction={{
|
||||||
onclick={() => (phantomDialogOpen = false)}
|
label: createPhantomMutation.isPending ? 'Creating...' : 'Create',
|
||||||
disabled={createPhantomMutation.isPending}
|
onclick: handleCreatePhantom,
|
||||||
>
|
disabled: !phantomName.trim() || createPhantomMutation.isPending
|
||||||
Cancel
|
}}
|
||||||
</Button>
|
/>
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onclick={handleCreatePhantom}
|
|
||||||
disabled={!phantomName.trim() || createPhantomMutation.isPending}
|
|
||||||
>
|
|
||||||
{createPhantomMutation.isPending ? 'Creating...' : 'Create'}
|
|
||||||
</Button>
|
|
||||||
{/snippet}
|
|
||||||
</ModalFooter>
|
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
@ -664,14 +646,14 @@
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
|
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
{#snippet children()}
|
onCancel={() => (editJoinDateDialogOpen = false)}
|
||||||
<Button variant="ghost" onclick={() => (editJoinDateDialogOpen = false)}>Cancel</Button>
|
primaryAction={{
|
||||||
<Button variant="primary" onclick={handleSaveJoinDate} disabled={!editJoinDate}>
|
label: 'Save',
|
||||||
Save
|
onclick: handleSaveJoinDate,
|
||||||
</Button>
|
disabled: !editJoinDate
|
||||||
{/snippet}
|
}}
|
||||||
</ModalFooter>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@
|
||||||
title: 'Components/UI/Dialog',
|
title: 'Components/UI/Dialog',
|
||||||
tags: ['autodocs']
|
tags: ['autodocs']
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Dummy handlers for storybook
|
||||||
|
const noop = () => {}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -67,12 +70,13 @@
|
||||||
<p>Are you sure you want to proceed with this action?</p>
|
<p>Are you sure you want to proceed with this action?</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
{#snippet children()}
|
onCancel={() => (withFooterOpen = false)}
|
||||||
<Button variant="secondary" onclick={() => (withFooterOpen = false)}>Cancel</Button>
|
primaryAction={{
|
||||||
<Button variant="primary" onclick={() => (withFooterOpen = false)}>Confirm</Button>
|
label: 'Confirm',
|
||||||
{/snippet}
|
onclick: () => (withFooterOpen = false)
|
||||||
</ModalFooter>
|
}}
|
||||||
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -112,12 +116,13 @@
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
{#snippet children()}
|
onCancel={() => (longContentOpen = false)}
|
||||||
<Button variant="secondary" onclick={() => (longContentOpen = false)}>Decline</Button>
|
primaryAction={{
|
||||||
<Button variant="primary" onclick={() => (longContentOpen = false)}>Accept</Button>
|
label: 'Accept',
|
||||||
{/snippet}
|
onclick: () => (longContentOpen = false)
|
||||||
</ModalFooter>
|
}}
|
||||||
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -160,12 +165,13 @@
|
||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
{#snippet children()}
|
onCancel={() => (formOpen = false)}
|
||||||
<Button variant="secondary" onclick={() => (formOpen = false)}>Cancel</Button>
|
primaryAction={{
|
||||||
<Button variant="primary" onclick={() => (formOpen = false)}>Save Changes</Button>
|
label: 'Save Changes',
|
||||||
{/snippet}
|
onclick: () => (formOpen = false)
|
||||||
</ModalFooter>
|
}}
|
||||||
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -186,12 +192,14 @@
|
||||||
</p>
|
</p>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</ModalBody>
|
</ModalBody>
|
||||||
<ModalFooter>
|
<ModalFooter
|
||||||
{#snippet children()}
|
onCancel={() => (confirmOpen = false)}
|
||||||
<Button variant="secondary" onclick={() => (confirmOpen = false)}>Cancel</Button>
|
primaryAction={{
|
||||||
<Button variant="destructive" onclick={() => (confirmOpen = false)}>Delete</Button>
|
label: 'Delete',
|
||||||
{/snippet}
|
onclick: () => (confirmOpen = false),
|
||||||
</ModalFooter>
|
destructive: true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue