Add Website settings for special-products source.
Businesses can choose product vs store item sourcing, with store as the default when the store module is enabled, and rename the FA special-products section. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
a7950f1f13
commit
193c63bfd2
@@ -68,6 +68,7 @@ import { WebsiteFaqPage } from './pages/WebsiteFaqPage'
|
||||
import { WebsiteBadgesPage } from './pages/WebsiteBadgesPage'
|
||||
import { SettingsPage } from './pages/SettingsPage'
|
||||
import { WebsiteStaticImagesPage } from './pages/WebsiteStaticImagesPage'
|
||||
import { WebsiteSettingsPage } from './pages/WebsiteSettingsPage'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
@@ -152,6 +153,7 @@ function App() {
|
||||
<Route path="website/subscriptions" element={<WebsiteSubscriptionsPage />} />
|
||||
<Route path="website/faq" element={<WebsiteFaqPage />} />
|
||||
<Route path="website/badges" element={<WebsiteBadgesPage />} />
|
||||
<Route path="website/settings" element={<WebsiteSettingsPage />} />
|
||||
<Route path="website/e-payment" element={<Navigate to="/settings" replace />} />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
@@ -214,6 +214,7 @@ export function Sidebar({ open = false }: SidebarProps) {
|
||||
{ labelKey: 'nav.website.subscriptions', to: '/website/subscriptions' },
|
||||
{ labelKey: 'nav.website.faq', to: '/website/faq' },
|
||||
{ labelKey: 'nav.website.badges', to: '/website/badges' },
|
||||
{ labelKey: 'nav.website.settings', to: '/website/settings' },
|
||||
],
|
||||
},
|
||||
{ type: 'link', id: 'settings', icon: Settings, labelKey: 'nav.settings', to: '/settings' },
|
||||
|
||||
@@ -78,6 +78,7 @@ const en = {
|
||||
'nav.website.subscriptions': 'Subscriptions',
|
||||
'nav.website.faq': 'FAQ',
|
||||
'nav.website.badges': 'Badges',
|
||||
'nav.website.settings': 'Settings',
|
||||
'nav.website.ePayment': 'E-Payment',
|
||||
'nav.help': 'Help Center',
|
||||
'nav.logout': 'Logout',
|
||||
@@ -1326,7 +1327,7 @@ const en = {
|
||||
'website.card.specialBrands.desc': 'Showcase partner or featured brands on your website homepage.',
|
||||
'website.card.specialBrands.link': 'Manage brands',
|
||||
'website.card.specialItems.desc':
|
||||
'Curate featured store items into categories for your website carousels.',
|
||||
'Curate featured products into categories for your website carousels.',
|
||||
'website.card.specialItems.link': 'Manage special items',
|
||||
'website.card.contact.desc': 'View submissions from your website contact form.',
|
||||
'website.card.contact.link': 'View submissions',
|
||||
@@ -1337,6 +1338,8 @@ const en = {
|
||||
'website.card.faq.link': 'Manage FAQ',
|
||||
'website.card.badges.desc': 'Show trust badges, certifications, and highlights on your website.',
|
||||
'website.card.badges.link': 'Manage badges',
|
||||
'website.card.settings.desc': 'Choose how special products are sourced for the website.',
|
||||
'website.card.settings.link': 'Manage settings',
|
||||
'settings.subtitle': 'Configure online payment gateways and checkout payment options.',
|
||||
|
||||
'website.scrollLeft': 'Scroll left',
|
||||
@@ -1529,7 +1532,7 @@ const en = {
|
||||
'website.specialBrands.pickErrorLoad': 'Unable to load brands.',
|
||||
|
||||
'website.specialItems.subtitle':
|
||||
'Curate featured store items into categories for your website carousels.',
|
||||
'Curate featured products into categories for your website carousels.',
|
||||
'website.specialItems.loading': 'Loading special categories...',
|
||||
'website.specialItems.empty': 'You have not added any special categories yet.',
|
||||
'website.specialItems.errorLoad': 'Unable to load special categories.',
|
||||
@@ -1563,6 +1566,19 @@ const en = {
|
||||
'website.specialItems.pickEmpty': 'All store items are already in this category.',
|
||||
'website.specialItems.pickSearch': 'Type 3+ characters to search store items',
|
||||
'website.specialItems.pickErrorLoad': 'Unable to load store items.',
|
||||
|
||||
'website.settings.title': 'Website settings',
|
||||
'website.settings.subtitle': 'Configure how website content is sourced and displayed.',
|
||||
'website.settings.section': 'Special products',
|
||||
'website.settings.loading': 'Loading settings...',
|
||||
'website.settings.errorLoad': 'Unable to load settings.',
|
||||
'website.settings.errorSave': 'Unable to save settings.',
|
||||
'website.settings.saved': 'Settings saved.',
|
||||
'website.settings.specialSource': 'Read special products from',
|
||||
'website.settings.specialSourceDesc':
|
||||
'Choose whether featured products on the website come from catalog products or store items. Defaults to store items when the store module is enabled.',
|
||||
'website.settings.source.product': 'Product',
|
||||
'website.settings.source.storeItem': 'Store item',
|
||||
'website.pick.addOneItem': 'Add 1 item',
|
||||
'website.pick.addItems': 'Add {count} items',
|
||||
|
||||
@@ -2072,11 +2088,12 @@ const fa: Record<MessageKey, string> = {
|
||||
'nav.website.sliders': 'اسلایدرها',
|
||||
'nav.website.specialCategories': 'دستههای ویژه',
|
||||
'nav.website.specialBrands': 'برندهای ویژه',
|
||||
'nav.website.specialItems': 'محصولات ویژه فروشگاه',
|
||||
'nav.website.specialItems': 'محصولات ویژه',
|
||||
'nav.website.contact': 'فرم تماس با ما',
|
||||
'nav.website.subscriptions': 'عضویتها',
|
||||
'nav.website.faq': 'سوالات متداول',
|
||||
'nav.website.badges': 'نشانها',
|
||||
'nav.website.settings': 'تنظیمات',
|
||||
'nav.website.ePayment': 'پرداخت الکترونیک',
|
||||
'nav.help': 'مرکز راهنما',
|
||||
'nav.logout': 'خروج',
|
||||
@@ -3325,7 +3342,7 @@ const fa: Record<MessageKey, string> = {
|
||||
'website.card.specialBrands.desc': 'برندهای شریک یا ویژه را در صفحه اصلی وبسایت نمایش دهید.',
|
||||
'website.card.specialBrands.link': 'مدیریت برندها',
|
||||
'website.card.specialItems.desc':
|
||||
'محصولات ویژه فروشگاه را در دستهها برای کاروسلهای وبسایت گردآوری کنید.',
|
||||
'محصولات ویژه را در دستهها برای کاروسلهای وبسایت گردآوری کنید.',
|
||||
'website.card.specialItems.link': 'مدیریت محصولات ویژه',
|
||||
'website.card.contact.desc': 'ارسالهای فرم تماس وبسایت را مشاهده کنید.',
|
||||
'website.card.contact.link': 'مشاهده ارسالها',
|
||||
@@ -3335,6 +3352,8 @@ const fa: Record<MessageKey, string> = {
|
||||
'website.card.faq.link': 'مدیریت سوالات متداول',
|
||||
'website.card.badges.desc': 'نشانهای اعتماد، گواهیها و برجستهها را در وبسایت نمایش دهید.',
|
||||
'website.card.badges.link': 'مدیریت نشانها',
|
||||
'website.card.settings.desc': 'منبع خواندن محصولات ویژه وبسایت را انتخاب کنید.',
|
||||
'website.card.settings.link': 'مدیریت تنظیمات',
|
||||
'settings.subtitle': 'درگاههای پرداخت آنلاین و گزینههای تسویه حساب را پیکربندی کنید.',
|
||||
|
||||
'website.scrollLeft': 'اسکرول به چپ',
|
||||
@@ -3526,7 +3545,7 @@ const fa: Record<MessageKey, string> = {
|
||||
'website.specialBrands.pickErrorLoad': 'بارگذاری برندها ممکن نشد.',
|
||||
|
||||
'website.specialItems.subtitle':
|
||||
'محصولات ویژه فروشگاه را برای کاروسلهای وبسایت در دستهها گردآوری کنید.',
|
||||
'محصولات ویژه را برای کاروسلهای وبسایت در دستهها گردآوری کنید.',
|
||||
'website.specialItems.loading': 'در حال بارگذاری دستههای ویژه...',
|
||||
'website.specialItems.empty': 'هنوز دسته ویژهای ثبت نکردهاید.',
|
||||
'website.specialItems.errorLoad': 'بارگذاری دستههای ویژه ممکن نشد.',
|
||||
@@ -3559,6 +3578,19 @@ const fa: Record<MessageKey, string> = {
|
||||
'website.specialItems.pickEmpty': 'همه محصولات فروشگاه قبلاً در این دسته هستند.',
|
||||
'website.specialItems.pickSearch': 'حداقل ۳ حرف برای جستجوی محصولات فروشگاه تایپ کنید',
|
||||
'website.specialItems.pickErrorLoad': 'بارگذاری محصولات فروشگاه ممکن نشد.',
|
||||
|
||||
'website.settings.title': 'تنظیمات وبسایت',
|
||||
'website.settings.subtitle': 'نحوه منبعگیری و نمایش محتوای وبسایت را پیکربندی کنید.',
|
||||
'website.settings.section': 'محصولات ویژه',
|
||||
'website.settings.loading': 'در حال بارگذاری تنظیمات...',
|
||||
'website.settings.errorLoad': 'بارگذاری تنظیمات ممکن نشد.',
|
||||
'website.settings.errorSave': 'ذخیره تنظیمات ممکن نشد.',
|
||||
'website.settings.saved': 'تنظیمات ذخیره شد.',
|
||||
'website.settings.specialSource': 'خواندن محصولات ویژه از',
|
||||
'website.settings.specialSourceDesc':
|
||||
'انتخاب کنید محصولات ویژه وبسایت از کاتالوگ محصول خوانده شوند یا از محصولات فروشگاه. اگر ماژول فروشگاه فعال باشد، پیشفرض محصول فروشگاه است.',
|
||||
'website.settings.source.product': 'محصول',
|
||||
'website.settings.source.storeItem': 'محصول فروشگاه',
|
||||
'website.pick.addOneItem': 'افزودن ۱ قلم',
|
||||
'website.pick.addItems': 'افزودن {count} قلم',
|
||||
|
||||
@@ -3688,7 +3720,7 @@ const fa: Record<MessageKey, string> = {
|
||||
'title.sliders': 'اسلایدرها',
|
||||
'title.specialCategories': 'دستههای ویژه',
|
||||
'title.specialBrands': 'برندهای ویژه',
|
||||
'title.specialItems': 'محصولات ویژه فروشگاه',
|
||||
'title.specialItems': 'محصولات ویژه',
|
||||
'title.contactForm': 'فرم تماس با ما',
|
||||
'title.subscriptions': 'عضویتها',
|
||||
'title.faq': 'سوالات متداول',
|
||||
@@ -4166,6 +4198,7 @@ export function getBusinessRouteTitleRules(locale: DashboardLocale): RouteTitleR
|
||||
{ match: '/website/subscriptions', labels: [t('title.website'), t('title.subscriptions')] },
|
||||
{ match: '/website/faq', labels: [t('title.website'), t('title.faq')] },
|
||||
{ match: '/website/badges', labels: [t('title.website'), t('title.badges')] },
|
||||
{ match: '/website/settings', labels: [t('title.website'), t('title.settings')] },
|
||||
{ match: '/website', labels: [t('title.website')] },
|
||||
{ match: '/settings', labels: [t('title.settings')] },
|
||||
{ match: '/', labels: [t('title.home')] },
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Award,
|
||||
Image,
|
||||
Images,
|
||||
Settings,
|
||||
Sparkles,
|
||||
} from 'lucide-react'
|
||||
import { SectionCard } from '../components/SectionCard'
|
||||
@@ -63,6 +64,13 @@ const websiteSections: {
|
||||
linkKey: 'website.card.badges.link',
|
||||
href: '/website/badges',
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
titleKey: 'nav.website.settings',
|
||||
descKey: 'website.card.settings.desc',
|
||||
linkKey: 'website.card.settings.link',
|
||||
href: '/website/settings',
|
||||
},
|
||||
]
|
||||
|
||||
export function WebsitePage() {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
.sourceRow {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.radioGroup {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 200px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.radioOption {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: var(--field-height);
|
||||
padding: var(--field-padding-y) var(--field-padding-x);
|
||||
font-size: var(--field-font-size);
|
||||
line-height: 1.4;
|
||||
color: var(--text-primary);
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
border: 1px solid rgba(148, 163, 184, 0.35);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, box-shadow 0.2s;
|
||||
}
|
||||
|
||||
.radioOption:has(input:checked) {
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 3px rgba(var(--primary-rgb) / 0.12);
|
||||
}
|
||||
|
||||
.radioOption:has(input:disabled) {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.radioOption input {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0;
|
||||
accent-color: var(--primary);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.radioGroup {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Breadcrumbs } from '../components/Breadcrumbs'
|
||||
import { PageTitle } from '../components/PageTitle'
|
||||
import { useToast } from '../context/ToastContext'
|
||||
import { useT } from '../i18n/useT'
|
||||
import { ApiError } from '../lib/api'
|
||||
import {
|
||||
getSettings,
|
||||
updateWebsiteSettings,
|
||||
type SpecialProductsSource,
|
||||
} from '../services/settingsService'
|
||||
import { hasBusinessModule, type BusinessModuleId } from '../utils/businessModules'
|
||||
import pageStyles from '../components/PageContent.module.css'
|
||||
import sharedStyles from './ProductSettingsPage.module.css'
|
||||
import styles from './WebsiteSettingsPage.module.css'
|
||||
|
||||
const SOURCE_OPTIONS: SpecialProductsSource[] = ['product', 'store_item']
|
||||
|
||||
function defaultSpecialProductsSource(
|
||||
enabledModules: string[] | undefined,
|
||||
): SpecialProductsSource {
|
||||
return hasBusinessModule(
|
||||
enabledModules as BusinessModuleId[] | undefined,
|
||||
'store',
|
||||
)
|
||||
? 'store_item'
|
||||
: 'product'
|
||||
}
|
||||
|
||||
export function WebsiteSettingsPage() {
|
||||
const t = useT()
|
||||
const { showToast } = useToast()
|
||||
const [source, setSource] = useState<SpecialProductsSource>('product')
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isSaving, setIsSaving] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController()
|
||||
void loadSettings(controller.signal)
|
||||
return () => controller.abort()
|
||||
}, [])
|
||||
|
||||
async function loadSettings(signal?: AbortSignal) {
|
||||
setIsLoading(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const data = await getSettings(signal)
|
||||
setSource(
|
||||
data.settings.website?.specialProductsSource ??
|
||||
defaultSpecialProductsSource(data.settings.modules?.enabled),
|
||||
)
|
||||
} catch (err) {
|
||||
if (err instanceof DOMException && err.name === 'AbortError') return
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError(t('website.settings.errorLoad'))
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSourceChange(next: SpecialProductsSource) {
|
||||
if (next === source || isSaving) return
|
||||
|
||||
const previous = source
|
||||
setSource(next)
|
||||
setIsSaving(true)
|
||||
setError('')
|
||||
|
||||
try {
|
||||
const data = await updateWebsiteSettings({ specialProductsSource: next })
|
||||
setSource(
|
||||
data.settings.website?.specialProductsSource ?? next,
|
||||
)
|
||||
showToast(t('website.settings.saved'), 'success')
|
||||
} catch (err) {
|
||||
setSource(previous)
|
||||
if (err instanceof ApiError) {
|
||||
setError(err.message)
|
||||
} else {
|
||||
setError(t('website.settings.errorSave'))
|
||||
}
|
||||
} finally {
|
||||
setIsSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={pageStyles.content}>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ label: 'Dashboard', href: '/' },
|
||||
{ label: 'Website', href: '/website' },
|
||||
{ label: 'Settings' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<div className={pageStyles.pageHeader}>
|
||||
<div>
|
||||
<PageTitle en="WEBSITE SETTINGS">{t('website.settings.title')}</PageTitle>
|
||||
<p className={pageStyles.pageSubtitle}>{t('website.settings.subtitle')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className={sharedStyles.alertError} role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className={sharedStyles.panel}>
|
||||
<h3 className={sharedStyles.sectionTitle}>{t('website.settings.section')}</h3>
|
||||
|
||||
{isLoading ? (
|
||||
<p className={sharedStyles.status}>{t('website.settings.loading')}</p>
|
||||
) : (
|
||||
<div className={sharedStyles.list}>
|
||||
<div className={`${sharedStyles.row} ${styles.sourceRow}`}>
|
||||
<div className={sharedStyles.rowText}>
|
||||
<span className={sharedStyles.rowLabel} id="special-products-source-label">
|
||||
{t('website.settings.specialSource')}
|
||||
</span>
|
||||
<p className={sharedStyles.rowDescription}>
|
||||
{t('website.settings.specialSourceDesc')}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={styles.radioGroup}
|
||||
role="radiogroup"
|
||||
aria-labelledby="special-products-source-label"
|
||||
>
|
||||
{SOURCE_OPTIONS.map((option) => (
|
||||
<label key={option} className={styles.radioOption}>
|
||||
<input
|
||||
type="radio"
|
||||
name="special-products-source"
|
||||
value={option}
|
||||
checked={source === option}
|
||||
disabled={isSaving}
|
||||
onChange={() => void handleSourceChange(option)}
|
||||
/>
|
||||
<span>
|
||||
{option === 'product'
|
||||
? t('website.settings.source.product')
|
||||
: t('website.settings.source.storeItem')}
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -90,11 +90,18 @@ export interface ModulesSettings {
|
||||
enabled: string[]
|
||||
}
|
||||
|
||||
export type SpecialProductsSource = 'product' | 'store_item'
|
||||
|
||||
export interface WebsiteSettings {
|
||||
specialProductsSource: SpecialProductsSource
|
||||
}
|
||||
|
||||
export interface BusinessSettings {
|
||||
branding: BrandingSettings
|
||||
dashboard: DashboardSettings
|
||||
store: StoreSettings
|
||||
modules?: ModulesSettings
|
||||
website?: WebsiteSettings
|
||||
}
|
||||
|
||||
export interface SettingsResponse {
|
||||
@@ -158,3 +165,11 @@ export async function updateStoreSettings(store: Partial<StoreSettings>) {
|
||||
body: { store },
|
||||
})
|
||||
}
|
||||
|
||||
export async function updateWebsiteSettings(website: Partial<WebsiteSettings>) {
|
||||
return apiRequest<SettingsResponse>(settingsPath(), {
|
||||
method: 'PATCH',
|
||||
auth: true,
|
||||
body: { website },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface ResolvedTenant {
|
||||
defaultLocale?: DashboardLocale
|
||||
enabledModules?: BusinessModuleId[]
|
||||
homeCharts?: [HomeChartId, HomeChartId]
|
||||
specialProductsSource?: 'product' | 'store_item'
|
||||
logoUrl?: string | null
|
||||
faviconUrl?: string | null
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ export interface BusinessSettings {
|
||||
orderProcessSteps: Array<{ id: string; label: string; color: string }>
|
||||
}
|
||||
modules: ModulesSettings
|
||||
website?: {
|
||||
specialProductsSource: 'product' | 'store_item'
|
||||
}
|
||||
}
|
||||
|
||||
export interface BusinessSettingsResponse {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
> **For AI agents:** Read this file at the start of a new chat before making changes.
|
||||
> Update this document when a major feature is completed or architecture changes.
|
||||
|
||||
Last updated: August 20, 2026
|
||||
Last updated: August 21, 2026
|
||||
|
||||
---
|
||||
|
||||
@@ -216,6 +216,7 @@ Add to `/etc/hosts` (one line per tenant):
|
||||
| `/website` | Website hub | Placeholder |
|
||||
| `/website/static-images` | Named website image slots, grouped by page | Yes |
|
||||
| `/website/special-items` | Special item carousels | Yes |
|
||||
| `/website/settings` | Website settings (special products source: product vs store item) | Yes |
|
||||
| `/website/contact` | Contact us submissions list | Yes |
|
||||
| `/website/subscriptions` | Subscriptions | Placeholder |
|
||||
| `/website/faq` | FAQ | Placeholder |
|
||||
@@ -272,11 +273,12 @@ Products accept optional `brandId` on create/update.
|
||||
- `POST /tenants/:host/contact-submissions` — public website contact form
|
||||
|
||||
### Business settings
|
||||
- `GET/PATCH /businesses/:businessId/settings` — `branding`, `dashboard`, `store`, `modules`
|
||||
- `GET/PATCH /businesses/:businessId/settings` — `branding`, `dashboard`, `store`, `modules`, `website`
|
||||
- `website.specialProductsSource`: `product` \| `store_item`. Unset → `store_item` if the store module is enabled, otherwise `product`.
|
||||
- `modules.enabled`: optional modules — business: `products`, `store`, `portfolio`, `blog`, `warehouse`, `videos`, `finance`; customer: `customer_products`. Missing key → all business modules enabled (legacy); customer modules stay opt-in. Super-admin only for modules PATCH.
|
||||
- `modules.charts`: two home chart slots (`none`, `orders_30d`, `customers_joined_1y`, `blog_views_30d`, `products_added_1y`); `none` hides that slot; defaults orders + customers.
|
||||
- Always-on (not in modules list): business — customers, website, profile, settings, home; customer — home, profile, addresses, orders, favorites
|
||||
- Tenant public: `GET /tenants/:host` includes `enabledModules` + `homeCharts`
|
||||
- Tenant public: `GET /tenants/:host` includes `enabledModules` + `homeCharts` + `specialProductsSource`
|
||||
|
||||
### Portfolios
|
||||
- `GET/POST/PATCH/DELETE /businesses/:businessId/portfolios`
|
||||
|
||||
Reference in New Issue
Block a user