Desktop v2 screens: releases (store cards, stage stepper, rollout ring), submit checklist, TestFlight cards; listings as store pages with completeness, framed screenshots with issue badges, product cards, price matrix with gap shading; health tiles and issue cards, store-style review feed with rating distribution, growth funnel, readiness score with fix-first, tools grid, store-info sheet; settings, accounts, folders, dependencies and onboarding in the v2 language
This commit is contained in:
parent
ce939ee063
commit
f131de9df2
47 changed files with 2309 additions and 927 deletions
|
|
@ -10,7 +10,9 @@ import { ago } from '../lib/format';
|
|||
import { withIgnored } from '../lib/identity';
|
||||
import { type AccountHealth, useData } from '../store';
|
||||
import { Icon, StoreGlyph } from '../ui/icons';
|
||||
import { Card, Chip, Empty, Notice, StudioMark } from '../ui/kit';
|
||||
import { Chip, Notice, StudioMark } from '../ui/kit';
|
||||
import { EmptyState, PageHeader, Section, StatTile } from '../ui/ds';
|
||||
import '../styles/accounts.css';
|
||||
import type { Tone } from '../lib/model';
|
||||
|
||||
const STATUS: Record<AccountCheck['status'], [Tone, Key]> = {
|
||||
|
|
@ -110,7 +112,7 @@ function StudioCard({ s, accounts, focus }: { s: Studio | null; accounts: Accoun
|
|||
const id = s ? `studio-${s.id}` : 'studio-none';
|
||||
useEffect(() => { if (focus) document.getElementById(id)?.scrollIntoView({ block: 'start' }); }, [focus, id]);
|
||||
return (
|
||||
<section className={`card studio-card${focus ? ' focus' : ''}`} id={id} aria-labelledby={`${id}-h`}>
|
||||
<section className={`ds-sec studio-card${focus ? ' focus' : ''}`} id={id} aria-labelledby={`${id}-h`}>
|
||||
<div className="card-head">
|
||||
<div className="row" style={{ gap: 12, minWidth: 0 }}>
|
||||
<StudioMark studio={s} size="lg" />
|
||||
|
|
@ -178,28 +180,31 @@ export function Accounts({ focus }: { focus?: string }) {
|
|||
);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<div className="ph">
|
||||
<div><h1>{t('nav.accounts')}</h1><p>{t('accounts.leadStudios')}</p></div>
|
||||
<div className="actions">
|
||||
<button className="btn" onClick={testAll} disabled={!!busy || !d.accounts.length}><Icon name="shield" className={busy === 'all' ? 'spin' : undefined} />{t('acct.testAll')}</button>
|
||||
<button className="btn btn-primary" onClick={() => d.importApps()} disabled={d.importBusy || !d.accounts.length}><Icon name="download" className={d.importBusy ? 'spin' : undefined} />{t('action.importApps')}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="page scr-accounts">
|
||||
<PageHeader icon="key" eyebrow={t('v2cfg.workspace')} title={t('nav.accounts')} subtitle={t('accounts.leadStudios')} actions={<>
|
||||
<button className="btn" onClick={testAll} disabled={!!busy || !d.accounts.length}><Icon name="shield" className={busy === 'all' ? 'spin' : undefined} />{t('acct.testAll')}</button>
|
||||
<button className="btn btn-primary" onClick={() => d.importApps()} disabled={d.importBusy || !d.accounts.length}><Icon name="download" className={d.importBusy ? 'spin' : undefined} />{t('action.importApps')}</button>
|
||||
</>} />
|
||||
{d.accounts.length > 0 && <div className="ds-grid acct-kpis">
|
||||
<StatTile icon="building" label={t('v2cfg.studios')} value={d.studios.length} />
|
||||
<StatTile icon="key" label={t('v2cfg.accounts')} value={d.accounts.length} sub={t('v2cfg.perStore', { play: d.accounts.filter(a => a.kind === 'play').length, apple: d.accounts.filter(a => a.kind === 'apple').length })} />
|
||||
<StatTile icon="check" tone="ok" label={t('v2cfg.keysWork')} value={d.accounts.filter(a => d.accountHealth[a.id]?.check?.status === 'ok').length} />
|
||||
<StatTile icon="alert" tone={d.accounts.some(a => { const h = d.accountHealth[a.id]; return !!h && (!!h.error || (!!h.check && h.check.status !== 'ok')); }) ? 'bad' : undefined} label={t('v2cfg.keysFailing')} value={d.accounts.filter(a => { const h = d.accountHealth[a.id]; return !!h && (!!h.error || (!!h.check && h.check.status !== 'ok')); }).length} />
|
||||
</div>}
|
||||
{error && <Notice tone="bad">{error}</Notice>}
|
||||
{!d.accounts.length && !d.studios.length && <Card pad={false}><Empty illo="key" title={t('accounts.emptyTitle')}>{t('accounts.emptyTextStudios')}</Empty></Card>}
|
||||
{!d.accounts.length && !d.studios.length && <Section><EmptyState icon="key"><b>{t('accounts.emptyTitle')}</b> {t('accounts.emptyTextStudios')}</EmptyState></Section>}
|
||||
{d.studios.map(s => <StudioCard key={s.id} s={s} accounts={d.accounts.filter(a => a.studio === s.id)} focus={focus === s.id} />)}
|
||||
{(unassigned.length > 0 || !d.studios.length) && d.accounts.length > 0 && <StudioCard s={null} accounts={unassigned} focus={focus === 'none'} />}
|
||||
|
||||
<div className="grid g3" style={{ alignItems: 'start' }}>
|
||||
<Card title={t('studio.new')} icon="building" sub={t('studio.newSub')}>
|
||||
<div className="acct-add">
|
||||
<Section title={t('studio.new')} icon="building" sub={t('studio.newSub')}>
|
||||
<form className="form" onSubmit={e => { e.preventDefault(); addStudio(); }}>
|
||||
<div className="field"><label htmlFor="studio-name">{t('studio.name')}</label><input id="studio-name" className="input" value={studioName} onChange={e => setStudioName(e.target.value)} maxLength={48} placeholder={t('studio.namePlaceholder')} autoComplete="off" /></div>
|
||||
<div className="field"><span className="legend">{t('studio.color')}</span><Swatches value={studioColor} onChange={setStudioColor} name={studioName} /></div>
|
||||
<div><button className="btn btn-primary" type="submit" disabled={!studioName.trim() || !!busy}><Icon name="plus" />{t('studio.add')}</button></div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="Google Play" icon="key" sub={t('accounts.playSubMany')}>
|
||||
</Section>
|
||||
<Section title="Google Play" icon="key" sub={t('accounts.playSubMany')}>
|
||||
<form className="form" onSubmit={e => { e.preventDefault(); addPlay(); }}>
|
||||
<div className="field"><label htmlFor="play-label">{t('accounts.label')}</label>
|
||||
<input id="play-label" className="input" type="text" value={playLabel} onChange={e => setPlayLabel(e.target.value)} placeholder={t('accounts.playPlaceholder')} maxLength={60} autoComplete="off" /></div>
|
||||
|
|
@ -207,8 +212,8 @@ export function Accounts({ focus }: { focus?: string }) {
|
|||
<p className="small muted">{t('accounts.playHow')}</p>
|
||||
<div><button className="btn btn-primary" type="submit" disabled={!playLabel.trim() || !!busy}><Icon name="key" />{t('accounts.choosePlay')}</button></div>
|
||||
</form>
|
||||
</Card>
|
||||
<Card title="App Store Connect" icon="key" sub={t('accounts.appleSubMany')}>
|
||||
</Section>
|
||||
<Section title="App Store Connect" icon="key" sub={t('accounts.appleSubMany')}>
|
||||
<form className="form" onSubmit={e => { e.preventDefault(); addApple(); }}>
|
||||
<div className="field"><label htmlFor="apple-label">{t('accounts.label')}</label>
|
||||
<input id="apple-label" className="input" type="text" value={appleLabel} onChange={e => setAppleLabel(e.target.value)} placeholder={t('accounts.applePlaceholder')} maxLength={60} autoComplete="off" /></div>
|
||||
|
|
@ -221,7 +226,7 @@ export function Accounts({ focus }: { focus?: string }) {
|
|||
</div>
|
||||
<div><button className="btn btn-primary" type="submit" disabled={!appleLabel.trim() || keyId.length !== 10 || issuerId.length < 36 || !!busy}><Icon name="key" />{t('accounts.chooseApple')}</button></div>
|
||||
</form>
|
||||
</Card>
|
||||
</Section>
|
||||
</div>
|
||||
<Notice tone="info" icon="shield">{t('accounts.safetyStudios')}</Notice>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue