Skip to content

Commit 4961d28

Browse files
committed
feat: add feedback system for closed support tickets
1 parent 34239ea commit 4961d28

File tree

2 files changed

+160
-3
lines changed

2 files changed

+160
-3
lines changed

apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCaseDetails.tsx

Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Spinner } from "@/components/ui/Spinner/Spinner";
2020
import { AutoResizeTextarea } from "@/components/ui/textarea";
2121
import { cn } from "@/lib/utils";
2222
import { ThirdwebMiniLogo } from "../../../../../../components/ThirdwebMiniLogo";
23+
import { submitSupportFeedback } from "../apis/feedback";
2324
import { sendMessageToTicket } from "../apis/support";
2425
import type { SupportMessage, SupportTicket } from "../types/tickets";
2526
import {
@@ -37,6 +38,65 @@ export function SupportCaseDetails({ ticket, team }: SupportCaseDetailsProps) {
3738
const [isSubmittingReply, setIsSubmittingReply] = useState(false);
3839
const [localMessages, setLocalMessages] = useState(ticket.messages || []);
3940

41+
// rating/feedback
42+
const [rating, setRating] = useState(0);
43+
const [feedback, setFeedback] = useState("");
44+
const [feedbackSubmitted, setFeedbackSubmitted] = useState(() => {
45+
// Check if feedback has already been submitted for this ticket
46+
if (typeof window !== "undefined") {
47+
const submittedTickets = JSON.parse(
48+
localStorage.getItem("feedbackSubmittedTickets") || "[]",
49+
);
50+
return submittedTickets.includes(ticket.id);
51+
}
52+
return false;
53+
});
54+
55+
const handleStarClick = (starIndex: number) => {
56+
setRating(starIndex + 1);
57+
};
58+
59+
const handleSendFeedback = async () => {
60+
if (rating === 0) {
61+
toast.error("Please select a rating");
62+
return;
63+
}
64+
65+
try {
66+
const result = await submitSupportFeedback({
67+
rating,
68+
feedback,
69+
ticketId: ticket.id,
70+
});
71+
72+
if ("error" in result) {
73+
throw new Error(result.error);
74+
}
75+
76+
toast.success("Thank you for your feedback!");
77+
setRating(0);
78+
setFeedback("");
79+
setFeedbackSubmitted(true);
80+
81+
// Store the ticket ID in localStorage to prevent future submissions
82+
if (typeof window !== "undefined") {
83+
const submittedTickets = JSON.parse(
84+
localStorage.getItem("feedbackSubmittedTickets") || "[]",
85+
);
86+
if (!submittedTickets.includes(ticket.id)) {
87+
submittedTickets.push(ticket.id);
88+
localStorage.setItem(
89+
"feedbackSubmittedTickets",
90+
JSON.stringify(submittedTickets),
91+
);
92+
}
93+
}
94+
} catch (error) {
95+
console.error("Failed to submit feedback:", error);
96+
toast.error("Failed to submit feedback. Please try again.");
97+
}
98+
};
99+
40100
const handleSendReply = async () => {
41101
if (!team.unthreadCustomerId) {
42102
toast.error("No unthread customer id found for this team");
@@ -149,11 +209,62 @@ export function SupportCaseDetails({ ticket, team }: SupportCaseDetailsProps) {
149209
)}
150210
</div>
151211

152-
{ticket.status === "closed" && (
212+
{ticket.status === "closed" && !feedbackSubmitted && (
213+
<div className="border-t p-6">
214+
<p className="text-muted-foreground text-sm">
215+
This ticket is closed. Give us a quick rating to let us know how
216+
we did!
217+
</p>
218+
219+
<div className="flex gap-2 mb-6 mt-4">
220+
{[1, 2, 3, 4, 5].map((starValue) => (
221+
<button
222+
key={`star-${starValue}`}
223+
type="button"
224+
onClick={() => handleStarClick(starValue - 1)}
225+
className="transition-colors"
226+
aria-label={`Rate ${starValue} out of 5 stars`}
227+
>
228+
<svg
229+
width="32"
230+
height="32"
231+
viewBox="0 0 24 24"
232+
fill={starValue <= rating ? "#ff00aa" : "none"}
233+
stroke={starValue <= rating ? "#ff00aa" : "#666"}
234+
strokeWidth={starValue <= rating ? "2" : "1"}
235+
className="hover:fill-pink-500 hover:stroke-pink-500 rounded-sm"
236+
rx="2"
237+
aria-hidden="true"
238+
>
239+
<polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26" />
240+
</svg>
241+
</button>
242+
))}
243+
</div>
244+
245+
<div className="relative">
246+
<textarea
247+
value={feedback}
248+
onChange={(e) => setFeedback(e.target.value)}
249+
placeholder="Optional: Tell us how we can improve."
250+
className="text-muted-foreground text-sm w-full bg-black text-white rounded-lg p-4 pr-28 min-h-[100px] resize-none border border-[#262626] focus:border-[#262626] focus:outline-none placeholder-[#A1A1A1]"
251+
/>
252+
<button
253+
type="button"
254+
onClick={handleSendFeedback}
255+
className="absolute mb-2 bottom-3 right-3 bg-white text-black px-4 py-2 rounded-full text-sm font-medium hover:bg-gray-100 transition-colors"
256+
>
257+
Send Feedback
258+
</button>
259+
</div>
260+
</div>
261+
)}
262+
263+
{ticket.status === "closed" && feedbackSubmitted && (
153264
<div className="border-t p-6">
154265
<p className="text-muted-foreground text-sm">
155-
This ticket is closed. If you need further assistance, please
156-
create a new ticket.
266+
Thank you for your feedback! We appreciate your input and will use
267+
it to improve our service.
157268
</p>
158269
</div>
159270
)}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"use server";
2+
3+
interface FeedbackData {
4+
rating: number;
5+
feedback: string;
6+
ticketId?: string;
7+
}
8+
9+
export async function submitSupportFeedback(
10+
data: FeedbackData,
11+
): Promise<{ success: true } | { error: string }> {
12+
try {
13+
const siwaUrl = process.env.NEXT_PUBLIC_SIWA_URL;
14+
15+
if (!siwaUrl) {
16+
throw new Error("SIWA URL not configured");
17+
}
18+
19+
const payload = {
20+
rating: data.rating,
21+
feedback: data.feedback,
22+
ticket_id: data.ticketId || null,
23+
};
24+
25+
const response = await fetch(`${siwaUrl}/v1/csat/saveCSATFeedback`, {
26+
method: "POST",
27+
headers: {
28+
"Content-Type": "application/json",
29+
"x-service-api-key": process.env.SERVICE_AUTH_KEY_SIWA || "",
30+
},
31+
body: JSON.stringify(payload),
32+
});
33+
34+
if (!response.ok) {
35+
const errorText = await response.text();
36+
return { error: `API Server error: ${response.status} - ${errorText}` };
37+
}
38+
39+
return { success: true };
40+
} catch (error) {
41+
console.error("Feedback submission error:", error);
42+
return {
43+
error: `Failed to submit feedback: ${error instanceof Error ? error.message : "Unknown error"}`,
44+
};
45+
}
46+
}

0 commit comments

Comments
 (0)