@meith/web 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/(board)/member/[id]/reputation/page.tsx +2 -2
- package/app/(board)/messages/[id]/page.tsx +28 -20
- package/app/(board)/messages/page.tsx +2 -2
- package/app/(board)/notifications/page.tsx +3 -1
- package/app/(board)/subscriptions/page.tsx +6 -2
- package/app/(board)/usercp/contacts/page.tsx +3 -7
- package/app/admin/api-tokens/page.tsx +3 -2
- package/app/admin/forums/[id]/page.tsx +25 -22
- package/app/admin/groups/memberships/page.tsx +6 -6
- package/app/admin/groups/promotions/page.tsx +14 -16
- package/app/admin/log/page.tsx +5 -3
- package/app/admin/plugins/page.tsx +1 -1
- package/app/admin/security/page.tsx +4 -3
- package/app/admin/system/page.tsx +13 -10
- package/app/admin/themes/page.tsx +1 -1
- package/bin/forum-web.mjs +3 -3
- package/bin/forum-web.test.ts +2 -2
- package/package.json +48 -48
- package/src/components/account/active-sessions.tsx +9 -6
- package/src/components/account/credential-proof-form.tsx +6 -7
- package/src/components/account/logout-form.tsx +5 -3
- package/src/components/account/notification-forms.tsx +9 -9
- package/src/components/account/relation-forms.tsx +5 -5
- package/src/components/account/reputation-forms.tsx +10 -10
- package/src/components/account/sign-in-methods.tsx +12 -6
- package/src/components/account/subscription-forms.tsx +7 -7
- package/src/components/account/two-factor-forms.tsx +12 -12
- package/src/components/account/usercp-forms.tsx +13 -13
- package/src/components/admin/admin-forms.tsx +7 -7
- package/src/components/admin/badge-forms.tsx +3 -3
- package/src/components/admin/ban-filter-forms.tsx +4 -4
- package/src/components/admin/branding-forms.tsx +3 -3
- package/src/components/admin/content-forms.tsx +15 -15
- package/src/components/admin/forum-forms.tsx +7 -7
- package/src/components/admin/group-forms.tsx +5 -5
- package/src/components/admin/mail-test-card.tsx +3 -3
- package/src/components/admin/marketplace-forms.tsx +4 -4
- package/src/components/admin/navigation-forms.tsx +3 -3
- package/src/components/admin/plugin-forms.tsx +7 -7
- package/src/components/admin/settings-form.tsx +5 -2
- package/src/components/admin/theme-forms.tsx +7 -10
- package/src/components/auth/form-controls.tsx +23 -1
- package/src/components/auth/second-factor-form.tsx +4 -4
- package/src/components/content/composer-intents.tsx +7 -6
- package/src/components/content/edit-post-form.tsx +7 -7
- package/src/components/content/poll-edit-form.tsx +3 -1
- package/src/components/content/poll.tsx +4 -2
- package/src/components/content/thanks-button.tsx +3 -3
- package/src/components/content/thread-rating.tsx +3 -3
- package/src/components/messages/message-forms.tsx +15 -15
- package/src/components/moderation/inline-moderation-form.tsx +19 -20
- package/src/components/moderation/queue-form.tsx +5 -7
- package/src/components/moderation/report-forms.tsx +8 -10
- package/src/components/moderation/signature-lock-form.tsx +5 -5
- package/src/components/moderation/thread-surgery-form.tsx +8 -5
- package/src/components/moderation/thread-tools-form.tsx +11 -22
- package/src/components/moderation/warning-forms.tsx +6 -6
- package/src/components/shell/onboarding-banner.tsx +6 -2
- package/src/components/shell/theme-switcher.tsx +6 -6
- package/src/components/shell/view-tabs.tsx +12 -7
- package/src/server/theme.ts +1 -1
- package/src/server/upgrade-notice.ts +1 -1
|
@@ -24,7 +24,7 @@ import {
|
|
|
24
24
|
updateWordFilterAction,
|
|
25
25
|
} from '@/server/content-admin-actions'
|
|
26
26
|
|
|
27
|
-
import { FormError, SubmitButton } from '../auth/form-controls'
|
|
27
|
+
import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
|
|
28
28
|
import { type Copy, fromCopy } from '../shell/copy'
|
|
29
29
|
import { INPUT, Saved } from './form-bits'
|
|
30
30
|
|
|
@@ -87,9 +87,9 @@ export function WordFilterRowForm({ filter, copy }: { filter: WordFilterValues;
|
|
|
87
87
|
|
|
88
88
|
<form action={removeAction}>
|
|
89
89
|
<input type="hidden" name="id" value={filter.id} />
|
|
90
|
-
<
|
|
90
|
+
<PendingButton showWorking className="text-xs text-muted-foreground hover:underline">
|
|
91
91
|
{fromCopy(copy, 'adminContent.filter.removeThis')}
|
|
92
|
-
</
|
|
92
|
+
</PendingButton>
|
|
93
93
|
</form>
|
|
94
94
|
</div>
|
|
95
95
|
)
|
|
@@ -143,9 +143,9 @@ export function DeletePrefixForm({ prefix, copy }: { prefix: PrefixValues; copy:
|
|
|
143
143
|
<form action={action} className="flex items-center gap-2">
|
|
144
144
|
<FormError message={state.error} />
|
|
145
145
|
<input type="hidden" name="id" value={prefix.id} />
|
|
146
|
-
<
|
|
146
|
+
<PendingButton showWorking className="text-xs text-muted-foreground hover:underline">
|
|
147
147
|
{fromCopy(copy, 'admin.remove')}
|
|
148
|
-
</
|
|
148
|
+
</PendingButton>
|
|
149
149
|
</form>
|
|
150
150
|
)
|
|
151
151
|
}
|
|
@@ -252,9 +252,9 @@ export function SmileyRowForm({ smiley, copy }: { smiley: SmileyValues; copy: Co
|
|
|
252
252
|
|
|
253
253
|
<form action={removeAction}>
|
|
254
254
|
<input type="hidden" name="id" value={smiley.id} />
|
|
255
|
-
<
|
|
255
|
+
<PendingButton showWorking className="text-xs text-destructive hover:underline">
|
|
256
256
|
{fromCopy(copy, 'admin.remove')}
|
|
257
|
-
</
|
|
257
|
+
</PendingButton>
|
|
258
258
|
</form>
|
|
259
259
|
</div>
|
|
260
260
|
)
|
|
@@ -362,9 +362,9 @@ export function DirectiveRowForm({ directive, copy }: { directive: DirectiveValu
|
|
|
362
362
|
|
|
363
363
|
<form action={removeAction}>
|
|
364
364
|
<input type="hidden" name="id" value={directive.id} />
|
|
365
|
-
<
|
|
365
|
+
<PendingButton showWorking className="text-xs text-destructive hover:underline">
|
|
366
366
|
{fromCopy(copy, 'admin.remove')}
|
|
367
|
-
</
|
|
367
|
+
</PendingButton>
|
|
368
368
|
</form>
|
|
369
369
|
</div>
|
|
370
370
|
)
|
|
@@ -422,9 +422,9 @@ export function DeleteAttachmentForm({ attachmentId, copy }: { attachmentId: num
|
|
|
422
422
|
<form action={action} className="shrink-0">
|
|
423
423
|
<FormError message={state.error} />
|
|
424
424
|
<input type="hidden" name="id" value={attachmentId} />
|
|
425
|
-
<
|
|
425
|
+
<PendingButton showWorking className="text-xs text-destructive hover:underline">
|
|
426
426
|
{fromCopy(copy, 'adminContent.attachment.delete')}
|
|
427
|
-
</
|
|
427
|
+
</PendingButton>
|
|
428
428
|
</form>
|
|
429
429
|
)
|
|
430
430
|
}
|
|
@@ -568,9 +568,9 @@ export function AnnouncementRowForm({
|
|
|
568
568
|
|
|
569
569
|
<form action={removeAction}>
|
|
570
570
|
<input type="hidden" name="id" value={announcement.id} />
|
|
571
|
-
<
|
|
571
|
+
<PendingButton showWorking className="text-xs text-destructive hover:underline">
|
|
572
572
|
{fromCopy(copy, 'admin.remove')}
|
|
573
|
-
</
|
|
573
|
+
</PendingButton>
|
|
574
574
|
</form>
|
|
575
575
|
</div>
|
|
576
576
|
)
|
|
@@ -670,9 +670,9 @@ export function CaptchaQuestionRowForm({
|
|
|
670
670
|
|
|
671
671
|
<form action={removeAction}>
|
|
672
672
|
<input type="hidden" name="id" value={question.id} />
|
|
673
|
-
<
|
|
673
|
+
<PendingButton showWorking className="text-xs text-destructive hover:underline">
|
|
674
674
|
{fromCopy(copy, 'admin.remove')}
|
|
675
|
-
</
|
|
675
|
+
</PendingButton>
|
|
676
676
|
</form>
|
|
677
677
|
</div>
|
|
678
678
|
)
|
|
@@ -5,7 +5,7 @@ import { useActionState } from 'react'
|
|
|
5
5
|
import type { MatrixCell, MatrixRow } from '@meith/authorization'
|
|
6
6
|
import { cn, Disclosure } from '@meith/ui'
|
|
7
7
|
|
|
8
|
-
import { PANEL_CARD, PANEL_LIST, PANEL_ROW } from '@/components/shell/panel-list'
|
|
8
|
+
import { PANEL_CARD, PANEL_LIST, PANEL_NOTE, PANEL_ROW } from '@/components/shell/panel-list'
|
|
9
9
|
import { EMPTY_STATE } from '@/server/auth-form-state'
|
|
10
10
|
import {
|
|
11
11
|
appointModeratorAction,
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
saveForumPermissionsAction,
|
|
18
18
|
} from '@/server/forum-admin-actions'
|
|
19
19
|
|
|
20
|
-
import { FormError, SubmitButton } from '../auth/form-controls'
|
|
20
|
+
import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
|
|
21
21
|
import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
|
|
22
22
|
import { INPUT, Saved } from './form-bits'
|
|
23
23
|
|
|
@@ -534,13 +534,13 @@ export function ModeratorsPanel({
|
|
|
534
534
|
|
|
535
535
|
return (
|
|
536
536
|
<section className="flex flex-col gap-4">
|
|
537
|
-
<h2 className="text-lg font-semibold
|
|
537
|
+
<h2 className="font-heading text-lg font-semibold">
|
|
538
538
|
{fromCopy(copy, 'adminForum.moderators')}
|
|
539
539
|
</h2>
|
|
540
540
|
|
|
541
541
|
<FormError message={removeState.error} />
|
|
542
542
|
{moderators.length === 0 ? (
|
|
543
|
-
<p className=
|
|
543
|
+
<p className={PANEL_NOTE}>{fromCopy(copy, 'adminForum.noModerators')}</p>
|
|
544
544
|
) : (
|
|
545
545
|
<ul className={PANEL_LIST}>
|
|
546
546
|
{moderators.map((moderator) => (
|
|
@@ -561,12 +561,12 @@ export function ModeratorsPanel({
|
|
|
561
561
|
<form action={remove}>
|
|
562
562
|
<input type="hidden" name="forumId" value={forumId} />
|
|
563
563
|
<input type="hidden" name="appointmentId" value={moderator.id} />
|
|
564
|
-
<
|
|
565
|
-
|
|
564
|
+
<PendingButton
|
|
565
|
+
showWorking
|
|
566
566
|
className="text-xs text-muted-foreground hover:text-foreground"
|
|
567
567
|
>
|
|
568
568
|
{fromCopy(copy, 'admin.remove')}
|
|
569
|
-
</
|
|
569
|
+
</PendingButton>
|
|
570
570
|
</form>
|
|
571
571
|
</li>
|
|
572
572
|
))}
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
updatePromotionRuleAction,
|
|
17
17
|
} from '@/server/group-admin-actions'
|
|
18
18
|
|
|
19
|
-
import { FormError, SubmitButton } from '../auth/form-controls'
|
|
19
|
+
import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
|
|
20
20
|
import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
|
|
21
21
|
import { INPUT, Saved } from './form-bits'
|
|
22
22
|
import { OklchPicker } from './oklch-picker'
|
|
@@ -579,18 +579,18 @@ export function PromotionRuleRowForm({
|
|
|
579
579
|
<form action={toggleAction} className="flex items-center">
|
|
580
580
|
<input type="hidden" name="id" value={rule.id} />
|
|
581
581
|
{!rule.enabled && <input type="hidden" name="enabled" value="1" />}
|
|
582
|
-
<
|
|
582
|
+
<PendingButton className="text-xs text-muted-foreground hover:underline">
|
|
583
583
|
{rule.enabled
|
|
584
584
|
? fromCopy(copy, 'adminGroup.rule.disable')
|
|
585
585
|
: fromCopy(copy, 'adminGroup.rule.enable')}
|
|
586
|
-
</
|
|
586
|
+
</PendingButton>
|
|
587
587
|
</form>
|
|
588
588
|
|
|
589
589
|
<form action={removeAction} className="flex items-center">
|
|
590
590
|
<input type="hidden" name="id" value={rule.id} />
|
|
591
|
-
<
|
|
591
|
+
<PendingButton className="text-xs text-destructive hover:underline">
|
|
592
592
|
{fromCopy(copy, 'admin.remove')}
|
|
593
|
-
</
|
|
593
|
+
</PendingButton>
|
|
594
594
|
</form>
|
|
595
595
|
</div>
|
|
596
596
|
<p className="text-xs text-muted-foreground">
|
|
@@ -6,6 +6,7 @@ import { PANEL_CARD } from '@/components/shell/panel-list'
|
|
|
6
6
|
import { EMPTY_STATE, type FormState } from '@/server/auth-form-state'
|
|
7
7
|
import { sendTestMailAction } from '@/server/mail-test-actions'
|
|
8
8
|
|
|
9
|
+
import { PendingButton } from '../auth/form-controls'
|
|
9
10
|
import { type Copy, fromCopy } from '../shell/copy'
|
|
10
11
|
import { Saved } from './form-bits'
|
|
11
12
|
|
|
@@ -66,15 +67,14 @@ export function MailTestCard({
|
|
|
66
67
|
|
|
67
68
|
<form action={submit} className="flex flex-col gap-2">
|
|
68
69
|
<div>
|
|
69
|
-
<
|
|
70
|
-
type="submit"
|
|
70
|
+
<PendingButton
|
|
71
71
|
disabled={pending || !sends}
|
|
72
72
|
className="inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground hover:opacity-90 disabled:opacity-60"
|
|
73
73
|
>
|
|
74
74
|
{pending
|
|
75
75
|
? fromCopy(copy, 'adminPanel.mail.sending')
|
|
76
76
|
: fromCopy(copy, 'adminPanel.mail.send')}
|
|
77
|
-
</
|
|
77
|
+
</PendingButton>
|
|
78
78
|
</div>
|
|
79
79
|
<p className="text-xs text-muted-foreground">
|
|
80
80
|
{copy['adminPanel.mail.testNoteLead']}
|
|
@@ -5,7 +5,7 @@ import { useActionState } from 'react'
|
|
|
5
5
|
import { EMPTY_STATE } from '@/server/auth-form-state'
|
|
6
6
|
import { refreshMarketplaceAction } from '@/server/marketplace-actions'
|
|
7
7
|
|
|
8
|
-
import { FormError } from '../auth/form-controls'
|
|
8
|
+
import { FormError, PendingButton } from '../auth/form-controls'
|
|
9
9
|
import { type Copy, fromCopy } from '../shell/copy'
|
|
10
10
|
import { Saved } from './form-bits'
|
|
11
11
|
|
|
@@ -18,12 +18,12 @@ export function MarketplaceRefreshForm({ copy }: { copy: Copy }) {
|
|
|
18
18
|
<Saved when={state.notice === 'refreshed'}>
|
|
19
19
|
{fromCopy(copy, 'adminPanel.marketplace.refreshed')}
|
|
20
20
|
</Saved>
|
|
21
|
-
<
|
|
22
|
-
|
|
21
|
+
<PendingButton
|
|
22
|
+
showWorking
|
|
23
23
|
className="inline-flex h-9 w-fit items-center justify-center rounded-md border border-border px-3 text-sm font-medium transition-colors hover:border-primary hover:bg-accent"
|
|
24
24
|
>
|
|
25
25
|
{fromCopy(copy, 'adminPanel.marketplace.refresh')}
|
|
26
|
-
</
|
|
26
|
+
</PendingButton>
|
|
27
27
|
</form>
|
|
28
28
|
)
|
|
29
29
|
}
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
updateNavigationItemAction,
|
|
10
10
|
} from '@/server/navigation-admin-actions'
|
|
11
11
|
|
|
12
|
-
import { FormError, SubmitButton } from '../auth/form-controls'
|
|
12
|
+
import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
|
|
13
13
|
import { type Copy, fromCopy } from '../shell/copy'
|
|
14
14
|
import { INPUT, Saved } from './form-bits'
|
|
15
15
|
|
|
@@ -193,9 +193,9 @@ export function NavigationItemRowForm({
|
|
|
193
193
|
|
|
194
194
|
<form action={removeAction}>
|
|
195
195
|
<input type="hidden" name="id" value={item.id} />
|
|
196
|
-
<
|
|
196
|
+
<PendingButton showWorking className="text-xs text-destructive hover:underline">
|
|
197
197
|
{fromCopy(copy, 'adminNavigation.removeThis')}
|
|
198
|
-
</
|
|
198
|
+
</PendingButton>
|
|
199
199
|
</form>
|
|
200
200
|
</div>
|
|
201
201
|
)
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
setPluginEnabledAction,
|
|
10
10
|
} from '@/server/plugin-admin-actions'
|
|
11
11
|
|
|
12
|
-
import { FormError, SubmitButton } from '../auth/form-controls'
|
|
12
|
+
import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
|
|
13
13
|
import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
|
|
14
14
|
import { INPUT, Saved } from './form-bits'
|
|
15
15
|
|
|
@@ -44,14 +44,14 @@ export function PluginEnableForm({
|
|
|
44
44
|
<FormError message={state.error} />
|
|
45
45
|
<input type="hidden" name="key" value={pluginKey} />
|
|
46
46
|
<input type="hidden" name="enabled" value={enabled ? '0' : '1'} />
|
|
47
|
-
<
|
|
48
|
-
|
|
47
|
+
<PendingButton
|
|
48
|
+
showWorking
|
|
49
49
|
className="inline-flex h-9 items-center justify-center rounded-md border border-border px-3 text-sm"
|
|
50
50
|
>
|
|
51
51
|
{enabled
|
|
52
52
|
? fromCopy(copy, 'adminPanel.plugin.disable')
|
|
53
53
|
: fromCopy(copy, 'adminPanel.plugin.enable')}
|
|
54
|
-
</
|
|
54
|
+
</PendingButton>
|
|
55
55
|
</form>
|
|
56
56
|
)
|
|
57
57
|
}
|
|
@@ -63,12 +63,12 @@ export function PluginHealthResetForm({ pluginKey, copy }: { pluginKey: string;
|
|
|
63
63
|
<form action={action} className="flex flex-col gap-2">
|
|
64
64
|
<FormError message={state.error} />
|
|
65
65
|
<input type="hidden" name="key" value={pluginKey} />
|
|
66
|
-
<
|
|
67
|
-
|
|
66
|
+
<PendingButton
|
|
67
|
+
showWorking
|
|
68
68
|
className="inline-flex h-9 items-center justify-center rounded-md border border-border px-3 text-sm"
|
|
69
69
|
>
|
|
70
70
|
{fromCopy(copy, 'adminPanel.plugin.clearFailures')}
|
|
71
|
-
</
|
|
71
|
+
</PendingButton>
|
|
72
72
|
</form>
|
|
73
73
|
)
|
|
74
74
|
}
|
|
@@ -2,12 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { useActionState } from 'react'
|
|
4
4
|
|
|
5
|
+
import { cn } from '@meith/ui'
|
|
6
|
+
|
|
5
7
|
import { saveAdminSettingsAction } from '@/server/admin-settings-actions'
|
|
6
8
|
import { EMPTY_STATE } from '@/server/auth-form-state'
|
|
7
9
|
import type { SettingFieldModel, SettingGroupModel } from '@/view/admin-settings'
|
|
8
10
|
|
|
9
11
|
import { FormError, SubmitButton } from '../auth/form-controls'
|
|
10
12
|
import { type Copy, fromCopy } from '../shell/copy'
|
|
13
|
+
import { PANEL_CARD } from '../shell/panel-list'
|
|
11
14
|
import { INPUT, Saved } from './form-bits'
|
|
12
15
|
|
|
13
16
|
function Control({ setting, copy }: { setting: SettingFieldModel; copy: Copy }) {
|
|
@@ -105,8 +108,8 @@ export function AdminSettingsForm({
|
|
|
105
108
|
<input type="hidden" name="keys" value={keys.join(',')} />
|
|
106
109
|
|
|
107
110
|
{groups.map((group) => (
|
|
108
|
-
<section key={group.group} className=
|
|
109
|
-
<h2 className="text-lg font-semibold
|
|
111
|
+
<section key={group.group} className={cn(PANEL_CARD, 'gap-5')}>
|
|
112
|
+
<h2 className="font-heading text-lg font-semibold">{group.label}</h2>
|
|
110
113
|
<div className="flex flex-col gap-5">
|
|
111
114
|
{group.settings.map((setting) => (
|
|
112
115
|
<div key={setting.key} className="flex flex-col gap-1">
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
} from '@/view/theme-draft'
|
|
30
30
|
import { BRAND_PRESETS, groupTokens } from '@/view/theme-tokens'
|
|
31
31
|
|
|
32
|
-
import { FormError, SubmitButton } from '../auth/form-controls'
|
|
32
|
+
import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
|
|
33
33
|
import { type Copy, formatFromCopy, fromCopy } from '../shell/copy'
|
|
34
34
|
import { Saved } from './form-bits'
|
|
35
35
|
import { OklchPicker } from './oklch-picker'
|
|
@@ -473,14 +473,13 @@ export function ThemeEditorForm({
|
|
|
473
473
|
: formatFromCopy(copy, 'adminTheme.saveChanges', { count: unsaved })}
|
|
474
474
|
</SubmitButton>
|
|
475
475
|
</span>
|
|
476
|
-
<
|
|
477
|
-
type="submit"
|
|
476
|
+
<PendingButton
|
|
478
477
|
name="intent"
|
|
479
478
|
value="preview"
|
|
480
479
|
className="inline-flex h-10 items-center justify-center rounded-md border border-border px-4 text-sm"
|
|
481
480
|
>
|
|
482
481
|
{fromCopy(copy, 'adminTheme.previewWithoutSaving')}
|
|
483
|
-
</
|
|
482
|
+
</PendingButton>
|
|
484
483
|
<span className="text-xs text-muted-foreground">
|
|
485
484
|
{unsaved === 0
|
|
486
485
|
? fromCopy(copy, 'adminTheme.nothingToSave')
|
|
@@ -593,13 +592,12 @@ export function ThemeStateForms({
|
|
|
593
592
|
{!isDefault && enabled && (
|
|
594
593
|
<form action={defaultAction}>
|
|
595
594
|
<input type="hidden" name="key" value={themeKey} />
|
|
596
|
-
<
|
|
597
|
-
type="submit"
|
|
595
|
+
<PendingButton
|
|
598
596
|
aria-label={formatFromCopy(copy, 'adminTheme.state.makeDefaultFor', { title })}
|
|
599
597
|
className={GHOST_BUTTON}
|
|
600
598
|
>
|
|
601
599
|
{fromCopy(copy, 'adminTheme.state.makeDefault')}
|
|
602
|
-
</
|
|
600
|
+
</PendingButton>
|
|
603
601
|
</form>
|
|
604
602
|
)}
|
|
605
603
|
|
|
@@ -607,8 +605,7 @@ export function ThemeStateForms({
|
|
|
607
605
|
<form action={enabledAction}>
|
|
608
606
|
<input type="hidden" name="key" value={themeKey} />
|
|
609
607
|
<input type="hidden" name="enabled" value={enabled ? 'false' : 'true'} />
|
|
610
|
-
<
|
|
611
|
-
type="submit"
|
|
608
|
+
<PendingButton
|
|
612
609
|
aria-label={formatFromCopy(
|
|
613
610
|
copy,
|
|
614
611
|
enabled ? 'adminTheme.state.turnOffFor' : 'adminTheme.state.turnOnFor',
|
|
@@ -619,7 +616,7 @@ export function ThemeStateForms({
|
|
|
619
616
|
{enabled
|
|
620
617
|
? fromCopy(copy, 'adminTheme.state.turnOff')
|
|
621
618
|
: fromCopy(copy, 'adminTheme.state.turnOn')}
|
|
622
|
-
</
|
|
619
|
+
</PendingButton>
|
|
623
620
|
</form>
|
|
624
621
|
)}
|
|
625
622
|
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useEffect, useRef } from 'react'
|
|
4
4
|
import { useFormStatus } from 'react-dom'
|
|
5
5
|
|
|
6
|
-
import { Alert, AlertDescription, AlertTitle, Input, Field as UiField } from '@meith/ui'
|
|
6
|
+
import { Alert, AlertDescription, AlertTitle, cn, Input, Field as UiField } from '@meith/ui'
|
|
7
7
|
import { Button } from '@meith/ui/button'
|
|
8
8
|
|
|
9
9
|
import { fromCopy, useCopy } from '../shell/copy'
|
|
@@ -31,6 +31,28 @@ export function SubmitButton({
|
|
|
31
31
|
)
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
export function PendingButton({
|
|
35
|
+
children,
|
|
36
|
+
showWorking = false,
|
|
37
|
+
className,
|
|
38
|
+
...props
|
|
39
|
+
}: React.ComponentProps<'button'> & { showWorking?: boolean }) {
|
|
40
|
+
const { pending } = useFormStatus()
|
|
41
|
+
const copy = useCopy()
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<button
|
|
45
|
+
{...props}
|
|
46
|
+
type="submit"
|
|
47
|
+
disabled={pending || props.disabled}
|
|
48
|
+
aria-busy={pending || undefined}
|
|
49
|
+
className={cn(className, 'disabled:cursor-not-allowed disabled:opacity-60')}
|
|
50
|
+
>
|
|
51
|
+
{pending && showWorking ? fromCopy(copy, 'form.working') : children}
|
|
52
|
+
</button>
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
34
56
|
export function useFocusOnFail<T extends HTMLElement>(failed: boolean) {
|
|
35
57
|
const { pending } = useFormStatus()
|
|
36
58
|
const ref = useRef<T>(null)
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { useActionState } from 'react'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { buttonVariants } from '@meith/ui'
|
|
6
6
|
|
|
7
7
|
import { abandonSecondFactorAction, verifySecondFactorAction } from '@/server/auth-actions'
|
|
8
8
|
import { EMPTY_STATE } from '@/server/auth-form-state'
|
|
9
9
|
|
|
10
10
|
import { type Copy, fromCopy } from '../shell/copy'
|
|
11
|
-
import { FormError, SubmitButton } from './form-controls'
|
|
11
|
+
import { FormError, PendingButton, SubmitButton } from './form-controls'
|
|
12
12
|
import { OtpField, otpRecoveryFromCopy } from './otp-field'
|
|
13
13
|
|
|
14
14
|
export function SecondFactorForm({
|
|
@@ -43,9 +43,9 @@ export function SecondFactorForm({
|
|
|
43
43
|
</form>
|
|
44
44
|
|
|
45
45
|
<form action={abandonSecondFactorAction}>
|
|
46
|
-
<
|
|
46
|
+
<PendingButton showWorking className={buttonVariants({ variant: 'ghost', size: 'sm' })}>
|
|
47
47
|
{fromCopy(copy, 'authForm.secondFactor.cancel')}
|
|
48
|
-
</
|
|
48
|
+
</PendingButton>
|
|
49
49
|
</form>
|
|
50
50
|
</div>
|
|
51
51
|
)
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import { buttonVariants } from '@meith/ui'
|
|
2
2
|
|
|
3
|
+
import { PendingButton } from '../auth/form-controls'
|
|
3
4
|
import { fromCopy, useCopy } from '../shell/copy'
|
|
4
5
|
|
|
5
6
|
export function ComposerIntents() {
|
|
6
7
|
const copy = useCopy()
|
|
7
8
|
return (
|
|
8
9
|
<>
|
|
9
|
-
<
|
|
10
|
-
type="submit"
|
|
10
|
+
<PendingButton
|
|
11
11
|
name="intent"
|
|
12
12
|
value="preview"
|
|
13
|
+
showWorking
|
|
13
14
|
className={buttonVariants({ variant: 'outline' })}
|
|
14
15
|
>
|
|
15
16
|
{fromCopy(copy, 'composer.preview')}
|
|
16
|
-
</
|
|
17
|
-
<
|
|
18
|
-
type="submit"
|
|
17
|
+
</PendingButton>
|
|
18
|
+
<PendingButton
|
|
19
19
|
name="intent"
|
|
20
20
|
value="save_draft"
|
|
21
|
+
showWorking
|
|
21
22
|
className={buttonVariants({ variant: 'ghost' })}
|
|
22
23
|
>
|
|
23
24
|
{fromCopy(copy, 'composer.saveDraft')}
|
|
24
|
-
</
|
|
25
|
+
</PendingButton>
|
|
25
26
|
</>
|
|
26
27
|
)
|
|
27
28
|
}
|
|
@@ -6,7 +6,7 @@ import { useActionState } from 'react'
|
|
|
6
6
|
import { EMPTY_STATE } from '@/server/auth-form-state'
|
|
7
7
|
import { deletePostAction, editPostAction, restorePostAction } from '@/server/content-actions'
|
|
8
8
|
|
|
9
|
-
import { FormError, SubmitButton } from '../auth/form-controls'
|
|
9
|
+
import { FormError, PendingButton, SubmitButton } from '../auth/form-controls'
|
|
10
10
|
import { type Copy, fromCopy } from '../shell/copy'
|
|
11
11
|
import { MarkdownEditor } from './markdown-editor'
|
|
12
12
|
|
|
@@ -53,14 +53,14 @@ export function EditPostForm({
|
|
|
53
53
|
|
|
54
54
|
<div className="flex flex-wrap gap-3">
|
|
55
55
|
<SubmitButton>{fromCopy(copy, 'composer.edit.submit')}</SubmitButton>
|
|
56
|
-
<
|
|
57
|
-
type="submit"
|
|
56
|
+
<PendingButton
|
|
58
57
|
name="intent"
|
|
59
58
|
value="preview"
|
|
59
|
+
showWorking
|
|
60
60
|
className="inline-flex h-10 items-center justify-center rounded-md border border-border px-4 text-sm font-medium transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
61
61
|
>
|
|
62
62
|
{fromCopy(copy, 'composer.preview')}
|
|
63
|
-
</
|
|
63
|
+
</PendingButton>
|
|
64
64
|
</div>
|
|
65
65
|
</form>
|
|
66
66
|
)
|
|
@@ -84,12 +84,12 @@ export function DeletePostForm({
|
|
|
84
84
|
<input type="hidden" name="postId" value={postId} />
|
|
85
85
|
<p className="text-sm text-muted-foreground">{fromCopy(copy, 'composer.edit.deleteBlurb')}</p>
|
|
86
86
|
<div>
|
|
87
|
-
<
|
|
88
|
-
|
|
87
|
+
<PendingButton
|
|
88
|
+
showWorking
|
|
89
89
|
className="inline-flex h-10 items-center justify-center rounded-md border border-destructive/40 px-4 text-sm font-medium text-destructive transition-opacity hover:opacity-90 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring"
|
|
90
90
|
>
|
|
91
91
|
{fromCopy(copy, 'composer.edit.delete')}
|
|
92
|
-
</
|
|
92
|
+
</PendingButton>
|
|
93
93
|
</div>
|
|
94
94
|
</form>
|
|
95
95
|
)
|
|
@@ -3,6 +3,8 @@ import { POLL_OPTION_LENGTH_MAX, POLL_QUESTION_MAX, type Poll } from '@meith/pol
|
|
|
3
3
|
import { getTranslator } from '@/server/i18n'
|
|
4
4
|
import { editPollAction } from '@/server/poll-actions'
|
|
5
5
|
|
|
6
|
+
import { PendingButton } from '../auth/form-controls'
|
|
7
|
+
|
|
6
8
|
const NEW_OPTION_SLOTS = ['first', 'second', 'third'] as const
|
|
7
9
|
|
|
8
10
|
function inputValue(at: Date | null): string {
|
|
@@ -89,7 +91,7 @@ export async function PollEditForm({ poll, threadId }: { poll: Poll; threadId: n
|
|
|
89
91
|
</label>
|
|
90
92
|
|
|
91
93
|
<div>
|
|
92
|
-
<
|
|
94
|
+
<PendingButton showWorking>{t.t('pollEdit.save')}</PendingButton>
|
|
93
95
|
</div>
|
|
94
96
|
</form>
|
|
95
97
|
)
|
|
@@ -3,6 +3,8 @@ import { POLL_CHOICES_UNLIMITED, type Poll } from '@meith/polls'
|
|
|
3
3
|
import { getTranslator } from '@/server/i18n'
|
|
4
4
|
import { votePollAction } from '@/server/poll-actions'
|
|
5
5
|
|
|
6
|
+
import { PendingButton } from '../auth/form-controls'
|
|
7
|
+
|
|
6
8
|
export async function PollForm({
|
|
7
9
|
poll,
|
|
8
10
|
threadId,
|
|
@@ -69,9 +71,9 @@ export async function PollForm({
|
|
|
69
71
|
</div>
|
|
70
72
|
))}
|
|
71
73
|
{mayCast ? (
|
|
72
|
-
<
|
|
74
|
+
<PendingButton showWorking>
|
|
73
75
|
{hasVoted ? t.t('pollForm.changeVote') : t.t('pollForm.vote')}
|
|
74
|
-
</
|
|
76
|
+
</PendingButton>
|
|
75
77
|
) : (
|
|
76
78
|
<p className="text-sm text-muted-foreground">
|
|
77
79
|
{closed ? t.t('pollForm.closed') : t.t('pollForm.votes', { count: total })}
|
|
@@ -7,6 +7,7 @@ import { cn } from '@meith/ui'
|
|
|
7
7
|
import { EMPTY_STATE } from '@/server/auth-form-state'
|
|
8
8
|
import { thankForPostAction } from '@/server/reputation-actions'
|
|
9
9
|
|
|
10
|
+
import { PendingButton } from '../auth/form-controls'
|
|
10
11
|
import { formatFromCopy, fromCopy, useCopy } from '../shell/copy'
|
|
11
12
|
|
|
12
13
|
export function ThanksButton({
|
|
@@ -31,8 +32,7 @@ export function ThanksButton({
|
|
|
31
32
|
<input type="hidden" name="userId" value={authorUserId} />
|
|
32
33
|
<input type="hidden" name="returnTo" value={returnTo} />
|
|
33
34
|
|
|
34
|
-
<
|
|
35
|
-
type="submit"
|
|
35
|
+
<PendingButton
|
|
36
36
|
aria-pressed={thanked}
|
|
37
37
|
className={cn(
|
|
38
38
|
'inline-flex h-8 items-center gap-1.5 rounded-md px-2.5 text-sm font-medium transition-colors',
|
|
@@ -55,7 +55,7 @@ export function ThanksButton({
|
|
|
55
55
|
</span>
|
|
56
56
|
</span>
|
|
57
57
|
)}
|
|
58
|
-
</
|
|
58
|
+
</PendingButton>
|
|
59
59
|
|
|
60
60
|
{state.error !== undefined && (
|
|
61
61
|
<span className="ml-2 text-xs text-destructive">{state.error}</span>
|
|
@@ -3,6 +3,7 @@ import { cn } from '@meith/ui'
|
|
|
3
3
|
|
|
4
4
|
import { rateThreadAction } from '@/server/thread-rating-actions'
|
|
5
5
|
|
|
6
|
+
import { PendingButton } from '../auth/form-controls'
|
|
6
7
|
import { type Copy, formatFromCopy, fromCopy } from '../shell/copy-record'
|
|
7
8
|
|
|
8
9
|
const STAR_PATH = 'M10 1.6l2.47 5.01 5.53.8-4 3.9.94 5.5L10 14.21l-4.94 2.6.94-5.5-4-3.9 5.53-.8z'
|
|
@@ -109,9 +110,8 @@ export function ThreadRatingForm({
|
|
|
109
110
|
const chosen = rating.mine !== null && value <= rating.mine
|
|
110
111
|
|
|
111
112
|
return (
|
|
112
|
-
<
|
|
113
|
+
<PendingButton
|
|
113
114
|
key={value}
|
|
114
|
-
type="submit"
|
|
115
115
|
name="rating"
|
|
116
116
|
value={value}
|
|
117
117
|
aria-label={
|
|
@@ -126,7 +126,7 @@ export function ThreadRatingForm({
|
|
|
126
126
|
)}
|
|
127
127
|
>
|
|
128
128
|
<Star fill={chosen ? 1 : 0} className="size-5" />
|
|
129
|
-
</
|
|
129
|
+
</PendingButton>
|
|
130
130
|
)
|
|
131
131
|
})}
|
|
132
132
|
</span>
|