Files
upstream-tldraw/apps/docs/components/docs/docs-feedback-widget.tsx
upstream-mirror a774ac673a
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
2026-09-23 16:12:52 +05:00

143 lines
4.8 KiB
TypeScript

'use client'
import { Field, Label, Textarea } from '@headlessui/react'
import { ArrowLongRightIcon, CheckCircleIcon, HandThumbDownIcon } from '@heroicons/react/20/solid'
import { HandThumbUpIcon } from '@heroicons/react/24/solid'
import { usePathname } from 'next/navigation'
import { FormEventHandler, useState } from 'react'
import { track } from '@/app/analytics'
import { cn } from '@/utils/cn'
import { useLocalStorageState } from '@/utils/storage'
const DEBUGGING = false
async function submitFeedback(
_sessionId: string,
_pathname: string,
_feedback: 1 | -1 | 0,
_note: string
) {
const feedback: { value: number; message?: string } = { value: _feedback }
if (_note !== '') feedback.message = _note
track('Feedback', feedback)
return
}
async function submitThumbsFeedback(_sessionId: string, _pathname: string, _feedback: 1 | -1 | 0) {
const feedback: { value: number; message?: string } = { value: _feedback }
track('Helpful', feedback)
return
}
export function DocsFeedbackWidget({ className }: { className?: string }) {
const pathname = usePathname()
const [didSubmit, setDidSubmit] = useLocalStorageState(pathname + '-feedback-submitted', false)
// Set the feedback session id to a random id and stash it in storage
const [sessionId] = useLocalStorageState(
'feedback-session-id',
Math.random().toString(36).substring(7)
)
const [state, setState] = useState<
'idle' | 'thumbs-up' | 'thumbs-down' | 'loading' | 'success' | 'error'
>('idle')
// Track outside the setState updater: React double-invokes updaters in StrictMode, which
// would double-count the vote.
const handleThumbs = (thumb: 'thumbs-up' | 'thumbs-down') => {
const next = state === thumb ? 'idle' : thumb
if (next === thumb) {
submitThumbsFeedback(sessionId, pathname, thumb === 'thumbs-up' ? 1 : -1)
}
setState(next)
}
const handleSubmit: FormEventHandler<HTMLFormElement> = async (e) => {
e.preventDefault()
if (state === 'loading') return
if (!sessionId) return
try {
const form = e.currentTarget
const formData = new FormData(form)
const feedback = formData.get('feedback') as string
setState('loading')
await submitFeedback(sessionId, pathname, state === 'thumbs-up' ? 1 : -1, feedback)
setState('success')
setTimeout(() => {
setDidSubmit(true)
}, 3000)
} catch {
setState('error')
}
}
// todo, improve this so that thumbs ups and thumbs downs are also captured
if (didSubmit && !(DEBUGGING && process.env.NODE_ENV === 'development')) return null
return (
<div
className={cn(
'-ml-4 px-4 bg-zinc-50 dark:bg-zinc-900 rounded-lg shrink-0 text-xs h-auto',
className
)}
>
{state !== 'success' && (
<div className="flex justify-between items-center">
<span>Is this page helpful?</span>
<div className="flex">
<button
onClick={() => handleThumbs('thumbs-down')}
className={cn(
'h-9 w-7 flex items-center justify-center pt-0.5',
state === 'thumbs-down' && 'text-blue-500',
state !== 'thumbs-down' && 'hover:text-black dark:hover:text-white'
)}
>
<HandThumbDownIcon className="h-4" />
</button>
<button
onClick={() => handleThumbs('thumbs-up')}
className={cn(
'h-9 w-7 flex items-center justify-center pb-0.5 -mr-1',
state === 'thumbs-up' && 'text-blue-500',
state !== 'thumbs-up' && 'hover:text-black dark:hover:text-white'
)}
>
<HandThumbUpIcon className="h-4" />
</button>
</div>
</div>
)}
{(state === 'thumbs-up' || state === 'thumbs-down' || state === 'loading') && (
<form onSubmit={handleSubmit} className="pb-3">
<Field>
<Label className="sr-only">Your feedback</Label>
<Textarea
name="feedback"
placeholder="Your feedback..."
rows={3}
className="resize-none bg-zinc-200/50 dark:bg-zinc-700/50 w-full rounded-md placeholder-zinc-400 dark:placeholder-zinc-600 text-black dark:text-white px-2 py-1 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:ring-offset-zinc-50 dark:focus:ring-offset-zinc-900"
/>
</Field>
<button
type="submit"
disabled={state === 'loading'}
className="bg-blue-500 rounded-md h-6 px-3 flex items-center text-white gap-1.5 font-medium ml-auto mt-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 focus:ring-offset-zinc-50 hover:bg-blue-600 dark:hover:bg-blue-400 dark:focus:ring-offset-zinc-900"
>
<span>Submit</span>
<ArrowLongRightIcon className="h-3.5" />
</button>
</form>
)}
{state === 'success' && (
<p className="h-9 flex items-center gap-1.5">
<CheckCircleIcon className="h-4 text-emerald-500 dark:text-emerald-400" />
<span>Thanks for your feedback!</span>
</p>
)}
</div>
)
}