Files
SkinbaseNova/.deploy/artwork-evolution-release/resources/js/Pages/Studio/StudioGroupSettings.jsx
2026-04-18 17:02:56 +02:00

187 lines
14 KiB
JavaScript

import React, { useMemo, useRef, useState } from 'react'
import { router, usePage } from '@inertiajs/react'
import StudioLayout from '../../Layouts/StudioLayout'
function resolveMediaPreviewUrl(path, filesCdnUrl) {
const trimmed = String(path || '').trim()
if (!trimmed) {
return ''
}
if (trimmed.startsWith('blob:') || trimmed.startsWith('data:') || trimmed.startsWith('http://') || trimmed.startsWith('https://')) {
return trimmed
}
return `${String(filesCdnUrl || '').replace(/\/$/, '')}/${trimmed.replace(/^\/+/, '')}`
}
export default function StudioGroupSettings() {
const { props } = usePage()
const group = props.studioGroup || {}
const filesCdnUrl = props?.cdn?.files_url || ''
const featuredArtworkOptions = Array.isArray(props.featuredArtworkOptions) ? props.featuredArtworkOptions : []
const avatarInputRef = useRef(null)
const bannerInputRef = useRef(null)
const [form, setForm] = useState({
name: group.name || '',
slug: group.slug || '',
headline: group.headline || '',
bio: group.bio || '',
type: group.type || '',
founded_at: group.founded_at ? String(group.founded_at).slice(0, 10) : '',
avatar_path: group.avatar_path || group.avatar_url || '',
banner_path: group.banner_path || group.banner_url || '',
visibility: group.visibility || 'public',
membership_policy: group.membership_policy || 'invite_only',
website_url: group.website_url || '',
links_json: Array.isArray(group.links) && group.links.length > 0 ? group.links : [{ label: '', url: '' }],
featured_artwork_id: group.featured_artwork_id || '',
avatar_file: null,
banner_file: null,
})
const [avatarPreview, setAvatarPreview] = useState('')
const [bannerPreview, setBannerPreview] = useState('')
const resolvedAvatarPreview = useMemo(() => avatarPreview || resolveMediaPreviewUrl(form.avatar_path || group.avatar_url, filesCdnUrl), [avatarPreview, form.avatar_path, group.avatar_url, filesCdnUrl])
const resolvedBannerPreview = useMemo(() => bannerPreview || resolveMediaPreviewUrl(form.banner_path || group.banner_url, filesCdnUrl), [bannerPreview, form.banner_path, group.banner_url, filesCdnUrl])
const selectedFeaturedArtwork = useMemo(
() => featuredArtworkOptions.find((item) => Number(item.id) === Number(form.featured_artwork_id)) || null,
[featuredArtworkOptions, form.featured_artwork_id],
)
const updateLink = (index, key, value) => {
setForm((current) => ({
...current,
links_json: current.links_json.map((item, itemIndex) => itemIndex === index ? { ...item, [key]: value } : item),
}))
}
const addLink = () => {
setForm((current) => ({
...current,
links_json: [...current.links_json, { label: '', url: '' }],
}))
}
const removeLink = (index) => {
setForm((current) => ({
...current,
links_json: current.links_json.filter((_, itemIndex) => itemIndex !== index),
}))
}
const submit = () => {
router.post(props.endpoints?.update, {
_method: 'patch',
...form,
links_json: form.links_json.filter((item) => item.label.trim() !== '' || item.url.trim() !== ''),
}, {
forceFormData: true,
})
}
const handleFileSelected = (field, setPreview) => (event) => {
const file = event.target.files?.[0] || null
setForm((current) => ({ ...current, [field]: file }))
setPreview(file ? URL.createObjectURL(file) : '')
}
const clearSelectedFile = (field, setPreview, inputRef) => {
setForm((current) => ({ ...current, [field]: null }))
setPreview('')
if (inputRef.current) {
inputRef.current.value = ''
}
}
const archiveGroup = () => {
if (!window.confirm('Archive this group? New group publishing will stop immediately until you reopen it through admin tooling.')) {
return
}
router.post(props.endpoints?.archive)
}
return (
<StudioLayout title={props.title} subtitle={props.description}>
<section className="mx-auto max-w-3xl rounded-[30px] border border-white/10 bg-white/[0.03] p-6">
<div className="grid gap-5">
<label className="grid gap-2 text-sm text-slate-200"><span>Name</span><input value={form.name} onChange={(event) => setForm((current) => ({ ...current, name: event.target.value }))} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" /></label>
<label className="grid gap-2 text-sm text-slate-200"><span>Slug</span><input value={form.slug} onChange={(event) => setForm((current) => ({ ...current, slug: event.target.value }))} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" /></label>
<label className="grid gap-2 text-sm text-slate-200"><span>Short description</span><input value={form.headline} onChange={(event) => setForm((current) => ({ ...current, headline: event.target.value }))} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" /></label>
<label className="grid gap-2 text-sm text-slate-200"><span>About</span><textarea value={form.bio} onChange={(event) => setForm((current) => ({ ...current, bio: event.target.value }))} rows={6} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" /></label>
<div className="grid gap-5 md:grid-cols-2">
<label className="grid gap-2 text-sm text-slate-200"><span>Type / category</span><input value={form.type} onChange={(event) => setForm((current) => ({ ...current, type: event.target.value }))} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" /></label>
<label className="grid gap-2 text-sm text-slate-200"><span>Founded date</span><input type="date" value={form.founded_at} onChange={(event) => setForm((current) => ({ ...current, founded_at: event.target.value }))} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" /></label>
</div>
<label className="grid gap-2 text-sm text-slate-200"><span>Website</span><input value={form.website_url} onChange={(event) => setForm((current) => ({ ...current, website_url: event.target.value }))} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" /></label>
<div className="grid gap-5 md:grid-cols-2">
<div className="grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-200">
<span className="text-sm font-semibold text-white">Avatar / logo</span>
<div className="flex h-28 w-28 items-center justify-center overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04]">
{resolvedAvatarPreview ? <img src={resolvedAvatarPreview} alt="Avatar preview" className="h-full w-full object-cover" /> : <i className="fa-solid fa-image text-slate-500" />}
</div>
<input ref={avatarInputRef} type="file" accept="image/png,image/jpeg,image/webp" onChange={handleFileSelected('avatar_file', setAvatarPreview)} className="hidden" />
<div className="flex flex-wrap gap-2">
<button type="button" onClick={() => avatarInputRef.current?.click()} className="rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white">Upload avatar</button>
{form.avatar_file ? <button type="button" onClick={() => clearSelectedFile('avatar_file', setAvatarPreview, avatarInputRef)} className="rounded-full border border-white/10 bg-transparent px-4 py-2 text-sm font-semibold text-slate-300">Use current path</button> : null}
</div>
<label className="grid gap-2 text-sm text-slate-200"><span>Or paste an image URL</span><input value={form.avatar_path} onChange={(event) => setForm((current) => ({ ...current, avatar_path: event.target.value }))} placeholder="https://" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" /></label>
</div>
<div className="grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4 text-sm text-slate-200">
<span className="text-sm font-semibold text-white">Cover image</span>
<div className="flex h-28 w-full items-center justify-center overflow-hidden rounded-[24px] border border-white/10 bg-white/[0.04]">
{resolvedBannerPreview ? <img src={resolvedBannerPreview} alt="Cover preview" className="h-full w-full object-cover" /> : <i className="fa-solid fa-panorama text-slate-500" />}
</div>
<input ref={bannerInputRef} type="file" accept="image/png,image/jpeg,image/webp" onChange={handleFileSelected('banner_file', setBannerPreview)} className="hidden" />
<div className="flex flex-wrap gap-2">
<button type="button" onClick={() => bannerInputRef.current?.click()} className="rounded-full border border-white/10 bg-white/[0.04] px-4 py-2 text-sm font-semibold text-white">Upload cover</button>
{form.banner_file ? <button type="button" onClick={() => clearSelectedFile('banner_file', setBannerPreview, bannerInputRef)} className="rounded-full border border-white/10 bg-transparent px-4 py-2 text-sm font-semibold text-slate-300">Use current path</button> : null}
</div>
<label className="grid gap-2 text-sm text-slate-200"><span>Or paste an image URL</span><input value={form.banner_path} onChange={(event) => setForm((current) => ({ ...current, banner_path: event.target.value }))} placeholder="https://" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" /></label>
</div>
</div>
<div className="grid gap-3 rounded-[24px] border border-white/10 bg-black/20 p-4">
<label className="grid gap-2 text-sm text-slate-200">
<span>Featured artwork</span>
<select value={form.featured_artwork_id} onChange={(event) => setForm((current) => ({ ...current, featured_artwork_id: event.target.value }))} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none">
<option value="">Use latest published artwork</option>
{featuredArtworkOptions.map((item) => <option key={item.id} value={item.id}>{item.title}</option>)}
</select>
</label>
{selectedFeaturedArtwork ? (
<div className="flex items-center gap-3 rounded-[20px] border border-white/10 bg-white/[0.04] p-3">
{selectedFeaturedArtwork.thumb ? <img src={selectedFeaturedArtwork.thumb} alt={selectedFeaturedArtwork.title} className="h-16 w-16 rounded-2xl object-cover" /> : null}
<div>
<div className="font-semibold text-white">{selectedFeaturedArtwork.title}</div>
<div className="text-sm text-slate-400">{selectedFeaturedArtwork.author || 'Group member'}</div>
</div>
</div>
) : (
<p className="text-sm text-slate-400">When this is empty, the public overview falls back to the latest published works automatically.</p>
)}
</div>
<label className="grid gap-2 text-sm text-slate-200"><span>Visibility</span><select value={form.visibility} onChange={(event) => setForm((current) => ({ ...current, visibility: event.target.value }))} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none">{(props.visibilityOptions || []).map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}</select></label>
<label className="grid gap-2 text-sm text-slate-200"><span>Membership policy</span><select value={form.membership_policy} onChange={(event) => setForm((current) => ({ ...current, membership_policy: event.target.value }))} className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none">{(props.membershipPolicyOptions || []).map((option) => <option key={option.value} value={option.value}>{option.label}</option>)}</select></label>
<div className="grid gap-3">
<div className="flex items-center justify-between gap-3">
<span className="text-sm text-slate-200">Links</span>
<button type="button" onClick={addLink} className="rounded-full border border-white/10 bg-white/[0.04] px-3 py-1.5 text-xs font-semibold text-white">Add link</button>
</div>
{form.links_json.map((item, index) => (
<div key={`link-${index}`} className="grid gap-3 md:grid-cols-[0.8fr_1.2fr_auto]">
<input value={item.label} onChange={(event) => updateLink(index, 'label', event.target.value)} placeholder="Label" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
<input value={item.url} onChange={(event) => updateLink(index, 'url', event.target.value)} placeholder="https://" className="rounded-2xl border border-white/10 bg-black/20 px-4 py-3 text-white outline-none" />
<button type="button" onClick={() => removeLink(index)} className="rounded-full border border-rose-300/20 bg-rose-400/10 px-4 py-2 text-sm font-semibold text-rose-100">Remove</button>
</div>
))}
</div>
<div className="flex justify-between gap-3"><button type="button" onClick={archiveGroup} className="rounded-full border border-rose-300/20 bg-rose-400/10 px-4 py-2 text-sm font-semibold text-rose-100">Archive group</button><button type="button" onClick={submit} className="rounded-full border border-sky-300/20 bg-sky-300/10 px-4 py-2 text-sm font-semibold text-sky-100">Save settings</button></div>
</div>
</section>
</StudioLayout>
)
}