Use the shadcn Alert component for the file-sharing hint instead of a bare Message element. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
241 lines
7.5 KiB
TypeScript
241 lines
7.5 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useRouter } from "next/navigation";
|
|
import { toast } from "sonner";
|
|
import { PlusIcon } from "lucide-react";
|
|
import { Button } from "@/components/ui/button";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { createTicket } from "@/app/(frontend)/helpdesk/actions";
|
|
import {
|
|
TICKET_CATEGORIES,
|
|
TICKET_PRIORITIES,
|
|
type TicketCategory,
|
|
type TicketPriority,
|
|
} from "@/lib/tickets/ticketMeta";
|
|
import { cn } from "@/lib/utils";
|
|
import { Alert } from "@/components/ui/alert";
|
|
|
|
function isTicketCategory(value: string): value is TicketCategory {
|
|
return TICKET_CATEGORIES.some((option) => option.value === value);
|
|
}
|
|
|
|
function isTicketPriority(value: string): value is TicketPriority {
|
|
return TICKET_PRIORITIES.some((option) => option.value === value);
|
|
}
|
|
|
|
export function CreateTicketDialog() {
|
|
const router = useRouter();
|
|
const [open, setOpen] = useState(false);
|
|
const [title, setTitle] = useState("");
|
|
const [category, setCategory] = useState<TicketCategory>("support_request");
|
|
const [priority, setPriority] = useState<TicketPriority>("medium");
|
|
const [description, setDescription] = useState("");
|
|
const [pending, setPending] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
function reset() {
|
|
setTitle("");
|
|
setCategory("support_request");
|
|
setPriority("medium");
|
|
setDescription("");
|
|
setPending(false);
|
|
setError(null);
|
|
}
|
|
|
|
async function handleSubmit() {
|
|
const trimmedTitle = title.trim();
|
|
const trimmedDescription = description.trim();
|
|
if (!trimmedTitle) {
|
|
setError("Add a short title for your request.");
|
|
return;
|
|
}
|
|
if (!trimmedDescription) {
|
|
setError("Describe the issue or request so the team can help.");
|
|
return;
|
|
}
|
|
|
|
setPending(true);
|
|
setError(null);
|
|
const result = await createTicket({
|
|
title: trimmedTitle,
|
|
description: trimmedDescription,
|
|
category,
|
|
priority,
|
|
});
|
|
|
|
if (result.success) {
|
|
setOpen(false);
|
|
reset();
|
|
toast.success("Ticket created");
|
|
router.refresh();
|
|
} else {
|
|
setError(result.error ?? "Could not create the ticket.");
|
|
setPending(false);
|
|
}
|
|
}
|
|
|
|
const getColorForCategoryLabel = (label: string) => {
|
|
switch (label.toLowerCase()) {
|
|
case "support_request":
|
|
return "text-blue-300";
|
|
case "feature_request":
|
|
return "text-green-300";
|
|
case "bug_report":
|
|
return "text-red-300";
|
|
default:
|
|
return "text-muted-foreground";
|
|
}
|
|
};
|
|
|
|
const getColorForPriorityLabel = (label: string) => {
|
|
switch (label.toLowerCase()) {
|
|
case "low":
|
|
return "text-zinc-300";
|
|
case "medium":
|
|
return "text-blue-300";
|
|
case "high":
|
|
return "text-amber-300";
|
|
case "critical":
|
|
return "text-red-300";
|
|
default:
|
|
return "text-muted-foreground";
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Dialog
|
|
open={open}
|
|
onOpenChange={(nextOpen) => {
|
|
setOpen(nextOpen);
|
|
if (!nextOpen) reset();
|
|
}}
|
|
>
|
|
<DialogTrigger asChild>
|
|
<Button size="sm">
|
|
<PlusIcon className="size-4" />
|
|
New ticket
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>New ticket</DialogTitle>
|
|
<DialogDescription>
|
|
Share the details of what you need help with. The helpdesk will follow up here.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<form
|
|
className="flex flex-col gap-4"
|
|
onSubmit={(event) => {
|
|
event.preventDefault();
|
|
void handleSubmit();
|
|
}}
|
|
>
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="ticket-title">Title</Label>
|
|
<Input
|
|
id="ticket-title"
|
|
placeholder="What do you need help with?"
|
|
value={title}
|
|
onChange={(event) => setTitle(event.target.value)}
|
|
disabled={pending}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="ticket-category">Category</Label>
|
|
<Select
|
|
value={category}
|
|
onValueChange={(value) => {
|
|
if (isTicketCategory(value)) setCategory(value);
|
|
}}
|
|
disabled={pending}
|
|
>
|
|
<SelectTrigger id="ticket-category" className={cn("w-full", getColorForCategoryLabel(category))}>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{TICKET_CATEGORIES.map((option) => (
|
|
<SelectItem key={option.value} value={option.value} className={getColorForCategoryLabel(option.value)}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="ticket-priority">Priority</Label>
|
|
<Select
|
|
value={priority}
|
|
onValueChange={(value) => {
|
|
if (isTicketPriority(value)) setPriority(value);
|
|
}}
|
|
disabled={pending}
|
|
>
|
|
<SelectTrigger id="ticket-priority" className={cn("w-full", getColorForPriorityLabel(priority))}>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
{TICKET_PRIORITIES.map((option) => (
|
|
<SelectItem key={option.value} value={option.value} className={getColorForPriorityLabel(option.value)}>
|
|
{option.label}
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<Alert variant="info" className="text-sm">
|
|
<p>
|
|
Need to share files or screenshots? You can upload them to a file sharing service and include the link in your description.
|
|
</p>
|
|
</Alert>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-1.5">
|
|
<Label htmlFor="ticket-description">Description</Label>
|
|
<Textarea
|
|
id="ticket-description"
|
|
rows={5}
|
|
placeholder="Include what happened, what you expected, and any useful context."
|
|
value={description}
|
|
onChange={(event) => setDescription(event.target.value)}
|
|
disabled={pending}
|
|
/>
|
|
</div>
|
|
|
|
{error && (
|
|
<p role="alert" className="text-sm text-red-400">
|
|
{error}
|
|
</p>
|
|
)}
|
|
|
|
<DialogFooter>
|
|
<Button type="button" variant="outline" onClick={() => setOpen(false)} disabled={pending}>
|
|
Cancel
|
|
</Button>
|
|
<Button type="submit" disabled={pending}>
|
|
{pending ? "Creating..." : "Create ticket"}
|
|
</Button>
|
|
</DialogFooter>
|
|
</form>
|
|
</DialogContent>
|
|
</Dialog>
|
|
);
|
|
}
|