diff --git a/app/components/TopBar.tsx b/app/components/TopBar.tsx index c917925..19231f0 100644 --- a/app/components/TopBar.tsx +++ b/app/components/TopBar.tsx @@ -11,6 +11,7 @@ import { useProgress } from './shell/Progress' import { AltGlyph, Button, IconButton, Tag, cx } from './ui' import { SavedIndicator } from './shell/SavedIndicator' import { ThemeToggle } from './shell/ThemeToggle' +import { FeedbackButton } from './feedback/Feedback' // The primary action follows the first step not done yet; Export takes over at the end. const NEXT_ACTION: Partial> = { @@ -123,6 +124,7 @@ export default function TopBar({ project }: { project: Project }) {
+ {showPreview && ( <> diff --git a/app/components/export/PlatformCard.tsx b/app/components/export/PlatformCard.tsx index 891bd1d..c915a9e 100644 --- a/app/components/export/PlatformCard.tsx +++ b/app/components/export/PlatformCard.tsx @@ -34,7 +34,7 @@ export function PlatformCard({

{description}

- {howTo != null &&

{howTo}

} + {howTo != null &&
{howTo}
}
{children}
{error && ( diff --git a/app/components/feedback/Feedback.tsx b/app/components/feedback/Feedback.tsx new file mode 100644 index 0000000..2ecaca0 --- /dev/null +++ b/app/components/feedback/Feedback.tsx @@ -0,0 +1,158 @@ +'use client' + +import { useEffect, useId, useRef, useState } from 'react' +import { describeBrowser, feedbackUrl, MAX_MESSAGE, type FeedbackKind } from '@/lib/feedback' +import { APP_VERSION } from '../shell/Rail' +import { Button, Checkbox, Field, Input, Seg, Textarea, cx } from '../ui' +import { XMark } from '../Icons' + +const KINDS: { value: FeedbackKind; label: string }[] = [ + { value: 'bug', label: 'Something’s wrong' }, + { value: 'idea', label: 'An idea' }, + { value: 'question', label: 'A question' }, +] + +const PLACEHOLDER: Record = { + bug: { + title: 'e.g. QSF import shows every block', + message: 'What did you do, what did you expect, and what happened instead?', + }, + idea: { + title: 'e.g. Presets for travel time and cost', + message: 'What are you trying to do, and where does UtilityLab fall short?', + }, + question: { title: 'e.g. How should I set priors for cost?', message: 'Your question' }, +} + +function FeedbackDialog({ open, onClose }: { open: boolean; onClose: () => void }) { + const ref = useRef(null) + const uid = useId() + const [kind, setKind] = useState('bug') + const [title, setTitle] = useState('') + const [message, setMessage] = useState('') + const [withContext, setWithContext] = useState(true) + + useEffect(() => { + const d = ref.current + if (!d) return + if (open && !d.open) d.showModal() + else if (!open && d.open) d.close() + }, [open]) + + const send = () => { + const url = feedbackUrl({ + kind, + title, + message, + context: withContext ? { version: APP_VERSION, browser: describeBrowser(navigator.userAgent) } : undefined, + }) + window.open(url, '_blank', 'noopener,noreferrer') + onClose() + } + + return ( + +
{ + e.preventDefault() + send() + }} + > +
+
+

+ Send feedback +

+

+ Bugs, ideas and questions all help, especially from people running stated choice experiments. +

+
+ +
+ +
+ + + {(fid) => ( + setTitle(e.target.value)} placeholder={PLACEHOLDER[kind].title} required /> + )} + + + {(fid, describedBy) => ( +