Files
av-planner/src/components/announcements/AnnouncementComposerModal.tsx
T
aarbitandClaude Sonnet 5 6cdfde822d Add site-wide announcements, account migration, and profile self-service
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
2026-09-28 10:05:11 -05:00

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>
)
}