2026-06-02 19:31:30 -04:00
|
|
|
import React, { useState } from 'react';
|
|
|
|
|
import { Room } from 'matrix-js-sdk';
|
2026-06-03 11:23:44 -04:00
|
|
|
import { Box, Button, Icon, IconButton, Icons, Text, config } from 'folds';
|
2026-06-02 19:31:30 -04:00
|
|
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
|
|
|
|
|
|
|
|
|
interface PollCreatorProps {
|
|
|
|
|
roomId: string;
|
|
|
|
|
room: Room;
|
|
|
|
|
onClose: () => void;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function PollCreator({ roomId, onClose }: PollCreatorProps) {
|
|
|
|
|
const mx = useMatrixClient();
|
|
|
|
|
const [question, setQuestion] = useState('');
|
|
|
|
|
const [options, setOptions] = useState<string[]>(['', '']);
|
2026-06-03 00:55:50 -04:00
|
|
|
const [isMultiple, setIsMultiple] = useState(false);
|
2026-06-02 19:31:30 -04:00
|
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const handleOptionChange = (index: number, value: string) => {
|
|
|
|
|
setOptions((prev) => {
|
|
|
|
|
const next = [...prev];
|
|
|
|
|
next[index] = value;
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleAddOption = () => {
|
|
|
|
|
if (options.length >= 10) return;
|
|
|
|
|
setOptions((prev) => [...prev, '']);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleRemoveOption = (index: number) => {
|
|
|
|
|
if (options.length <= 2) return;
|
|
|
|
|
setOptions((prev) => prev.filter((_, i) => i !== index));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleSubmit = async () => {
|
|
|
|
|
const trimmedQuestion = question.trim();
|
|
|
|
|
if (!trimmedQuestion) {
|
|
|
|
|
setError('Please enter a question.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const filledOptions = options.map((o) => o.trim()).filter((o) => o.length > 0);
|
|
|
|
|
if (filledOptions.length < 2) {
|
|
|
|
|
setError('Please provide at least 2 answer options.');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setError(null);
|
|
|
|
|
setSubmitting(true);
|
|
|
|
|
try {
|
|
|
|
|
await mx.sendEvent(roomId, 'm.poll.start' as any, {
|
|
|
|
|
'm.poll': {
|
|
|
|
|
question: { 'm.text': trimmedQuestion },
|
|
|
|
|
answers: filledOptions.map((o, i) => ({ 'm.id': `${i}`, 'm.text': o })),
|
2026-06-03 00:55:50 -04:00
|
|
|
max_selections: isMultiple ? filledOptions.length : 1,
|
2026-06-02 19:31:30 -04:00
|
|
|
kind: 'm.poll.undisclosed',
|
|
|
|
|
},
|
|
|
|
|
body: trimmedQuestion,
|
|
|
|
|
msgtype: 'm.text',
|
|
|
|
|
});
|
|
|
|
|
onClose();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Failed to send poll.');
|
|
|
|
|
setSubmitting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
position: 'fixed',
|
|
|
|
|
inset: 0,
|
|
|
|
|
zIndex: 1000,
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
justifyContent: 'center',
|
|
|
|
|
background: 'rgba(0,0,0,0.5)',
|
|
|
|
|
}}
|
|
|
|
|
onClick={(e) => {
|
|
|
|
|
if (e.target === e.currentTarget) onClose();
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
background: 'var(--bg-surface)',
|
|
|
|
|
borderRadius: config.radii.R400,
|
|
|
|
|
padding: config.space.S500,
|
|
|
|
|
width: '100%',
|
|
|
|
|
maxWidth: '420px',
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexDirection: 'column',
|
|
|
|
|
gap: config.space.S300,
|
|
|
|
|
boxShadow: '0 8px 32px rgba(0,0,0,0.24)',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Box direction="Row" alignItems="Center" justifyContent="SpaceBetween">
|
|
|
|
|
<Text size="H4">Create Poll</Text>
|
|
|
|
|
<IconButton
|
|
|
|
|
size="300"
|
|
|
|
|
radii="300"
|
|
|
|
|
variant="SurfaceVariant"
|
|
|
|
|
onClick={onClose}
|
|
|
|
|
aria-label="Close poll creator"
|
|
|
|
|
>
|
|
|
|
|
<Icon src={Icons.Cross} size="100" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Box>
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: config.space.S100 }}>
|
|
|
|
|
<Text size="L400">Question</Text>
|
|
|
|
|
<input
|
|
|
|
|
style={{
|
|
|
|
|
background: 'var(--bg-surface-low)',
|
|
|
|
|
border: '1px solid var(--bg-surface-border)',
|
|
|
|
|
borderRadius: config.radii.R300,
|
|
|
|
|
padding: `${config.space.S200} ${config.space.S300}`,
|
|
|
|
|
color: 'var(--tc-surface-high)',
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
outline: 'none',
|
|
|
|
|
width: '100%',
|
|
|
|
|
boxSizing: 'border-box',
|
|
|
|
|
}}
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder="Ask a question..."
|
|
|
|
|
value={question}
|
|
|
|
|
onChange={(e) => setQuestion(e.target.value)}
|
|
|
|
|
autoFocus
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: config.space.S100 }}>
|
|
|
|
|
<Text size="L400">Options</Text>
|
|
|
|
|
{options.map((opt, index) => (
|
|
|
|
|
<div
|
|
|
|
|
key={index}
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: config.space.S100,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<input
|
|
|
|
|
style={{
|
|
|
|
|
flex: 1,
|
|
|
|
|
background: 'var(--bg-surface-low)',
|
|
|
|
|
border: '1px solid var(--bg-surface-border)',
|
|
|
|
|
borderRadius: config.radii.R300,
|
|
|
|
|
padding: `${config.space.S200} ${config.space.S300}`,
|
|
|
|
|
color: 'var(--tc-surface-high)',
|
|
|
|
|
fontSize: '14px',
|
|
|
|
|
outline: 'none',
|
|
|
|
|
boxSizing: 'border-box',
|
|
|
|
|
}}
|
|
|
|
|
type="text"
|
|
|
|
|
placeholder={`Option ${index + 1}`}
|
|
|
|
|
value={opt}
|
|
|
|
|
onChange={(e) => handleOptionChange(index, e.target.value)}
|
|
|
|
|
/>
|
|
|
|
|
<IconButton
|
|
|
|
|
size="300"
|
|
|
|
|
radii="300"
|
|
|
|
|
variant="SurfaceVariant"
|
|
|
|
|
onClick={() => handleRemoveOption(index)}
|
|
|
|
|
disabled={options.length <= 2}
|
|
|
|
|
aria-label={`Remove option ${index + 1}`}
|
|
|
|
|
>
|
|
|
|
|
<Icon src={Icons.Cross} size="100" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
{options.length < 10 && (
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
onClick={handleAddOption}
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
gap: config.space.S100,
|
|
|
|
|
background: 'none',
|
|
|
|
|
border: 'none',
|
|
|
|
|
cursor: 'pointer',
|
|
|
|
|
color: 'var(--tc-surface-low)',
|
|
|
|
|
fontSize: '13px',
|
|
|
|
|
padding: `${config.space.S100} 0`,
|
|
|
|
|
alignSelf: 'flex-start',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<Icon src={Icons.Plus} size="100" />
|
|
|
|
|
<span>Add Option</span>
|
|
|
|
|
</button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-06-03 11:23:44 -04:00
|
|
|
<Box direction="Column" gap="100">
|
2026-06-02 19:31:30 -04:00
|
|
|
<Text size="L400">Selection Type</Text>
|
2026-06-03 11:23:44 -04:00
|
|
|
<Box gap="200">
|
2026-06-03 00:55:50 -04:00
|
|
|
{(['single', 'multiple'] as const).map((type) => {
|
|
|
|
|
const active = type === 'multiple' ? isMultiple : !isMultiple;
|
|
|
|
|
return (
|
2026-06-03 11:23:44 -04:00
|
|
|
<Button
|
2026-06-03 00:55:50 -04:00
|
|
|
key={type}
|
2026-06-03 11:23:44 -04:00
|
|
|
size="300"
|
|
|
|
|
variant="Primary"
|
|
|
|
|
fill={active ? 'Solid' : 'None'}
|
|
|
|
|
radii="300"
|
2026-06-03 00:55:50 -04:00
|
|
|
onClick={() => setIsMultiple(type === 'multiple')}
|
|
|
|
|
>
|
2026-06-03 11:23:44 -04:00
|
|
|
<Text size="B300">{type === 'single' ? 'Single choice' : 'Multiple choice'}</Text>
|
|
|
|
|
</Button>
|
2026-06-03 00:55:50 -04:00
|
|
|
);
|
|
|
|
|
})}
|
2026-06-03 11:23:44 -04:00
|
|
|
</Box>
|
|
|
|
|
</Box>
|
2026-06-02 19:31:30 -04:00
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<Text size="T200" style={{ color: 'var(--tc-danger-normal)' }}>
|
|
|
|
|
{error}
|
|
|
|
|
</Text>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<Box direction="Row" justifyContent="End" gap="200">
|
2026-06-03 11:23:44 -04:00
|
|
|
<Button
|
|
|
|
|
size="400"
|
|
|
|
|
variant="Secondary"
|
|
|
|
|
fill="Soft"
|
|
|
|
|
radii="300"
|
2026-06-02 19:31:30 -04:00
|
|
|
onClick={onClose}
|
|
|
|
|
type="button"
|
2026-06-03 11:23:44 -04:00
|
|
|
>
|
|
|
|
|
<Text size="B400">Cancel</Text>
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
size="400"
|
|
|
|
|
variant="Primary"
|
|
|
|
|
fill="Solid"
|
|
|
|
|
radii="300"
|
2026-06-02 19:31:30 -04:00
|
|
|
onClick={handleSubmit}
|
2026-06-03 11:23:44 -04:00
|
|
|
type="button"
|
2026-06-02 19:31:30 -04:00
|
|
|
disabled={submitting}
|
|
|
|
|
>
|
2026-06-03 11:23:44 -04:00
|
|
|
<Text size="B400">{submitting ? 'Creating…' : 'Create Poll'}</Text>
|
|
|
|
|
</Button>
|
2026-06-02 19:31:30 -04:00
|
|
|
</Box>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|