@spree/seller-dashboard 6.0.3 → 6.0.4

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.
Files changed (59) hide show
  1. package/package.json +23 -23
  2. package/src/api-client.ts +2 -0
  3. package/src/components/account-dialog.tsx +1 -1
  4. package/src/components/centered-message.tsx +1 -1
  5. package/src/components/finance/balance-summary.tsx +1 -1
  6. package/src/components/orders/fulfillment-deliveries.tsx +1 -1
  7. package/src/components/orders/fulfillments-card.tsx +30 -20
  8. package/src/components/orders/order-cards.tsx +115 -102
  9. package/src/components/orders/order-payment-card.tsx +2 -3
  10. package/src/components/orders/post-sale-cards.tsx +79 -47
  11. package/src/components/orders/post-sale-create-dialogs.tsx +15 -0
  12. package/src/components/orders/returns-card.tsx +44 -36
  13. package/src/components/orders/shipping-label-row.tsx +1 -1
  14. package/src/components/orders/use-product-link.tsx +10 -0
  15. package/src/components/product-status-card.tsx +2 -2
  16. package/src/components/seller-address-card.tsx +2 -2
  17. package/src/components/seller-returns-location-card.tsx +2 -7
  18. package/src/components/seller-switcher.tsx +1 -1
  19. package/src/hooks/use-fulfillments.ts +33 -22
  20. package/src/hooks/use-profile.ts +38 -0
  21. package/src/locales/ar.json +35 -31
  22. package/src/locales/de.json +35 -31
  23. package/src/locales/en.json +35 -31
  24. package/src/locales/fr.json +35 -31
  25. package/src/locales/ja.json +35 -31
  26. package/src/locales/pl.json +35 -31
  27. package/src/locales/vi.json +35 -31
  28. package/src/locales/zh-CN.json +35 -31
  29. package/src/nav/default.ts +0 -10
  30. package/src/nav/settings.ts +53 -0
  31. package/src/pages/delivery-methods.tsx +47 -49
  32. package/src/pages/forgot-password.tsx +5 -5
  33. package/src/pages/general-settings.tsx +233 -0
  34. package/src/pages/home.tsx +1 -1
  35. package/src/pages/login.tsx +3 -3
  36. package/src/pages/onboarding.tsx +26 -23
  37. package/src/pages/order.tsx +2 -1
  38. package/src/pages/orders.tsx +8 -15
  39. package/src/pages/payout.tsx +7 -4
  40. package/src/pages/payouts.tsx +23 -2
  41. package/src/pages/policies.tsx +12 -8
  42. package/src/pages/product.tsx +18 -4
  43. package/src/pages/products.tsx +0 -4
  44. package/src/pages/profile-settings.tsx +221 -0
  45. package/src/pages/reset-password.tsx +3 -3
  46. package/src/pages/seller-picker.tsx +1 -1
  47. package/src/pages/taxes.tsx +40 -0
  48. package/src/pages/team.tsx +1 -1
  49. package/src/routeTree.gen.ts +63 -0
  50. package/src/routes/_authenticated/$sellerId/profile.tsx +5 -3
  51. package/src/routes/_authenticated/$sellerId/settings/general.tsx +6 -0
  52. package/src/routes/_authenticated/$sellerId/settings/profile.tsx +6 -0
  53. package/src/routes/_authenticated/$sellerId/settings/taxes.tsx +6 -0
  54. package/src/routes/_authenticated/$sellerId/settings.tsx +3 -7
  55. package/src/schemas/account.ts +1 -1
  56. package/src/tables/products.tsx +6 -5
  57. package/src/components/read-row.tsx +0 -16
  58. package/src/components/seller-business-card.tsx +0 -152
  59. package/src/pages/profile.tsx +0 -395
@@ -9,7 +9,6 @@ import {
9
9
  ExportButton,
10
10
  ImportButton,
11
11
  ImportWizardDialog,
12
- PageHeader,
13
12
  ResourceTable,
14
13
  Subject,
15
14
  } from '@spree/dashboard-core'
