@burdenoff/microfe-coolandlovely 2026.706.2 → 2026.706.3
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/dist/coolandlovely/CoolAndLovelyRoot.js +62 -46
- package/dist/coolandlovely/CoolAndLovelyRoot.js.map +1 -1
- package/dist/coolandlovely/components/AddToLookbookButton.js +22 -22
- package/dist/coolandlovely/components/AddToLookbookButton.js.map +1 -1
- package/dist/coolandlovely/components/BrandCover.js +39 -0
- package/dist/coolandlovely/components/BrandCover.js.map +1 -0
- package/dist/coolandlovely/components/BrandCreatorAvatars.js +57 -0
- package/dist/coolandlovely/components/BrandCreatorAvatars.js.map +1 -0
- package/dist/coolandlovely/components/ColourSwatchRow.js +86 -0
- package/dist/coolandlovely/components/ColourSwatchRow.js.map +1 -0
- package/dist/coolandlovely/components/Comments.js +4 -4
- package/dist/coolandlovely/components/Comments.js.map +1 -1
- package/dist/coolandlovely/components/FileImage.js.map +1 -1
- package/dist/coolandlovely/components/ImageUploadField.js +139 -57
- package/dist/coolandlovely/components/ImageUploadField.js.map +1 -1
- package/dist/coolandlovely/components/LookCard.js +77 -0
- package/dist/coolandlovely/components/LookCard.js.map +1 -0
- package/dist/coolandlovely/components/MetricsCard.js +1 -1
- package/dist/coolandlovely/components/MetricsCard.js.map +1 -1
- package/dist/coolandlovely/components/ShopItemCard.js +45 -0
- package/dist/coolandlovely/components/ShopItemCard.js.map +1 -0
- package/dist/coolandlovely/components/StepTracker.js +35 -0
- package/dist/coolandlovely/components/StepTracker.js.map +1 -0
- package/dist/coolandlovely/components/Thumbnail.js +15 -11
- package/dist/coolandlovely/components/Thumbnail.js.map +1 -1
- package/dist/coolandlovely/components/VisibilityChip.js +32 -0
- package/dist/coolandlovely/components/VisibilityChip.js.map +1 -0
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js +94 -69
- package/dist/coolandlovely/hooks/useCoolAndLovelyApi.js.map +1 -1
- package/dist/coolandlovely/hooks/useFiles.js +49 -32
- package/dist/coolandlovely/hooks/useFiles.js.map +1 -1
- package/dist/coolandlovely/pages/HomePage.js +112 -133
- package/dist/coolandlovely/pages/HomePage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandDetailPage.js +253 -242
- package/dist/coolandlovely/pages/brands/BrandDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandFormPage.js +116 -112
- package/dist/coolandlovely/pages/brands/BrandFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/brands/BrandsListPage.js +13 -13
- package/dist/coolandlovely/pages/brands/BrandsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js +117 -95
- package/dist/coolandlovely/pages/collaborations/CollaborationDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationFormPage.js +1 -1
- package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js +50 -43
- package/dist/coolandlovely/pages/collaborations/CollaborationsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionDetailPage.js +131 -108
- package/dist/coolandlovely/pages/collections/CollectionDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/collections/CollectionFormPage.js +1 -1
- package/dist/coolandlovely/pages/collections/CollectionsListPage.js +59 -56
- package/dist/coolandlovely/pages/collections/CollectionsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/discover/DiscoverPage.js +1 -1
- package/dist/coolandlovely/pages/discover/DiscoverPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js +175 -147
- package/dist/coolandlovely/pages/items/StyleItemDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/items/StyleItemFormPage.js +1 -1
- package/dist/coolandlovely/pages/items/StyleItemsListPage.js +71 -62
- package/dist/coolandlovely/pages/items/StyleItemsListPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js +149 -127
- package/dist/coolandlovely/pages/lookbooks/LookbookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js +69 -53
- package/dist/coolandlovely/pages/lookbooks/LookbooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookDetailPage.js +135 -113
- package/dist/coolandlovely/pages/looks/LookDetailPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LookFormPage.js +122 -110
- package/dist/coolandlovely/pages/looks/LookFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/looks/LooksListPage.js +60 -97
- package/dist/coolandlovely/pages/looks/LooksListPage.js.map +1 -1
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js +372 -210
- package/dist/coolandlovely/pages/profile/StyleProfilePage.js.map +1 -1
- package/dist/coolandlovely/pages/trends/TrendsPage.js +1 -1
- package/dist/coolandlovely/pages/trends/TrendsPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js +130 -94
- package/dist/coolandlovely/pages/wardrobe/WardrobeFormPage.js.map +1 -1
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js +75 -76
- package/dist/coolandlovely/pages/wardrobe/WardrobeListPage.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrandDetailPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandDetailPage.tsx"],"sourcesContent":["/**\n * Brand Detail Page\n * @module coolandlovely/pages/brands\n *\n * Renders a single brand storefront (`cooBrand`) with its publish-lifecycle\n * transitions (`cooTransitionBrand`), collections and style items.\n */\n\nimport { useParams } from 'react-router-dom';\nimport {\n Store,\n Star,\n ExternalLink,\n Pencil,\n ArrowLeft,\n Shapes,\n Shirt,\n Loader2,\n Plus,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { TagList } from '../../components/TagList';\nimport { Comments } from '../../components/Comments';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrand, useTransitionBrand } from '../../hooks';\nimport { categoryIcon } from '../../utils/icons';\nimport { formatPrice, formatDate, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooBrandStatus } from '../../types';\n\n/** Allowed next states per the svc brand state machine (UX guidance only;\n * the service is the authority and rejects illegal jumps). */\nconst NEXT_STATES: Record<CooBrandStatus, CooBrandStatus[]> = {\n DRAFT: ['PENDING_REVIEW', 'ARCHIVED'],\n PENDING_REVIEW: ['PUBLISHED', 'DRAFT', 'ARCHIVED'],\n PUBLISHED: ['SUSPENDED', 'ARCHIVED'],\n SUSPENDED: ['PUBLISHED', 'ARCHIVED'],\n ARCHIVED: [],\n};\n\nexport function BrandDetailPage() {\n const { brandId } = useParams<{ brandId: string }>();\n const { navigateTo } = useCoolAndLovely();\n const query = useBrand(brandId);\n const transition = useTransitionBrand();\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Brand\">\n <LoadingView label=\"Loading brand…\" />\n </PageLayout>\n );\n }\n\n if (query.isError || !query.data) {\n return (\n <PageLayout title=\"Brand\">\n <ErrorView\n title=\"Brand not found\"\n message=\"This storefront could not be loaded or does not exist.\"\n onRetry={() => void query.refetch()}\n />\n </PageLayout>\n );\n }\n\n const brand = query.data;\n const collections = brand.collections ?? [];\n const items = brand.items ?? [];\n const nextStates = NEXT_STATES[brand.status];\n // Only render a website link for safe http(s) URLs (block javascript:/data: sinks).\n const safeWebsiteUrl =\n brand.websiteUrl && /^https?:\\/\\//i.test(brand.websiteUrl) ? brand.websiteUrl : undefined;\n\n return (\n <PageLayout\n title={brand.name}\n description={`@${brand.handle}`}\n actions={\n <div className=\"flex flex-wrap items-center gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/brands/${brand.id}/edit`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90\"\n >\n <Pencil className=\"h-4 w-4\" /> Edit\n </button>\n </div>\n }\n >\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Main column */}\n <div className=\"lg:col-span-2\">\n <GlassCard treatment=\"glass\" className=\"overflow-hidden p-0\">\n <Thumbnail\n icon={<Store className=\"h-12 w-12\" />}\n seed={brand.id}\n fileId={brand.cover?.id ?? brand.logo?.id}\n alt={brand.name}\n className=\"h-44 w-full\"\n />\n <div className=\"p-6\">\n <div className=\"flex items-center gap-2\">\n <h2 className=\"text-xl font-semibold text-text-primary\">{brand.name}</h2>\n {brand.isFeatured && (\n <Star className=\"h-4 w-4 fill-status-warning-text text-status-warning-text\" />\n )}\n <StatusBadge status={brand.status} className=\"ml-auto\" />\n </div>\n {brand.tagline && <p className=\"mt-2 text-sm text-text-primary\">{brand.tagline}</p>}\n {brand.bio && <p className=\"mt-3 text-sm text-text-secondary\">{brand.bio}</p>}\n\n {brand.aesthetics.length > 0 && (\n <div className=\"mt-4\">\n <p className=\"mb-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Aesthetics\n </p>\n <TagList tags={brand.aesthetics.map(titleCase)} tone=\"accent\" max={12} />\n </div>\n )}\n {brand.tags.length > 0 && (\n <div className=\"mt-3\">\n <p className=\"mb-1.5 text-xs font-medium uppercase tracking-wide text-text-secondary\">\n Tags\n </p>\n <TagList tags={brand.tags} max={12} />\n </div>\n )}\n {safeWebsiteUrl ? (\n <a\n href={safeWebsiteUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"mt-4 inline-flex max-w-full items-center gap-1.5 text-sm font-medium text-action-primary-bg hover:underline\"\n >\n <ExternalLink className=\"h-4 w-4 shrink-0\" />{' '}\n <span className=\"break-all\">{safeWebsiteUrl}</span>\n </a>\n ) : brand.websiteUrl ? (\n <p className=\"mt-4 break-all text-sm text-text-secondary\">{brand.websiteUrl}</p>\n ) : null}\n </div>\n </GlassCard>\n\n {/* Collections */}\n <section className=\"mt-6\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h3 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Shapes className=\"h-5 w-5\" /> Collections ({collections.length})\n </h3>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/collections/new?brandId=${brand.id}`)}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Plus className=\"h-4 w-4\" /> Add collection\n </button>\n </div>\n {collections.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No collections yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {collections.map((c) => (\n <button\n key={c.id}\n type=\"button\"\n onClick={() => navigateTo(`/collections/${c.id}`)}\n className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3 text-left transition-colors hover:bg-bg-sunken/50\"\n >\n <Thumbnail\n icon={<Shapes className=\"h-5 w-5\" />}\n seed={c.id}\n fileId={c.cover?.id}\n alt={c.name}\n className=\"h-11 w-11 shrink-0\"\n />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{c.name}</p>\n <p className=\"text-xs text-text-secondary\">{titleCase(c.season)}</p>\n </div>\n <StatusBadge status={c.status} />\n </button>\n ))}\n </div>\n )}\n </section>\n\n {/* Items */}\n <section className=\"mt-6\">\n <h3 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Shirt className=\"h-5 w-5\" /> Style items ({items.length})\n </h3>\n {items.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No items yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3\">\n {items.map((item) => (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => navigateTo(`/items/${item.id}`)}\n className=\"overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left transition-all hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <Thumbnail\n icon={categoryIcon(item.category, 24)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"h-24 w-full\"\n />\n <div className=\"p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{item.name}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatPrice(item.price, item.currency)}\n </p>\n </div>\n </button>\n ))}\n </div>\n )}\n </section>\n\n <Comments entityType=\"CooBrand\" entityId={brand.id} />\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-4\">\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Lifecycle</h3>\n <div className=\"mb-3 flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">Current:</span>\n <StatusBadge status={brand.status} />\n </div>\n {nextStates.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n This brand is archived (terminal state).\n </p>\n ) : (\n <div className=\"flex flex-col gap-2\">\n {nextStates.map((next) => (\n <button\n key={next}\n type=\"button\"\n disabled={transition.isPending}\n onClick={() => transition.mutate({ id: brand.id, status: next })}\n className={cn(\n 'inline-flex items-center justify-center gap-2 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50'\n )}\n >\n {transition.isPending && <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />}\n Move to {titleCase(next)}\n </button>\n ))}\n </div>\n )}\n {transition.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {transition.error instanceof Error ? transition.error.message : 'Transition failed'}\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Details</h3>\n <dl className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Handle</dt>\n <dd className=\"text-text-primary\">@{brand.handle}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Featured</dt>\n <dd className=\"text-text-primary\">{brand.isFeatured ? 'Yes' : 'No'}</dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Created</dt>\n <dd className=\"text-text-primary\">\n {formatDate(brand.createdAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Updated</dt>\n <dd className=\"text-text-primary\">\n {formatDate(brand.updatedAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n </dl>\n </GlassCard>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BrandDetailPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandDetailPage.tsx"],"sourcesContent":["/**\n * Brand Detail Page\n * @module coolandlovely/pages/brands\n *\n * Renders a single brand storefront (`cooBrand`) with its publish-lifecycle\n * transitions (`cooTransitionBrand`), collections and style items.\n */\n\nimport { useParams } from 'react-router-dom';\nimport { Store, ExternalLink, Pencil, ArrowLeft, Shapes, Shirt, Loader2, Plus } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { StatusBadge } from '../../components/StatusBadge';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { Thumbnail } from '../../components/Thumbnail';\nimport { BrandCover } from '../../components/BrandCover';\nimport { TagList } from '../../components/TagList';\nimport { Comments } from '../../components/Comments';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrand, useTransitionBrand, useUpdateBrand } from '../../hooks';\nimport { categoryIcon } from '../../utils/icons';\nimport { formatPrice, formatDate, titleCase } from '../../utils/formatters';\nimport { cn } from '../../utils/cn';\nimport type { CooBrandStatus } from '../../types';\n\n/** Allowed next states per the svc brand state machine (UX guidance only;\n * the service is the authority and rejects illegal jumps). */\nconst NEXT_STATES: Record<CooBrandStatus, CooBrandStatus[]> = {\n DRAFT: ['PENDING_REVIEW', 'ARCHIVED'],\n PENDING_REVIEW: ['PUBLISHED', 'DRAFT', 'ARCHIVED'],\n PUBLISHED: ['SUSPENDED', 'ARCHIVED'],\n SUSPENDED: ['PUBLISHED', 'ARCHIVED'],\n ARCHIVED: [],\n};\n\nexport function BrandDetailPage() {\n const { brandId } = useParams<{ brandId: string }>();\n const { navigateTo } = useCoolAndLovely();\n const query = useBrand(brandId);\n const transition = useTransitionBrand();\n const updateBrand = useUpdateBrand();\n\n if (query.isLoading) {\n return (\n <PageLayout title=\"Brand\">\n <LoadingView label=\"Loading brand…\" />\n </PageLayout>\n );\n }\n\n if (query.isError || !query.data) {\n return (\n <PageLayout title=\"Brand\">\n <ErrorView\n title=\"Brand not found\"\n message=\"This storefront could not be loaded or does not exist.\"\n onRetry={() => void query.refetch()}\n />\n </PageLayout>\n );\n }\n\n const brand = query.data;\n const collections = brand.collections ?? [];\n const items = brand.items ?? [];\n const nextStates = NEXT_STATES[brand.status];\n // Only render a website link for safe http(s) URLs (block javascript:/data: sinks).\n const safeWebsiteUrl =\n brand.websiteUrl && /^https?:\\/\\//i.test(brand.websiteUrl) ? brand.websiteUrl : undefined;\n\n return (\n <PageLayout title={brand.name} description={`@${brand.handle}`}>\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Main column */}\n <div className=\"space-y-6 lg:col-span-2\">\n {/* Storefront hero — cover + logo + identity overlay, floating actions */}\n <BrandCover\n coverFileId={brand.cover?.id}\n logoFileId={brand.logo?.id}\n name={brand.name}\n seed={brand.id}\n className=\"overflow-hidden rounded-xl border border-border-seam shadow-sm\"\n overlay={\n <>\n <div className=\"absolute right-3 top-3 flex flex-wrap items-center justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/brands')}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-bg-surface/80 px-3 py-2 text-sm font-medium text-text-primary shadow-sm backdrop-blur transition-colors hover:bg-bg-surface\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Back\n </button>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/brands/${brand.id}/edit`)}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text shadow-sm transition-colors hover:bg-action-primary-bg/90\"\n >\n <Pencil className=\"h-4 w-4\" /> Edit\n </button>\n </div>\n <div className=\"absolute inset-x-0 bottom-0 flex items-end gap-3 p-4 sm:gap-4 sm:p-5\">\n <Thumbnail\n icon={<Store className=\"h-7 w-7\" />}\n seed={brand.id}\n fileId={brand.logo?.id}\n alt={`${brand.name} logo`}\n className=\"h-16 w-16 shrink-0 rounded-full shadow-md ring-2 ring-bg-surface\"\n />\n <div className=\"min-w-0 pb-1\">\n <div className=\"flex flex-wrap items-center gap-x-2 gap-y-0.5\">\n <h2 className=\"truncate text-xl font-semibold text-text-primary sm:text-2xl\">\n {brand.name}\n </h2>\n <span className=\"text-sm text-text-secondary\">@{brand.handle}</span>\n </div>\n {brand.tagline && (\n <p className=\"mt-1 line-clamp-2 text-sm text-text-primary\">{brand.tagline}</p>\n )}\n </div>\n </div>\n </>\n }\n />\n\n {/* Storefront meta — aesthetics, status, bio, tags, website */}\n <section className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center justify-between gap-3\">\n {brand.aesthetics.length > 0 ? (\n <TagList tags={brand.aesthetics.map(titleCase)} tone=\"accent\" max={12} />\n ) : (\n <span />\n )}\n <StatusBadge status={brand.status} />\n </div>\n {brand.bio && <p className=\"text-sm text-text-secondary\">{brand.bio}</p>}\n {brand.tags.length > 0 && <TagList tags={brand.tags} max={12} />}\n {safeWebsiteUrl ? (\n <a\n href={safeWebsiteUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n className=\"inline-flex max-w-full items-center gap-1.5 text-sm font-medium text-action-primary-bg hover:underline\"\n >\n <ExternalLink className=\"h-4 w-4 shrink-0\" />{' '}\n <span className=\"break-all\">{safeWebsiteUrl}</span>\n </a>\n ) : brand.websiteUrl ? (\n <p className=\"break-all text-sm text-text-secondary\">{brand.websiteUrl}</p>\n ) : null}\n </section>\n\n {/* Collections */}\n <section className=\"mt-6\">\n <div className=\"mb-3 flex items-center justify-between gap-2\">\n <h3 className=\"flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Shapes className=\"h-5 w-5\" /> Collections ({collections.length})\n </h3>\n <button\n type=\"button\"\n onClick={() => navigateTo(`/collections/new?brandId=${brand.id}`)}\n className=\"inline-flex shrink-0 items-center gap-1.5 rounded-lg border border-border-seam px-3 py-1.5 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <Plus className=\"h-4 w-4\" /> Add collection\n </button>\n </div>\n {collections.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No collections yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {collections.map((c) => (\n <button\n key={c.id}\n type=\"button\"\n onClick={() => navigateTo(`/collections/${c.id}`)}\n className=\"flex items-center gap-3 rounded-lg border border-border-seam bg-bg-surface p-3 text-left transition-colors hover:bg-bg-sunken/50\"\n >\n <Thumbnail\n icon={<Shapes className=\"h-5 w-5\" />}\n seed={c.id}\n fileId={c.cover?.id}\n alt={c.name}\n className=\"h-11 w-11 shrink-0\"\n />\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{c.name}</p>\n <p className=\"text-xs text-text-secondary\">{titleCase(c.season)}</p>\n </div>\n <StatusBadge status={c.status} />\n </button>\n ))}\n </div>\n )}\n </section>\n\n {/* Items */}\n <section className=\"mt-6\">\n <h3 className=\"mb-3 flex items-center gap-2 text-lg font-semibold text-text-primary\">\n <Shirt className=\"h-5 w-5\" /> Style items ({items.length})\n </h3>\n {items.length === 0 ? (\n <p className=\"rounded-lg border border-border-seam bg-bg-surface p-4 text-sm text-text-secondary\">\n No items yet.\n </p>\n ) : (\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3\">\n {items.map((item) => (\n <button\n key={item.id}\n type=\"button\"\n onClick={() => navigateTo(`/items/${item.id}`)}\n className=\"overflow-hidden rounded-lg border border-border-seam bg-bg-surface text-left transition-all hover:border-accent-foreground/20 hover:shadow-md\"\n >\n <Thumbnail\n icon={categoryIcon(item.category, 24)}\n seed={item.id}\n fileId={item.image?.id}\n alt={item.name}\n className=\"h-24 w-full\"\n />\n <div className=\"p-3\">\n <p className=\"truncate text-sm font-medium text-text-primary\">{item.name}</p>\n <p className=\"mt-0.5 text-xs text-text-secondary\">\n {formatPrice(item.price, item.currency)}\n </p>\n </div>\n </button>\n ))}\n </div>\n )}\n </section>\n </div>\n\n {/* Sidebar */}\n <div className=\"space-y-4\">\n <GlassCard treatment=\"insight\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Lifecycle</h3>\n <div className=\"mb-3 flex items-center gap-2\">\n <span className=\"text-sm text-text-secondary\">Current:</span>\n <StatusBadge status={brand.status} />\n </div>\n {nextStates.length === 0 ? (\n <p className=\"text-xs text-text-secondary\">\n This brand is archived (terminal state).\n </p>\n ) : (\n <div className=\"flex flex-col gap-2\">\n {nextStates.map((next) => (\n <button\n key={next}\n type=\"button\"\n disabled={transition.isPending}\n onClick={() => transition.mutate({ id: brand.id, status: next })}\n className={cn(\n 'inline-flex items-center justify-center gap-2 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken disabled:opacity-50'\n )}\n >\n {transition.isPending && <Loader2 className=\"h-3.5 w-3.5 animate-spin\" />}\n Move to {titleCase(next)}\n </button>\n ))}\n </div>\n )}\n {transition.isError && (\n <p className=\"mt-2 text-xs text-status-error-text\">\n {transition.error instanceof Error ? transition.error.message : 'Transition failed'}\n </p>\n )}\n </GlassCard>\n\n <GlassCard treatment=\"plain\" className=\"p-5\">\n <h3 className=\"mb-3 text-sm font-semibold text-text-primary\">Details</h3>\n <dl className=\"space-y-2 text-sm\">\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Handle</dt>\n <dd className=\"text-text-primary\">@{brand.handle}</dd>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <dt className=\"text-text-secondary\">Featured</dt>\n <dd className=\"flex items-center gap-2\">\n <button\n type=\"button\"\n role=\"switch\"\n aria-checked={brand.isFeatured}\n aria-label={brand.isFeatured ? 'Unfeature this brand' : 'Feature this brand'}\n disabled={updateBrand.isPending}\n onClick={() =>\n updateBrand.mutate({\n id: brand.id,\n input: { isFeatured: !brand.isFeatured },\n })\n }\n className={cn(\n 'relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)] disabled:opacity-60',\n brand.isFeatured ? 'bg-action-primary-bg' : 'bg-bg-sunken'\n )}\n >\n <span\n className={cn(\n 'inline-block h-4 w-4 rounded-full bg-bg-surface shadow transition-transform',\n brand.isFeatured ? 'translate-x-[1.125rem]' : 'translate-x-0.5'\n )}\n />\n </button>\n {updateBrand.isPending && (\n <Loader2 className=\"h-3.5 w-3.5 animate-spin text-text-secondary\" />\n )}\n </dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Created</dt>\n <dd className=\"text-text-primary\">\n {formatDate(brand.createdAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n <div className=\"flex justify-between gap-2\">\n <dt className=\"text-text-secondary\">Updated</dt>\n <dd className=\"text-text-primary\">\n {formatDate(brand.updatedAt, { hour: undefined, minute: undefined })}\n </dd>\n </div>\n </dl>\n </GlassCard>\n\n {/* Reviews & feedback — moved into the sidebar to match the storefront layout */}\n <Comments entityType=\"CooBrand\" entityId={brand.id} className=\"mt-0\" />\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -3,17 +3,17 @@ import { PageLayout as t } from "../../components/PageLayout.js";
|
|
|
3
3
|
import { ErrorView as n, LoadingView as r } from "../../components/StateViews.js";
|
|
4
4
|
import { COO_FILE_FOLDERS as i } from "../../hooks/useFiles.js";
|
|
5
5
|
import { useBrand as a, useCreateBrand as o, useUpdateBrand as s } from "../../hooks/useCoolAndLovelyApi.js";
|
|
6
|
-
import "../../hooks/index.js";
|
|
7
6
|
import { parseCsv as c } from "../../utils/formatters.js";
|
|
7
|
+
import "../../hooks/index.js";
|
|
8
8
|
import { TextAreaField as l, TextField as u, ToggleField as d } from "../../components/Field.js";
|
|
9
9
|
import { ImageUploadField as f } from "../../components/ImageUploadField.js";
|
|
10
10
|
import { useEffect as p, useState as m } from "react";
|
|
11
|
-
import {
|
|
12
|
-
import { useParams as
|
|
13
|
-
import { jsx as
|
|
14
|
-
import { GlassCard as
|
|
11
|
+
import { Loader2 as h, Save as g, Store as _ } from "lucide-react";
|
|
12
|
+
import { useParams as v } from "react-router-dom";
|
|
13
|
+
import { jsx as y, jsxs as b } from "react/jsx-runtime";
|
|
14
|
+
import { GlassCard as x } from "@burdenoff/fe-libs/ui";
|
|
15
15
|
//#region src/coolandlovely/pages/brands/BrandFormPage.tsx
|
|
16
|
-
var
|
|
16
|
+
var S = {
|
|
17
17
|
name: "",
|
|
18
18
|
handle: "",
|
|
19
19
|
tagline: "",
|
|
@@ -25,12 +25,12 @@ var C = {
|
|
|
25
25
|
coverFileId: "",
|
|
26
26
|
isFeatured: !1
|
|
27
27
|
};
|
|
28
|
-
function
|
|
29
|
-
let { brandId:
|
|
28
|
+
function C() {
|
|
29
|
+
let { brandId: C } = v(), w = !!C, { navigateTo: T } = e(), E = a(w ? C : void 0), D = o(), O = s(), [k, A] = m(S), [j, M] = m({});
|
|
30
30
|
p(() => {
|
|
31
|
-
if (
|
|
32
|
-
let e =
|
|
33
|
-
|
|
31
|
+
if (w && E.data) {
|
|
32
|
+
let e = E.data;
|
|
33
|
+
A({
|
|
34
34
|
name: e.name,
|
|
35
35
|
handle: e.handle,
|
|
36
36
|
tagline: e.tagline ?? "",
|
|
@@ -43,179 +43,183 @@ function w() {
|
|
|
43
43
|
isFeatured: e.isFeatured
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
-
}, [
|
|
47
|
-
let
|
|
46
|
+
}, [w, E.data]);
|
|
47
|
+
let N = (e, t) => A((n) => ({
|
|
48
48
|
...n,
|
|
49
49
|
[e]: t
|
|
50
|
-
})),
|
|
50
|
+
})), P = w ? O : D, F = () => {
|
|
51
51
|
let e = {};
|
|
52
|
-
return
|
|
52
|
+
return k.name.trim() || (e.name = "Name is required"), !w && !k.handle.trim() && (e.handle = "Handle is required"), M(e), Object.keys(e).length === 0;
|
|
53
53
|
};
|
|
54
|
-
return
|
|
54
|
+
return w && E.isLoading ? /* @__PURE__ */ y(t, {
|
|
55
55
|
title: "Edit brand",
|
|
56
|
-
children: /* @__PURE__ */
|
|
57
|
-
}) :
|
|
56
|
+
children: /* @__PURE__ */ y(r, { label: "Loading brand…" })
|
|
57
|
+
}) : w && (E.isError || !E.data) ? /* @__PURE__ */ y(t, {
|
|
58
58
|
title: "Edit brand",
|
|
59
|
-
children: /* @__PURE__ */
|
|
59
|
+
children: /* @__PURE__ */ y(n, {
|
|
60
60
|
title: "Brand not found",
|
|
61
|
-
onRetry: () => void
|
|
61
|
+
onRetry: () => void E.refetch()
|
|
62
62
|
})
|
|
63
|
-
}) : /* @__PURE__ */
|
|
64
|
-
title:
|
|
65
|
-
description:
|
|
66
|
-
|
|
67
|
-
type: "button",
|
|
68
|
-
onClick: () => E(T && w ? `/brands/${w}` : "/brands"),
|
|
69
|
-
className: "inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken",
|
|
70
|
-
children: [/* @__PURE__ */ b(h, { className: "h-4 w-4" }), " Cancel"]
|
|
71
|
-
}),
|
|
72
|
-
children: /* @__PURE__ */ b("form", {
|
|
63
|
+
}) : /* @__PURE__ */ y(t, {
|
|
64
|
+
title: w ? "Edit brand" : "New brand",
|
|
65
|
+
description: w ? "Update this storefront" : "Launch a new storefront on the platform",
|
|
66
|
+
children: /* @__PURE__ */ y("form", {
|
|
73
67
|
onSubmit: (e) => {
|
|
74
|
-
e.preventDefault(),
|
|
75
|
-
id:
|
|
68
|
+
e.preventDefault(), F() && (w && C ? O.mutate({
|
|
69
|
+
id: C,
|
|
76
70
|
input: {
|
|
77
|
-
name:
|
|
78
|
-
tagline:
|
|
79
|
-
bio:
|
|
80
|
-
aesthetics: c(
|
|
81
|
-
tags: c(
|
|
82
|
-
websiteUrl:
|
|
83
|
-
logoFileId:
|
|
84
|
-
coverFileId:
|
|
85
|
-
isFeatured:
|
|
71
|
+
name: k.name.trim(),
|
|
72
|
+
tagline: k.tagline.trim() || void 0,
|
|
73
|
+
bio: k.bio.trim() || void 0,
|
|
74
|
+
aesthetics: c(k.aesthetics),
|
|
75
|
+
tags: c(k.tags),
|
|
76
|
+
websiteUrl: k.websiteUrl.trim() || void 0,
|
|
77
|
+
logoFileId: k.logoFileId || void 0,
|
|
78
|
+
coverFileId: k.coverFileId || void 0,
|
|
79
|
+
isFeatured: k.isFeatured
|
|
86
80
|
}
|
|
87
|
-
}, { onSuccess: () =>
|
|
88
|
-
name:
|
|
89
|
-
handle:
|
|
90
|
-
tagline:
|
|
91
|
-
bio:
|
|
92
|
-
aesthetics: c(
|
|
93
|
-
tags: c(
|
|
94
|
-
websiteUrl:
|
|
95
|
-
logoFileId:
|
|
96
|
-
coverFileId:
|
|
97
|
-
}, { onSuccess: (e) =>
|
|
81
|
+
}, { onSuccess: () => T(`/brands/${C}`) }) : D.mutate({
|
|
82
|
+
name: k.name.trim(),
|
|
83
|
+
handle: k.handle.trim(),
|
|
84
|
+
tagline: k.tagline.trim() || void 0,
|
|
85
|
+
bio: k.bio.trim() || void 0,
|
|
86
|
+
aesthetics: c(k.aesthetics),
|
|
87
|
+
tags: c(k.tags),
|
|
88
|
+
websiteUrl: k.websiteUrl.trim() || void 0,
|
|
89
|
+
logoFileId: k.logoFileId || void 0,
|
|
90
|
+
coverFileId: k.coverFileId || void 0
|
|
91
|
+
}, { onSuccess: (e) => {
|
|
92
|
+
k.isFeatured ? O.mutate({
|
|
93
|
+
id: e.id,
|
|
94
|
+
input: { isFeatured: !0 }
|
|
95
|
+
}, { onSettled: () => T(`/brands/${e.id}`) }) : T(`/brands/${e.id}`);
|
|
96
|
+
} }));
|
|
98
97
|
},
|
|
99
98
|
className: "mx-auto max-w-2xl",
|
|
100
|
-
children: /* @__PURE__ */ x
|
|
99
|
+
children: /* @__PURE__ */ b(x, {
|
|
101
100
|
treatment: "plain",
|
|
102
101
|
className: "p-6",
|
|
103
102
|
children: [
|
|
104
|
-
/* @__PURE__ */
|
|
103
|
+
/* @__PURE__ */ b("div", {
|
|
105
104
|
className: "mb-6 flex items-center gap-3",
|
|
106
|
-
children: [/* @__PURE__ */
|
|
105
|
+
children: [/* @__PURE__ */ y("div", {
|
|
107
106
|
className: "flex h-11 w-11 items-center justify-center rounded-lg bg-accent-purple-subtle text-text-primary",
|
|
108
|
-
children: /* @__PURE__ */
|
|
109
|
-
}), /* @__PURE__ */
|
|
107
|
+
children: /* @__PURE__ */ y(_, { className: "h-5 w-5" })
|
|
108
|
+
}), /* @__PURE__ */ b("div", { children: [/* @__PURE__ */ y("h2", {
|
|
110
109
|
className: "text-base font-semibold text-text-primary",
|
|
111
110
|
children: "Storefront details"
|
|
112
|
-
}), /* @__PURE__ */
|
|
111
|
+
}), /* @__PURE__ */ y("p", {
|
|
113
112
|
className: "text-xs text-text-secondary",
|
|
114
113
|
children: "Name your storefront and tell shoppers what your brand stands for."
|
|
115
114
|
})] })]
|
|
116
115
|
}),
|
|
117
|
-
/* @__PURE__ */
|
|
116
|
+
/* @__PURE__ */ b("div", {
|
|
118
117
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
119
|
-
children: [/* @__PURE__ */
|
|
118
|
+
children: [/* @__PURE__ */ y(u, {
|
|
120
119
|
id: "name",
|
|
121
120
|
label: "Brand name",
|
|
122
|
-
value:
|
|
123
|
-
onChange: (e) =>
|
|
121
|
+
value: k.name,
|
|
122
|
+
onChange: (e) => N("name", e),
|
|
124
123
|
placeholder: "Aurora Atelier",
|
|
125
124
|
required: !0,
|
|
126
|
-
error:
|
|
127
|
-
}), /* @__PURE__ */
|
|
125
|
+
error: j.name
|
|
126
|
+
}), /* @__PURE__ */ y(u, {
|
|
128
127
|
id: "handle",
|
|
129
128
|
label: "Handle",
|
|
130
|
-
value:
|
|
131
|
-
onChange: (e) =>
|
|
129
|
+
value: k.handle,
|
|
130
|
+
onChange: (e) => N("handle", e),
|
|
132
131
|
placeholder: "aurora-atelier",
|
|
133
|
-
required: !
|
|
134
|
-
hint:
|
|
135
|
-
error:
|
|
132
|
+
required: !w,
|
|
133
|
+
hint: w ? "Handle cannot be changed after creation" : "Unique per workspace",
|
|
134
|
+
error: j.handle
|
|
136
135
|
})]
|
|
137
136
|
}),
|
|
138
|
-
/* @__PURE__ */ b(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
137
|
+
/* @__PURE__ */ b("div", {
|
|
138
|
+
className: "mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
139
|
+
children: [/* @__PURE__ */ y(u, {
|
|
140
|
+
id: "tagline",
|
|
141
|
+
label: "Tagline",
|
|
142
|
+
value: k.tagline,
|
|
143
|
+
onChange: (e) => N("tagline", e),
|
|
144
|
+
placeholder: "Quiet luxury, loud ideas"
|
|
145
|
+
}), /* @__PURE__ */ y(l, {
|
|
146
|
+
id: "bio",
|
|
147
|
+
label: "Bio",
|
|
148
|
+
value: k.bio,
|
|
149
|
+
onChange: (e) => N("bio", e),
|
|
150
|
+
placeholder: "Tell shoppers what your brand stands for…",
|
|
151
|
+
rows: 4
|
|
152
|
+
})]
|
|
154
153
|
}),
|
|
155
|
-
/* @__PURE__ */
|
|
154
|
+
/* @__PURE__ */ b("div", {
|
|
156
155
|
className: "mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
157
|
-
children: [/* @__PURE__ */
|
|
156
|
+
children: [/* @__PURE__ */ y(u, {
|
|
158
157
|
id: "aesthetics",
|
|
159
158
|
label: "Aesthetics",
|
|
160
|
-
value:
|
|
161
|
-
onChange: (e) =>
|
|
159
|
+
value: k.aesthetics,
|
|
160
|
+
onChange: (e) => N("aesthetics", e),
|
|
162
161
|
placeholder: "minimal, Y2K, coastal",
|
|
163
162
|
hint: "Comma-separated descriptors"
|
|
164
|
-
}), /* @__PURE__ */
|
|
163
|
+
}), /* @__PURE__ */ y(u, {
|
|
165
164
|
id: "tags",
|
|
166
165
|
label: "Tags",
|
|
167
|
-
value:
|
|
168
|
-
onChange: (e) =>
|
|
166
|
+
value: k.tags,
|
|
167
|
+
onChange: (e) => N("tags", e),
|
|
169
168
|
placeholder: "sustainable, handmade",
|
|
170
169
|
hint: "Comma-separated"
|
|
171
170
|
})]
|
|
172
171
|
}),
|
|
173
|
-
/* @__PURE__ */
|
|
172
|
+
/* @__PURE__ */ y(u, {
|
|
174
173
|
id: "websiteUrl",
|
|
175
174
|
label: "Website",
|
|
176
175
|
type: "url",
|
|
177
|
-
value:
|
|
178
|
-
onChange: (e) =>
|
|
176
|
+
value: k.websiteUrl,
|
|
177
|
+
onChange: (e) => N("websiteUrl", e),
|
|
179
178
|
placeholder: "https://example.com",
|
|
180
179
|
className: "mt-4"
|
|
181
180
|
}),
|
|
182
|
-
/* @__PURE__ */
|
|
181
|
+
/* @__PURE__ */ b("div", {
|
|
183
182
|
className: "mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2",
|
|
184
|
-
children: [/* @__PURE__ */
|
|
183
|
+
children: [/* @__PURE__ */ y(f, {
|
|
185
184
|
label: "Logo",
|
|
186
|
-
value:
|
|
187
|
-
onChange: (e) =>
|
|
185
|
+
value: k.logoFileId,
|
|
186
|
+
onChange: (e) => N("logoFileId", e),
|
|
188
187
|
folder: i.brands,
|
|
189
188
|
hint: "Square storefront logo"
|
|
190
|
-
}), /* @__PURE__ */
|
|
189
|
+
}), /* @__PURE__ */ y(f, {
|
|
191
190
|
label: "Cover",
|
|
192
191
|
aspect: "wide",
|
|
193
|
-
value:
|
|
194
|
-
onChange: (e) =>
|
|
192
|
+
value: k.coverFileId,
|
|
193
|
+
onChange: (e) => N("coverFileId", e),
|
|
195
194
|
folder: i.brands,
|
|
196
195
|
hint: "Wide storefront banner"
|
|
197
196
|
})]
|
|
198
197
|
}),
|
|
199
|
-
|
|
198
|
+
/* @__PURE__ */ y(d, {
|
|
200
199
|
id: "isFeatured",
|
|
201
200
|
label: "Feature this brand",
|
|
202
|
-
checked:
|
|
203
|
-
onChange: (e) =>
|
|
201
|
+
checked: k.isFeatured,
|
|
202
|
+
onChange: (e) => N("isFeatured", e),
|
|
204
203
|
hint: "Featured brands surface in the Featured filter and carry a star badge.",
|
|
205
204
|
className: "mt-4"
|
|
206
205
|
}),
|
|
207
|
-
|
|
206
|
+
P.isError && /* @__PURE__ */ y("p", {
|
|
208
207
|
className: "mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text",
|
|
209
|
-
children:
|
|
208
|
+
children: P.error instanceof Error ? P.error.message : "Something went wrong"
|
|
210
209
|
}),
|
|
211
210
|
/* @__PURE__ */ b("div", {
|
|
212
211
|
className: "mt-6 flex justify-end gap-2",
|
|
213
|
-
children: /* @__PURE__ */
|
|
212
|
+
children: [/* @__PURE__ */ y("button", {
|
|
213
|
+
type: "button",
|
|
214
|
+
onClick: () => T(w && C ? `/brands/${C}` : "/brands"),
|
|
215
|
+
className: "inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken",
|
|
216
|
+
children: "Cancel"
|
|
217
|
+
}), /* @__PURE__ */ b("button", {
|
|
214
218
|
type: "submit",
|
|
215
|
-
disabled:
|
|
219
|
+
disabled: P.isPending,
|
|
216
220
|
className: "inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50",
|
|
217
|
-
children: [
|
|
218
|
-
})
|
|
221
|
+
children: [P.isPending ? /* @__PURE__ */ y(h, { className: "h-4 w-4 animate-spin" }) : /* @__PURE__ */ y(g, { className: "h-4 w-4" }), w ? "Save changes" : "Create brand"]
|
|
222
|
+
})]
|
|
219
223
|
})
|
|
220
224
|
]
|
|
221
225
|
})
|
|
@@ -223,6 +227,6 @@ function w() {
|
|
|
223
227
|
});
|
|
224
228
|
}
|
|
225
229
|
//#endregion
|
|
226
|
-
export {
|
|
230
|
+
export { C as BrandFormPage };
|
|
227
231
|
|
|
228
232
|
//# sourceMappingURL=BrandFormPage.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrandFormPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandFormPage.tsx"],"sourcesContent":["/**\n * Brand Create / Edit Page\n * @module coolandlovely/pages/brands\n *\n * Drives `cooCreateBrand` (new) and `cooUpdateBrand` (edit). The route param\n * `brandId` distinguishes the two modes.\n */\n\nimport { useEffect, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { ArrowLeft, Loader2, Save, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { TextField, TextAreaField, ToggleField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrand, useCreateBrand, useUpdateBrand } from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { parseCsv } from '../../utils/formatters';\n\ninterface FormState {\n name: string;\n handle: string;\n tagline: string;\n bio: string;\n aesthetics: string;\n tags: string;\n websiteUrl: string;\n logoFileId: string;\n coverFileId: string;\n isFeatured: boolean;\n}\n\nconst EMPTY: FormState = {\n name: '',\n handle: '',\n tagline: '',\n bio: '',\n aesthetics: '',\n tags: '',\n websiteUrl: '',\n logoFileId: '',\n coverFileId: '',\n isFeatured: false,\n};\n\nexport function BrandFormPage() {\n const { brandId } = useParams<{ brandId: string }>();\n const isEdit = Boolean(brandId);\n const { navigateTo } = useCoolAndLovely();\n\n const existing = useBrand(isEdit ? brandId : undefined);\n const createBrand = useCreateBrand();\n const updateBrand = useUpdateBrand();\n\n const [form, setForm] = useState<FormState>(EMPTY);\n const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});\n\n useEffect(() => {\n if (isEdit && existing.data) {\n const b = existing.data;\n setForm({\n name: b.name,\n handle: b.handle,\n tagline: b.tagline ?? '',\n bio: b.bio ?? '',\n aesthetics: b.aesthetics.join(', '),\n tags: b.tags.join(', '),\n websiteUrl: b.websiteUrl ?? '',\n logoFileId: b.logo?.id ?? '',\n coverFileId: b.cover?.id ?? '',\n isFeatured: b.isFeatured,\n });\n }\n }, [isEdit, existing.data]);\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((f) => ({ ...f, [key]: value }));\n\n const mutation = isEdit ? updateBrand : createBrand;\n\n const validate = (): boolean => {\n const next: Partial<Record<keyof FormState, string>> = {};\n if (!form.name.trim()) next.name = 'Name is required';\n if (!isEdit && !form.handle.trim()) next.handle = 'Handle is required';\n setErrors(next);\n return Object.keys(next).length === 0;\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!validate()) return;\n\n if (isEdit && brandId) {\n updateBrand.mutate(\n {\n id: brandId,\n input: {\n name: form.name.trim(),\n tagline: form.tagline.trim() || undefined,\n bio: form.bio.trim() || undefined,\n aesthetics: parseCsv(form.aesthetics),\n tags: parseCsv(form.tags),\n websiteUrl: form.websiteUrl.trim() || undefined,\n logoFileId: form.logoFileId || undefined,\n coverFileId: form.coverFileId || undefined,\n isFeatured: form.isFeatured,\n },\n },\n { onSuccess: () => navigateTo(`/brands/${brandId}`) }\n );\n } else {\n createBrand.mutate(\n {\n name: form.name.trim(),\n handle: form.handle.trim(),\n tagline: form.tagline.trim() || undefined,\n bio: form.bio.trim() || undefined,\n aesthetics: parseCsv(form.aesthetics),\n tags: parseCsv(form.tags),\n websiteUrl: form.websiteUrl.trim() || undefined,\n logoFileId: form.logoFileId || undefined,\n coverFileId: form.coverFileId || undefined,\n },\n { onSuccess: (brand) => navigateTo(`/brands/${brand.id}`) }\n );\n }\n };\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title=\"Edit brand\">\n <LoadingView label=\"Loading brand…\" />\n </PageLayout>\n );\n }\n\n if (isEdit && (existing.isError || !existing.data)) {\n return (\n <PageLayout title=\"Edit brand\">\n <ErrorView title=\"Brand not found\" onRetry={() => void existing.refetch()} />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={isEdit ? 'Edit brand' : 'New brand'}\n description={isEdit ? 'Update this storefront' : 'Launch a new storefront on the platform'}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && brandId ? `/brands/${brandId}` : '/brands')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-3 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> Cancel\n </button>\n }\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-accent-purple-subtle text-text-primary\">\n <Store className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Storefront details</h2>\n <p className=\"text-xs text-text-secondary\">\n Name your storefront and tell shoppers what your brand stands for.\n </p>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"name\"\n label=\"Brand name\"\n value={form.name}\n onChange={(v) => setField('name', v)}\n placeholder=\"Aurora Atelier\"\n required\n error={errors.name}\n />\n <TextField\n id=\"handle\"\n label=\"Handle\"\n value={form.handle}\n onChange={(v) => setField('handle', v)}\n placeholder=\"aurora-atelier\"\n required={!isEdit}\n hint={isEdit ? 'Handle cannot be changed after creation' : 'Unique per workspace'}\n error={errors.handle}\n />\n </div>\n\n <TextField\n id=\"tagline\"\n label=\"Tagline\"\n value={form.tagline}\n onChange={(v) => setField('tagline', v)}\n placeholder=\"Quiet luxury, loud ideas\"\n className=\"mt-4\"\n />\n\n <TextAreaField\n id=\"bio\"\n label=\"Bio\"\n value={form.bio}\n onChange={(v) => setField('bio', v)}\n placeholder=\"Tell shoppers what your brand stands for…\"\n rows={4}\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"aesthetics\"\n label=\"Aesthetics\"\n value={form.aesthetics}\n onChange={(v) => setField('aesthetics', v)}\n placeholder=\"minimal, Y2K, coastal\"\n hint=\"Comma-separated descriptors\"\n />\n <TextField\n id=\"tags\"\n label=\"Tags\"\n value={form.tags}\n onChange={(v) => setField('tags', v)}\n placeholder=\"sustainable, handmade\"\n hint=\"Comma-separated\"\n />\n </div>\n\n <TextField\n id=\"websiteUrl\"\n label=\"Website\"\n type=\"url\"\n value={form.websiteUrl}\n onChange={(v) => setField('websiteUrl', v)}\n placeholder=\"https://example.com\"\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <ImageUploadField\n label=\"Logo\"\n value={form.logoFileId}\n onChange={(id) => setField('logoFileId', id)}\n folder={COO_FILE_FOLDERS.brands}\n hint=\"Square storefront logo\"\n />\n <ImageUploadField\n label=\"Cover\"\n aspect=\"wide\"\n value={form.coverFileId}\n onChange={(id) => setField('coverFileId', id)}\n folder={COO_FILE_FOLDERS.brands}\n hint=\"Wide storefront banner\"\n />\n </div>\n\n {isEdit && (\n <ToggleField\n id=\"isFeatured\"\n label=\"Feature this brand\"\n checked={form.isFeatured}\n onChange={(v) => setField('isFeatured', v)}\n hint=\"Featured brands surface in the Featured filter and carry a star badge.\"\n className=\"mt-4\"\n />\n )}\n\n {mutation.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {mutation.error instanceof Error ? mutation.error.message : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"submit\"\n disabled={mutation.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {mutation.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {isEdit ? 'Save changes' : 'Create brand'}\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BrandFormPage.js","names":[],"sources":["../../../../src/coolandlovely/pages/brands/BrandFormPage.tsx"],"sourcesContent":["/**\n * Brand Create / Edit Page\n * @module coolandlovely/pages/brands\n *\n * Drives `cooCreateBrand` (new) and `cooUpdateBrand` (edit). The route param\n * `brandId` distinguishes the two modes.\n */\n\nimport { useEffect, useState } from 'react';\nimport { useParams } from 'react-router-dom';\nimport { Loader2, Save, Store } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components/PageLayout';\nimport { LoadingView, ErrorView } from '../../components/StateViews';\nimport { TextField, TextAreaField, ToggleField } from '../../components/Field';\nimport { ImageUploadField } from '../../components/ImageUploadField';\nimport { useCoolAndLovely } from '../../context/CoolAndLovelyContext';\nimport { useBrand, useCreateBrand, useUpdateBrand } from '../../hooks';\nimport { COO_FILE_FOLDERS } from '../../hooks/useFiles';\nimport { parseCsv } from '../../utils/formatters';\n\ninterface FormState {\n name: string;\n handle: string;\n tagline: string;\n bio: string;\n aesthetics: string;\n tags: string;\n websiteUrl: string;\n logoFileId: string;\n coverFileId: string;\n isFeatured: boolean;\n}\n\nconst EMPTY: FormState = {\n name: '',\n handle: '',\n tagline: '',\n bio: '',\n aesthetics: '',\n tags: '',\n websiteUrl: '',\n logoFileId: '',\n coverFileId: '',\n isFeatured: false,\n};\n\nexport function BrandFormPage() {\n const { brandId } = useParams<{ brandId: string }>();\n const isEdit = Boolean(brandId);\n const { navigateTo } = useCoolAndLovely();\n\n const existing = useBrand(isEdit ? brandId : undefined);\n const createBrand = useCreateBrand();\n const updateBrand = useUpdateBrand();\n\n const [form, setForm] = useState<FormState>(EMPTY);\n const [errors, setErrors] = useState<Partial<Record<keyof FormState, string>>>({});\n\n useEffect(() => {\n if (isEdit && existing.data) {\n const b = existing.data;\n setForm({\n name: b.name,\n handle: b.handle,\n tagline: b.tagline ?? '',\n bio: b.bio ?? '',\n aesthetics: b.aesthetics.join(', '),\n tags: b.tags.join(', '),\n websiteUrl: b.websiteUrl ?? '',\n logoFileId: b.logo?.id ?? '',\n coverFileId: b.cover?.id ?? '',\n isFeatured: b.isFeatured,\n });\n }\n }, [isEdit, existing.data]);\n\n const setField = <K extends keyof FormState>(key: K, value: FormState[K]) =>\n setForm((f) => ({ ...f, [key]: value }));\n\n const mutation = isEdit ? updateBrand : createBrand;\n\n const validate = (): boolean => {\n const next: Partial<Record<keyof FormState, string>> = {};\n if (!form.name.trim()) next.name = 'Name is required';\n if (!isEdit && !form.handle.trim()) next.handle = 'Handle is required';\n setErrors(next);\n return Object.keys(next).length === 0;\n };\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n if (!validate()) return;\n\n if (isEdit && brandId) {\n updateBrand.mutate(\n {\n id: brandId,\n input: {\n name: form.name.trim(),\n tagline: form.tagline.trim() || undefined,\n bio: form.bio.trim() || undefined,\n aesthetics: parseCsv(form.aesthetics),\n tags: parseCsv(form.tags),\n websiteUrl: form.websiteUrl.trim() || undefined,\n logoFileId: form.logoFileId || undefined,\n coverFileId: form.coverFileId || undefined,\n isFeatured: form.isFeatured,\n },\n },\n { onSuccess: () => navigateTo(`/brands/${brandId}`) }\n );\n } else {\n createBrand.mutate(\n {\n name: form.name.trim(),\n handle: form.handle.trim(),\n tagline: form.tagline.trim() || undefined,\n bio: form.bio.trim() || undefined,\n aesthetics: parseCsv(form.aesthetics),\n tags: parseCsv(form.tags),\n websiteUrl: form.websiteUrl.trim() || undefined,\n logoFileId: form.logoFileId || undefined,\n coverFileId: form.coverFileId || undefined,\n },\n {\n onSuccess: (brand) => {\n // `CooCreateBrandInput` has no `isFeatured`; if the creator toggled\n // Featured on, persist it with a follow-up `cooUpdateBrand` before\n // navigating (navigate regardless of that call's outcome).\n if (form.isFeatured) {\n updateBrand.mutate(\n { id: brand.id, input: { isFeatured: true } },\n { onSettled: () => navigateTo(`/brands/${brand.id}`) }\n );\n } else {\n navigateTo(`/brands/${brand.id}`);\n }\n },\n }\n );\n }\n };\n\n if (isEdit && existing.isLoading) {\n return (\n <PageLayout title=\"Edit brand\">\n <LoadingView label=\"Loading brand…\" />\n </PageLayout>\n );\n }\n\n if (isEdit && (existing.isError || !existing.data)) {\n return (\n <PageLayout title=\"Edit brand\">\n <ErrorView title=\"Brand not found\" onRetry={() => void existing.refetch()} />\n </PageLayout>\n );\n }\n\n return (\n <PageLayout\n title={isEdit ? 'Edit brand' : 'New brand'}\n description={isEdit ? 'Update this storefront' : 'Launch a new storefront on the platform'}\n >\n <form onSubmit={handleSubmit} className=\"mx-auto max-w-2xl\">\n <GlassCard treatment=\"plain\" className=\"p-6\">\n <div className=\"mb-6 flex items-center gap-3\">\n <div className=\"flex h-11 w-11 items-center justify-center rounded-lg bg-accent-purple-subtle text-text-primary\">\n <Store className=\"h-5 w-5\" />\n </div>\n <div>\n <h2 className=\"text-base font-semibold text-text-primary\">Storefront details</h2>\n <p className=\"text-xs text-text-secondary\">\n Name your storefront and tell shoppers what your brand stands for.\n </p>\n </div>\n </div>\n\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"name\"\n label=\"Brand name\"\n value={form.name}\n onChange={(v) => setField('name', v)}\n placeholder=\"Aurora Atelier\"\n required\n error={errors.name}\n />\n <TextField\n id=\"handle\"\n label=\"Handle\"\n value={form.handle}\n onChange={(v) => setField('handle', v)}\n placeholder=\"aurora-atelier\"\n required={!isEdit}\n hint={isEdit ? 'Handle cannot be changed after creation' : 'Unique per workspace'}\n error={errors.handle}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"tagline\"\n label=\"Tagline\"\n value={form.tagline}\n onChange={(v) => setField('tagline', v)}\n placeholder=\"Quiet luxury, loud ideas\"\n />\n <TextAreaField\n id=\"bio\"\n label=\"Bio\"\n value={form.bio}\n onChange={(v) => setField('bio', v)}\n placeholder=\"Tell shoppers what your brand stands for…\"\n rows={4}\n />\n </div>\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <TextField\n id=\"aesthetics\"\n label=\"Aesthetics\"\n value={form.aesthetics}\n onChange={(v) => setField('aesthetics', v)}\n placeholder=\"minimal, Y2K, coastal\"\n hint=\"Comma-separated descriptors\"\n />\n <TextField\n id=\"tags\"\n label=\"Tags\"\n value={form.tags}\n onChange={(v) => setField('tags', v)}\n placeholder=\"sustainable, handmade\"\n hint=\"Comma-separated\"\n />\n </div>\n\n <TextField\n id=\"websiteUrl\"\n label=\"Website\"\n type=\"url\"\n value={form.websiteUrl}\n onChange={(v) => setField('websiteUrl', v)}\n placeholder=\"https://example.com\"\n className=\"mt-4\"\n />\n\n <div className=\"mt-4 grid grid-cols-1 gap-4 sm:grid-cols-2\">\n <ImageUploadField\n label=\"Logo\"\n value={form.logoFileId}\n onChange={(id) => setField('logoFileId', id)}\n folder={COO_FILE_FOLDERS.brands}\n hint=\"Square storefront logo\"\n />\n <ImageUploadField\n label=\"Cover\"\n aspect=\"wide\"\n value={form.coverFileId}\n onChange={(id) => setField('coverFileId', id)}\n folder={COO_FILE_FOLDERS.brands}\n hint=\"Wide storefront banner\"\n />\n </div>\n\n <ToggleField\n id=\"isFeatured\"\n label=\"Feature this brand\"\n checked={form.isFeatured}\n onChange={(v) => setField('isFeatured', v)}\n hint=\"Featured brands surface in the Featured filter and carry a star badge.\"\n className=\"mt-4\"\n />\n\n {mutation.isError && (\n <p className=\"mt-4 rounded-lg bg-status-error-bg-subtle px-3 py-2 text-sm text-status-error-text\">\n {mutation.error instanceof Error ? mutation.error.message : 'Something went wrong'}\n </p>\n )}\n\n <div className=\"mt-6 flex justify-end gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo(isEdit && brandId ? `/brands/${brandId}` : '/brands')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-seam px-4 py-2 text-sm font-medium text-text-primary transition-colors hover:bg-bg-sunken\"\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n disabled={mutation.isPending}\n className=\"inline-flex items-center gap-1.5 rounded-lg bg-action-primary-bg px-5 py-2 text-sm font-medium text-action-primary-text transition-colors hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {mutation.isPending ? (\n <Loader2 className=\"h-4 w-4 animate-spin\" />\n ) : (\n <Save className=\"h-4 w-4\" />\n )}\n {isEdit ? 'Save changes' : 'Create brand'}\n </button>\n </div>\n </GlassCard>\n </form>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -3,10 +3,10 @@ import { cn as t } from "../../utils/cn.js";
|
|
|
3
3
|
import { PageLayout as n } from "../../components/PageLayout.js";
|
|
4
4
|
import { StatusBadge as r } from "../../components/StatusBadge.js";
|
|
5
5
|
import { ErrorView as i, LoadingView as a } from "../../components/StateViews.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { useBrands as o } from "../../hooks/useCoolAndLovelyApi.js";
|
|
7
|
+
import { titleCase as s } from "../../utils/formatters.js";
|
|
8
8
|
import "../../hooks/index.js";
|
|
9
|
-
import {
|
|
9
|
+
import { BrandCover as c } from "../../components/BrandCover.js";
|
|
10
10
|
import { TagList as l } from "../../components/TagList.js";
|
|
11
11
|
import { EmptyState as u } from "../../components/EmptyState.js";
|
|
12
12
|
import { useState as d } from "react";
|
|
@@ -40,7 +40,7 @@ var b = 24, x = [
|
|
|
40
40
|
}
|
|
41
41
|
];
|
|
42
42
|
function S() {
|
|
43
|
-
let { navigateTo: S } = e(), [C, w] = d("ALL"), [T, E] = d(!1), [D, O] = d(0), k =
|
|
43
|
+
let { navigateTo: S } = e(), [C, w] = d("ALL"), [T, E] = d(!1), [D, O] = d(0), k = o({
|
|
44
44
|
status: C === "ALL" ? void 0 : C,
|
|
45
45
|
featured: T ? !0 : void 0,
|
|
46
46
|
pagination: {
|
|
@@ -94,18 +94,18 @@ function S() {
|
|
|
94
94
|
children: A.map((e) => /* @__PURE__ */ y("button", {
|
|
95
95
|
type: "button",
|
|
96
96
|
onClick: () => S(`/brands/${e.id}`),
|
|
97
|
-
className: "overflow-hidden rounded-
|
|
98
|
-
children: [/* @__PURE__ */ v(
|
|
99
|
-
|
|
97
|
+
className: "group flex flex-col overflow-hidden rounded-xl border border-border-seam bg-bg-surface text-left shadow-sm transition-all hover:border-accent-foreground/20 hover:shadow-md",
|
|
98
|
+
children: [/* @__PURE__ */ v(c, {
|
|
99
|
+
coverFileId: e.cover?.id,
|
|
100
|
+
logoFileId: e.logo?.id,
|
|
101
|
+
name: e.name,
|
|
100
102
|
seed: e.id,
|
|
101
|
-
|
|
102
|
-
alt: e.name,
|
|
103
|
-
className: "h-32 w-full"
|
|
103
|
+
showLogo: !0
|
|
104
104
|
}), /* @__PURE__ */ y("div", {
|
|
105
|
-
className: "
|
|
105
|
+
className: "px-4 pb-4 pt-3",
|
|
106
106
|
children: [
|
|
107
107
|
/* @__PURE__ */ y("div", {
|
|
108
|
-
className: "flex items-start justify-between gap-2",
|
|
108
|
+
className: "flex min-h-[2.5rem] items-start justify-between gap-2 pl-20",
|
|
109
109
|
children: [/* @__PURE__ */ y("div", {
|
|
110
110
|
className: "min-w-0",
|
|
111
111
|
children: [/* @__PURE__ */ y("div", {
|
|
@@ -125,7 +125,7 @@ function S() {
|
|
|
125
125
|
children: e.tagline
|
|
126
126
|
}),
|
|
127
127
|
e.aesthetics.length > 0 && /* @__PURE__ */ v(l, {
|
|
128
|
-
tags: e.aesthetics.map(
|
|
128
|
+
tags: e.aesthetics.map(s),
|
|
129
129
|
max: 4,
|
|
130
130
|
className: "mt-3",
|
|
131
131
|
tone: "accent"
|