Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 | 1x 1x 1x 1x 85x 85x 85x 85x 85x 2x 1x 1x 1x 85x 85x 85x 85x 2x 2x 64x 15x | /**
* CommunityTopicSuggestPanel — Story 7.1 "Topics From the Floor".
*
* Login-gated attendee surface: a logged-in attendee suggests a future event topic.
* Submit-only (no browse). Flows into the existing topic pool tagged source=COMMUNITY.
*
* Tailwind-only by design — it lives on the PublicLayout-based AttendeeDashboardPage, which
* must not import MUI (public-page bundle boundary). The route is login-gated upstream.
*/
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useMutation } from '@tanstack/react-query';
import { Lightbulb } from 'lucide-react';
import { Card } from '@/components/public/ui/card';
import { Button } from '@/components/public/ui/button';
import { Input } from '@/components/public/ui/input';
import { Textarea } from '@/components/public/ui/textarea';
import { Label } from '@/components/public/ui/label';
import { suggestTopicAsAttendee } from '@/services/api/partnerTopicsApi';
const MIN_TITLE = 5;
const MAX_TITLE = 255;
const MAX_DESC = 500;
export const CommunityTopicSuggestPanel = () => {
const { t } = useTranslation('common');
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [submitted, setSubmitted] = useState(false);
const mutation = useMutation({
mutationFn: () =>
suggestTopicAsAttendee({
title: title.trim(),
description: description.trim() || null,
}),
onSuccess: () => {
setSubmitted(true);
setTitle('');
setDescription('');
},
});
const titleValid = title.trim().length >= MIN_TITLE && title.trim().length <= MAX_TITLE;
const descValid = description.length <= MAX_DESC;
const canSubmit = titleValid && descValid && !mutation.isPending;
return (
<Card className="p-6 text-left" data-testid="community-topic-suggest-panel">
<div className="flex items-center gap-2 mb-2">
<Lightbulb className="h-5 w-5 text-blue-400" />
<h2 className="text-sm font-medium text-zinc-300">{t('attendee.suggestTopic.heading')}</h2>
</div>
<p className="text-sm text-zinc-500 mb-4">{t('attendee.suggestTopic.intro')}</p>
{submitted ? (
<p className="text-sm text-green-400" data-testid="community-topic-success" role="status">
{t('attendee.suggestTopic.success')}
</p>
) : (
<form
onSubmit={(e) => {
e.preventDefault();
Eif (canSubmit) mutation.mutate();
}}
className="space-y-4"
>
<div className="space-y-1.5">
<Label htmlFor="community-topic-title">{t('attendee.suggestTopic.titleLabel')}</Label>
<Input
id="community-topic-title"
data-testid="community-topic-title"
value={title}
maxLength={MAX_TITLE}
onChange={(e) => setTitle(e.target.value)}
placeholder={t('attendee.suggestTopic.titlePlaceholder')}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="community-topic-description">
{t('attendee.suggestTopic.descriptionLabel')}
</Label>
<Textarea
id="community-topic-description"
data-testid="community-topic-description"
value={description}
maxLength={MAX_DESC}
onChange={(e) => setDescription(e.target.value)}
placeholder={t('attendee.suggestTopic.descriptionPlaceholder')}
rows={3}
/>
</div>
{mutation.isError && (
<p className="text-sm text-red-400" data-testid="community-topic-error" role="alert">
{t('attendee.suggestTopic.error')}
</p>
)}
<Button type="submit" disabled={!canSubmit} data-testid="community-topic-submit">
{mutation.isPending
? t('attendee.suggestTopic.submitting')
: t('attendee.suggestTopic.submit')}
</Button>
</form>
)}
</Card>
);
};
export default CommunityTopicSuggestPanel;
|