feat(tracker-web): roadmap submit + email-prompt modals via shared Modal (UX-3.2)
Replace the bespoke local Modal (and its slate/blue/white chrome) with the shared @bytelyst/ui Modal (Radix dialog — focus-trap/Esc/scroll-lock) for both the Submit Idea and vote email-prompt dialogs. The dialog titles become the accessible heading; form fields move to Input/Select/Textarea and the submit-result message to AlertBanner. Behaviour preserved. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
parent
f612d2ecd1
commit
51c30ed73d
@ -6,6 +6,8 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
Input,
|
Input,
|
||||||
Select,
|
Select,
|
||||||
|
Textarea,
|
||||||
|
Modal,
|
||||||
Badge,
|
Badge,
|
||||||
StatusDot,
|
StatusDot,
|
||||||
MetricCard,
|
MetricCard,
|
||||||
@ -352,127 +354,118 @@ export default function RoadmapPage() {
|
|||||||
</main>
|
</main>
|
||||||
|
|
||||||
{/* Email prompt modal */}
|
{/* Email prompt modal */}
|
||||||
{showEmailPrompt && (
|
|
||||||
<Modal
|
<Modal
|
||||||
onClose={() => {
|
open={showEmailPrompt}
|
||||||
|
onOpenChange={open => {
|
||||||
|
if (!open) {
|
||||||
setShowEmailPrompt(false);
|
setShowEmailPrompt(false);
|
||||||
setPendingVoteId(null);
|
setPendingVoteId(null);
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
title="Enter your email to vote"
|
||||||
|
description="We use your email to track your votes. One vote per item."
|
||||||
|
size="sm"
|
||||||
>
|
>
|
||||||
<h3 className="text-lg font-semibold mb-2">Enter your email to vote</h3>
|
<div className="space-y-4">
|
||||||
<p className="text-sm text-slate-500 mb-4">
|
<Input
|
||||||
We use your email to track your votes. One vote per item.
|
|
||||||
</p>
|
|
||||||
<input
|
|
||||||
type="email"
|
type="email"
|
||||||
|
aria-label="Email"
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
value={voteEmail}
|
value={voteEmail}
|
||||||
onChange={e => setVoteEmail(e.target.value)}
|
onChange={e => setVoteEmail(e.target.value)}
|
||||||
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-sm mb-4"
|
|
||||||
onKeyDown={e => e.key === 'Enter' && handleEmailSubmit()}
|
onKeyDown={e => e.key === 'Enter' && handleEmailSubmit()}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
<button
|
<Button
|
||||||
|
variant="ghost"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setShowEmailPrompt(false);
|
setShowEmailPrompt(false);
|
||||||
setPendingVoteId(null);
|
setPendingVoteId(null);
|
||||||
}}
|
}}
|
||||||
className="px-4 py-2 text-sm text-slate-500 hover:text-slate-700"
|
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button onClick={handleEmailSubmit}>Vote</Button>
|
||||||
onClick={handleEmailSubmit}
|
</div>
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-sm font-medium"
|
|
||||||
>
|
|
||||||
Vote
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Submit modal */}
|
{/* Submit modal */}
|
||||||
{showSubmit && (
|
|
||||||
<Modal
|
<Modal
|
||||||
onClose={() => {
|
open={showSubmit}
|
||||||
|
onOpenChange={open => {
|
||||||
|
if (!open) {
|
||||||
setShowSubmit(false);
|
setShowSubmit(false);
|
||||||
setSubmitSuccess('');
|
setSubmitSuccess('');
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
|
title="Submit an Idea"
|
||||||
>
|
>
|
||||||
<h3 className="text-lg font-semibold mb-4">Submit an Idea</h3>
|
|
||||||
{submitSuccess ? (
|
{submitSuccess ? (
|
||||||
<div
|
<AlertBanner
|
||||||
className={`p-3 rounded-lg text-sm mb-4 ${submitSuccess.startsWith('Error') ? 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-300' : 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-300'}`}
|
tone={submitSuccess.startsWith('Error') ? 'error' : 'success'}
|
||||||
|
className="mb-4"
|
||||||
>
|
>
|
||||||
{submitSuccess}
|
{submitSuccess}
|
||||||
</div>
|
</AlertBanner>
|
||||||
) : null}
|
) : null}
|
||||||
<form onSubmit={handleSubmit} className="space-y-3">
|
<form onSubmit={handleSubmit} className="space-y-3">
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<div className="grid grid-cols-2 gap-3">
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
aria-label="Your name"
|
||||||
placeholder="Your name"
|
placeholder="Your name"
|
||||||
value={submitForm.name}
|
value={submitForm.name}
|
||||||
onChange={e => setSubmitForm({ ...submitForm, name: e.target.value })}
|
onChange={e => setSubmitForm({ ...submitForm, name: e.target.value })}
|
||||||
required
|
required
|
||||||
className="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-sm"
|
|
||||||
/>
|
/>
|
||||||
<input
|
<Input
|
||||||
type="email"
|
type="email"
|
||||||
|
aria-label="Your email"
|
||||||
placeholder="Your email"
|
placeholder="Your email"
|
||||||
value={submitForm.email}
|
value={submitForm.email}
|
||||||
onChange={e => setSubmitForm({ ...submitForm, email: e.target.value })}
|
onChange={e => setSubmitForm({ ...submitForm, email: e.target.value })}
|
||||||
required
|
required
|
||||||
className="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-sm"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<Select
|
||||||
value={submitForm.type}
|
value={submitForm.type}
|
||||||
onChange={e => setSubmitForm({ ...submitForm, type: e.target.value })}
|
onChange={e => setSubmitForm({ ...submitForm, type: e.target.value })}
|
||||||
aria-label="Request type"
|
aria-label="Request type"
|
||||||
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-sm"
|
options={[
|
||||||
>
|
{ value: 'feature', label: 'Feature Request' },
|
||||||
<option value="feature">Feature Request</option>
|
{ value: 'bug', label: 'Bug Report' },
|
||||||
<option value="bug">Bug Report</option>
|
{ value: 'task', label: 'Task' },
|
||||||
<option value="task">Task</option>
|
]}
|
||||||
</select>
|
/>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
aria-label="Title"
|
||||||
placeholder="Title — what would you like to see?"
|
placeholder="Title — what would you like to see?"
|
||||||
value={submitForm.title}
|
value={submitForm.title}
|
||||||
onChange={e => setSubmitForm({ ...submitForm, title: e.target.value })}
|
onChange={e => setSubmitForm({ ...submitForm, title: e.target.value })}
|
||||||
required
|
required
|
||||||
maxLength={500}
|
maxLength={500}
|
||||||
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-sm"
|
|
||||||
/>
|
/>
|
||||||
<textarea
|
<Textarea
|
||||||
|
aria-label="Description"
|
||||||
placeholder="Describe your idea or issue in detail (optional)"
|
placeholder="Describe your idea or issue in detail (optional)"
|
||||||
value={submitForm.description}
|
value={submitForm.description}
|
||||||
onChange={e => setSubmitForm({ ...submitForm, description: e.target.value })}
|
onChange={e => setSubmitForm({ ...submitForm, description: e.target.value })}
|
||||||
rows={4}
|
rows={4}
|
||||||
maxLength={5000}
|
maxLength={5000}
|
||||||
className="w-full px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-800 text-sm resize-none"
|
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-end gap-2 pt-2">
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
<button
|
<Button type="button" variant="ghost" onClick={() => setShowSubmit(false)}>
|
||||||
type="button"
|
|
||||||
onClick={() => setShowSubmit(false)}
|
|
||||||
className="px-4 py-2 text-sm text-slate-500 hover:text-slate-700"
|
|
||||||
>
|
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button type="submit" loading={submitting}>
|
||||||
type="submit"
|
|
||||||
disabled={submitting}
|
|
||||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white rounded-lg text-sm font-medium"
|
|
||||||
>
|
|
||||||
{submitting ? 'Submitting...' : 'Submit'}
|
{submitting ? 'Submitting...' : 'Submit'}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Modal>
|
</Modal>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -578,14 +571,3 @@ function ItemRow({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function Modal({ children, onClose }: { children: React.ReactNode; onClose: () => void }) {
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<div className="absolute inset-0 bg-black/40 backdrop-blur-sm" onClick={onClose} />
|
|
||||||
<div className="relative bg-white dark:bg-slate-800 rounded-2xl shadow-2xl border border-slate-200 dark:border-slate-700 p-6 w-full max-w-md">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user