From 35011001baed674c4540f0fba261f4ea06662d5f Mon Sep 17 00:00:00 2001 From: Gregor Klevze Date: Fri, 1 May 2026 07:45:37 +0200 Subject: [PATCH] Replace native selects with NovaSelect --- resources/js/Layouts/SettingsLayout.jsx | 21 +- resources/js/Layouts/StudioLayout.jsx | 90 +- resources/js/Pages/CategoriesPage.jsx | 82 +- .../Pages/Collection/CollectionDashboard.jsx | 67 +- .../Collection/CollectionFeaturedIndex.jsx | 113 +-- .../js/Pages/Collection/CollectionManage.jsx | 377 +++---- .../js/Pages/Collection/CollectionShow.jsx | 7 +- .../Collection/CollectionStaffProgramming.jsx | 17 +- .../Collection/CollectionStaffSurfaces.jsx | 24 +- .../Collection/FeaturedArtworksAdmin.jsx | 23 +- .../Pages/Collection/NovaCardsAdminIndex.jsx | 71 +- .../Collection/NovaCardsAssetPackAdmin.jsx | 15 +- .../Collection/NovaCardsChallengeAdmin.jsx | 23 +- .../Collection/NovaCardsCollectionAdmin.jsx | 28 +- .../Collection/NovaCardsTemplateAdmin.jsx | 53 +- .../js/Pages/Collection/SavedCollections.jsx | 5 +- resources/js/Pages/Group/GroupShow.jsx | 11 +- .../js/Pages/Moderation/AiBiographyAdmin.jsx | 19 +- .../Pages/Moderation/ArtworkMaturityQueue.jsx | 37 +- resources/js/Pages/Studio/StudioActivity.jsx | 17 +- .../js/Pages/Studio/StudioArtworkEdit.jsx | 18 +- resources/js/Pages/Studio/StudioAssets.jsx | 58 +- resources/js/Pages/Studio/StudioCalendar.jsx | 250 ++++- .../js/Pages/Studio/StudioCardEditor.jsx | 158 ++- resources/js/Pages/Studio/StudioComments.jsx | 39 +- resources/js/Pages/Studio/StudioFollowers.jsx | 17 +- .../js/Pages/Studio/StudioGroupAssets.jsx | 25 +- .../Studio/StudioGroupChallengeEditor.jsx | 101 +- .../js/Pages/Studio/StudioGroupCreate.jsx | 17 +- .../Pages/Studio/StudioGroupEventEditor.jsx | 25 +- .../Pages/Studio/StudioGroupInvitations.jsx | 7 +- .../js/Pages/Studio/StudioGroupMembers.jsx | 13 +- .../js/Pages/Studio/StudioGroupPostEditor.jsx | 9 +- .../Pages/Studio/StudioGroupProjectEditor.jsx | 50 +- .../Pages/Studio/StudioGroupRecruitment.jsx | 25 +- .../Pages/Studio/StudioGroupReleaseEditor.jsx | 56 +- .../js/Pages/Studio/StudioGroupReleases.jsx | 5 +- .../js/Pages/Studio/StudioGroupSettings.jsx | 14 +- resources/js/Pages/Studio/StudioInbox.jsx | 9 +- .../js/Pages/Studio/StudioNewsEditor.jsx | 684 ++++++++++--- resources/js/Pages/Studio/StudioNewsIndex.jsx | 68 +- .../js/Pages/Studio/StudioPreferences.jsx | 49 +- resources/js/Pages/Studio/StudioScheduled.jsx | 17 +- resources/js/Pages/Studio/StudioSearch.jsx | 5 +- .../js/Pages/Studio/StudioUploadQueue.jsx | 921 ++++++++++++++++++ .../js/Pages/Studio/StudioWorldsIndex.jsx | 31 +- .../components/Studio/BulkCategoryModal.jsx | 27 +- .../js/components/docs/DocsSidebarNav.jsx | 27 +- .../js/components/editor/StoryEditor.tsx | 693 +++++++------ .../nova-cards/NovaCardPresetPicker.jsx | 14 +- resources/js/components/ui/Select.jsx | 113 +-- .../js/components/upload/CategorySelector.jsx | 41 +- .../js/components/uploads/UploadWizard.jsx | 17 +- .../suggestions/WorldSuggestionActions.jsx | 55 ++ .../suggestions/WorldSuggestionFilters.jsx | 40 + 55 files changed, 3136 insertions(+), 1662 deletions(-) create mode 100644 resources/js/Pages/Studio/StudioUploadQueue.jsx create mode 100644 resources/js/components/worlds/editor/suggestions/WorldSuggestionActions.jsx create mode 100644 resources/js/components/worlds/editor/suggestions/WorldSuggestionFilters.jsx diff --git a/resources/js/Layouts/SettingsLayout.jsx b/resources/js/Layouts/SettingsLayout.jsx index bb016814..392fa838 100644 --- a/resources/js/Layouts/SettingsLayout.jsx +++ b/resources/js/Layouts/SettingsLayout.jsx @@ -1,5 +1,6 @@ import React, { useState } from 'react' import { Link, usePage } from '@inertiajs/react' +import NovaSelect from '../components/ui/NovaSelect' const navItems = [ { label: 'Profile', href: '/dashboard/profile', icon: 'fa-solid fa-user' }, @@ -110,20 +111,16 @@ export default function SettingsLayout({ children, title, sections = null, activ
{hasSectionMode ? (
-
))} + {isStaff && ( +
+

Administration

+ +
+ )}
diff --git a/resources/js/Pages/CategoriesPage.jsx b/resources/js/Pages/CategoriesPage.jsx index de56f1d8..c369b75b 100644 --- a/resources/js/Pages/CategoriesPage.jsx +++ b/resources/js/Pages/CategoriesPage.jsx @@ -2,6 +2,7 @@ import React, { startTransition, useDeferredValue, useEffect, useRef, useState } import { createRoot } from 'react-dom/client' import CategoryCard from '../components/category/CategoryCard' import Pagination from '../components/forum/Pagination' +import NovaSelect from '../components/ui/NovaSelect' const SORT_OPTIONS = [ { value: 'popular', label: 'Popular' }, @@ -13,6 +14,26 @@ const PAGE_SIZE = 24 const numberFormatter = new Intl.NumberFormat() +function normalizeInitialData(initialData) { + if (!initialData || typeof initialData !== 'object') { + return { + data: [], + meta: { current_page: 1, last_page: 1, per_page: PAGE_SIZE, total: 0 }, + summary: { total_categories: 0, total_artworks: 0 }, + popular_categories: [], + request: { query: '', sort: 'popular', page: 1 }, + } + } + + return { + data: Array.isArray(initialData.data) ? initialData.data : [], + meta: initialData.meta || { current_page: 1, last_page: 1, per_page: PAGE_SIZE, total: 0 }, + summary: initialData.summary || { total_categories: 0, total_artworks: 0 }, + popular_categories: Array.isArray(initialData.popular_categories) ? initialData.popular_categories : [], + request: initialData.request || { query: '', sort: 'popular', page: 1 }, + } +} + function LoadingGrid() { return (
@@ -113,17 +134,21 @@ function syncQueryState({ page, sort, query }) { window.history.replaceState({}, '', url.toString()) } -function CategoriesPage({ apiUrl = '/api/categories', pageTitle = 'Categories', pageDescription = '' }) { - const [categories, setCategories] = useState([]) - const [popularCategories, setPopularCategories] = useState([]) - const [meta, setMeta] = useState({ current_page: 1, last_page: 1, per_page: PAGE_SIZE, total: 0 }) - const [summary, setSummary] = useState({ total_categories: 0, total_artworks: 0 }) - const [loading, setLoading] = useState(true) +function CategoriesPage({ apiUrl = '/api/categories', pageTitle = 'Categories', pageDescription = '', initialData = null }) { + const bootstrap = normalizeInitialData(initialData) + const hasInitialData = initialData !== null + const initialRequestRef = useRef(hasInitialData ? bootstrap.request : null) + + const [categories, setCategories] = useState(() => (hasInitialData ? bootstrap.data : [])) + const [popularCategories, setPopularCategories] = useState(() => (hasInitialData ? bootstrap.popular_categories : [])) + const [meta, setMeta] = useState(() => (hasInitialData ? bootstrap.meta : { current_page: 1, last_page: 1, per_page: PAGE_SIZE, total: 0 })) + const [summary, setSummary] = useState(() => (hasInitialData ? bootstrap.summary : { total_categories: 0, total_artworks: 0 })) + const [loading, setLoading] = useState(() => !hasInitialData) const [loadingMore, setLoadingMore] = useState(false) const [error, setError] = useState(false) - const [searchQuery, setSearchQuery] = useState(() => getInitialSearchQuery()) - const [sort, setSort] = useState(() => getInitialSort()) - const [currentPage, setCurrentPage] = useState(() => getInitialPage()) + const [searchQuery, setSearchQuery] = useState(() => (hasInitialData ? (bootstrap.request.query || '') : getInitialSearchQuery())) + const [sort, setSort] = useState(() => (hasInitialData ? (bootstrap.request.sort || 'popular') : getInitialSort())) + const [currentPage, setCurrentPage] = useState(() => (hasInitialData ? (bootstrap.request.page || 1) : getInitialPage())) const deferredQuery = useDeferredValue(searchQuery) const sentinelRef = useRef(null) @@ -199,6 +224,20 @@ function CategoriesPage({ apiUrl = '/api/categories', pageTitle = 'Categories', } useEffect(() => { + const initialRequest = initialRequestRef.current + + if (initialRequest) { + const sameQuery = (initialRequest.query || '') === deferredQuery + const sameSort = (initialRequest.sort || 'popular') === sort + const samePage = Number(initialRequest.page || 1) === currentPage + + initialRequestRef.current = null + + if (sameQuery && sameSort && samePage) { + return undefined + } + } + const controller = new AbortController() void loadCategories({ @@ -334,24 +373,19 @@ function CategoriesPage({ apiUrl = '/api/categories', pageTitle = 'Categories', /> -
@@ -438,6 +472,7 @@ function CategoriesPage({ apiUrl = '/api/categories', pageTitle = 'Categories', ) } +if (typeof document !== 'undefined') { const mountElement = document.getElementById('categories-page-root') if (mountElement) { @@ -452,5 +487,6 @@ if (mountElement) { createRoot(mountElement).render() } +} export default CategoriesPage diff --git a/resources/js/Pages/Collection/CollectionDashboard.jsx b/resources/js/Pages/Collection/CollectionDashboard.jsx index 265833e6..667b9653 100644 --- a/resources/js/Pages/Collection/CollectionDashboard.jsx +++ b/resources/js/Pages/Collection/CollectionDashboard.jsx @@ -1,6 +1,8 @@ import React from 'react' import { Head, usePage } from '@inertiajs/react' import CollectionCard from '../../components/profile/collections/CollectionCard' +import Checkbox from '../../components/ui/Checkbox' +import NovaSelect from '../../components/ui/NovaSelect' const DEFAULT_SEARCH_FILTERS = { q: '', @@ -262,13 +264,7 @@ function BulkActionsPanel({
- + onFormChange('action', val)} searchable={false} options={[{ value: 'archive', label: 'Archive' }, { value: 'assign_campaign', label: 'Assign campaign' }, { value: 'update_lifecycle', label: 'Update lifecycle' }, { value: 'request_ai_review', label: 'Request AI review' }, { value: 'mark_editorial_review', label: 'Mark editorial review' }]} /> {form.action === 'assign_campaign' ? ( @@ -284,11 +280,7 @@ function BulkActionsPanel({ {form.action === 'update_lifecycle' ? ( - + onFormChange('lifecycle_state', val)} searchable={false} options={[{ value: 'draft', label: 'Draft' }, { value: 'published', label: 'Published' }, { value: 'archived', label: 'Archived' }]} /> ) : null} @@ -344,15 +336,13 @@ function SearchResults({ state, endpoints, selectedIds, onToggleSelected }) { {state.collections.map((collection) => (
- +
@@ -584,56 +574,27 @@ export default function CollectionDashboard() { - + updateFilter('type', val)} placeholder="Any type" options={(Array.isArray(filterOptions.types) ? filterOptions.types : []).map((o) => ({ value: o, label: titleize(o) }))} /> - + updateFilter('visibility', val)} placeholder="Any visibility" options={(Array.isArray(filterOptions.visibilities) ? filterOptions.visibilities : []).map((o) => ({ value: o, label: titleize(o) }))} /> - + updateFilter('lifecycle_state', val)} placeholder="Any lifecycle" options={(Array.isArray(filterOptions.lifecycleStates) ? filterOptions.lifecycleStates : []).map((o) => ({ value: o, label: titleize(o) }))} /> - + updateFilter('workflow_state', val)} placeholder="Any workflow" options={(Array.isArray(filterOptions.workflowStates) ? filterOptions.workflowStates : []).map((o) => ({ value: o, label: titleize(o) }))} /> - + updateFilter('health_state', val)} placeholder="Any health state" options={(Array.isArray(filterOptions.healthStates) ? filterOptions.healthStates : []).map((o) => ({ value: o, label: titleize(o) }))} /> - + updateFilter('placement_eligibility', val)} placeholder="Any placement state" searchable={false} options={[{ value: '1', label: 'Eligible' }, { value: '0', label: 'Blocked' }]} />
diff --git a/resources/js/Pages/Collection/CollectionFeaturedIndex.jsx b/resources/js/Pages/Collection/CollectionFeaturedIndex.jsx index ea07987a..2cdbd77c 100644 --- a/resources/js/Pages/Collection/CollectionFeaturedIndex.jsx +++ b/resources/js/Pages/Collection/CollectionFeaturedIndex.jsx @@ -1,7 +1,8 @@ import React from 'react' -import { usePage } from '@inertiajs/react' +import { router, usePage } from '@inertiajs/react' import CollectionCard from '../../components/profile/collections/CollectionCard' import SeoHead from '../../components/seo/SeoHead' +import NovaSelect from '../../components/ui/NovaSelect' const SEARCH_SELECT_OPTIONS = { type: [ @@ -174,6 +175,33 @@ function SearchPanel({ search }) { const options = search.options || {} const chips = activeSearchChips(filters) + const [localFilters, setLocalFilters] = React.useState({ + q: filters.q || '', + type: filters.type || '', + sort: filters.sort || 'trending', + category: filters.category || '', + mode: filters.mode || '', + style: filters.style || '', + lifecycle_state: filters.lifecycle_state || '', + theme: filters.theme || '', + health_state: filters.health_state || '', + color: filters.color || '', + campaign_key: filters.campaign_key || '', + program_key: filters.program_key || '', + quality_tier: filters.quality_tier || '', + }) + + function updateFilter(key, val) { + setLocalFilters((curr) => ({ ...curr, [key]: val })) + } + + function handleSubmit(event) { + event.preventDefault() + const params = {} + Object.entries(localFilters).forEach(([k, v]) => { if (v) params[k] = v }) + router.get('/collections/search', params) + } + return (
@@ -183,75 +211,20 @@ function SearchPanel({ search }) {
{search?.meta?.total ?? 0} results
-
- - - - - - - - - - - - - + + updateFilter('q', e.target.value)} placeholder="Search title or summary" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35 xl:col-span-2" /> + updateFilter('type', val)} placeholder="All types" searchable={false} options={[{ value: 'personal', label: 'Personal' }, { value: 'community', label: 'Community' }, { value: 'editorial', label: 'Editorial' }]} /> + updateFilter('sort', val)} searchable={false} options={[{ value: 'trending', label: 'Trending' }, { value: 'recent', label: 'Recent' }, { value: 'quality', label: 'Quality' }, { value: 'evergreen', label: 'Evergreen' }]} /> + updateFilter('category', val)} placeholder="Any category" options={(options.category || []).map((item) => ({ value: item.value, label: item.label }))} /> + updateFilter('mode', val)} placeholder="Any curation mode" searchable={false} options={[{ value: 'manual', label: 'Manual' }, { value: 'smart', label: 'Smart' }]} /> + updateFilter('style', val)} placeholder="Any style signal" options={(options.style || []).map((item) => ({ value: item.value, label: item.label }))} /> + updateFilter('lifecycle_state', val)} placeholder="Any lifecycle" searchable={false} options={[{ value: 'published', label: 'Published' }, { value: 'featured', label: 'Featured' }, { value: 'archived', label: 'Archived' }]} /> + updateFilter('theme', val)} placeholder="Any theme" options={(options.theme || []).map((item) => ({ value: item.value, label: item.label }))} /> + updateFilter('health_state', val)} placeholder="Any quality state" searchable={false} options={[{ value: 'healthy', label: 'Healthy' }, { value: 'needs_metadata', label: 'Needs metadata' }, { value: 'stale', label: 'Stale' }, { value: 'low_content', label: 'Low content' }, { value: 'broken_items', label: 'Broken items' }, { value: 'weak_cover', label: 'Weak cover' }, { value: 'low_engagement', label: 'Low engagement' }, { value: 'duplicate_risk', label: 'Duplicate risk' }, { value: 'merge_candidate', label: 'Merge candidate' }]} /> + updateFilter('color', val)} placeholder="Any color palette" options={(options.color || []).map((item) => ({ value: item.value, label: item.label }))} /> + updateFilter('campaign_key', e.target.value)} placeholder="Campaign key" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35" /> + updateFilter('program_key', e.target.value)} placeholder="Program key" className="rounded-2xl border border-white/10 bg-[#0d1726] px-4 py-3 text-sm text-white outline-none transition focus:border-sky-300/35" /> + updateFilter('quality_tier', val)} placeholder="Any quality tier" options={(options.quality_tier || []).map((item) => ({ value: item.value, label: item.label }))} />
Reset diff --git a/resources/js/Pages/Collection/CollectionManage.jsx b/resources/js/Pages/Collection/CollectionManage.jsx index d39882fd..734002c1 100644 --- a/resources/js/Pages/Collection/CollectionManage.jsx +++ b/resources/js/Pages/Collection/CollectionManage.jsx @@ -2,6 +2,8 @@ import React, { useEffect, useMemo, useState } from 'react' import { Head, usePage } from '@inertiajs/react' import CollectionCard from '../../components/profile/collections/CollectionCard' import CollectionVisibilityBadge from '../../components/profile/collections/CollectionVisibilityBadge' +import Checkbox from '../../components/ui/Checkbox' +import NovaSelect from '../../components/ui/NovaSelect' function getCsrfToken() { if (typeof document === 'undefined') return '' @@ -466,15 +468,19 @@ function MemberCard({ member, onRoleChange, onRemove, onAccept, onDecline, onTra
{member?.can_revoke ? ( - +
+ onRoleChange?.(member, value)} + options={[ + { value: 'editor', label: 'Editor' }, + { value: 'contributor', label: 'Contributor' }, + { value: 'viewer', label: 'Viewer' }, + ]} + searchable={false} + className="text-xs font-semibold uppercase tracking-[0.14em]" + /> +
) : null} {member?.can_accept ? : null} {member?.can_decline ? : null} @@ -528,28 +534,27 @@ function LayoutModuleCard({ module, index, total, onToggle, onSlotChange, onMove
{module.label}

{module.description}

- +
- + onChange={(value) => onSlotChange(module.key, value)} + options={module.slots.map((slot) => ({ + value: slot, + label: slot === 'full' ? 'Full width' : slot === 'main' ? 'Main column' : 'Sidebar', + }))} + searchable={false} + />
@@ -646,27 +651,21 @@ function SmartRuleRow({
- + onChange={(value) => onFieldChange(value)} + options={fieldOptions.map((option) => ({ value: option.value, label: option.label }))} + searchable={false} + /> - + onChange={(value) => onOperatorChange(value)} + options={operatorOptions.map((option) => ({ value: option.value, label: option.label }))} + searchable={false} + /> {rule.field === 'created_at' ? ( @@ -688,23 +687,20 @@ function SmartRuleRow({ ) : rule.field === 'is_featured' || rule.field === 'is_mature' ? ( - + onChange={(value) => onValueChange(value === 'true')} + options={rule.field === 'is_featured' + ? [ + { value: 'true', label: 'Featured only' }, + { value: 'false', label: 'Not featured' }, + ] + : [ + { value: 'true', label: 'Mature only' }, + { value: 'false', label: 'Not mature' }, + ]} + searchable={false} + /> ) : rule.field === 'tags' ? ( @@ -718,16 +714,13 @@ function SmartRuleRow({ ) : valueOptions.length ? ( - + onChange={(value) => onValueChange(value)} + options={valueOptions.map((option) => ({ value: option.value, label: option.label }))} + placeholder="Select one" + searchable={false} + /> ) : ( @@ -1917,11 +1910,7 @@ export default function CollectionManage() { /> - + updateForm('visibility', val)} searchable={false} options={[{ value: 'public', label: 'Public — visible to everyone' }, { value: 'unlisted', label: 'Unlisted — accessible by link only' }, { value: 'private', label: 'Private — only you can see it' }]} />
@@ -1965,12 +1954,7 @@ export default function CollectionManage() { /> - + updateForm('presentation_style', val)} searchable={false} options={[{ value: 'standard', label: 'Standard' }, { value: 'editorial_grid', label: 'Editorial Grid' }, { value: 'hero_grid', label: 'Hero Grid' }, { value: 'masonry', label: 'Masonry' }]} />
@@ -1986,73 +1970,49 @@ export default function CollectionManage() {
- + updateForm('emphasis_mode', val)} searchable={false} options={[{ value: 'cover_heavy', label: 'Cover Heavy' }, { value: 'balanced', label: 'Balanced' }, { value: 'artwork_first', label: 'Artwork First' }]} /> - + updateForm('theme_token', val)} searchable={false} options={[{ value: 'default', label: 'Default' }, { value: 'subtle-blue', label: 'Subtle Blue' }, { value: 'violet', label: 'Violet' }, { value: 'amber', label: 'Amber' }]} />
{!isSmartMode ? ( - + updateForm('sort_mode', val)} searchable={false} options={[{ value: 'manual', label: 'Manual' }, { value: 'newest', label: 'Newest first' }, { value: 'oldest', label: 'Oldest first' }, { value: 'popular', label: 'Most popular' }]} /> ) : ( - + onChange={(val) => setSmartRules((current) => ({ ...current, match: val }))} + searchable={false} + options={[{ value: 'all', label: 'All rules' }, { value: 'any', label: 'Any rule' }]} + /> )} {!isSmartMode ? ( - + placeholder="Automatic cover" + options={attachedCoverOptions.map((a) => ({ value: String(a.id), label: a.title }))} + /> ) : ( - + options={(smartRuleOptions?.sort_options || [])} + /> )}
@@ -2061,48 +2021,32 @@ export default function CollectionManage() {
- + updateForm('type', val)} searchable={false} options={[{ value: 'personal', label: 'Personal' }, { value: 'community', label: 'Community' }, { value: 'editorial', label: 'Editorial' }]} /> - + updateForm('collaboration_mode', val)} searchable={false} options={[{ value: 'closed', label: 'Closed — curated by you only' }, { value: 'invite_only', label: 'Invite only' }, { value: 'open', label: 'Open submissions' }]} />
- - - - +
+ updateForm('allow_submissions', event.target.checked)} label="Allow submissions" /> +
+
+ updateForm('allow_comments', event.target.checked)} label="Allow comments" /> +
+
+ updateForm('allow_saves', event.target.checked)} label="Allow saves" /> +
+
+ updateForm('commercial_eligibility', event.target.checked)} label="Commercially eligible" /> +
{form.type === 'editorial' ? (
- + updateForm('editorial_owner_mode', val)} searchable={false} options={[{ value: 'creator', label: 'Current curator' }, { value: 'staff_account', label: 'Staff account' }, { value: 'system', label: 'System editorial identity' }]} /> {form.editorial_owner_mode === 'staff_account' ? ( @@ -2156,13 +2100,7 @@ export default function CollectionManage() { updateForm('campaign_label', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={120} /> - + updateForm('spotlight_style', val)} searchable={false} options={[{ value: 'default', label: 'Default' }, { value: 'editorial', label: 'Editorial' }, { value: 'seasonal', label: 'Seasonal' }, { value: 'challenge', label: 'Challenge' }, { value: 'community', label: 'Community' }]} /> updateForm('banner_text', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={200} /> @@ -2195,14 +2133,7 @@ export default function CollectionManage() { - + updateForm('lifecycle_state', val)} searchable={false} options={[{ value: 'draft', label: 'Draft' }, { value: 'scheduled', label: 'Scheduled' }, { value: 'published', label: 'Published' }, { value: 'featured', label: 'Featured' }, { value: 'archived', label: 'Archived' }, { value: 'expired', label: 'Expired' }]} />
@@ -2244,10 +2175,9 @@ export default function CollectionManage() { updateForm('brand_safe_status', event.target.value)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" maxLength={40} /> - +
+ updateForm('analytics_enabled', event.target.checked)} label="Analytics enabled" /> +
@@ -2800,19 +2730,9 @@ export default function CollectionManage() {
setInviteUsername(event.target.value)} placeholder="username" className="min-w-0 flex-1 rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" /> - + setInviteRole(val)} searchable={false} options={[{ value: 'editor', label: 'Editor' }, { value: 'contributor', label: 'Contributor' }, { value: 'viewer', label: 'Viewer' }]} />
- + setInviteExpiryMode(val)} searchable={false} options={[{ value: 'default', label: `Default (${inviteExpiryDays} days)` }, ...inviteExpiryOptions.map((days) => ({ value: String(days), label: `${days} day${days === 1 ? '' : 's'}` })), { value: 'custom', label: 'Custom date' }]} /> {inviteExpiryMode === 'custom' ? ( setInviteCustomExpiry(event.target.value)} className="rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none transition focus:border-sky-300/35 focus:bg-white/[0.06]" /> ) : ( @@ -3116,16 +3036,12 @@ export default function CollectionManage() {
- + onChange={(value) => setSelectedLinkedCollectionId(value)} + options={linkedCollectionOptions.map((item) => ({ value: String(item.id), label: item.title }))} + placeholder="Select a collection" + />
) : ( diff --git a/resources/js/Pages/Collection/CollectionStaffProgramming.jsx b/resources/js/Pages/Collection/CollectionStaffProgramming.jsx index ac03f650..af6c7090 100644 --- a/resources/js/Pages/Collection/CollectionStaffProgramming.jsx +++ b/resources/js/Pages/Collection/CollectionStaffProgramming.jsx @@ -2,6 +2,8 @@ import React from 'react' import { Head, usePage } from '@inertiajs/react' import CollectionCard from '../../components/profile/collections/CollectionCard' import ShareToast from '../../components/ui/ShareToast' +import Checkbox from '../../components/ui/Checkbox' +import NovaSelect from '../../components/ui/NovaSelect' function getCsrfToken() { if (typeof document === 'undefined') return '' @@ -618,9 +620,7 @@ export default function CollectionStaffProgramming() {
- + setAssignmentForm((current) => ({ ...current, collection_id: val }))} options={collectionOptions.map((o) => ({ value: String(o.id), label: o.title }))} /> setAssignmentForm((current) => ({ ...current, program_key: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" maxLength={80} /> @@ -715,9 +715,7 @@ export default function CollectionStaffProgramming() {
- + setSelectedCollectionId(val)} options={collectionOptions.map((o) => ({ value: String(o.id), label: o.title }))} />
@@ -824,10 +822,9 @@ export default function CollectionStaffProgramming() { ) :
Partner, sponsorship, ownership, and review metadata remain admin-only. Moderators can still manage experiment and promotion hooks here.
}
- +
+ setHooksForm((current) => ({ ...current, placement_eligibility: event.target.checked }))} label="Placement eligible override" /> +
diff --git a/resources/js/Pages/Collection/CollectionStaffSurfaces.jsx b/resources/js/Pages/Collection/CollectionStaffSurfaces.jsx index 60e52932..23d54484 100644 --- a/resources/js/Pages/Collection/CollectionStaffSurfaces.jsx +++ b/resources/js/Pages/Collection/CollectionStaffSurfaces.jsx @@ -1,6 +1,8 @@ import React from 'react' import { Head, usePage } from '@inertiajs/react' import CollectionCard from '../../components/profile/collections/CollectionCard' +import Checkbox from '../../components/ui/Checkbox' +import NovaSelect from '../../components/ui/NovaSelect' function getCsrfToken() { if (typeof document === 'undefined') return '' @@ -401,13 +403,13 @@ export default function CollectionStaffSurfaces() {
setDefinitionForm((current) => ({ ...current, surface_key: event.target.value }))} disabled={Boolean(definitionForm.id)} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none disabled:cursor-not-allowed disabled:opacity-60" maxLength={120} /> setDefinitionForm((current) => ({ ...current, title: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" maxLength={160} /> - - + setDefinitionForm((current) => ({ ...current, mode: val }))} searchable={false} options={[{ value: 'manual', label: 'Manual' }, { value: 'automatic', label: 'Automatic' }, { value: 'hybrid', label: 'Hybrid' }]} /> + setDefinitionForm((current) => ({ ...current, ranking_mode: val }))} searchable={false} options={[{ value: 'ranking_score', label: 'Ranking score' }, { value: 'recent_activity', label: 'Recent activity' }, { value: 'quality_score', label: 'Quality score' }]} /> setDefinitionForm((current) => ({ ...current, max_items: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" /> setDefinitionForm((current) => ({ ...current, starts_at: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" /> setDefinitionForm((current) => ({ ...current, ends_at: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" /> setDefinitionForm((current) => ({ ...current, fallback_surface_key: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-white/[0.04] px-4 py-3 text-white outline-none" maxLength={120} /> - +
setDefinitionForm((current) => ({ ...current, is_active: event.target.checked }))} label="Active" />