import { useCallback, useEffect, useState } from 'react'; import { getSikshyaApi, SIKSHYA_ENDPOINTS } from '../api'; import { GatedFeatureWorkspace } from '../components/GatedFeatureWorkspace'; import { AddonSettingsPage } from './AddonSettingsPage'; import { ApiErrorPanel } from '../components/shared/ApiErrorPanel'; import { EmbeddableShell } from '../components/shared/EmbeddableShell'; import { HorizontalEditorTabs } from '../components/shared/HorizontalEditorTabs'; import { ListPanel } from '../components/shared/list/ListPanel'; import { ListEmptyState } from '../components/shared/list/ListEmptyState'; import { ButtonPrimary } from '../components/shared/buttons'; import { useSikshyaDialog } from '../components/shared/SikshyaDialogContext'; import { useAsyncData } from '../hooks/useAsyncData'; import { useAddonEnabled } from '../hooks/useAddons'; import { isFeatureEnabled, resolveGatedWorkspaceMode } from '../lib/licensing'; import { appViewHref } from '../lib/appUrl'; import type { SikshyaReactConfig } from '../types'; import { __ } from '../lib/i18n'; type BundleRow = { id: number; title: string; slug: string; price: number | string; currency: string; status: string; visible_in_listing: boolean; course_count: number; edit_url: string; single_url: string; }; type ListResp = { ok?: boolean; bundles?: BundleRow[] }; type BundlesTabId = 'bundles' | 'settings'; /** Open the course builder on the Pricing tab for a given course / bundle post. */ function courseBuilderHref(config: SikshyaReactConfig, courseId: number): string { return appViewHref(config, 'add-course', { course_id: String(courseId), force_bundle_ui: '1' }); } export function BundlesPage(props: { embedded?: boolean; config: SikshyaReactConfig; title: string }) { const { config, title } = props; const dialog = useSikshyaDialog(); const featureOk = isFeatureEnabled(config, 'course_bundles'); const addon = useAddonEnabled('course_bundles'); const mode = resolveGatedWorkspaceMode(featureOk, addon.enabled, addon.loading); const enabled = mode === 'full'; const [workspaceTab, setWorkspaceTab] = useState('bundles'); useEffect(() => { if (!enabled) { setWorkspaceTab('bundles'); } }, [enabled]); const [creating, setCreating] = useState(false); const [createError, setCreateError] = useState(null); const [titleInput, setTitleInput] = useState(''); const [priceInput, setPriceInput] = useState(''); // Purchase link per bundle (fetched on demand when row is expanded). const [purchaseUrls, setPurchaseUrls] = useState>({}); const [loadingLinks, setLoadingLinks] = useState>({}); const listLoader = useCallback(async () => { if (!enabled) return { ok: true, bundles: [] as BundleRow[] }; return getSikshyaApi().get(SIKSHYA_ENDPOINTS.pro.bundles); }, [enabled]); const list = useAsyncData(listLoader, [enabled]); const bundleRows = list.data?.bundles ?? []; const fetchPurchaseLink = async (id: number) => { if (purchaseUrls[id] || loadingLinks[id]) return; setLoadingLinks((p) => ({ ...p, [id]: true })); try { const r = await getSikshyaApi().get<{ ok?: boolean; url?: string }>( SIKSHYA_ENDPOINTS.pro.bundlePurchaseLink(id) ); if (r.ok && r.url) { setPurchaseUrls((p) => ({ ...p, [id]: r.url as string })); } } finally { setLoadingLinks((p) => ({ ...p, [id]: false })); } }; const [expandedId, setExpandedId] = useState(null); useEffect(() => { if (expandedId && !purchaseUrls[expandedId]) { void fetchPurchaseLink(expandedId); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [expandedId]); /** Create a bare bundle post then immediately open the Course Builder on the Pricing tab. */ const onCreate = async () => { const t = titleInput.trim(); if (!t) { setCreateError('Bundle title is required.'); return; } setCreateError(null); setCreating(true); try { const r = await getSikshyaApi().post<{ ok?: boolean; id?: number; edit_url?: string; message?: string }>( SIKSHYA_ENDPOINTS.pro.bundles, { title: t, price: parseFloat(priceInput) || 0, visible_in_listing: true, } ); if (r.ok && r.id) { // Go straight to the Course Builder → Pricing tab where the instructor // fills in the bundle details (price, included courses, visibility). window.location.href = courseBuilderHref(config, r.id); } else { setCreateError(r.message || 'Could not create bundle.'); setCreating(false); } } catch (err) { setCreateError(err instanceof Error ? err.message : 'Could not create bundle.'); setCreating(false); } }; const onDelete = async (id: number) => { const ok = await dialog.confirm({ title: __('Move bundle to trash?', 'sikshya'), message: __('Existing enrollments are not affected.', 'sikshya'), confirmLabel: __('Move to trash', 'sikshya'), variant: 'danger', }); if (!ok) return; try { await getSikshyaApi().delete(SIKSHYA_ENDPOINTS.pro.bundle(id)); list.refetch(); if (expandedId === id) setExpandedId(null); } catch (err) { void dialog.alert({ title: __('Delete failed', 'sikshya'), message: err instanceof Error ? err.message : 'Delete failed.', }); } }; return ( list.refetch()}> {list.loading ? __('Refreshing…', 'sikshya') : __('Refresh', 'sikshya')} ) : null } > addon.enable()} addonError={addon.error} > {enabled ? (
setWorkspaceTab(id as BundlesTabId)} />
) : null} {enabled && workspaceTab === 'settings' ? ( ) : null} {workspaceTab === 'settings' ? null : ( <> {/* ── How bundles work notice ── */}

{__('Bundles live in the Course Builder', 'sikshya')}

A bundle is a regular course with {__('type = Course Bundle', 'sikshya')}. After creating one below, you land directly on the Pricing & Access tab where you set the price, pick the included courses, and control listing visibility — all in one place.

{/* ── Quick create ── */}

{__('New bundle', 'sikshya')}

Give it a title, hit Create — you will be taken to the Course Builder to finish the setup.

void onCreate()} disabled={creating || !titleInput.trim()}> {creating ? __('Creating…', 'sikshya') : __('Create & configure →', 'sikshya')}
{createError ? (

{createError}

) : null}
{/* ── Bundle list ── */} {list.error && workspaceTab === 'bundles' ? ( list.refetch()} /> ) : null}
Existing bundles ({bundleRows.length})
{list.loading ? (
{__('Loading…', 'sikshya')}
) : bundleRows.length === 0 ? ( ) : (
    {bundleRows.map((b) => { const isExpanded = expandedId === b.id; const purchaseUrl = purchaseUrls[b.id]; return (
  • {/* ── Row summary ── */}
    Edit in Course Builder {b.single_url ? ( ) : null}
    {/* ── Expanded: purchase / storefront link ── */} {isExpanded ? (

    Storefront "add to cart" link

    Share this URL on landing pages. It adds all bundle courses to the cart at the bundle price.

    {loadingLinks[b.id] ? (

    {__('Generating link…', 'sikshya')}

    ) : purchaseUrl ? (
    e.target.select()} />
    ) : ( )}
    Open Course Builder → Pricing tab Set price, pick included courses, toggle visibility
    ) : null}
  • ); })}
)}
)}
); }