Files
SkinbaseNova/resources/js/Pages/Studio/StudioPreferences.jsx

303 lines
15 KiB
JavaScript

import React, { useEffect, useState } from 'react'
import { usePage } from '@inertiajs/react'
import StudioLayout from '../../Layouts/StudioLayout'
import { studioSurface, trackStudioEvent } from '../../utils/studioEvents'
const shortcutOptions = [
{ value: '/dashboard/profile', label: 'Dashboard profile' },
{ value: '/dashboard/notifications', label: 'Notifications' },
{ value: '/dashboard/comments/received', label: 'Received comments' },
{ value: '/dashboard/followers', label: 'Followers' },
{ value: '/dashboard/following', label: 'Following' },
{ value: '/dashboard/favorites', label: 'Favorites' },
{ value: '/dashboard/artworks', label: 'Artwork dashboard' },
{ value: '/dashboard/gallery', label: 'Gallery' },
{ value: '/dashboard/awards', label: 'Awards' },
{ value: '/creator/stories', label: 'Story dashboard' },
{ value: '/studio', label: 'Creator Studio' },
]
const widgetOptions = [
{ value: 'quick_stats', label: 'Quick stats' },
{ value: 'continue_working', label: 'Continue working' },
{ value: 'scheduled_items', label: 'Scheduled items' },
{ value: 'recent_activity', label: 'Recent activity' },
{ value: 'top_performers', label: 'Top performers' },
{ value: 'draft_reminders', label: 'Draft reminders' },
{ value: 'module_summaries', label: 'Module summaries' },
{ value: 'growth_hints', label: 'Growth hints' },
{ value: 'active_challenges', label: 'Active challenges' },
{ value: 'creator_health', label: 'Creator health' },
{ value: 'featured_status', label: 'Featured status' },
{ value: 'comments_snapshot', label: 'Comments snapshot' },
{ value: 'stale_drafts', label: 'Stale drafts' },
]
const landingOptions = [
['overview', 'Overview'],
['content', 'Content'],
['drafts', 'Drafts'],
['scheduled', 'Scheduled'],
['calendar', 'Calendar'],
['inbox', 'Inbox'],
['analytics', 'Analytics'],
['growth', 'Growth'],
['challenges', 'Challenges'],
['search', 'Search'],
['preferences', 'Preferences'],
]
async function requestJson(url, method, body) {
const response = await fetch(url, {
method,
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '',
'X-Requested-With': 'XMLHttpRequest',
},
body: body ? JSON.stringify(body) : undefined,
})
const payload = await response.json().catch(() => ({}))
if (!response.ok) {
throw new Error(payload?.message || payload?.error || 'Request failed')
}
return payload
}
export default function StudioPreferences() {
const { props } = usePage()
const preferences = props.preferences || {}
const [form, setForm] = useState({
default_content_view: preferences.default_content_view || 'grid',
analytics_range_days: preferences.analytics_range_days || 30,
dashboard_shortcuts: preferences.dashboard_shortcuts || [],
draft_behavior: preferences.draft_behavior || 'resume-last',
default_landing_page: preferences.default_landing_page || 'overview',
widget_visibility: preferences.widget_visibility || {},
widget_order: preferences.widget_order || widgetOptions.map((option) => option.value),
card_density: preferences.card_density || 'comfortable',
scheduling_timezone: preferences.scheduling_timezone || '',
})
const [saving, setSaving] = useState(false)
useEffect(() => {
setForm({
default_content_view: preferences.default_content_view || 'grid',
analytics_range_days: preferences.analytics_range_days || 30,
dashboard_shortcuts: preferences.dashboard_shortcuts || [],
draft_behavior: preferences.draft_behavior || 'resume-last',
default_landing_page: preferences.default_landing_page || 'overview',
widget_visibility: preferences.widget_visibility || {},
widget_order: preferences.widget_order || widgetOptions.map((option) => option.value),
card_density: preferences.card_density || 'comfortable',
scheduling_timezone: preferences.scheduling_timezone || '',
})
}, [preferences])
const toggleShortcut = (value) => {
setForm((current) => ({
...current,
dashboard_shortcuts: current.dashboard_shortcuts.includes(value)
? current.dashboard_shortcuts.filter((entry) => entry !== value)
: [...current.dashboard_shortcuts, value].slice(0, 8),
}))
}
const toggleWidget = (value) => {
setForm((current) => ({
...current,
widget_visibility: {
...current.widget_visibility,
[value]: !(current.widget_visibility?.[value] !== false),
},
}))
}
const moveWidget = (value, direction) => {
setForm((current) => {
const items = [...current.widget_order]
const index = items.indexOf(value)
if (index < 0) return current
const nextIndex = direction === 'up' ? index - 1 : index + 1
if (nextIndex < 0 || nextIndex >= items.length) return current
const swapped = items[nextIndex]
items[nextIndex] = value
items[index] = swapped
trackStudioEvent('studio_widget_reordered', {
surface: studioSurface(),
module: 'preferences',
meta: {
widget: value,
direction,
from: index + 1,
to: nextIndex + 1,
},
})
return { ...current, widget_order: items }
})
}
const saveSettings = async () => {
setSaving(true)
try {
await requestJson(props.endpoints.save, 'PUT', form)
window.alert('Studio preferences saved.')
} catch (error) {
window.alert(error?.message || 'Unable to save Studio preferences.')
} finally {
setSaving(false)
}
}
return (
<StudioLayout title={props.title} subtitle={props.description}>
<div className="grid gap-6 xl:grid-cols-[minmax(0,1fr)_360px]">
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
<div className="flex items-center justify-between gap-4">
<div>
<h2 className="text-lg font-semibold text-white">Workspace preferences</h2>
<p className="mt-1 text-sm text-slate-400">Choose where Studio opens, how dense content cards feel, and which overview modules stay visible.</p>
</div>
<button type="button" onClick={saveSettings} disabled={saving} className="inline-flex items-center gap-2 rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100 disabled:opacity-50">
<i className="fa-solid fa-floppy-disk" />
{saving ? 'Saving...' : 'Save preferences'}
</button>
</div>
<div className="mt-6 grid gap-4 md:grid-cols-2">
<label className="space-y-2 text-sm text-slate-300">
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Default content view</span>
<select value={form.default_content_view} onChange={(event) => setForm((current) => ({ ...current, default_content_view: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white">
<option value="grid" className="bg-slate-900">Grid</option>
<option value="list" className="bg-slate-900">List</option>
</select>
</label>
<label className="space-y-2 text-sm text-slate-300">
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Analytics date range</span>
<select value={form.analytics_range_days} onChange={(event) => setForm((current) => ({ ...current, analytics_range_days: Number(event.target.value) }))} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white">
{[7, 14, 30, 60, 90].map((days) => (
<option key={days} value={days} className="bg-slate-900">Last {days} days</option>
))}
</select>
</label>
<label className="space-y-2 text-sm text-slate-300 md:col-span-2">
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Draft behavior</span>
<select value={form.draft_behavior} onChange={(event) => setForm((current) => ({ ...current, draft_behavior: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white">
<option value="resume-last" className="bg-slate-900">Resume the last draft I edited</option>
<option value="open-drafts" className="bg-slate-900">Open the drafts library first</option>
<option value="focus-published" className="bg-slate-900">Open published content first</option>
</select>
</label>
<label className="space-y-2 text-sm text-slate-300">
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Default landing page</span>
<select value={form.default_landing_page} onChange={(event) => setForm((current) => ({ ...current, default_landing_page: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white">
{landingOptions.map(([value, label]) => (
<option key={value} value={value} className="bg-slate-900">{label}</option>
))}
</select>
</label>
<label className="space-y-2 text-sm text-slate-300">
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Card density</span>
<select value={form.card_density} onChange={(event) => setForm((current) => ({ ...current, card_density: event.target.value }))} className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white">
<option value="comfortable" className="bg-slate-900">Comfortable</option>
<option value="compact" className="bg-slate-900">Compact</option>
</select>
</label>
<label className="space-y-2 text-sm text-slate-300 md:col-span-2">
<span className="block text-[11px] font-semibold uppercase tracking-[0.18em] text-slate-500">Scheduling timezone</span>
<input value={form.scheduling_timezone} onChange={(event) => setForm((current) => ({ ...current, scheduling_timezone: event.target.value }))} placeholder="Europe/Helsinki" className="w-full rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-sm text-white" />
</label>
</div>
<div className="mt-6">
<div className="flex items-center justify-between gap-4">
<div>
<h3 className="text-base font-semibold text-white">Dashboard shortcuts</h3>
<p className="mt-1 text-sm text-slate-400">Pin up to 8 destinations that should stay easy to reach from the wider workspace.</p>
</div>
<span className="rounded-full border border-white/10 px-3 py-1 text-xs uppercase tracking-[0.16em] text-slate-400">{form.dashboard_shortcuts.length}/8 selected</span>
</div>
<div className="mt-4 grid gap-3 md:grid-cols-2">
{shortcutOptions.map((option) => {
const active = form.dashboard_shortcuts.includes(option.value)
return (
<button key={option.value} type="button" onClick={() => toggleShortcut(option.value)} className={`flex items-center justify-between rounded-[22px] border px-4 py-3 text-left transition ${active ? 'border-sky-300/25 bg-sky-300/10 text-sky-100' : 'border-white/10 bg-black/20 text-slate-300'}`}>
<span>{option.label}</span>
<i className={`fa-solid ${active ? 'fa-circle-check' : 'fa-circle'} text-sm`} />
</button>
)
})}
</div>
</div>
<div className="mt-6">
<h3 className="text-base font-semibold text-white">Overview widgets</h3>
<p className="mt-1 text-sm text-slate-400">Show, hide, and prioritize dashboard sections for your daily workflow.</p>
<div className="mt-4 space-y-3">
{form.widget_order.map((widgetKey, index) => {
const option = widgetOptions.find((entry) => entry.value === widgetKey)
if (!option) return null
const enabled = form.widget_visibility?.[widgetKey] !== false
return (
<div key={widgetKey} className="flex flex-col gap-3 rounded-[22px] border border-white/10 bg-black/20 p-4 md:flex-row md:items-center md:justify-between">
<div>
<div className="text-sm font-semibold text-white">{option.label}</div>
<div className="mt-1 text-xs uppercase tracking-[0.16em] text-slate-500">Position {index + 1}</div>
</div>
<div className="flex flex-wrap items-center gap-2">
<button type="button" onClick={() => toggleWidget(widgetKey)} className={`rounded-full border px-3 py-1.5 text-xs ${enabled ? 'border-sky-300/25 bg-sky-300/10 text-sky-100' : 'border-white/10 text-slate-300'}`}>
{enabled ? 'Visible' : 'Hidden'}
</button>
<button type="button" onClick={() => moveWidget(widgetKey, 'up')} className="rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-300">Up</button>
<button type="button" onClick={() => moveWidget(widgetKey, 'down')} className="rounded-full border border-white/10 px-3 py-1.5 text-xs text-slate-300">Down</button>
</div>
</div>
)
})}
</div>
</div>
</section>
<div className="space-y-6">
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
<h2 className="text-lg font-semibold text-white">Related surfaces</h2>
<div className="mt-4 space-y-3">
{(props.links || []).map((link) => (
<a key={link.url} href={link.url} className="block rounded-[22px] border border-white/10 bg-black/20 p-4 transition hover:border-white/20">
<div className="flex items-center gap-3 text-sky-100">
<i className={link.icon} />
<span className="text-base font-semibold text-white">{link.label}</span>
</div>
</a>
))}
</div>
</section>
<section className="rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
<h2 className="text-lg font-semibold text-white">Preference notes</h2>
<div className="mt-4 space-y-3 text-sm text-slate-400">
<div className="rounded-[22px] border border-white/10 bg-black/20 p-4">Landing page and widget order are stored in the shared Studio preference record, so new Creator Studio surfaces can plug into the same contract without another migration.</div>
<div className="rounded-[22px] border border-white/10 bg-black/20 p-4">Analytics range and card density stay here so Analytics, Growth, and the main dashboard can stay visually consistent.</div>
</div>
</section>
</div>
</div>
</StudioLayout>
)
}