From 31e46af4936ec001e4ac61c10190fd506f1b4710 Mon Sep 17 00:00:00 2001 From: bunker-admin Date: Tue, 3 Mar 2026 12:25:05 -0700 Subject: [PATCH] Add enhanced poll insert modal with list + quick create to docs editor Replace the bare text input modal with a two-tab PollInsertModal that lets users browse/search existing polls or create a new one inline, following the same pattern as AdPickerModal. Bunker Admin --- .../components/scheduling/PollInsertModal.tsx | 237 ++++++++++++++++++ admin/src/pages/DocsPage.tsx | 35 +-- 2 files changed, 245 insertions(+), 27 deletions(-) create mode 100644 admin/src/components/scheduling/PollInsertModal.tsx diff --git a/admin/src/components/scheduling/PollInsertModal.tsx b/admin/src/components/scheduling/PollInsertModal.tsx new file mode 100644 index 00000000..bc9e191d --- /dev/null +++ b/admin/src/components/scheduling/PollInsertModal.tsx @@ -0,0 +1,237 @@ +import { useState, useEffect } from 'react'; +import { Modal, Tabs, Table, Button, Input, Tag, Form, Select, DatePicker, TimePicker, Space, Spin, Typography, message } from 'antd'; +import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; +import { api } from '@/lib/api'; +import type { SchedulingPoll, PollsListResponse, SchedulingPollStatus } from '@/types/api'; +import { POLL_STATUS_COLORS, POLL_STATUS_LABELS } from '@/types/api'; + +const { Text } = Typography; + +const TIMEZONE_OPTIONS = [ + 'America/Vancouver', + 'America/Edmonton', + 'America/Regina', + 'America/Winnipeg', + 'America/Toronto', + 'America/Halifax', + 'America/St_Johns', + 'America/New_York', + 'America/Chicago', + 'America/Denver', + 'America/Los_Angeles', +]; + +interface PollInsertModalProps { + open: boolean; + onCancel: () => void; + onInsert: (slug: string) => void; +} + +export function PollInsertModal({ open, onCancel, onInsert }: PollInsertModalProps) { + const [polls, setPolls] = useState([]); + const [loading, setLoading] = useState(false); + const [search, setSearch] = useState(''); + + // Create tab state + const [form] = Form.useForm(); + const [creating, setCreating] = useState(false); + + useEffect(() => { + if (!open) return; + setLoading(true); + setSearch(''); + api.get('/meeting-planner', { params: { limit: 100 } }) + .then(({ data }) => setPolls(data.polls || [])) + .catch(() => setPolls([])) + .finally(() => setLoading(false)); + }, [open]); + + const filtered = search + ? polls.filter((p) => { + const q = search.toLowerCase(); + return p.title.toLowerCase().includes(q) || p.slug.toLowerCase().includes(q); + }) + : polls; + + const handleCreate = async (values: any) => { + setCreating(true); + try { + const options = values.options.map((opt: any) => ({ + date: opt.date.format('YYYY-MM-DD'), + startTime: opt.startTime.format('HH:mm'), + endTime: opt.endTime.format('HH:mm'), + })); + const { data } = await api.post('/meeting-planner', { + title: values.title, + description: values.description, + timezone: values.timezone, + allowAnonymous: true, + notifyOnVote: true, + options, + }); + message.success('Poll created'); + form.resetFields(); + onInsert(data.slug); + } catch { + message.error('Failed to create poll'); + } finally { + setCreating(false); + } + }; + + const columns = [ + { + title: 'Title', + dataIndex: 'title', + key: 'title', + ellipsis: true, + }, + { + title: 'Slug', + dataIndex: 'slug', + key: 'slug', + width: 160, + ellipsis: true, + render: (slug: string) => {slug}, + }, + { + title: 'Status', + dataIndex: 'status', + key: 'status', + width: 90, + render: (status: SchedulingPollStatus) => ( + {POLL_STATUS_LABELS[status]} + ), + }, + { + title: 'Options', + key: 'options', + width: 70, + align: 'center' as const, + render: (_: unknown, r: SchedulingPoll) => r._count?.options ?? '–', + }, + { + title: 'Votes', + key: 'votes', + width: 60, + align: 'center' as const, + render: (_: unknown, r: SchedulingPoll) => r._count?.votes ?? '–', + }, + { + title: '', + key: 'action', + width: 80, + render: (_: unknown, record: SchedulingPoll) => ( + + ), + }, + ]; + + return ( + + + setSearch(e.target.value)} + allowClear + style={{ marginBottom: 12 }} + /> + {loading ? ( +
+ ) : ( + + )} + + ), + }, + { + key: 'create', + label: 'Create New', + children: ( + + + + + + + + + setPollSlugInput(e.target.value)} - onPressEnter={handlePollInsert} - autoFocus - /> - + onCancel={() => setPollInsertOpen(false)} + onInsert={handlePollInsert} + /> {/* Custom right-click context menu with submenus */} {ctxMenu && (