* feat(ui): improve mobile sidebar and dashboard layout - Enlarge compass logo on dashboard page (size-14 idle, size-10 active) - Reposition logo higher with -mt-16 margin - Add 6rem spacing between logo and chat - Remove feedback hover button from bottom right - Add event-based feedback dialog opening for mobile sidebar - Remove feedback buttons from site header (mobile and desktop) - Add mobile theme toggle button to header - Increase mobile menu hitbox to size-10 - Reduce search hitbox to separate clickable area - Remove redundant Compass/Get Help/Assistant/Search from sidebar - Rename "People" to "Team" - Add mobile-only feedback button to sidebar footer - Reduce mobile sidebar width to 10rem max-width - Center sidebar menu icons and labels on mobile - Clean up mobile-specific padding variants * chore: add local development setup system - Create .dev-setup directory with patches and scripts - Add apply-dev.sh to easily enable local dev without WorkOS - Add restore-dev.sh to revert to original code - Document all changes in README.md - Store cloudflare-context.ts in files/ as new dev-only file - Support re-apply patches for fresh development sessions This allows running Compass locally without WorkOS authentication for development and testing purposes. --------- Co-authored-by: Avery Felts <averyfelts@Averys-MacBook-Air.local>
188 lines
6.4 KiB
TypeScript
Executable File
188 lines
6.4 KiB
TypeScript
Executable File
"use client"
|
|
|
|
import { createContext, useContext, useState, useEffect } from "react"
|
|
import { usePathname } from "next/navigation"
|
|
import { useAgentOptional } from "@/components/agent/chat-provider"
|
|
import { MessageCircle } from "lucide-react"
|
|
import { toast } from "sonner"
|
|
import { Button } from "@/components/ui/button"
|
|
import { cn } from "@/lib/utils"
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogDescription,
|
|
} from "@/components/ui/dialog"
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select"
|
|
import { Textarea } from "@/components/ui/textarea"
|
|
import { Input } from "@/components/ui/input"
|
|
import { Label } from "@/components/ui/label"
|
|
|
|
const FeedbackContext = createContext<{ open: () => void }>({ open: () => {} })
|
|
|
|
export function useFeedback() {
|
|
return useContext(FeedbackContext)
|
|
}
|
|
|
|
export function openFeedbackDialog() {
|
|
window.dispatchEvent(new CustomEvent("open-feedback-dialog"))
|
|
}
|
|
|
|
export function FeedbackCallout() {
|
|
const { open } = useFeedback()
|
|
return (
|
|
<p className="text-primary font-semibold">
|
|
Have feedback?{" "}
|
|
<button onClick={open} className="underline underline-offset-2 hover:opacity-80">
|
|
Let us know what you think
|
|
</button>
|
|
{" "}— we'd love to hear from you.
|
|
</p>
|
|
)
|
|
}
|
|
|
|
export function FeedbackWidget({ children }: { children?: React.ReactNode }) {
|
|
const [dialogOpen, setDialogOpen] = useState(false)
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [type, setType] = useState<string>("")
|
|
const [message, setMessage] = useState("")
|
|
const [name, setName] = useState("")
|
|
const [email, setEmail] = useState("")
|
|
const pathname = usePathname()
|
|
const agentContext = useAgentOptional()
|
|
const chatOpen = agentContext?.isOpen ?? false
|
|
|
|
function resetForm() {
|
|
setType("")
|
|
setMessage("")
|
|
setName("")
|
|
setEmail("")
|
|
}
|
|
|
|
async function handleSubmit(e: React.FormEvent) {
|
|
e.preventDefault()
|
|
if (!type || !message.trim()) return
|
|
|
|
setSubmitting(true)
|
|
try {
|
|
const res = await fetch("/api/feedback", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
type,
|
|
message,
|
|
name: name || undefined,
|
|
email: email || undefined,
|
|
pageUrl: pathname,
|
|
userAgent: navigator.userAgent,
|
|
viewportWidth: window.innerWidth,
|
|
viewportHeight: window.innerHeight,
|
|
}),
|
|
})
|
|
|
|
if (res.ok) {
|
|
toast.success("Feedback submitted, thank you!")
|
|
resetForm()
|
|
setDialogOpen(false)
|
|
} else {
|
|
const data = await res.json() as { error?: string }
|
|
toast.error(data.error || "Something went wrong")
|
|
}
|
|
} catch {
|
|
toast.error("Failed to submit feedback")
|
|
} finally {
|
|
setSubmitting(false)
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
const handleOpenFeedback = () => setDialogOpen(true)
|
|
window.addEventListener("open-feedback-dialog", handleOpenFeedback)
|
|
return () => window.removeEventListener("open-feedback-dialog", handleOpenFeedback)
|
|
}, [])
|
|
|
|
return (
|
|
<FeedbackContext.Provider value={{ open: () => setDialogOpen(true) }}>
|
|
{children}
|
|
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
|
<DialogContent className="max-w-[calc(100%-2rem)] sm:max-w-md p-4 sm:p-6">
|
|
<DialogHeader className="space-y-1.5">
|
|
<DialogTitle className="text-base sm:text-lg">Send Feedback</DialogTitle>
|
|
<DialogDescription className="text-xs sm:text-sm">
|
|
Report a bug, request a feature, or ask a question. Feedback is sent directly to developers for review.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<form onSubmit={handleSubmit} className="grid gap-3 sm:gap-4">
|
|
<div className="grid gap-1.5">
|
|
<Label htmlFor="feedback-type" className="text-xs sm:text-sm">Type</Label>
|
|
<Select value={type} onValueChange={setType}>
|
|
<SelectTrigger id="feedback-type" className="h-9">
|
|
<SelectValue placeholder="Select type..." />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="bug">Bug Report</SelectItem>
|
|
<SelectItem value="feature">Feature Request</SelectItem>
|
|
<SelectItem value="question">Question</SelectItem>
|
|
<SelectItem value="general">General Feedback</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="grid gap-1.5">
|
|
<Label htmlFor="feedback-message" className="text-xs sm:text-sm">Message</Label>
|
|
<Textarea
|
|
id="feedback-message"
|
|
value={message}
|
|
onChange={(e) => setMessage(e.target.value)}
|
|
placeholder="Describe your feedback..."
|
|
maxLength={2000}
|
|
rows={3}
|
|
required
|
|
className="text-sm resize-none"
|
|
/>
|
|
<p className="text-xs text-muted-foreground text-right">
|
|
{message.length}/2000
|
|
</p>
|
|
</div>
|
|
|
|
<div className="grid sm:grid-cols-2 gap-3 sm:gap-4">
|
|
<div className="grid gap-1.5">
|
|
<Label htmlFor="feedback-name" className="text-xs sm:text-sm">Name (optional)</Label>
|
|
<Input
|
|
id="feedback-name"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Your name"
|
|
className="h-9 text-sm"
|
|
/>
|
|
</div>
|
|
<div className="grid gap-1.5">
|
|
<Label htmlFor="feedback-email" className="text-xs sm:text-sm">Email (optional)</Label>
|
|
<Input
|
|
id="feedback-email"
|
|
type="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
placeholder="you@example.com"
|
|
className="h-9 text-sm"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<Button type="submit" disabled={submitting || !type || !message.trim()} className="h-9 text-sm">
|
|
{submitting ? "Submitting..." : "Submit Feedback"}
|
|
</Button>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</FeedbackContext.Provider>
|
|
)
|
|
}
|