chore(history): squash 16 commit(s) from 2026-09-26
- chore(backend): sync Cargo.lock with gateway dev-dependency - feat(frontend): ClickGui replica matching the in-game GUI — category panels, Modules/Settings switch, theme cards - feat(frontend): floating glass top bar in the mod's UI language - fix(frontend): soft night backdrop for the ClickGui, editor that fits one screen - chore(frontend): replace em dashes in UI copy with lighter separators - refactor(frontend): split landing sections and theme editor into subfolders (4-files-per-folder rule) - feat(frontend): login and registration in the mod's glass panel style - feat(frontend): link the game with a device code - feat(frontend): account page with avatar upload and linked games - feat(frontend): my configs with share codes and publishing - feat(frontend): public showcase listing and detail pages - chore(frontend): drop em dashes from visible copy - feat(frontend): atmospheric backdrop, glass download block and ClickGui-style FAQ on the landing - feat(frontend): Konami code, DevTools console greeting and ASCII 404 block - feat: public player profiles at /u/:id and author link in the showcase - fix(frontend): stop the theme editor overflowing horizontally at 375px
This commit is contained in:
parent
7f4b532f99
commit
ff2edc8eb6
116 changed files with 3534 additions and 1020 deletions
75
frontend/src/features/showcase/components/CopyToSlot.tsx
Normal file
75
frontend/src/features/showcase/components/CopyToSlot.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link, useLocation } from 'react-router'
|
||||
import { Notice } from '../../../shared/ui/Notice'
|
||||
import { ApiError } from '../../../shared/api/client'
|
||||
import { useSession } from '../../auth/session'
|
||||
import { listSlots } from '../../configs/api'
|
||||
import { copyToSlot } from '../api'
|
||||
|
||||
const SLOTS = [1, 2, 3, 4] as const
|
||||
|
||||
export function CopyToSlot({ listingId }: { listingId: string }) {
|
||||
const { t } = useTranslation('showcase')
|
||||
const { status } = useSession()
|
||||
const location = useLocation()
|
||||
const client = useQueryClient()
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [copiedSlot, setCopiedSlot] = useState<number | null>(null)
|
||||
|
||||
const { data: slots } = useQuery({ queryKey: ['configs'], queryFn: listSlots, enabled: status === 'authenticated' })
|
||||
const occupied = new Set((slots ?? []).map((s) => s.slot))
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (slot: number) => copyToSlot(listingId, slot),
|
||||
onSuccess: (_data, slot) => {
|
||||
setError(null)
|
||||
setCopiedSlot(slot)
|
||||
client.invalidateQueries({ queryKey: ['configs'] })
|
||||
},
|
||||
onError: (err) => {
|
||||
setError(err instanceof ApiError && err.status === 409 ? t('copy.conflict') : t('page.error'))
|
||||
},
|
||||
})
|
||||
|
||||
if (status !== 'authenticated') {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-frost">{t('copy.loginPrompt')}</p>
|
||||
<Link to="/login" state={{ from: location.pathname }} className="text-ice hover:underline">
|
||||
{t('copy.login')}
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<h2 className="font-display text-lg text-snow">{t('copy.heading')}</h2>
|
||||
{copiedSlot ? (
|
||||
<Notice tone="info">
|
||||
{t('copy.success', { slot: copiedSlot })}{' '}
|
||||
<Link to="/configs" className="text-ice hover:underline">
|
||||
{t('copy.openConfigs')}
|
||||
</Link>
|
||||
</Notice>
|
||||
) : null}
|
||||
{error ? <Notice tone="error">{error}</Notice> : null}
|
||||
<div role="group" aria-label={t('copy.heading')} className="inline-flex overflow-hidden rounded-md border border-shoal">
|
||||
{SLOTS.map((slot) => (
|
||||
<button
|
||||
key={slot}
|
||||
type="button"
|
||||
disabled={occupied.has(slot) || mutation.isPending}
|
||||
title={occupied.has(slot) ? t('copy.occupied') : undefined}
|
||||
onClick={() => mutation.mutate(slot)}
|
||||
className="border-r border-shoal px-4 py-2 text-sm font-semibold text-snow last:border-r-0 hover:bg-ice/10 disabled:cursor-not-allowed disabled:text-frost/50 disabled:hover:bg-transparent"
|
||||
>
|
||||
{t('copy.slot', { slot })}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue