@spree/seller-dashboard 1.0.0-beta.1
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/LICENSE +21 -0
- package/README.md +31 -0
- package/dist/vite-plugin/index.js +51 -0
- package/package.json +64 -0
- package/src/api-client.ts +203 -0
- package/src/components/centered-message.tsx +8 -0
- package/src/components/finance/balance-summary.tsx +69 -0
- package/src/components/onboarding-nav-badge.tsx +38 -0
- package/src/components/orders/fulfillment-deliveries.tsx +116 -0
- package/src/components/orders/fulfillment-delivery-dialog.tsx +82 -0
- package/src/components/orders/fulfillment-edit-dialog.tsx +95 -0
- package/src/components/orders/fulfillment-fulfill-form.tsx +132 -0
- package/src/components/orders/fulfillment-label-upload-dialog.tsx +95 -0
- package/src/components/orders/fulfillment-split-dialog.tsx +142 -0
- package/src/components/orders/fulfillments-card.tsx +289 -0
- package/src/components/orders/line-label.ts +28 -0
- package/src/components/orders/order-cancel-dialog.tsx +74 -0
- package/src/components/orders/order-cards.tsx +297 -0
- package/src/components/orders/order-earnings-card.tsx +99 -0
- package/src/components/orders/order-notes-cards.tsx +83 -0
- package/src/components/orders/order-payment-card.tsx +94 -0
- package/src/components/orders/post-sale-cards.tsx +347 -0
- package/src/components/orders/post-sale-create-dialogs.tsx +201 -0
- package/src/components/orders/returns-card.tsx +258 -0
- package/src/components/orders/shipping-label-row.tsx +124 -0
- package/src/components/product-status-card.tsx +111 -0
- package/src/components/read-row.tsx +16 -0
- package/src/components/retryable-error.tsx +23 -0
- package/src/components/seller-address-card.tsx +123 -0
- package/src/components/seller-business-card.tsx +152 -0
- package/src/components/seller-returns-location-card.tsx +151 -0
- package/src/components/seller-switcher.tsx +124 -0
- package/src/components/seller-tax-identifiers-card.tsx +63 -0
- package/src/create-router.ts +32 -0
- package/src/hooks/use-fulfillments.ts +155 -0
- package/src/hooks/use-ledger.ts +46 -0
- package/src/hooks/use-order.ts +62 -0
- package/src/hooks/use-post-sale.ts +202 -0
- package/src/hooks/use-reasons.ts +34 -0
- package/src/hooks/use-stock-locations.ts +23 -0
- package/src/hooks/use-store-timezone.ts +26 -0
- package/src/i18n.ts +26 -0
- package/src/index.ts +27 -0
- package/src/lib/packing-slip.ts +109 -0
- package/src/locales/en.json +841 -0
- package/src/nav/default.ts +108 -0
- package/src/nav/settings.ts +114 -0
- package/src/pages/accept-invitation.tsx +127 -0
- package/src/pages/delivery-methods.tsx +647 -0
- package/src/pages/earnings.tsx +44 -0
- package/src/pages/forgot-password.tsx +100 -0
- package/src/pages/home.tsx +132 -0
- package/src/pages/login.tsx +94 -0
- package/src/pages/onboarding.tsx +539 -0
- package/src/pages/order.tsx +119 -0
- package/src/pages/orders.tsx +33 -0
- package/src/pages/payout.tsx +154 -0
- package/src/pages/payouts.tsx +32 -0
- package/src/pages/policies.tsx +292 -0
- package/src/pages/product.tsx +250 -0
- package/src/pages/products.tsx +228 -0
- package/src/pages/profile.tsx +389 -0
- package/src/pages/reset-password.tsx +105 -0
- package/src/pages/seller-picker.tsx +46 -0
- package/src/pages/team.tsx +468 -0
- package/src/panel-chrome.tsx +36 -0
- package/src/routeTree.gen.ts +590 -0
- package/src/routes/__root.tsx +15 -0
- package/src/routes/_authenticated/$sellerId/earnings/index.tsx +15 -0
- package/src/routes/_authenticated/$sellerId/index.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/onboarding.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/orders/$orderId.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/orders/index.tsx +15 -0
- package/src/routes/_authenticated/$sellerId/payouts/$payoutId.tsx +7 -0
- package/src/routes/_authenticated/$sellerId/payouts/index.tsx +15 -0
- package/src/routes/_authenticated/$sellerId/products/$productId.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/products/index.tsx +23 -0
- package/src/routes/_authenticated/$sellerId/products/new.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/profile.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/settings/delivery-methods.tsx +14 -0
- package/src/routes/_authenticated/$sellerId/settings/index.tsx +18 -0
- package/src/routes/_authenticated/$sellerId/settings/package-types.tsx +21 -0
- package/src/routes/_authenticated/$sellerId/settings/policies.tsx +14 -0
- package/src/routes/_authenticated/$sellerId/settings/stock-locations.tsx +22 -0
- package/src/routes/_authenticated/$sellerId/settings/team.tsx +6 -0
- package/src/routes/_authenticated/$sellerId/settings.tsx +18 -0
- package/src/routes/_authenticated/$sellerId.tsx +49 -0
- package/src/routes/_authenticated/index.tsx +48 -0
- package/src/routes/_authenticated.tsx +13 -0
- package/src/routes/accept-invitation.$invitationId.tsx +19 -0
- package/src/routes/forgot-password.tsx +15 -0
- package/src/routes/login.tsx +15 -0
- package/src/routes/reset-password.tsx +21 -0
- package/src/schemas/fulfillment.ts +31 -0
- package/src/seller-dashboard.tsx +50 -0
- package/src/styles.css +18 -0
- package/src/tables/delivery-methods.tsx +68 -0
- package/src/tables/orders.tsx +116 -0
- package/src/tables/payouts.tsx +87 -0
- package/src/tables/policies.tsx +37 -0
- package/src/tables/products.tsx +56 -0
- package/src/tables/transfers.tsx +118 -0
- package/src/vite/index.ts +87 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { hasVisibleSettingsEntries, i18n, nav } from '@spree/dashboard-core'
|
|
2
|
+
import {
|
|
3
|
+
BanknoteIcon,
|
|
4
|
+
ClipboardCheckIcon,
|
|
5
|
+
HandCoinsIcon,
|
|
6
|
+
HomeIcon,
|
|
7
|
+
PackageIcon,
|
|
8
|
+
SettingsIcon,
|
|
9
|
+
StoreIcon,
|
|
10
|
+
TagIcon,
|
|
11
|
+
} from '@spree/dashboard-ui/icons'
|
|
12
|
+
import { OnboardingNavBadge } from '../components/onboarding-nav-badge'
|
|
13
|
+
|
|
14
|
+
// The panel's built-in sidebar. Registered into the same registry a plugin
|
|
15
|
+
// writes to, so a marketplace can remove, reorder or add beside these with
|
|
16
|
+
// `defineDashboardPlugin({ nav: ... })` instead of forking the chrome.
|
|
17
|
+
//
|
|
18
|
+
// Positions leave room on either side (100/200/…) exactly as the operator's
|
|
19
|
+
// dashboard does, so a plugin slots between built-ins without renumbering.
|
|
20
|
+
//
|
|
21
|
+
// Subjects are the seller-branch CanCanCan subjects the API's `/me`
|
|
22
|
+
// serializes; an entry a member cannot act on is hidden by the same
|
|
23
|
+
// permission filter the operator's sidebar uses.
|
|
24
|
+
|
|
25
|
+
nav.add({
|
|
26
|
+
key: 'home',
|
|
27
|
+
label: i18n.t('nav.home'),
|
|
28
|
+
path: '/',
|
|
29
|
+
icon: HomeIcon,
|
|
30
|
+
position: 50,
|
|
31
|
+
subject: 'seller_profile',
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// High in the rail while a seller is still being admitted: until they are
|
|
35
|
+
// approved it is the only thing that matters. A marketplace that admits
|
|
36
|
+
// sellers some other way removes it like any other entry.
|
|
37
|
+
nav.add({
|
|
38
|
+
key: 'onboarding',
|
|
39
|
+
label: i18n.t('nav.onboarding'),
|
|
40
|
+
path: '/onboarding',
|
|
41
|
+
icon: ClipboardCheckIcon,
|
|
42
|
+
position: 100,
|
|
43
|
+
subject: 'seller_profile',
|
|
44
|
+
badge: OnboardingNavBadge,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
nav.add({
|
|
48
|
+
key: 'products',
|
|
49
|
+
label: i18n.t('nav.products'),
|
|
50
|
+
path: '/products',
|
|
51
|
+
icon: TagIcon,
|
|
52
|
+
position: 110,
|
|
53
|
+
subject: 'Spree::Product',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
nav.add({
|
|
57
|
+
key: 'orders',
|
|
58
|
+
label: i18n.t('nav.orders'),
|
|
59
|
+
path: '/orders',
|
|
60
|
+
icon: PackageIcon,
|
|
61
|
+
position: 120,
|
|
62
|
+
subject: 'Spree::Order',
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
// The seller's own books. Two entries rather than one because they answer
|
|
66
|
+
// different questions: what an order earned, and whether last week's
|
|
67
|
+
// settlement went out. Both gated on the ledger key, so a teammate given
|
|
68
|
+
// orders but not money sees neither.
|
|
69
|
+
nav.add({
|
|
70
|
+
key: 'earnings',
|
|
71
|
+
label: i18n.t('nav.earnings'),
|
|
72
|
+
path: '/earnings',
|
|
73
|
+
icon: BanknoteIcon,
|
|
74
|
+
position: 130,
|
|
75
|
+
subject: 'seller_earnings',
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
nav.add({
|
|
79
|
+
key: 'payouts',
|
|
80
|
+
label: i18n.t('nav.payouts'),
|
|
81
|
+
path: '/payouts',
|
|
82
|
+
icon: HandCoinsIcon,
|
|
83
|
+
position: 140,
|
|
84
|
+
subject: 'seller_earnings',
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
nav.add({
|
|
88
|
+
key: 'profile',
|
|
89
|
+
label: i18n.t('nav.profile'),
|
|
90
|
+
path: '/profile',
|
|
91
|
+
icon: StoreIcon,
|
|
92
|
+
position: 150,
|
|
93
|
+
subject: 'seller_profile',
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Settings opens the landing page (and the secondary rail on wide viewports).
|
|
97
|
+
// The entries inside it live in `nav/settings.ts`. Bottom section and gated
|
|
98
|
+
// on there being something to see, exactly as the operator's dashboard does
|
|
99
|
+
// it — a member with no settings authority never opens an empty shell.
|
|
100
|
+
nav.add({
|
|
101
|
+
key: 'settings',
|
|
102
|
+
label: i18n.t('nav.settings'),
|
|
103
|
+
path: '/settings',
|
|
104
|
+
icon: SettingsIcon,
|
|
105
|
+
if: ({ permissions }) => hasVisibleSettingsEntries(permissions),
|
|
106
|
+
section: 'bottom',
|
|
107
|
+
position: 100,
|
|
108
|
+
})
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { i18n, settingsNav } from '@spree/dashboard-core'
|
|
2
|
+
import {
|
|
3
|
+
PackageIcon,
|
|
4
|
+
ScrollTextIcon,
|
|
5
|
+
TruckIcon,
|
|
6
|
+
UsersRoundIcon,
|
|
7
|
+
WarehouseIcon,
|
|
8
|
+
} from '@spree/dashboard-ui/icons'
|
|
9
|
+
|
|
10
|
+
// The panel's settings rail, registered into the same registry the operator's
|
|
11
|
+
// dashboard writes to — so a marketplace adds, removes or reorders entries
|
|
12
|
+
// with `defineDashboardPlugin` rather than forking the chrome.
|
|
13
|
+
//
|
|
14
|
+
// Deliberately short. Most of what an operator configures is the
|
|
15
|
+
// marketplace's, not a seller's; what belongs here is what a seller owns and
|
|
16
|
+
// only they can answer.
|
|
17
|
+
|
|
18
|
+
settingsNav.addGroup({
|
|
19
|
+
key: 'fulfillment',
|
|
20
|
+
labelKey: 'admin.settings_nav.groups.fulfillment',
|
|
21
|
+
position: 200,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
settingsNav.addGroup({
|
|
25
|
+
key: 'legal',
|
|
26
|
+
label: i18n.t('nav.legal'),
|
|
27
|
+
position: 250,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
settingsNav.addGroup({
|
|
31
|
+
key: 'team',
|
|
32
|
+
labelKey: 'admin.settings_nav.groups.team',
|
|
33
|
+
position: 300,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
settingsNav.add({
|
|
37
|
+
key: 'settings.stock-locations',
|
|
38
|
+
labelKey: 'admin.settings_nav.items.stock_locations',
|
|
39
|
+
descriptionKey: 'admin.settings_nav.descriptions.stock_locations',
|
|
40
|
+
keywords: ['warehouse', 'inventory', 'fulfillment', 'pickup'],
|
|
41
|
+
path: '/stock-locations',
|
|
42
|
+
icon: WarehouseIcon,
|
|
43
|
+
group: 'fulfillment',
|
|
44
|
+
position: 100,
|
|
45
|
+
// The seller-branch subject the API's `/me` serializes. A member without
|
|
46
|
+
// stock permission never sees the entry.
|
|
47
|
+
subject: 'Spree::StockLocation',
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// How this seller's goods ship. Beside stock locations because the two are
|
|
51
|
+
// one question asked twice — where the goods are, and how they leave.
|
|
52
|
+
settingsNav.add({
|
|
53
|
+
key: 'settings.delivery-methods',
|
|
54
|
+
label: i18n.t('nav.delivery_methods'),
|
|
55
|
+
descriptionKey: 'admin.settings_nav.descriptions.delivery_methods',
|
|
56
|
+
keywords: ['shipping', 'rates', 'carriers', 'delivery'],
|
|
57
|
+
path: '/delivery-methods',
|
|
58
|
+
icon: TruckIcon,
|
|
59
|
+
group: 'fulfillment',
|
|
60
|
+
position: 200,
|
|
61
|
+
// The seller-branch subject the API's `/me` serializes. A member without
|
|
62
|
+
// delivery permission never sees the entry.
|
|
63
|
+
subject: 'Spree::DeliveryMethod',
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// What this seller packs their goods into. Beside the other two because the
|
|
67
|
+
// three answer one question together: where the goods are, how they leave,
|
|
68
|
+
// and what they leave in (docs/plans/6.0-seller-package-types.md).
|
|
69
|
+
settingsNav.add({
|
|
70
|
+
key: 'settings.package-types',
|
|
71
|
+
label: i18n.t('nav.package_types'),
|
|
72
|
+
descriptionKey: 'admin.settings_nav.descriptions.package_types',
|
|
73
|
+
keywords: ['box', 'carton', 'pallet', 'container', 'packaging', 'parcel'],
|
|
74
|
+
path: '/package-types',
|
|
75
|
+
icon: PackageIcon,
|
|
76
|
+
group: 'fulfillment',
|
|
77
|
+
position: 300,
|
|
78
|
+
// The seller-branch subject the API's `/me` serializes. A member without
|
|
79
|
+
// packaging permission never sees the entry.
|
|
80
|
+
subject: 'Spree::PackageType',
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
// Who runs this seller. Under settings rather than in the main rail, matching
|
|
84
|
+
// where the operator's dashboard puts its own staff page: managing people is
|
|
85
|
+
// configuration, not day-to-day selling.
|
|
86
|
+
settingsNav.add({
|
|
87
|
+
key: 'settings.team',
|
|
88
|
+
label: i18n.t('nav.team'),
|
|
89
|
+
descriptionKey: 'admin.settings_nav.descriptions.team',
|
|
90
|
+
keywords: ['users', 'staff', 'invite', 'members'],
|
|
91
|
+
path: '/team',
|
|
92
|
+
icon: UsersRoundIcon,
|
|
93
|
+
group: 'team',
|
|
94
|
+
position: 100,
|
|
95
|
+
subject: 'seller_profile',
|
|
96
|
+
action: 'update',
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// The seller's own legal documents. Under settings because publishing them is
|
|
100
|
+
// configuration a seller does once, not day-to-day selling — and because what
|
|
101
|
+
// they must publish is the marketplace's onboarding checklist, which links
|
|
102
|
+
// here.
|
|
103
|
+
settingsNav.add({
|
|
104
|
+
key: 'settings.policies',
|
|
105
|
+
label: i18n.t('nav.policies'),
|
|
106
|
+
descriptionKey: 'admin.settings_nav.descriptions.policies',
|
|
107
|
+
keywords: ['legal', 'terms', 'privacy', 'returns', 'refund'],
|
|
108
|
+
path: '/policies',
|
|
109
|
+
icon: ScrollTextIcon,
|
|
110
|
+
group: 'legal',
|
|
111
|
+
position: 100,
|
|
112
|
+
subject: 'seller_profile',
|
|
113
|
+
action: 'update',
|
|
114
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { useAuth } from '@spree/dashboard-core'
|
|
2
|
+
import { Button, Field, FieldError, FieldGroup, FieldLabel, Input } from '@spree/dashboard-ui'
|
|
3
|
+
import { useQuery } from '@tanstack/react-query'
|
|
4
|
+
import { useNavigate } from '@tanstack/react-router'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
import { useForm } from 'react-hook-form'
|
|
7
|
+
import { useTranslation } from 'react-i18next'
|
|
8
|
+
import { sellerClient } from '../api-client'
|
|
9
|
+
import { CenteredMessage } from '../components/centered-message'
|
|
10
|
+
|
|
11
|
+
interface AcceptValues {
|
|
12
|
+
password: string
|
|
13
|
+
password_confirmation: string
|
|
14
|
+
first_name: string
|
|
15
|
+
last_name: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Joining a seller's team from the emailed link.
|
|
20
|
+
*
|
|
21
|
+
* The invitation id and token in the URL are the whole credential — nobody is
|
|
22
|
+
* signed in yet. Accepting returns a seller session, so the new member lands
|
|
23
|
+
* in the panel rather than being bounced to a login form.
|
|
24
|
+
*/
|
|
25
|
+
export function AcceptInvitationPage({
|
|
26
|
+
invitationId,
|
|
27
|
+
token,
|
|
28
|
+
}: {
|
|
29
|
+
invitationId: string
|
|
30
|
+
token?: string
|
|
31
|
+
}) {
|
|
32
|
+
const { t } = useTranslation()
|
|
33
|
+
const { acceptInvitation } = useAuth()
|
|
34
|
+
const navigate = useNavigate()
|
|
35
|
+
const [error, setError] = useState<string | null>(null)
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
data: invitation,
|
|
39
|
+
isLoading,
|
|
40
|
+
error: lookupError,
|
|
41
|
+
} = useQuery({
|
|
42
|
+
queryKey: ['seller', 'invitation', invitationId],
|
|
43
|
+
queryFn: () => sellerClient().auth.lookupInvitation(invitationId, token ?? ''),
|
|
44
|
+
enabled: Boolean(token),
|
|
45
|
+
retry: false,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const form = useForm<AcceptValues>({
|
|
49
|
+
defaultValues: { password: '', password_confirmation: '', first_name: '', last_name: '' },
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (!token) return <CenteredMessage>{t('accept_invitation.invalid_link')}</CenteredMessage>
|
|
53
|
+
if (isLoading) return <CenteredMessage>{t('common.loading')}</CenteredMessage>
|
|
54
|
+
if (lookupError || !invitation) {
|
|
55
|
+
return <CenteredMessage>{t('accept_invitation.not_found')}</CenteredMessage>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function onSubmit(values: AcceptValues) {
|
|
59
|
+
setError(null)
|
|
60
|
+
try {
|
|
61
|
+
await acceptInvitation(invitationId, token ?? '', values)
|
|
62
|
+
// The index route decides where to go from there — it already knows how
|
|
63
|
+
// to skip the picker for someone who runs exactly one seller. Without
|
|
64
|
+
// this the form simply sits there after a successful accept, and the
|
|
65
|
+
// obvious next move is to submit again, against an invitation that has
|
|
66
|
+
// now been consumed.
|
|
67
|
+
navigate({ to: '/', replace: true })
|
|
68
|
+
} catch {
|
|
69
|
+
setError(t('accept_invitation.failed'))
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const { errors, isSubmitting } = form.formState
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex min-h-screen items-center justify-center p-6">
|
|
77
|
+
<div className="w-full max-w-sm">
|
|
78
|
+
<h1 className="font-medium text-xl">{t('accept_invitation.title')}</h1>
|
|
79
|
+
<p className="mt-1 mb-6 text-muted-foreground text-sm">
|
|
80
|
+
{t('accept_invitation.subtitle', { email: invitation.email })}
|
|
81
|
+
</p>
|
|
82
|
+
|
|
83
|
+
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
84
|
+
<FieldGroup>
|
|
85
|
+
<Field>
|
|
86
|
+
<FieldLabel htmlFor="first_name">{t('accept_invitation.first_name')}</FieldLabel>
|
|
87
|
+
<Input id="first_name" {...form.register('first_name')} />
|
|
88
|
+
</Field>
|
|
89
|
+
|
|
90
|
+
<Field>
|
|
91
|
+
<FieldLabel htmlFor="last_name">{t('accept_invitation.last_name')}</FieldLabel>
|
|
92
|
+
<Input id="last_name" {...form.register('last_name')} />
|
|
93
|
+
</Field>
|
|
94
|
+
|
|
95
|
+
<Field>
|
|
96
|
+
<FieldLabel htmlFor="password">{t('accept_invitation.password')}</FieldLabel>
|
|
97
|
+
<Input
|
|
98
|
+
id="password"
|
|
99
|
+
type="password"
|
|
100
|
+
aria-invalid={!!errors.password || undefined}
|
|
101
|
+
{...form.register('password', { required: true })}
|
|
102
|
+
/>
|
|
103
|
+
<FieldError errors={[errors.password]} />
|
|
104
|
+
</Field>
|
|
105
|
+
|
|
106
|
+
<Field>
|
|
107
|
+
<FieldLabel htmlFor="password_confirmation">
|
|
108
|
+
{t('accept_invitation.password_confirmation')}
|
|
109
|
+
</FieldLabel>
|
|
110
|
+
<Input
|
|
111
|
+
id="password_confirmation"
|
|
112
|
+
type="password"
|
|
113
|
+
{...form.register('password_confirmation')}
|
|
114
|
+
/>
|
|
115
|
+
</Field>
|
|
116
|
+
|
|
117
|
+
{error && <p className="text-destructive text-sm">{error}</p>}
|
|
118
|
+
|
|
119
|
+
<Button type="submit" disabled={isSubmitting}>
|
|
120
|
+
{isSubmitting ? t('accept_invitation.submitting') : t('accept_invitation.submit')}
|
|
121
|
+
</Button>
|
|
122
|
+
</FieldGroup>
|
|
123
|
+
</form>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
)
|
|
127
|
+
}
|