@@ -149,10 +148,7 @@ export function ProductsPage({ search }: { search: ResourceSearch & { import?: s
149
148
 
150
149
  return (
151
150
  <div className="flex flex-col gap-4">
152
- <PageHeader title={t('products.title')} sticky={false} />
153
-
154
151
  <ResourceTable<Product>
155
- hideHeader
156
152
  tableKey="seller-products"
157
153
  queryKey="seller-products"
158
154
  queryFn={(params) => sellerClient().products.list(params)}
@@ -0,0 +1,221 @@
1
+ import { zodResolver } from '@hookform/resolvers/zod'
2
+ import {
3
+ blankToNull,
4
+ extensionFormValues,
5
+ extensionSubmitValues,
6
+ mapSpreeErrorsToForm,
7
+ PageHeader,
8
+ ResourceImageField,
9
+ SettingsSection,
10
+ Slot,
11
+ sellerFormSchema,
12
+ sellerFormValues,
13
+ signedIdParam,
14
+ } from '@spree/dashboard-core'
15
+ import {
16
+ CardContent,
17
+ Field,
18
+ FieldDescription,
19
+ FieldError,
20
+ FieldGroup,
21
+ FieldLabel,
22
+ FormActions,
23
+ Input,
24
+ ResourceLayout,
25
+ RichTextEditor,
26
+ useFormSubmitShortcut,
27
+ } from '@spree/dashboard-ui'
28
+ import { ImageIcon, StoreIcon } from '@spree/dashboard-ui/icons'
29
+ import type { Profile } from '@spree/seller-sdk'
30
+ import { useState } from 'react'
31
+ import { Controller, FormProvider, useForm } from 'react-hook-form'
32
+ import { useTranslation } from 'react-i18next'
33
+ import type { z } from 'zod/v4'
34
+ import { CenteredMessage } from '../components/centered-message'
35
+ import { useProfile, useUpdateProfile } from '../hooks/use-profile'
36
+
37
+ /**
38
+ * What buyers see of the seller — name, description and images — as one
39
+ * form. Everything else the seller configures lives on the other settings
40
+ * pages.
41
+ */
42
+ export function ProfileSettingsPage() {
43
+ const { t } = useTranslation()
44
+ const { data: profile, isLoading, error } = useProfile()
45
+
46
+ if (isLoading) return <CenteredMessage>{t('common.loading')}</CenteredMessage>
47
+ if (error || !profile) return <CenteredMessage>{t('common.error')}</CenteredMessage>
48
+
49
+ return <ProfileForm profile={profile} />
50
+ }
51
+
52
+ // Only what this page shows. The operator's schema also checks fields edited
53
+ // elsewhere (the emails), and an error on a field that is not on the page
54
+ // would block Save with nothing to see.
55
+ const profileFormSchema = sellerFormSchema.pick({
56
+ name: true,
57
+ about: true,
58
+ logo_signed_id: true,
59
+ logo_preview_url: true,
60
+ logo_cleared: true,
61
+ square_logo_signed_id: true,
62
+ square_logo_preview_url: true,
63
+ square_logo_cleared: true,
64
+ cover_photo_signed_id: true,
65
+ cover_photo_preview_url: true,
66
+ cover_photo_cleared: true,
67
+ })
68
+
69
+ type ProfileFormValues = z.infer<typeof profileFormSchema>
70
+
71
+ // Fields a plugin adds through this page's slots register under this key and
72
+ // save with the page; the API accepts what extensions opened to sellers.
73
+ const FORM_KEY = 'seller.profile'
74
+
75
+ function formValues(profile: Profile) {
76
+ return { ...sellerFormValues(profile), ...extensionFormValues(FORM_KEY, profile) }
77
+ }
78
+
79
+ function ProfileForm({ profile }: { profile: Profile }) {
80
+ const { t } = useTranslation()
81
+ const update = useUpdateProfile()
82
+
83
+ const form = useForm<ProfileFormValues>({
84
+ resolver: zodResolver(profileFormSchema),
85
+ defaultValues: formValues(profile),
86
+ })
87
+
88
+ // An image's signed id only exists once its upload finishes, so saving
89
+ // before then would silently drop the new image.
90
+ const [uploading, setUploading] = useState<ReadonlySet<string>>(new Set())
91
+ const trackUpload = (kind: string) => (active: boolean) =>
92
+ setUploading((current) => {
93
+ const next = new Set(current)
94
+ if (active) next.add(kind)
95
+ else next.delete(kind)
96
+ return next
97
+ })
98
+
99
+ async function onSubmit(values: ProfileFormValues) {
100
+ if (uploading.size > 0) return
101
+
102
+ try {
103
+ const updated = await update.mutateAsync({
104
+ ...extensionSubmitValues(FORM_KEY, form),
105
+ name: values.name,
106
+ about: blankToNull(values.about),
107
+ ...signedIdParam('logo', values.logo_signed_id, values.logo_cleared),
108
+ ...signedIdParam('square_logo', values.square_logo_signed_id, values.square_logo_cleared),
109
+ ...signedIdParam('cover_photo', values.cover_photo_signed_id, values.cover_photo_cleared),
110
+ })
111
+ // Saved images come back as server URLs, so the upload fields start
112
+ // empty again rather than holding the files just sent.
113
+ form.reset(formValues(updated))
114
+ } catch (err) {
115
+ // Anything that is not a field error has already been toasted.
116
+ mapSpreeErrorsToForm(err, form.setError)
117
+ }
118
+ }
119
+
120
+ useFormSubmitShortcut(form, onSubmit)
121
+
122
+ const { errors } = form.formState
123
+
124
+ return (
125
+ <FormProvider {...form}>
126
+ <form onSubmit={form.handleSubmit(onSubmit)}>
127
+ <ResourceLayout
128
+ header={
129
+ <PageHeader
130
+ title={t('nav.profile')}
131
+ description={t('profile.subtitle')}
132
+ sticky={false}
133
+ actions={<FormActions form={form} disabled={uploading.size > 0} />}
134
+ />
135
+ }
136
+ main={
137
+ <>
138
+ {errors.root?.message && (
139
+ <p className="text-base text-destructive" role="alert">
140
+ {errors.root.message}
141
+ </p>
142
+ )}
143
+ <SettingsSection title={t('profile.sections.details')} icon={<StoreIcon />}>
144
+ <CardContent>
145
+ <FieldGroup>
146
+ <Field>
147
+ <FieldLabel htmlFor="seller-name">{t('admin.fields.name.label')}</FieldLabel>
148
+ <Input
149
+ id="seller-name"
150
+ aria-invalid={!!errors.name || undefined}
151
+ {...form.register('name')}
152
+ />
153
+ <FieldError errors={[errors.name]} />
154
+ </Field>
155
+ <Field>
156
+ <FieldLabel htmlFor="seller-about">
157
+ {t('admin.fields.seller.about.label')}
158
+ </FieldLabel>
159
+ <Controller
160
+ control={form.control}
161
+ name="about"
162
+ render={({ field }) => (
163
+ <RichTextEditor
164
+ id="seller-about"
165
+ ariaLabel={t('admin.fields.seller.about.label')}
166
+ value={field.value ?? ''}
167
+ onChange={field.onChange}
168
+ />
169
+ )}
170
+ />
171
+ <FieldDescription>{t('admin.fields.seller.about.help')}</FieldDescription>
172
+ </Field>
173
+ </FieldGroup>
174
+ </CardContent>
175
+ </SettingsSection>
176
+
177
+ <SettingsSection title={t('profile.sections.images')} icon={<ImageIcon />}>
178
+ <CardContent>
179
+ <FieldGroup>
180
+ <ResourceImageField
181
+ form={form}
182
+ kind="logo"
183
+ onUploadingChange={trackUpload('logo')}
184
+ serverUrl={profile.logo_url}
185
+ square
186
+ translationNamespace="admin.sellers"
187
+ labelKey="logo_label"
188
+ helpKey="logo_help"
189
+ />
190
+ <ResourceImageField
191
+ form={form}
192
+ kind="square_logo"
193
+ onUploadingChange={trackUpload('square_logo')}
194
+ serverUrl={profile.square_logo_url}
195
+ square
196
+ translationNamespace="admin.sellers"
197
+ labelKey="square_logo_label"
198
+ helpKey="square_logo_help"
199
+ />
200
+ <ResourceImageField
201
+ form={form}
202
+ kind="cover_photo"
203
+ onUploadingChange={trackUpload('cover_photo')}
204
+ serverUrl={profile.cover_photo_url}
205
+ translationNamespace="admin.sellers"
206
+ labelKey="cover_photo_label"
207
+ helpKey="cover_photo_help"
208
+ />
209
+ </FieldGroup>
210
+ </CardContent>
211
+ </SettingsSection>
212
+
213
+ <Slot name="seller.profile.form_main" context={{ profile }} />
214
+ <Slot name="seller.profile.form_sidebar" context={{ profile }} />
215
+ </>
216
+ }
217
+ />
218
+ </form>
219
+ </FormProvider>
220
+ )
221
+ }
@@ -49,12 +49,12 @@ export function ResetPasswordPage({ token }: { token?: string }) {
49
49
  <div className="flex min-h-screen items-center justify-center p-6">
50
50
  <div className="w-full max-w-sm">
51
51
  <h1 className="font-medium text-xl">{t('reset_password.title')}</h1>
52
- <p className="mt-1 mb-6 text-muted-foreground text-sm">{t('reset_password.subtitle')}</p>
52
+ <p className="mt-1 mb-6 text-muted-foreground text-base">{t('reset_password.subtitle')}</p>
53
53
 
54
54
  <form onSubmit={form.handleSubmit(onSubmit)}>
55
55
  <FieldGroup>
56
56
  {error && (
57
- <p className="text-destructive text-sm" role="alert">
57
+ <p className="text-destructive text-base" role="alert">
58
58
  {error}
59
59
  </p>
60
60
  )}
@@ -94,7 +94,7 @@ export function ResetPasswordPage({ token }: { token?: string }) {
94
94
  {isSubmitting ? t('reset_password.submitting') : t('reset_password.submit')}
95
95
  </Button>
96
96
 
97
- <Link to="/forgot-password" className="text-sm underline underline-offset-4">
97
+ <Link to="/forgot-password" className="text-base underline underline-offset-4">
98
98
  {t('reset_password.request_new_link')}
99
99
  </Link>
100
100
  </FieldGroup>
@@ -30,7 +30,7 @@ export function SellerPicker() {
30
30
  <CardTitle>{t('seller_picker.title')}</CardTitle>
31
31
  </CardHeader>
32
32
  <CardContent className="flex flex-col gap-2">
33
- <p className="mb-2 text-muted-foreground text-sm">{t('seller_picker.subtitle')}</p>
33
+ <p className="mb-2 text-muted-foreground text-base">{t('seller_picker.subtitle')}</p>
34
34
  {sellers.map((seller) => (
35
35
  <Button key={seller.id} variant="outline" className="justify-between" asChild>
36
36
  <Link to="/$sellerId" params={{ sellerId: seller.id }}>
@@ -0,0 +1,40 @@
1
+ import { PageHeader, ReadRow, SettingsSection } from '@spree/dashboard-core'
2
+ import { CardContent, ResourceLayout } from '@spree/dashboard-ui'
3
+ import { ReceiptTextIcon } from '@spree/dashboard-ui/icons'
4
+ import { useTranslation } from 'react-i18next'
5
+ import { SellerTaxIdentifiersCard } from '../components/seller-tax-identifiers-card'
6
+ import { useProfile } from '../hooks/use-profile'
7
+
8
+ /**
9
+ * The seller's tax registrations, then who remits tax on their sales. The
10
+ * second is the marketplace's decision, so it is shown read-only: the seller
11
+ * API refuses to write it.
12
+ */
13
+ export function TaxesPage() {
14
+ const { t } = useTranslation()
15
+ const { data: profile } = useProfile()
16
+
17
+ return (
18
+ <ResourceLayout
19
+ header={
20
+ <PageHeader title={t('taxes.title')} description={t('taxes.description')} sticky={false} />
21
+ }
22
+ main={
23
+ <>
24
+ <SellerTaxIdentifiersCard />
25
+ <SettingsSection
26
+ title={t('profile.tax_remittance')}
27
+ icon={<ReceiptTextIcon />}
28
+ description={t('profile.operator_managed')}
29
+ >
30
+ <CardContent>
31
+ <ReadRow label={t('profile.tax_remittance')}>
32
+ {profile && t(`taxes.remittance.${profile.tax_remittance}`)}
33
+ </ReadRow>
34
+ </CardContent>
35
+ </SettingsSection>
36
+ </>
37
+ }
38
+ />
39
+ )
40
+ }
@@ -365,7 +365,7 @@ function InvitationRow({ invitation }: { invitation: Invitation }) {
365
365
  <span className="font-medium">{invitation.email}</span>
366
366
  </div>
367
367
  </TableCell>
368
- <TableCell className="text-muted-foreground text-sm">
368
+ <TableCell className="text-muted-foreground text-base">
369
369
  {invitation.expires_at ? (
370
370
  <span className="inline-flex items-center gap-1">
371
371
  <ClockIcon className="size-3.5" />
@@ -30,9 +30,12 @@ import { Route as ProductsProductIdRouteImport } from './routes/_authenticated/$
30
30
  import { Route as ProductsNewRouteImport } from './routes/_authenticated/$sellerId/products/new'
31
31
  import { Route as SettingsIndexRouteImport } from './routes/_authenticated/$sellerId/settings/index'
32
32
  import { Route as SettingsDeliveryMethodsRouteImport } from './routes/_authenticated/$sellerId/settings/delivery-methods'
33
+ import { Route as SettingsGeneralRouteImport } from './routes/_authenticated/$sellerId/settings/general'
33
34
  import { Route as SettingsPackageTypesRouteImport } from './routes/_authenticated/$sellerId/settings/package-types'
34
35
  import { Route as SettingsPoliciesRouteImport } from './routes/_authenticated/$sellerId/settings/policies'
36
+ import { Route as SettingsProfileRouteImport } from './routes/_authenticated/$sellerId/settings/profile'
35
37
  import { Route as SettingsStockLocationsRouteImport } from './routes/_authenticated/$sellerId/settings/stock-locations'
38
+ import { Route as SettingsTaxesRouteImport } from './routes/_authenticated/$sellerId/settings/taxes'
36
39
  import { Route as SettingsTeamRouteImport } from './routes/_authenticated/$sellerId/settings/team'
37
40
 
38
41
  const authenticatedRoute = authenticatedRouteImport.update({
@@ -140,6 +143,11 @@ const SettingsDeliveryMethodsRoute = SettingsDeliveryMethodsRouteImport.update({
140
143
  path: '/delivery-methods',
141
144
  getParentRoute: () => SettingsRoute,
142
145
  } as any)
146
+ const SettingsGeneralRoute = SettingsGeneralRouteImport.update({
147
+ id: '/general',
148
+ path: '/general',
149
+ getParentRoute: () => SettingsRoute,
150
+ } as any)
143
151
  const SettingsPackageTypesRoute = SettingsPackageTypesRouteImport.update({
144
152
  id: '/package-types',
145
153
  path: '/package-types',
@@ -150,11 +158,21 @@ const SettingsPoliciesRoute = SettingsPoliciesRouteImport.update({
150
158
  path: '/policies',
151
159
  getParentRoute: () => SettingsRoute,
152
160
  } as any)
161
+ const SettingsProfileRoute = SettingsProfileRouteImport.update({
162
+ id: '/profile',
163
+ path: '/profile',
164
+ getParentRoute: () => SettingsRoute,
165
+ } as any)
153
166
  const SettingsStockLocationsRoute = SettingsStockLocationsRouteImport.update({
154
167
  id: '/stock-locations',
155
168
  path: '/stock-locations',
156
169
  getParentRoute: () => SettingsRoute,
157
170
  } as any)
171
+ const SettingsTaxesRoute = SettingsTaxesRouteImport.update({
172
+ id: '/taxes',
173
+ path: '/taxes',
174
+ getParentRoute: () => SettingsRoute,
175
+ } as any)
158
176
  const SettingsTeamRoute = SettingsTeamRouteImport.update({
159
177
  id: '/team',
160
178
  path: '/team',
@@ -177,9 +195,12 @@ export interface FileRoutesByFullPath {
177
195
  '/$sellerId/products/$productId': typeof ProductsProductIdRoute
178
196
  '/$sellerId/products/new': typeof ProductsNewRoute
179
197
  '/$sellerId/settings/delivery-methods': typeof SettingsDeliveryMethodsRoute
198
+ '/$sellerId/settings/general': typeof SettingsGeneralRoute
180
199
  '/$sellerId/settings/package-types': typeof SettingsPackageTypesRoute
181
200
  '/$sellerId/settings/policies': typeof SettingsPoliciesRoute
201
+ '/$sellerId/settings/profile': typeof SettingsProfileRoute
182
202
  '/$sellerId/settings/stock-locations': typeof SettingsStockLocationsRoute
203
+ '/$sellerId/settings/taxes': typeof SettingsTaxesRoute
183
204
  '/$sellerId/settings/team': typeof SettingsTeamRoute
184
205
  '/$sellerId/earnings/': typeof EarningsIndexRoute
185
206
  '/$sellerId/orders/': typeof OrdersIndexRoute
@@ -201,9 +222,12 @@ export interface FileRoutesByTo {
201
222
  '/$sellerId/products/$productId': typeof ProductsProductIdRoute
202
223
  '/$sellerId/products/new': typeof ProductsNewRoute
203
224
  '/$sellerId/settings/delivery-methods': typeof SettingsDeliveryMethodsRoute
225
+ '/$sellerId/settings/general': typeof SettingsGeneralRoute
204
226
  '/$sellerId/settings/package-types': typeof SettingsPackageTypesRoute
205
227
  '/$sellerId/settings/policies': typeof SettingsPoliciesRoute
228
+ '/$sellerId/settings/profile': typeof SettingsProfileRoute
206
229
  '/$sellerId/settings/stock-locations': typeof SettingsStockLocationsRoute
230
+ '/$sellerId/settings/taxes': typeof SettingsTaxesRoute
207
231
  '/$sellerId/settings/team': typeof SettingsTeamRoute
208
232
  '/$sellerId/earnings': typeof EarningsIndexRoute
209
233
  '/$sellerId/orders': typeof OrdersIndexRoute
@@ -229,9 +253,12 @@ export interface FileRoutesById {
229
253
  '/_authenticated/$sellerId/products/$productId': typeof ProductsProductIdRoute
230
254
  '/_authenticated/$sellerId/products/new': typeof ProductsNewRoute
231
255
  '/_authenticated/$sellerId/settings/delivery-methods': typeof SettingsDeliveryMethodsRoute
256
+ '/_authenticated/$sellerId/settings/general': typeof SettingsGeneralRoute
232
257
  '/_authenticated/$sellerId/settings/package-types': typeof SettingsPackageTypesRoute
233
258
  '/_authenticated/$sellerId/settings/policies': typeof SettingsPoliciesRoute
259
+ '/_authenticated/$sellerId/settings/profile': typeof SettingsProfileRoute
234
260
  '/_authenticated/$sellerId/settings/stock-locations': typeof SettingsStockLocationsRoute
261
+ '/_authenticated/$sellerId/settings/taxes': typeof SettingsTaxesRoute
235
262
  '/_authenticated/$sellerId/settings/team': typeof SettingsTeamRoute
236
263
  '/_authenticated/$sellerId/earnings/': typeof EarningsIndexRoute
237
264
  '/_authenticated/$sellerId/orders/': typeof OrdersIndexRoute
@@ -257,9 +284,12 @@ export interface FileRouteTypes {
257
284
  | '/$sellerId/products/$productId'
258
285
  | '/$sellerId/products/new'
259
286
  | '/$sellerId/settings/delivery-methods'
287
+ | '/$sellerId/settings/general'
260
288
  | '/$sellerId/settings/package-types'
261
289
  | '/$sellerId/settings/policies'
290
+ | '/$sellerId/settings/profile'
262
291
  | '/$sellerId/settings/stock-locations'
292
+ | '/$sellerId/settings/taxes'
263
293
  | '/$sellerId/settings/team'
264
294
  | '/$sellerId/earnings/'
265
295
  | '/$sellerId/orders/'
@@ -281,9 +311,12 @@ export interface FileRouteTypes {
281
311
  | '/$sellerId/products/$productId'
282
312
  | '/$sellerId/products/new'
283
313
  | '/$sellerId/settings/delivery-methods'
314
+ | '/$sellerId/settings/general'
284
315
  | '/$sellerId/settings/package-types'
285
316
  | '/$sellerId/settings/policies'
317
+ | '/$sellerId/settings/profile'
286
318
  | '/$sellerId/settings/stock-locations'
319
+ | '/$sellerId/settings/taxes'
287
320
  | '/$sellerId/settings/team'
288
321
  | '/$sellerId/earnings'
289
322
  | '/$sellerId/orders'
@@ -308,9 +341,12 @@ export interface FileRouteTypes {
308
341
  | '/_authenticated/$sellerId/products/$productId'
309
342
  | '/_authenticated/$sellerId/products/new'
310
343
  | '/_authenticated/$sellerId/settings/delivery-methods'
344
+ | '/_authenticated/$sellerId/settings/general'
311
345
  | '/_authenticated/$sellerId/settings/package-types'
312
346
  | '/_authenticated/$sellerId/settings/policies'
347
+ | '/_authenticated/$sellerId/settings/profile'
313
348
  | '/_authenticated/$sellerId/settings/stock-locations'
349
+ | '/_authenticated/$sellerId/settings/taxes'
314
350
  | '/_authenticated/$sellerId/settings/team'
315
351
  | '/_authenticated/$sellerId/earnings/'
316
352
  | '/_authenticated/$sellerId/orders/'
@@ -476,6 +512,13 @@ declare module '@tanstack/react-router' {
476
512
  preLoaderRoute: typeof SettingsDeliveryMethodsRouteImport
477
513
  parentRoute: typeof SettingsRoute
478
514
  }
515
+ '/_authenticated/$sellerId/settings/general': {
516
+ id: '/_authenticated/$sellerId/settings/general'
517
+ path: '/general'
518
+ fullPath: '/$sellerId/settings/general'
519
+ preLoaderRoute: typeof SettingsGeneralRouteImport
520
+ parentRoute: typeof SettingsRoute
521
+ }
479
522
  '/_authenticated/$sellerId/settings/package-types': {
480
523
  id: '/_authenticated/$sellerId/settings/package-types'
481
524
  path: '/package-types'
@@ -490,6 +533,13 @@ declare module '@tanstack/react-router' {
490
533
  preLoaderRoute: typeof SettingsPoliciesRouteImport
491
534
  parentRoute: typeof SettingsRoute
492
535
  }
536
+ '/_authenticated/$sellerId/settings/profile': {
537
+ id: '/_authenticated/$sellerId/settings/profile'
538
+ path: '/profile'
539
+ fullPath: '/$sellerId/settings/profile'
540
+ preLoaderRoute: typeof SettingsProfileRouteImport
541
+ parentRoute: typeof SettingsRoute
542
+ }
493
543
  '/_authenticated/$sellerId/settings/stock-locations': {
494
544
  id: '/_authenticated/$sellerId/settings/stock-locations'
495
545
  path: '/stock-locations'
@@ -497,6 +547,13 @@ declare module '@tanstack/react-router' {
497
547
  preLoaderRoute: typeof SettingsStockLocationsRouteImport
498
548
  parentRoute: typeof SettingsRoute
499
549
  }
550
+ '/_authenticated/$sellerId/settings/taxes': {
551
+ id: '/_authenticated/$sellerId/settings/taxes'
552
+ path: '/taxes'
553
+ fullPath: '/$sellerId/settings/taxes'
554
+ preLoaderRoute: typeof SettingsTaxesRouteImport
555
+ parentRoute: typeof SettingsRoute
556
+ }
500
557
  '/_authenticated/$sellerId/settings/team': {
501
558
  id: '/_authenticated/$sellerId/settings/team'
502
559
  path: '/team'
@@ -509,18 +566,24 @@ declare module '@tanstack/react-router' {
509
566
 
510
567
  interface SettingsRouteChildren {
511
568
  SettingsDeliveryMethodsRoute: typeof SettingsDeliveryMethodsRoute
569
+ SettingsGeneralRoute: typeof SettingsGeneralRoute
512
570
  SettingsPackageTypesRoute: typeof SettingsPackageTypesRoute
513
571
  SettingsPoliciesRoute: typeof SettingsPoliciesRoute
572
+ SettingsProfileRoute: typeof SettingsProfileRoute
514
573
  SettingsStockLocationsRoute: typeof SettingsStockLocationsRoute
574
+ SettingsTaxesRoute: typeof SettingsTaxesRoute
515
575
  SettingsTeamRoute: typeof SettingsTeamRoute
516
576
  SettingsIndexRoute: typeof SettingsIndexRoute
517
577
  }
518
578
 
519
579
  const SettingsRouteChildren: SettingsRouteChildren = {
520
580
  SettingsDeliveryMethodsRoute: SettingsDeliveryMethodsRoute,
581
+ SettingsGeneralRoute: SettingsGeneralRoute,
521
582
  SettingsPackageTypesRoute: SettingsPackageTypesRoute,
522
583
  SettingsPoliciesRoute: SettingsPoliciesRoute,
584
+ SettingsProfileRoute: SettingsProfileRoute,
523
585
  SettingsStockLocationsRoute: SettingsStockLocationsRoute,
586
+ SettingsTaxesRoute: SettingsTaxesRoute,
524
587
  SettingsTeamRoute: SettingsTeamRoute,
525
588
  SettingsIndexRoute: SettingsIndexRoute,
526
589
  }
@@ -1,6 +1,8 @@
1
- import { createFileRoute } from '@tanstack/react-router'
2
- import { ProfilePage } from '../../../pages/profile'
1
+ import { createFileRoute, redirect } from '@tanstack/react-router'
3
2
 
3
+ // The profile moved under settings; old links and bookmarks follow it there.
4
4
  export const Route = createFileRoute('/_authenticated/$sellerId/profile')({
5
- component: ProfilePage,
5
+ beforeLoad: ({ params }) => {
6
+ throw redirect({ to: '/$sellerId/settings/profile', params })
7
+ },
6
8
  })
@@ -0,0 +1,6 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { GeneralSettingsPage } from '../../../../pages/general-settings'
3
+
4
+ export const Route = createFileRoute('/_authenticated/$sellerId/settings/general')({
5
+ component: GeneralSettingsPage,
6
+ })
@@ -0,0 +1,6 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { ProfileSettingsPage } from '../../../../pages/profile-settings'
3
+
4
+ export const Route = createFileRoute('/_authenticated/$sellerId/settings/profile')({
5
+ component: ProfileSettingsPage,
6
+ })
@@ -0,0 +1,6 @@
1
+ import { createFileRoute } from '@tanstack/react-router'
2
+ import { TaxesPage } from '../../../../pages/taxes'
3
+
4
+ export const Route = createFileRoute('/_authenticated/$sellerId/settings/taxes')({
5
+ component: TaxesPage,
6
+ })
@@ -1,18 +1,14 @@
1
+ import { SettingsContainer } from '@spree/dashboard-core'
1
2
  import { createFileRoute, Outlet } from '@tanstack/react-router'
2
3
 
3
4
  export const Route = createFileRoute('/_authenticated/$sellerId/settings')({
4
5
  component: SettingsLayout,
5
6
  })
6
7
 
7
- /**
8
- * Content padding for the settings area. The rail itself lives in the chrome,
9
- * one level up, so it can span full height beside the top bar — this layout
10
- * owns only the right-hand column, matching the dashboard's settings layout.
11
- */
12
8
  function SettingsLayout() {
13
9
  return (
14
- <div className="container mx-auto flex flex-1 flex-col gap-4 p-4 lg:p-6">
10
+ <SettingsContainer>
15
11
  <Outlet />
16
- </div>
12
+ </SettingsContainer>
17
13
  )
18
14
  }
@@ -3,7 +3,7 @@ import type { AccountUpdateParams, MeResponse } from '@spree/seller-sdk'
3
3
  import { z } from 'zod/v4'
4
4
 
5
5
  // The signed-in person's own account (PATCH /me) — not the seller business
6
- // they act for, which is `pages/profile.tsx`. Every field is optional: the
6
+ // they act for, which is `pages/profile-settings.tsx`. Every field is optional: the
7
7
  // panel asks for a name but does not require one.
8
8
  //
9
9
  // The panel's languages are NOT listed here. They are derived at render time
@@ -1,6 +1,6 @@
1
1
  import { PRODUCT_STATUSES } from '@spree/core'
2
2
  import { defineTable, formatPrice, ProductStockSummary } from '@spree/dashboard-core'
3
- import { ResourceNameCell, StatusBadge, Thumbnail } from '@spree/dashboard-ui'
3
+ import { ProductIdentity, StatusBadge } from '@spree/dashboard-ui'
4
4
  import { PackageIcon } from '@spree/dashboard-ui/icons'
5
5
  import type { Product } from '@spree/seller-sdk'
6
6
  import i18n from 'i18next'
@@ -19,10 +19,11 @@ defineTable<Product>('seller-products', {
19
19
  sortable: true,
20
20
  default: true,
21
21
  render: (product) => (
22
- <div className="flex items-center gap-3">
23
- <Thumbnail src={product.thumbnail_url} alt={product.name} />
24
- <ResourceNameCell id={product.id} dataAttr="data-product-id" name={product.name} />
25
- </div>
22
+ <ProductIdentity
23
+ name={product.name}
24
+ thumbnailUrl={product.thumbnail_url}
25
+ render={<button type="button" data-product-id={product.id} />}
26
+ />
26
27
  ),
27
28
  },
28
29
  {
@@ -1,16 +0,0 @@
1
- import type { ReactNode } from 'react'
2
- import { useTranslation } from 'react-i18next'
3
-
4
- /** One labelled value in a read-only card, with a placeholder when unset. */
5
- export function ReadRow({ label, children }: { label: string; children: ReactNode }) {
6
- const { t } = useTranslation()
7
-
8
- return (
9
- <div className="flex items-start justify-between gap-3 text-sm">
10
- <span className="text-muted-foreground">{label}</span>
11
- <span className="text-right">
12
- {children ?? <span className="text-muted-foreground">{t('profile.not_provided')}</span>}
13
- </span>
14
- </div>
15
- )
16
- }