Announcements: a Super Admin (or an Admin individually flagged via profiles.can_post_announcements) can post/retire a site-wide banner. Account migration: a Super Admin can move a locked-out user's diagrams, private catalog entries, and submissions to another account, with a migration-history log; ProfileModal adds the self-service half (link a new Google identity via Supabase manual linking, then unlink the old one, while signed in as the account being migrated). Also reworks the top bar's flat button row into grouped dropdown menus (Diagram / Admin / Account) now that there are enough entries to need it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017DUU6CnxECCDeqDNYJgr5x
80 lines
3.0 KiB
TypeScript
80 lines
3.0 KiB
TypeScript
import { useState } from 'react'
|
|
import { useAnnouncementStore } from '../../state/announcementStore'
|
|
import Modal from '../common/Modal'
|
|
|
|
/** Reachable only by whoever can_manage_announcements() allows (every Super
|
|
* Admin, or an Admin individually flagged — see AdminUsersModal). Shows
|
|
* what's currently live regardless of whether *this* viewer has dismissed
|
|
* it themselves — that flag is personal, not a proxy for "is it still
|
|
* showing to everyone else." */
|
|
export default function AnnouncementComposerModal({ onClose }: { onClose: () => void }) {
|
|
const current = useAnnouncementStore((s) => s.current)
|
|
const post = useAnnouncementStore((s) => s.post)
|
|
const retireCurrent = useAnnouncementStore((s) => s.retireCurrent)
|
|
const [message, setMessage] = useState('')
|
|
const [busy, setBusy] = useState(false)
|
|
|
|
const runAction = async (action: () => Promise<void>) => {
|
|
setBusy(true)
|
|
try {
|
|
await action()
|
|
} finally {
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
const handlePost = () => {
|
|
const trimmed = message.trim()
|
|
if (!trimmed) return
|
|
runAction(async () => {
|
|
await post(trimmed)
|
|
setMessage('')
|
|
})
|
|
}
|
|
|
|
return (
|
|
<Modal title="Site Announcement" onClose={onClose} width="max-w-lg">
|
|
<div className="space-y-3">
|
|
{current ? (
|
|
<div className="rounded border border-amber-200 bg-amber-50 p-2.5 text-sm text-amber-900">
|
|
<p className="mb-2">{current.message}</p>
|
|
<button
|
|
onClick={() => runAction(retireCurrent)}
|
|
disabled={busy}
|
|
className="rounded bg-white px-2 py-1 text-xs font-medium text-amber-700 hover:bg-amber-100 disabled:cursor-not-allowed disabled:opacity-50"
|
|
>
|
|
Retire now
|
|
</button>
|
|
</div>
|
|
) : (
|
|
<p className="text-xs italic text-slate-400">Nothing is currently showing.</p>
|
|
)}
|
|
|
|
<label className="block text-xs font-medium text-slate-600">
|
|
{current ? 'Post a new announcement (replaces the one above)' : 'Post an announcement'}
|
|
<textarea
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
rows={3}
|
|
placeholder="e.g. The XLR-to-TRS adapter cable's compatibility changed on 2026-09-16 — check any diagrams using it."
|
|
className="mt-1 w-full rounded border border-slate-300 px-2 py-1.5 text-sm"
|
|
/>
|
|
</label>
|
|
|
|
<div className="flex justify-end gap-2 border-t border-slate-100 pt-3">
|
|
<button onClick={onClose} className="rounded px-3 py-1.5 text-xs text-slate-600 hover:bg-slate-100">
|
|
Close
|
|
</button>
|
|
<button
|
|
onClick={handlePost}
|
|
disabled={busy || !message.trim()}
|
|
className="rounded bg-indigo-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-40"
|
|
>
|
|
Post
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</Modal>
|
|
)
|
|
}
|