From 36e6b09d45b30b957d09cb212e449596012b3a28 Mon Sep 17 00:00:00 2001
From: Yash <67926590+Yash094@users.noreply.github.com>
Date: Fri, 11 Jul 2025 17:28:53 +0530
Subject: [PATCH 01/18] support-in-dashboard v1
---
apps/dashboard/src/@/api/support.ts | 328 +++++
.../(team)/~/support/SupportLayout.tsx | 24 +
.../support/_components/SupportAIChatCard.tsx | 60 +
.../_components/SupportCasesClient.tsx | 1123 +++++++++++++++++
.../~/support/_components/SupportTabs.tsx | 64 +
.../contact-forms/account/index.tsx | 29 +
.../connect/AffectedAreaInput.tsx | 59 +
.../contact-forms/connect/index.tsx | 133 ++
.../contact-forms/contracts/index.tsx | 133 ++
.../contact-forms/engine/index.tsx | 57 +
.../_components/contact-forms/other/index.tsx | 53 +
.../contact-forms/payments/index.tsx | 50 +
.../tokens-marketplace/index.tsx | 24 +
.../shared/SupportForm_DescriptionInput.tsx | 33 +
.../shared/SupportForm_SelectInput.tsx | 60 +
.../shared/SupportForm_TextInput.tsx | 36 +
.../shared/SupportForm_UnityInput.tsx | 48 +
.../[team_slug]/(team)/~/support/layout.tsx | 37 +
.../[team_slug]/(team)/~/support/page.tsx | 40 +
19 files changed, 2391 insertions(+)
create mode 100644 apps/dashboard/src/@/api/support.ts
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/SupportLayout.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportAIChatCard.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTabs.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/account/index.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/AffectedAreaInput.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/index.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/contracts/index.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/engine/index.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/other/index.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/payments/index.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/tokens-marketplace/index.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_DescriptionInput.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_SelectInput.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_TextInput.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_UnityInput.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/layout.tsx
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/page.tsx
diff --git a/apps/dashboard/src/@/api/support.ts b/apps/dashboard/src/@/api/support.ts
new file mode 100644
index 00000000000..286f793490c
--- /dev/null
+++ b/apps/dashboard/src/@/api/support.ts
@@ -0,0 +1,328 @@
+"use server";
+import "server-only";
+import { NEXT_PUBLIC_THIRDWEB_API_HOST } from "@/constants/public-envs";
+import { getAuthToken, getAuthTokenWalletAddress } from "./auth-token";
+
+export interface SupportTicket {
+ id: string;
+ status: "needs_response" | "in_progress" | "on_hold" | "closed" | "resolved";
+ createdAt: string;
+ updatedAt: string;
+ messages?: SupportMessage[];
+}
+
+interface SupportMessage {
+ id: string;
+ content: string;
+ createdAt: string;
+ timestamp: string;
+ author?: {
+ name: string;
+ email: string;
+ type: "user" | "customer";
+ };
+}
+
+interface CreateSupportTicketRequest {
+ message: string;
+ teamSlug: string;
+ title: string;
+}
+
+interface SendMessageRequest {
+ ticketId: string;
+ teamSlug: string;
+ message: string;
+}
+
+export async function getSupportTicketsByTeam(
+ teamSlug: string,
+ authToken?: string,
+): Promise {
+ if (!teamSlug) {
+ throw new Error("Team slug is required to fetch support tickets");
+ }
+
+ const token = authToken || (await getAuthToken());
+ if (!token) {
+ throw new Error("No auth token available");
+ }
+
+ // URL encode the team slug to handle special characters like #
+ const encodedTeamSlug = encodeURIComponent(teamSlug);
+ const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/list`;
+
+ // Build the POST payload according to API spec
+ const payload = {
+ limit: 50,
+ descending: true,
+ };
+
+ const response = await fetch(apiUrl, {
+ body: JSON.stringify(payload),
+ cache: "no-store",
+ headers: {
+ Accept: "application/json",
+ "Accept-Encoding": "identity",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ method: "POST",
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`API Server error: ${response.status} - ${errorText}`);
+ }
+ const data: { data?: SupportTicket[] } = await response.json();
+ const conversations = data.data || [];
+ return conversations;
+}
+
+interface RawSupportMessage {
+ id: string;
+ text?: string;
+ timestamp?: string;
+ createdAt?: string;
+ isPrivateNote?: boolean;
+ sentByUser?: {
+ name: string;
+ email: string;
+ isExternal: boolean;
+ };
+ // Add any other fields you use from the API
+}
+
+export async function getSupportTicket(
+ ticketId: string,
+ teamSlug: string,
+ authToken?: string,
+): Promise {
+ if (!ticketId || !teamSlug) {
+ throw new Error("Ticket ID and team slug are required");
+ }
+
+ const token = authToken || (await getAuthToken());
+ if (!token) {
+ throw new Error("No auth token available");
+ }
+
+ // URL encode the team slug to handle special characters like #
+ const encodedTeamSlug = encodeURIComponent(teamSlug);
+ const encodedTicketId = encodeURIComponent(ticketId);
+
+ const messagesPayload = {
+ limit: 100,
+ descending: false,
+ };
+
+ // Fetch conversation details and messages in parallel
+ const [conversationResponse, messagesResponse] = await Promise.all([
+ fetch(
+ `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}`,
+ {
+ cache: "no-store",
+ headers: {
+ Accept: "application/json",
+ "Accept-Encoding": "identity",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ method: "GET",
+ },
+ ),
+ fetch(
+ `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}/messages/list`,
+ {
+ body: JSON.stringify(messagesPayload),
+ cache: "no-store",
+ headers: {
+ Accept: "application/json",
+ "Accept-Encoding": "identity",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ },
+ method: "POST",
+ },
+ ),
+ ]);
+
+ if (!conversationResponse.ok) {
+ if (conversationResponse.status === 404) {
+ return null; // Ticket not found
+ }
+ const errorText = await conversationResponse.text();
+ throw new Error(
+ `API Server error: ${conversationResponse.status} - ${errorText}`,
+ );
+ }
+
+ const conversation: SupportTicket = await conversationResponse.json();
+
+ // Fetch and map messages if the messages request was successful
+ if (messagesResponse.ok) {
+ const messagesData: { data?: unknown[] } = await messagesResponse.json();
+ const rawMessages = messagesData.data || [];
+ console.log("rawMessages", rawMessages);
+ // Transform the raw messages to match our interface
+ const messages: SupportMessage[] = (rawMessages as RawSupportMessage[])
+ .filter((msg) => {
+ // Filter out messages without content - check both text and text fields
+ const hasContent = msg.text && msg.text.length > 0;
+ const hasText = msg.text && msg.text.trim().length > 0;
+ // Filter out private notes - they should not be shown to customers
+ const isNotPrivateNote = !msg.isPrivateNote;
+ return (hasContent || hasText) && isNotPrivateNote;
+ })
+ .map((msg) => {
+ // Use text if available and is a non-empty array, otherwise fall back to text
+ let content = "";
+ if (typeof msg.text === "string" && msg.text.trim().length > 0) {
+ content = msg.text;
+ }
+
+ // Clean up 'Email:' line to show only the plain email if it contains a mailto link
+ if (content) {
+ content = content
+ .split("\n")
+ .map((line) => {
+ if (line.trim().toLowerCase().startsWith("email:")) {
+ // Extract email from
+ const match = line.match(/]+)\|[^>]+>/);
+ if (match) {
+ return `Email: ${match[1]}`;
+ }
+ }
+ return line;
+ })
+ .join("\n");
+ }
+
+ // Map the author information from sentByUser if available
+ const author = msg.sentByUser
+ ? {
+ name: msg.sentByUser.name,
+ email: msg.sentByUser.email,
+ type: (msg.sentByUser.isExternal ? "customer" : "user") as
+ | "user"
+ | "customer",
+ }
+ : undefined;
+
+ return {
+ id: msg.id,
+ content: content,
+ createdAt: msg.timestamp || msg.createdAt || "",
+ timestamp: msg.timestamp || msg.createdAt || "",
+ author: author,
+ };
+ });
+
+ conversation.messages = messages;
+ } else {
+ // Don't throw error, just leave messages empty
+ const _errorText = await messagesResponse.text();
+ conversation.messages = [];
+ }
+
+ return conversation;
+}
+
+export async function createSupportTicket(
+ request: CreateSupportTicketRequest,
+): Promise {
+ if (!request.teamSlug) {
+ throw new Error("Team slug is required to create support ticket");
+ }
+
+ const token = await getAuthToken();
+ if (!token) {
+ throw new Error("No auth token available");
+ }
+
+ // Fetch wallet address (server-side)
+ const walletAddress = await getAuthTokenWalletAddress();
+
+ // URL encode the team slug to handle special characters like #
+ const encodedTeamSlug = encodeURIComponent(request.teamSlug);
+ const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations`;
+
+ // Build the payload for creating a conversation
+ // If the message does not already include wallet address, prepend it
+ let message = request.message;
+ if (!message.includes("Wallet address:")) {
+ message = `Wallet address: ${String(walletAddress || "-")}\n${message}`;
+ }
+
+ const payload = {
+ markdown: message.trim(),
+ title: request.title,
+ };
+
+ const body = JSON.stringify(payload);
+ const headers: Record = {
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ "Accept-Encoding": "identity",
+ };
+
+ const response = await fetch(apiUrl, {
+ body,
+ headers,
+ method: "POST",
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`API Server error: ${response.status} - ${errorText}`);
+ }
+
+ const createdConversation: SupportTicket = await response.json();
+ return createdConversation;
+}
+
+export async function sendMessageToTicket(
+ request: SendMessageRequest,
+): Promise {
+ if (!request.ticketId || !request.teamSlug) {
+ throw new Error("Ticket ID and team slug are required");
+ }
+
+ const token = await getAuthToken();
+ if (!token) {
+ throw new Error("No auth token available");
+ }
+
+ // URL encode the team slug and ticket ID to handle special characters like #
+ const encodedTeamSlug = encodeURIComponent(request.teamSlug);
+ const encodedTicketId = encodeURIComponent(request.ticketId);
+ const apiUrl = `${NEXT_PUBLIC_THIRDWEB_API_HOST}/v1/teams/${encodedTeamSlug}/support-conversations/${encodedTicketId}/messages`;
+
+ // Build the payload for sending a message
+ // Append /unthread send for customer messages to ensure proper routing
+ const messageWithUnthread = `${request.message.trim()}\n/unthread send`;
+ const payload = {
+ markdown: messageWithUnthread,
+ };
+
+ const body = JSON.stringify(payload);
+ const headers: Record = {
+ Accept: "application/json",
+ Authorization: `Bearer ${token}`,
+ "Content-Type": "application/json",
+ "Accept-Encoding": "identity",
+ };
+
+ const response = await fetch(apiUrl, {
+ body,
+ headers,
+ method: "POST",
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ throw new Error(`API Server error: ${response.status} - ${errorText}`);
+ }
+ // Message sent successfully, no need to return anything
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/SupportLayout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/SupportLayout.tsx
new file mode 100644
index 00000000000..2d7ac797ceb
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/SupportLayout.tsx
@@ -0,0 +1,24 @@
+"use client";
+
+import { cn } from "@/lib/utils";
+
+export function SupportLayout(props: { children: React.ReactNode }) {
+ // If mobile navigation is needed in the future, add state and logic here.
+ const showFullNavOnMobile = true;
+
+ return (
+
+ {/* Page content */}
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportAIChatCard.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportAIChatCard.tsx
new file mode 100644
index 00000000000..a398c715f1d
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportAIChatCard.tsx
@@ -0,0 +1,60 @@
+"use client";
+import { BotIcon } from "lucide-react";
+import { useState } from "react";
+
+export function SupportAIChatCard({
+ _authToken,
+ _teamId,
+ onStartChat,
+}: {
+ _authToken?: string;
+ _teamId?: string;
+ onStartChat: (message: string) => void;
+}) {
+ const [message, setMessage] = useState("");
+
+ return (
+
+
+
+
+
+
+
Ask AI for support
+
+
+ Online
+
+
+
+
+
+ I’ll help you troubleshoot. If I can’t fix it, I’ll pass it to our
+ support team.
+
+
+
+ setMessage(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && !e.shiftKey && message.trim()) {
+ e.preventDefault();
+ onStartChat(message);
+ }
+ }}
+ placeholder="Type a message..."
+ value={message}
+ />
+ onStartChat(message)}
+ type="button"
+ >
+ Start Chat
+
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx
new file mode 100644
index 00000000000..1d786a70add
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx
@@ -0,0 +1,1123 @@
+"use client";
+
+import { format } from "date-fns";
+import {
+ ArrowRightIcon,
+ BotIcon,
+ LoaderCircleIcon,
+ SendIcon,
+} from "lucide-react";
+import dynamic from "next/dynamic";
+import { useEffect, useRef, useState } from "react";
+import { toast } from "sonner";
+import type { SupportTicket } from "@/api/support";
+import {
+ createSupportTicket,
+ getSupportTicket,
+ sendMessageToTicket,
+} from "@/api/support";
+import type { Team } from "@/api/team";
+import { MarkdownRenderer } from "@/components/blocks/markdown-renderer";
+import { Reasoning } from "@/components/chat/Reasoning";
+import { Badge } from "@/components/ui/badge";
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import { Textarea } from "@/components/ui/textarea";
+import { useDashboardRouter } from "@/lib/DashboardRouter";
+import { SupportAIChatCard } from "./SupportAIChatCard";
+import { SupportTabs } from "./SupportTabs";
+import { SupportForm_SelectInput } from "./shared/SupportForm_SelectInput";
+
+// Dynamic imports for contact forms using named exports
+const ConnectSupportForm = dynamic(
+ () => import("./contact-forms/connect").then((mod) => mod.ConnectSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const EngineSupportForm = dynamic(
+ () => import("./contact-forms/engine").then((mod) => mod.EngineSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const ContractSupportForm = dynamic(
+ () =>
+ import("./contact-forms/contracts").then((mod) => mod.ContractSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const AccountSupportForm = dynamic(
+ () => import("./contact-forms/account").then((mod) => mod.AccountSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const OtherSupportForm = dynamic(
+ () => import("./contact-forms/other").then((mod) => mod.OtherSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const PaymentsSupportForm = dynamic(
+ () =>
+ import("./contact-forms/payments").then((mod) => mod.PaymentsSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const TokensMarketplaceSupportForm = dynamic(
+ () =>
+ import("./contact-forms/tokens-marketplace").then(
+ (mod) => mod.TokensMarketplaceSupportForm,
+ ),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+
+const productOptions = [
+ {
+ component: ,
+ label: "Wallets",
+ },
+ {
+ component: ,
+ label: "Transactions",
+ },
+ {
+ component: ,
+ label: "Payments",
+ },
+ {
+ component: ,
+ label: "Contracts",
+ },
+ {
+ component: ,
+ label: "Tokens / Marketplace",
+ },
+ {
+ component: ,
+ label: "Account",
+ },
+ {
+ component: ,
+ label: "Other",
+ },
+];
+
+function ProductAreaSelection(props: {
+ productLabel: string;
+ setProductLabel: (val: string) => void;
+}) {
+ const { productLabel, setProductLabel } = props;
+
+ return (
+
+ o.label)}
+ promptText="Brief description of your issue"
+ required={true}
+ value={productLabel}
+ />
+ {productOptions.find((o) => o.label === productLabel)?.component}
+
+ );
+}
+
+interface SupportCasesClientProps {
+ tickets: SupportTicket[];
+ team: Team;
+ authToken?: string;
+}
+
+export default function SupportCasesClient({
+ tickets,
+ team,
+ authToken,
+}: SupportCasesClientProps) {
+ const [selectedCaseId, setSelectedCaseId] = useState(null);
+ const [selectedCaseDetails, setSelectedCaseDetails] =
+ useState(null);
+ const [isLoadingCaseDetails, setIsLoadingCaseDetails] = useState(false);
+ const [activeTab, setActiveTab] = useState("all");
+ const [searchQuery, setSearchQuery] = useState("");
+ const [replyMessage, setReplyMessage] = useState("");
+ const [isSubmittingReply, setIsSubmittingReply] = useState(false);
+ const messagesEndRef = useRef(null);
+ const [showAIChat, setShowAIChat] = useState(false);
+ const [chatMessages, setChatMessages] = useState<
+ {
+ id: number;
+ content: string;
+ isUser: boolean;
+ timestamp: string;
+ isSuccessMessage?: boolean;
+ }[]
+ >([]);
+ const [chatInput, setChatInput] = useState("");
+ const [conversationId, setConversationId] = useState(
+ undefined,
+ );
+ const chatContainerRef = useRef(null);
+ const [_inputFocused, _setInputFocusedd] = useState(false);
+ const _router = useDashboardRouter();
+ const replySectionRef = useRef(null);
+
+ // Form states
+ const [showCreateForm, setShowCreateForm] = useState(false);
+ const [productLabel, setProductLabel] = useState("");
+ const [isSubmittingForm, setIsSubmittingForm] = useState(false);
+ const formRef = useRef(null);
+ const _formContainerRef = useRef(null);
+
+ const selectedCase =
+ selectedCaseDetails || tickets.find((c) => c.id === selectedCaseId);
+
+ // Scroll to bottom when messages change
+ // eslint-disable-next-line no-restricted-syntax
+ useEffect(() => {
+ if (messagesEndRef.current && selectedCaseDetails?.messages) {
+ messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
+ }
+ }, [selectedCaseDetails?.messages]);
+
+ // eslint-disable-next-line no-restricted-syntax
+ useEffect(() => {
+ if (chatContainerRef.current && chatMessages.length > 0) {
+ chatContainerRef.current.scrollTop =
+ chatContainerRef.current.scrollHeight;
+ }
+ }, [chatMessages]);
+
+ // Scroll to show new form fields when product type changes or form updates
+ // eslint-disable-next-line no-restricted-syntax
+ useEffect(() => {
+ if (showCreateForm && chatContainerRef.current) {
+ const scrollToBottom = () => {
+ if (chatContainerRef.current) {
+ chatContainerRef.current.scrollTo({
+ top: chatContainerRef.current.scrollHeight,
+ behavior: "smooth",
+ });
+ }
+ };
+
+ // Set up a MutationObserver to watch for form changes
+ const formContainer = chatContainerRef.current.querySelector("form");
+ if (formContainer) {
+ const observer = new MutationObserver((mutations) => {
+ let shouldScroll = false;
+ mutations.forEach((mutation) => {
+ if (
+ mutation.type === "childList" &&
+ mutation.addedNodes.length > 0
+ ) {
+ // Check if any added nodes contain form fields
+ mutation.addedNodes.forEach((node) => {
+ if (node.nodeType === Node.ELEMENT_NODE) {
+ const element = node as Element;
+ if (
+ element.querySelector("input, textarea, select") ||
+ element.matches("input, textarea, select")
+ ) {
+ shouldScroll = true;
+ }
+ }
+ });
+ }
+ });
+
+ if (shouldScroll) {
+ setTimeout(scrollToBottom, 100);
+ }
+ });
+
+ observer.observe(formContainer, {
+ childList: true,
+ subtree: true,
+ });
+
+ // Initial scroll when form appears
+ scrollToBottom();
+ setTimeout(scrollToBottom, 100);
+ setTimeout(scrollToBottom, 300);
+
+ return () => observer.disconnect();
+ }
+ }
+ }, [showCreateForm]);
+
+ // Scroll to reply section when a case is selected
+ // eslint-disable-next-line no-restricted-syntax
+ useEffect(() => {
+ if (selectedCase && replySectionRef.current && chatContainerRef.current) {
+ const chat = chatContainerRef.current;
+ const reply = replySectionRef.current;
+ // Calculate offset of reply section relative to chat container
+ const chatRect = chat.getBoundingClientRect();
+ const replyRect = reply.getBoundingClientRect();
+ const offset = replyRect.top - chatRect.top + chat.scrollTop;
+ chat.scrollTo({ top: offset, behavior: "smooth" });
+ }
+ }, [selectedCase]);
+
+ // Scroll to bottom of AI chat when messages change
+ // eslint-disable-next-line no-restricted-syntax
+ useEffect(() => {
+ if (showAIChat && messagesEndRef.current) {
+ messagesEndRef.current.scrollIntoView({ behavior: "smooth" });
+ }
+ }, [showAIChat]);
+
+ const handleFormSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!productLabel) {
+ toast.error("Please select what you need help with");
+ return;
+ }
+
+ if (!formRef.current) return;
+ const formData = new FormData(formRef.current);
+ const description = formData.get("markdown") as string;
+
+ if (!description?.trim()) {
+ toast.error("Please provide a description");
+ return;
+ }
+
+ setIsSubmittingForm(true);
+
+ try {
+ // Get all extra fields from the form
+ const extraFields = Array.from(formData.entries()).filter(([key]) =>
+ key.startsWith("extraInfo_"),
+ );
+
+ // Format the message
+ let formattedMessage = `Email: ${String(team.billingEmail ?? "-")}\nName: ${String(team.name ?? "-")}\nProduct: ${String(productLabel ?? "-")}`;
+
+ // Add all extra fields above the message
+ if (extraFields.length > 0) {
+ extraFields.forEach(([key, value]) => {
+ if (value) {
+ const fieldName = key.replace("extraInfo_", "").replace(/_/g, " ");
+ formattedMessage += `\n${fieldName}: ${String(value)}`;
+ }
+ });
+ }
+
+ formattedMessage += `\nMessage:\n${String(description ?? "-")}`;
+
+ if (conversationId) {
+ formattedMessage += `\n\n---\nAI Conversation ID: ${conversationId}`;
+ }
+
+ await createSupportTicket({
+ message: formattedMessage,
+ teamSlug: team.slug,
+ title: `${productLabel} Issue - ${team.billingEmail} (${team.billingPlan})`,
+ });
+
+ // Add success message to chat
+ const successMsg = {
+ content:
+ "Great! Your support case has been created successfully. Our technical team will review it and get back to you soon. You can continue chatting with me if you have any other questions.",
+ id: Date.now(),
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ isSuccessMessage: true,
+ };
+
+ setChatMessages((msgs) => [...msgs, successMsg]);
+ setShowCreateForm(false);
+ setProductLabel("");
+
+ toast.success("Support ticket created successfully!");
+ } catch (error) {
+ console.error("Error creating support ticket:", error);
+ toast.error("Failed to create support ticket. Please try again.");
+ } finally {
+ setIsSubmittingForm(false);
+ }
+ };
+
+ const handleSelectCase = async (ticketId: string) => {
+ setSelectedCaseId(ticketId);
+ setSelectedCaseDetails(null);
+ setIsLoadingCaseDetails(true);
+
+ try {
+ const ticketDetails = await getSupportTicket(
+ ticketId,
+ team.slug,
+ authToken,
+ );
+ if (ticketDetails) {
+ setSelectedCaseDetails(ticketDetails);
+ }
+ } catch (_error) {
+ toast.error("Failed to load ticket details");
+ } finally {
+ setIsLoadingCaseDetails(false);
+ }
+ };
+
+ const handleBackToCases = () => {
+ setSelectedCaseId(null);
+ setSelectedCaseDetails(null);
+ setReplyMessage("");
+ };
+
+ const handleSendReply = async () => {
+ if (
+ !selectedCase ||
+ !replyMessage.trim() ||
+ !team.unthreadCustomerId ||
+ !team.billingEmail
+ ) {
+ toast.error("Please enter a message");
+ return;
+ }
+
+ setIsSubmittingReply(true);
+
+ try {
+ await sendMessageToTicket({
+ message: replyMessage,
+ teamSlug: team.slug,
+ ticketId: selectedCase.id,
+ });
+
+ toast.success("Reply sent successfully!");
+ setReplyMessage("");
+
+ // Force refresh the ticket details to show the new message
+ setSelectedCaseDetails(null);
+ setIsLoadingCaseDetails(true);
+
+ try {
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+ const ticketDetails = await getSupportTicket(
+ selectedCase.id,
+ team.slug,
+ authToken,
+ );
+ if (ticketDetails) {
+ setSelectedCaseDetails(ticketDetails);
+ }
+ } catch (refreshError) {
+ console.error("Error refreshing ticket details:", refreshError);
+ } finally {
+ setIsLoadingCaseDetails(false);
+ }
+ } catch (_error) {
+ toast.error("Failed to send reply. Please try again.");
+ } finally {
+ setIsSubmittingReply(false);
+ }
+ };
+
+ const handleKeyDown = (e: React.KeyboardEvent) => {
+ if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
+ e.preventDefault();
+ handleSendReply();
+ }
+ };
+
+ // Filter tickets based on active tab and search query
+ const filteredTickets = tickets.filter((ticket) => {
+ // Filter by tab
+ let matchesTab = true;
+ switch (activeTab) {
+ case "open":
+ matchesTab =
+ (ticket.status as string) === "in_progress" ||
+ (ticket.status as string) === "needs_response" ||
+ (ticket.status as string) === "on_hold";
+ break;
+ case "closed":
+ matchesTab = ticket.status === "resolved" || ticket.status === "closed";
+ break;
+ default:
+ matchesTab = true;
+ }
+
+ // Filter by search query
+ const matchesSearch =
+ searchQuery === "" ||
+ ticket.id.toLowerCase().includes(searchQuery.toLowerCase());
+
+ return matchesTab && matchesSearch;
+ });
+
+ // Helper function to check if ticket matches search query
+ const matchesSearch = (ticket: SupportTicket) => {
+ return (
+ searchQuery === "" ||
+ ticket.id.toLowerCase().includes(searchQuery.toLowerCase())
+ );
+ };
+
+ // Calculate counts for tabs
+ const counts = {
+ all: tickets.filter(matchesSearch).length,
+ closed: tickets.filter(
+ (ticket) =>
+ (ticket.status === "resolved" || ticket.status === "closed") &&
+ matchesSearch(ticket),
+ ).length,
+ open: tickets.filter(
+ (ticket) =>
+ ((ticket.status as string) === "in_progress" ||
+ (ticket.status as string) === "needs_response" ||
+ (ticket.status as string) === "on_hold") &&
+ matchesSearch(ticket),
+ ).length,
+ };
+
+ const getStatusColor = (status: string) => {
+ switch (status.toLowerCase()) {
+ case "resolved":
+ case "closed":
+ return "border-gray-500 text-gray-500 bg-gray-500/10";
+ case "in_progress":
+ return "border-red-500 text-red-500 bg-red-500/10";
+ case "needs_response":
+ return "border-yellow-500 text-yellow-500 bg-yellow-500/10";
+ case "on_hold":
+ return "border-purple-500 text-purple-500 bg-purple-500/10";
+ default:
+ return "border-yellow-500 text-yellow-500 bg-yellow-500/10";
+ }
+ };
+
+ const getStatusLabel = (status: string, _ticket?: SupportTicket) => {
+ const statusLower = status.toLowerCase();
+
+ switch (statusLower) {
+ case "closed":
+ return "Closed";
+ case "resolved":
+ return "Resolved";
+ case "in_progress":
+ return "Needs Response";
+ case "needs_response":
+ return "In Progress";
+ case "on_hold":
+ return "On Hold";
+ default:
+ return "In Progress";
+ }
+ };
+
+ async function sendMessageToSiwa(
+ message: string,
+ currentConversationId?: string,
+ ) {
+ const apiUrl = process.env.NEXT_PUBLIC_SIWA_URL;
+ const payload = {
+ conversationId: currentConversationId,
+ message,
+ source: "support-in-dashboard",
+ };
+ const response = await fetch(`${apiUrl}/v1/chat`, {
+ body: JSON.stringify(payload),
+ headers: {
+ Authorization: `Bearer ${authToken}`,
+ "Content-Type": "application/json",
+ "x-team-id": team.id,
+ },
+ method: "POST",
+ });
+ const data = await response.json();
+ if (data.conversationId && data.conversationId !== conversationId) {
+ setConversationId(data.conversationId);
+ }
+ return data.data;
+ }
+
+ async function handleStartChat(initialMessage: string) {
+ setShowAIChat(true);
+ // Update AI greeting message
+ const initialAIMsg = {
+ content:
+ "Hi! I’m thirdweb’s AI assistant — I’ll help you troubleshoot. If I can’t fix it, I’ll pass it to our support",
+ id: Date.now(),
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ };
+ const userMsg = {
+ content: initialMessage,
+ id: Date.now() + 1,
+ isUser: true,
+ timestamp: new Date().toISOString(),
+ };
+ setChatMessages([
+ initialAIMsg,
+ userMsg,
+ {
+ content: "__reasoning__",
+ id: Date.now() + 2,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ setChatInput("");
+ try {
+ const aiResponse = await sendMessageToSiwa(initialMessage);
+ setChatMessages([
+ initialAIMsg,
+ userMsg,
+ {
+ content: aiResponse,
+ id: Date.now() + 3,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ } catch (_error) {
+ setChatMessages([
+ initialAIMsg,
+ userMsg,
+ {
+ content: "Sorry, something went wrong. Please try again.",
+ id: Date.now() + 4,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ }
+ }
+
+ async function handleChatSend() {
+ if (!chatInput.trim()) return;
+ const userMsg = {
+ content: chatInput,
+ id: Date.now(),
+ isUser: true,
+ timestamp: new Date().toISOString(),
+ };
+ setChatMessages((msgs) => [
+ ...msgs,
+ userMsg,
+ {
+ content: "__reasoning__",
+ id: Date.now() + 1,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ setChatInput("");
+ try {
+ const aiResponse = await sendMessageToSiwa(chatInput, conversationId);
+ setChatMessages((msgs) => [
+ ...msgs.slice(0, -1), // remove loading
+ {
+ content: aiResponse,
+ id: Date.now() + 2,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ } catch (_error) {
+ setChatMessages((msgs) => [
+ ...msgs.slice(0, -1),
+ {
+ content: "Sorry, something went wrong. Please try again.",
+ id: Date.now() + 3,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ },
+ ]);
+ }
+ }
+
+ function handleChatKeyPress(e: React.KeyboardEvent) {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleChatSend();
+ }
+ }
+
+ if (showAIChat) {
+ return (
+
+
+
+ {
+ setShowAIChat(false);
+ setChatMessages([]);
+ setShowCreateForm(false);
+ }}
+ variant="outline"
+ className="border-[#1F1F1F] bg-[#0A0A0A] text-white hover:bg-[#1F1F1F] hover:text-white"
+ >
+ ← Back to Support Portal
+
+
+
+
+
+ {/* Chat Header */}
+
+
+
+
Ask AI for support
+
Online
+
+
+
+ {/* Chat Messages */}
+
+ {chatMessages.map((message, index) => (
+
+
+ {message.isUser ? (
+
+ {message.content}
+
+ ) : message.content === "__reasoning__" ? (
+
+ ) : (
+
+ )}
+
+ {format(new Date(message.timestamp), "h:mm a")}
+
+
+ {/* Show Back to Support Portal button for success message */}
+ {!message.isUser && message.isSuccessMessage && (
+
+
{
+ setShowAIChat(false);
+ setChatMessages([]);
+ setShowCreateForm(false);
+ }}
+ size="sm"
+ className="bg-[#2663EB] hover:bg-[#2663EB]/80 text-white transition-opacity"
+ >
+ Back to Support Portal
+
+
+
+ )}
+
+ {/* Show Create Support Case button in the AI response (index 2) - only if form not shown */}
+ {!message.isUser &&
+ index === chatMessages.length - 1 &&
+ !showCreateForm &&
+ !message.isSuccessMessage &&
+ message.content !== "__reasoning__" && (
+
+
setShowCreateForm(true)}
+ size="sm"
+ className="bg-[#2663EB] hover:bg-[#2663EB]/80 text-white transition-opacity"
+ >
+ Create Support Case
+
+
+
+ )}
+
+ {/* Show Support Case Form in the same message bubble when button is clicked */}
+ {!message.isUser &&
+ index === chatMessages.length - 1 &&
+ showCreateForm &&
+ message.content !== "__reasoning__" && (
+
+
+
+ Create Support Case
+
+
+ Let's create a detailed support case for our
+ technical team.
+
+
+
+
+
+ )}
+
+
+ ))}
+
+
+
+ {/* Chat Input */}
+ {!showCreateForm && (
+
+
+
+
+ Press Enter to send, Shift+Enter for new line
+
+
+ )}
+
+
+
+
+ );
+ }
+
+ if (selectedCase) {
+ return (
+
+
+
+
+ ← Back to Cases
+
+
+
+
+
+
+ Ticket #{selectedCase.id}
+
+
+
+ {getStatusLabel(selectedCase.status, selectedCase)}
+
+
+ #{selectedCase.id}
+
+
+
+
+
+
+
+
Status:
+
+ {getStatusLabel(selectedCase.status, selectedCase)}
+
+
+
+
Created:
+
+ {format(new Date(selectedCase.createdAt), "MMM d, yyyy")}
+
+
+
+
+
+
+ Messages
+
+
+ {isLoadingCaseDetails ? (
+
+ ) : selectedCase.messages &&
+ selectedCase.messages.length > 0 ? (
+ selectedCase.messages.map((message) => {
+ const isCustomer = message.author?.type === "customer";
+ const authorName = message.author?.name || "Support";
+ const displayName = isCustomer ? "You" : authorName;
+
+ let messageContent =
+ message.content || "No content available";
+
+ if (messageContent.includes("/unthread send")) {
+ messageContent = messageContent
+ .replace("/unthread send", "")
+ .trim();
+ }
+
+ const messageDate =
+ message.timestamp || message.createdAt;
+
+ return (
+
+
+
+
+ {displayName}
+
+
+ {!isCustomer ? "Support" : "Customer"}
+
+
+
+ {format(
+ new Date(messageDate),
+ "MMM d, yyyy 'at' h:mm a",
+ )}
+
+
+
+
+ );
+ })
+ ) : (
+
+
+ No messages yet for this ticket.
+
+
+ )}
+
+
+
+ {/* Reply Section */}
+ {selectedCase.status !== "closed" &&
+ selectedCase.status !== "resolved" ? (
+
+
+ Reply to this case
+
+
+
+ ) : (
+
+
+ This ticket is closed. If you need further assistance,
+ please create a new ticket.
+
+
+ )}
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+ {filteredTickets.length === 0 ? (
+
+
+
+ No cases found
+
+
+ {activeTab === "all"
+ ? "You don't have any support cases yet."
+ : `No ${activeTab} cases found.`}
+
+
+
+ ) : (
+
+ {filteredTickets.map((ticket) => (
+
handleSelectCase(ticket.id)}
+ type="button"
+ >
+
+
+
+
+ {getStatusLabel(ticket.status, ticket)}
+
+
+
+
+
+ Ticket #{ticket.id}
+
+
+
+
+ {format(
+ new Date(ticket.updatedAt || ticket.createdAt),
+ "MMM d, yyyy",
+ )}
+
+
+
+
+ ))}
+
+ )}
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTabs.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTabs.tsx
new file mode 100644
index 00000000000..c08de6a0e47
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTabs.tsx
@@ -0,0 +1,64 @@
+import { SearchIcon } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { cn } from "@/lib/utils";
+
+interface SupportTabsProps {
+ activeTab: string;
+ onTabChange: (tab: string) => void;
+ searchQuery: string;
+ onSearchChange: (query: string) => void;
+ counts: {
+ all: number;
+ open: number;
+ closed: number;
+ };
+}
+
+export function SupportTabs({
+ activeTab,
+ onTabChange,
+ searchQuery,
+ onSearchChange,
+ counts,
+}: SupportTabsProps) {
+ const tabs = [
+ { count: counts.all, id: "all", label: "All" },
+ { count: counts.open, id: "open", label: "Open" },
+ { count: counts.closed, id: "closed", label: "Closed" },
+ ];
+
+ return (
+
+ {/* Search Bar */}
+
+
+ onSearchChange(e.target.value)}
+ placeholder="Search cases..."
+ value={searchQuery}
+ />
+
+
+ {/* Tab Buttons */}
+
+ {tabs.map((tab) => (
+ onTabChange(tab.id)}
+ variant="ghost"
+ >
+ {tab.label} ({tab.count})
+
+ ))}
+
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/account/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/account/index.tsx
new file mode 100644
index 00000000000..f7fb8fde39a
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/account/index.tsx
@@ -0,0 +1,29 @@
+import { useState } from "react";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+
+const ACCOUNT_PROBLEM_AREAS = [
+ "Pricing inquiry",
+ "Billing inquiry",
+ "Usage inquiry",
+ "Other",
+];
+
+export function AccountSupportForm() {
+ const [problemArea, setProblemArea] = useState("");
+
+ return (
+ <>
+
+ {problemArea && }
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/AffectedAreaInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/AffectedAreaInput.tsx
new file mode 100644
index 00000000000..5c086920bc7
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/AffectedAreaInput.tsx
@@ -0,0 +1,59 @@
+import { useState } from "react";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
+import { UnitySupportForm } from "../../shared/SupportForm_UnityInput";
+
+const AFFECTED_AREAS = ["Dashboard", "Application"];
+
+export const AffectedAreaInput = () => {
+ const [selectedAffectedArea, setSelectedAffectedArea] = useState("");
+ const [selectedSDK, setSelectedSDK] = useState("");
+ return (
+ <>
+
+ {selectedAffectedArea &&
+ (selectedAffectedArea === "Application" ? (
+ <>
+
+ {selectedSDK && (
+ <>
+ {selectedSDK === "Unity" && }
+
+
+
+ >
+ )}
+ >
+ ) : (
+
+ ))}
+ >
+ );
+};
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/index.tsx
new file mode 100644
index 00000000000..ea1a3dce64f
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/index.tsx
@@ -0,0 +1,133 @@
+import { type ReactElement, useState } from "react";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
+import { UnitySupportForm } from "../../shared/SupportForm_UnityInput";
+import { AffectedAreaInput } from "./AffectedAreaInput";
+
+type ProblemAreaItem = {
+ label: string;
+ component: ReactElement;
+};
+
+const SDKVersionInput = () => (
+
+);
+
+const OSSelect = () => {
+ const [selectedOS, setSelectedOS] = useState("");
+ return (
+
+ );
+};
+
+const PROBLEM_AREAS: ProblemAreaItem[] = [
+ {
+ component: ,
+ label: "Embedded wallet login issues",
+ },
+ {
+ component: ,
+ label: "Embedded wallet transaction issues",
+ },
+ {
+ component: ,
+ label: "Embedded wallet Custom Auth",
+ },
+ {
+ component: ,
+ label: "Account Abstraction",
+ },
+ {
+ component: ,
+ label: "In-app wallet",
+ },
+ {
+ component: (
+ <>
+
+
+
+ >
+ ),
+ label: "Connect SDKs",
+ },
+ {
+ component: (
+ <>
+
+
+
+ >
+ ),
+ label: "Unity SDK",
+ },
+ {
+ component: (
+ <>
+
+
+
+
+
+ >
+ ),
+ label: ".NET SDK",
+ },
+ {
+ component: ,
+ label: "Pay",
+ },
+ {
+ component: ,
+ label: "Auth",
+ },
+];
+
+export function ConnectSupportForm() {
+ const [selectedProblemArea, setSelectedProblemArea] = useState("");
+
+ return (
+ <>
+ o.label)}
+ promptText="Select a problem area"
+ required={true}
+ value={selectedProblemArea}
+ />
+ {PROBLEM_AREAS.find((o) => o.label === selectedProblemArea)?.component}
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/contracts/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/contracts/index.tsx
new file mode 100644
index 00000000000..7e3ac2f4e0f
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/contracts/index.tsx
@@ -0,0 +1,133 @@
+import { type ReactElement, useState } from "react";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
+
+type ProblemAreaItem = {
+ label: string;
+ component: ReactElement;
+};
+
+const NetworkInput = () => (
+
+);
+
+const ContractAddressInput = () => (
+
+);
+
+const ContractFunctionInput = () => (
+
+);
+
+const ContractTypeInput = () => (
+
+);
+
+const ContractAffectedAreaInput = () => {
+ const [selectedAffectedArea, setSelectedAffectedArea] = useState("");
+ return (
+
+ );
+};
+
+const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
+ {
+ component: (
+ <>
+
+
+
+
+ >
+ ),
+ label: "Deploying a contract",
+ },
+ {
+ component: (
+ <>
+
+
+
+
+ >
+ ),
+ label: "Contract verification",
+ },
+ {
+ component: (
+ <>
+
+
+
+
+
+ >
+ ),
+ label: "Calling a function in my contract",
+ },
+ {
+ component: (
+ <>
+
+ >
+ ),
+ label: "Developing a custom contract",
+ },
+ {
+ component: (
+ <>
+
+ >
+ ),
+ label: "Other",
+ },
+];
+
+export function ContractSupportForm() {
+ const [problemArea, setProblemArea] = useState("");
+ return (
+ <>
+ o.label)}
+ promptText="Select a problem area"
+ required={true}
+ value={problemArea}
+ />
+ {CONTRACT_PROBLEM_AREAS.find((o) => o.label === problemArea)?.component}
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/engine/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/engine/index.tsx
new file mode 100644
index 00000000000..4ed24f4caee
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/engine/index.tsx
@@ -0,0 +1,57 @@
+import { useState } from "react";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
+
+const ENGINE_TYPES = ["Cloud-Hosted", "Self-Hosted"];
+const ENGINE_PROBLEM_AREAS = [
+ "SSL Issues",
+ "Transaction queueing issues",
+ "401 - Unauthorized",
+ "404 - Endpoint Not Found",
+ "Other",
+];
+
+export function EngineSupportForm() {
+ const [selectedEngineType, setSelectedEngineType] = useState("");
+ const [problemArea, setProblemArea] = useState("");
+ return (
+ <>
+ {" "}
+ {selectedEngineType && (
+ <>
+
+
+ {problemArea && (
+ <>
+
+
+ >
+ )}
+ >
+ )}
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/other/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/other/index.tsx
new file mode 100644
index 00000000000..0708928027d
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/other/index.tsx
@@ -0,0 +1,53 @@
+import { type ReactElement, useState } from "react";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+
+const SharedOtherProblemComponent = (
+ <>
+
+ >
+);
+
+type ProblemAreaItem = {
+ label: string;
+ component: ReactElement;
+};
+
+const OTHER_PROBLEM_AREAS: ProblemAreaItem[] = [
+ {
+ component: SharedOtherProblemComponent,
+ label: "General inquiry",
+ },
+ {
+ component: SharedOtherProblemComponent,
+ label: "Security",
+ },
+ {
+ component: SharedOtherProblemComponent,
+ label: "Feedback",
+ },
+ {
+ component: SharedOtherProblemComponent,
+ label: "Other",
+ },
+];
+
+function OtherSupportForm() {
+ const [problemArea, setProblemArea] = useState("");
+ return (
+ <>
+ o.label)}
+ promptText="Select a problem area"
+ required={true}
+ value={problemArea}
+ />
+ {OTHER_PROBLEM_AREAS.find((o) => o.label === problemArea)?.component}
+ >
+ );
+}
+
+export { OtherSupportForm };
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/payments/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/payments/index.tsx
new file mode 100644
index 00000000000..ce5d54770dc
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/payments/index.tsx
@@ -0,0 +1,50 @@
+import { useState } from "react";
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
+import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
+
+const PAYMENT_AREAS = ["Dashboard", "Application"];
+
+export function PaymentsSupportForm() {
+ const [area, setArea] = useState("");
+
+ return (
+ <>
+
+ {area === "Application" && (
+ <>
+
+
+
+ >
+ )}
+ {(area === "Application" || area === "Dashboard") && }
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/tokens-marketplace/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/tokens-marketplace/index.tsx
new file mode 100644
index 00000000000..0693a8b5f5f
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/tokens-marketplace/index.tsx
@@ -0,0 +1,24 @@
+import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
+import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
+
+export function TokensMarketplaceSupportForm() {
+ return (
+ <>
+
+
+
+ >
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_DescriptionInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_DescriptionInput.tsx
new file mode 100644
index 00000000000..6465b752885
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_DescriptionInput.tsx
@@ -0,0 +1,33 @@
+import { Label } from "@/components/ui/label";
+import { Textarea } from "@/components/ui/textarea";
+
+type Props = {
+ placeholder?: string;
+};
+
+const defaultDescription =
+ "Please describe the issue you're encountering in detail, including steps that led to the error, any error messages, troubleshooting steps you've already taken, and the product(s), dashboard, or SDKs involved.";
+
+export const DescriptionInput = (props: Props) => {
+ return (
+
+
+ Description
+ •
+
+
+
+
+ );
+};
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_SelectInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_SelectInput.tsx
new file mode 100644
index 00000000000..3221368a1d0
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_SelectInput.tsx
@@ -0,0 +1,60 @@
+import { Label } from "@/components/ui/label";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select";
+
+type Props = {
+ options: string[];
+ promptText: string;
+ formLabel: string;
+ name: string;
+ required: boolean;
+ value: string | undefined;
+ onValueChange: (value: string) => void;
+};
+
+export const SupportForm_SelectInput = (props: Props) => {
+ const { options, formLabel, name, required, promptText } = props;
+
+ return (
+
+
+ {formLabel}
+ {required && (
+ •
+ )}
+
+
+ {
+ props.onValueChange(val);
+ }}
+ required={required}
+ value={props.value}
+ >
+
+ {props.value}
+
+
+ {options.map((option) => (
+
+ {option}
+
+ ))}
+
+
+
+ );
+};
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_TextInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_TextInput.tsx
new file mode 100644
index 00000000000..143bdbf96a9
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_TextInput.tsx
@@ -0,0 +1,36 @@
+import type { HTMLInputTypeAttribute } from "react";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+type Props = {
+ formLabel: string;
+ formValue: `extraInfo_${string}`;
+ required: boolean;
+ inputType: HTMLInputTypeAttribute | undefined;
+ placeholder?: string;
+};
+
+export const SupportForm_TextInput = (props: Props) => {
+ const { formLabel, formValue, required, placeholder, inputType } = props;
+ return (
+
+
+ {formLabel}
+ {required && (
+ •
+ )}
+
+
+
+
+ );
+};
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_UnityInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_UnityInput.tsx
new file mode 100644
index 00000000000..bd68eda89ea
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_UnityInput.tsx
@@ -0,0 +1,48 @@
+import { useState } from "react";
+import { SupportForm_SelectInput } from "./SupportForm_SelectInput";
+import { SupportForm_TextInput } from "./SupportForm_TextInput";
+
+const OPERATING_SYSTEMS = ["Windows", "MacOS", "Linux", "Other"];
+const TARGET_PLATFORMS = [
+ "Windows Standalone",
+ "MacOS Standalone",
+ "Linux Standalone",
+ "WebGL",
+ "Android",
+ "iOS",
+ "Other",
+];
+
+export const UnitySupportForm = () => {
+ const [selectedOS, setSelectedOS] = useState("");
+ const [selectedTargetPlatform, setSelectedTargetPlatform] =
+ useState("");
+ return (
+ <>
+
+
+
+ >
+ );
+};
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/layout.tsx
new file mode 100644
index 00000000000..55405a4562e
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/layout.tsx
@@ -0,0 +1,37 @@
+import { redirect } from "next/navigation";
+import { getAuthToken } from "@/api/auth-token";
+import { getTeamBySlug } from "@/api/team";
+import { getClientThirdwebClient } from "@/constants/thirdweb-client.client";
+import { getValidAccount } from "../../../../../account/settings/getAccount";
+import { loginRedirect } from "../../../../../login/loginRedirect";
+import { SupportLayout } from "./SupportLayout";
+
+export default async function Layout({
+ params,
+ children,
+}: {
+ params: Promise<{ team_slug: string }>;
+ children: React.ReactNode;
+}) {
+ const resolvedParams = await params;
+ const [_account, team, authToken] = await Promise.all([
+ getValidAccount(`/team/${resolvedParams.team_slug}/~/support`),
+ getTeamBySlug(resolvedParams.team_slug),
+ getAuthToken(),
+ ]);
+
+ if (!authToken) {
+ loginRedirect(`/team/${resolvedParams.team_slug}/~/support`);
+ }
+
+ if (!team) {
+ redirect("/team");
+ }
+
+ const _client = getClientThirdwebClient({
+ jwt: authToken,
+ teamId: team.id,
+ });
+
+ return {children} ;
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/page.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/page.tsx
new file mode 100644
index 00000000000..7465d5b739d
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/page.tsx
@@ -0,0 +1,40 @@
+import { notFound } from "next/navigation";
+import { getAuthToken } from "@/api/auth-token";
+import { getSupportTicketsByTeam, type SupportTicket } from "@/api/support";
+import { getTeamBySlug } from "@/api/team";
+import SupportCasesClient from "./_components/SupportCasesClient";
+
+export default async function Page(props: {
+ params: Promise<{
+ team_slug: string;
+ }>;
+}) {
+ const params = await props.params;
+
+ const [team, token] = await Promise.all([
+ getTeamBySlug(params.team_slug),
+ getAuthToken(),
+ ]);
+
+ if (!team || !token) {
+ notFound();
+ }
+
+ // Fetch real support tickets for this team using team slug
+ let supportTickets: SupportTicket[] = [];
+
+ try {
+ supportTickets = await getSupportTicketsByTeam(params.team_slug, token);
+ } catch (_error) {
+ // Return empty array instead of crashing the page
+ supportTickets = [];
+ }
+
+ return (
+
+ );
+}
From b7c72f49adbeb380d9b2bfe44623967f1692a2b2 Mon Sep 17 00:00:00 2001
From: Yash <67926590+Yash094@users.noreply.github.com>
Date: Fri, 11 Jul 2025 17:43:30 +0530
Subject: [PATCH 02/18] lint fixes
---
apps/dashboard/src/@/api/support.ts | 1 -
.../app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx | 6 ++++++
2 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/apps/dashboard/src/@/api/support.ts b/apps/dashboard/src/@/api/support.ts
index 286f793490c..c71eaedb58d 100644
--- a/apps/dashboard/src/@/api/support.ts
+++ b/apps/dashboard/src/@/api/support.ts
@@ -163,7 +163,6 @@ export async function getSupportTicket(
if (messagesResponse.ok) {
const messagesData: { data?: unknown[] } = await messagesResponse.json();
const rawMessages = messagesData.data || [];
- console.log("rawMessages", rawMessages);
// Transform the raw messages to match our interface
const messages: SupportMessage[] = (rawMessages as RawSupportMessage[])
.filter((msg) => {
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx
index d28df97442b..f62b53af56e 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/TeamSidebarLayout.tsx
@@ -6,6 +6,7 @@ import {
DatabaseIcon,
DollarSignIcon,
FileTextIcon,
+ HelpCircleIcon,
HomeIcon,
SettingsIcon,
WalletCardsIcon,
@@ -64,6 +65,11 @@ export function TeamSidebarLayout(props: {
icon: DatabaseIcon,
label: "Usage",
},
+ {
+ href: `${layoutPath}/~/support`,
+ icon: HelpCircleIcon,
+ label: "Support",
+ },
...(props.chainSubscriptions.length > 0
? [
{
From 6b5da014bed4ed90def6c0c58869c24535d53ce6 Mon Sep 17 00:00:00 2001
From: Yash <67926590+Yash094@users.noreply.github.com>
Date: Mon, 14 Jul 2025 14:56:16 +0530
Subject: [PATCH 03/18] updates based on feedback
---
.../@/components/chat/CustomChatButton.tsx | 5 +-
.../@/components/chat/CustomChatContent.tsx | 25 +-
.../src/@/components/chat/CustomChats.tsx | 74 +++++
.../app/(app)/(dashboard)/support/page.tsx | 26 +-
.../Header/SecondaryNav/SecondaryNav.tsx | 2 +-
.../(app)/team/[team_slug]/(team)/layout.tsx | 28 +-
.../_components/SupportCasesClient.tsx | 262 ++----------------
.../support/_components/SupportTicketForm.tsx | 208 ++++++++++++++
.../[project_slug]/(sidebar)/layout.tsx | 2 +-
9 files changed, 358 insertions(+), 274 deletions(-)
create mode 100644 apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm.tsx
diff --git a/apps/dashboard/src/@/components/chat/CustomChatButton.tsx b/apps/dashboard/src/@/components/chat/CustomChatButton.tsx
index c6ebb0bf673..b6eb475792b 100644
--- a/apps/dashboard/src/@/components/chat/CustomChatButton.tsx
+++ b/apps/dashboard/src/@/components/chat/CustomChatButton.tsx
@@ -3,6 +3,7 @@
import { MessageCircleIcon, XIcon } from "lucide-react";
import { useCallback, useRef, useState } from "react";
import { createThirdwebClient } from "thirdweb";
+import type { Team } from "@/api/team";
import { Button } from "@/components/ui/button";
import { NEXT_PUBLIC_DASHBOARD_CLIENT_ID } from "@/constants/public-envs";
import { cn } from "@/lib/utils";
@@ -21,7 +22,7 @@ export function CustomChatButton(props: {
label: string;
examplePrompts: string[];
authToken: string | undefined;
- teamId: string | undefined;
+ team: Team; // changed from teamId
clientId: string | undefined;
requireLogin?: boolean;
}) {
@@ -82,7 +83,7 @@ export function CustomChatButton(props: {
}))}
networks={props.networks}
requireLogin={props.requireLogin}
- teamId={props.teamId}
+ team={props.team} // pass full team object
/>
)}
diff --git a/apps/dashboard/src/@/components/chat/CustomChatContent.tsx b/apps/dashboard/src/@/components/chat/CustomChatContent.tsx
index 6d46e6e0092..d0cb45d8964 100644
--- a/apps/dashboard/src/@/components/chat/CustomChatContent.tsx
+++ b/apps/dashboard/src/@/components/chat/CustomChatContent.tsx
@@ -5,6 +5,7 @@ import { usePathname } from "next/navigation";
import { useCallback, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
import { useActiveWalletConnectionStatus } from "thirdweb/react";
+import type { Team } from "@/api/team";
import { Button } from "@/components/ui/button";
import { NebulaIcon } from "@/icons/NebulaIcon";
import { ChatBar } from "./ChatBar";
@@ -14,7 +15,7 @@ import type { ExamplePrompt, NebulaContext } from "./types";
export default function CustomChatContent(props: {
authToken: string | undefined;
- teamId: string | undefined;
+ team: Team;
clientId: string | undefined;
client: ThirdwebClient;
examplePrompts: ExamplePrompt[];
@@ -31,14 +32,14 @@ export default function CustomChatContent(props: {
client={props.client}
clientId={props.clientId}
examplePrompts={props.examplePrompts}
- teamId={props.teamId}
+ team={props.team}
/>
);
}
function CustomChatContentLoggedIn(props: {
authToken: string;
- teamId: string | undefined;
+ team: Team;
clientId: string | undefined;
client: ThirdwebClient;
examplePrompts: ExamplePrompt[];
@@ -55,6 +56,10 @@ function CustomChatContentLoggedIn(props: {
const [enableAutoScroll, setEnableAutoScroll] = useState(false);
const connectionStatus = useActiveWalletConnectionStatus();
+ // Support form state
+ const [showSupportForm, setShowSupportForm] = useState(false);
+ const [productLabel, setProductLabel] = useState("");
+
const handleSendMessage = useCallback(
async (userMessage: UserMessage) => {
const abortController = new AbortController();
@@ -96,7 +101,7 @@ function CustomChatContentLoggedIn(props: {
headers: {
Authorization: `Bearer ${props.authToken}`,
"Content-Type": "application/json",
- ...(props.teamId ? { "x-team-id": props.teamId } : {}),
+ ...(props.team.id ? { "x-team-id": props.team.id } : {}),
...(props.clientId ? { "x-client-id": props.clientId } : {}),
},
method: "POST",
@@ -132,7 +137,7 @@ function CustomChatContentLoggedIn(props: {
setEnableAutoScroll(false);
}
},
- [props.authToken, props.clientId, props.teamId, sessionId],
+ [props.authToken, props.clientId, props.team.id, sessionId],
);
const handleFeedback = useCallback(
@@ -165,7 +170,7 @@ function CustomChatContentLoggedIn(props: {
headers: {
Authorization: `Bearer ${props.authToken}`,
"Content-Type": "application/json",
- ...(props.teamId ? { "x-team-id": props.teamId } : {}),
+ ...(props.team.id ? { "x-team-id": props.team.id } : {}),
},
method: "POST",
});
@@ -188,7 +193,7 @@ function CustomChatContentLoggedIn(props: {
// Consider implementing retry logic here
}
},
- [sessionId, props.authToken, props.teamId, messages],
+ [sessionId, props.authToken, props.team.id, messages],
);
const showEmptyState = !userHasSubmittedMessage && messages.length === 0;
@@ -212,8 +217,14 @@ function CustomChatContentLoggedIn(props: {
sessionId={sessionId}
setEnableAutoScroll={setEnableAutoScroll}
useSmallText
+ showSupportForm={showSupportForm}
+ setShowSupportForm={setShowSupportForm}
+ productLabel={productLabel}
+ setProductLabel={setProductLabel}
+ team={props.team}
/>
)}
+ {/* Removed floating support case button and form */}
{
chatAbortController?.abort();
diff --git a/apps/dashboard/src/@/components/chat/CustomChats.tsx b/apps/dashboard/src/@/components/chat/CustomChats.tsx
index d3953f18f7b..ab7bccafb4d 100644
--- a/apps/dashboard/src/@/components/chat/CustomChats.tsx
+++ b/apps/dashboard/src/@/components/chat/CustomChats.tsx
@@ -1,14 +1,18 @@
import {
AlertCircleIcon,
+ ArrowRightIcon,
MessageCircleIcon,
ThumbsDownIcon,
ThumbsUpIcon,
} from "lucide-react";
import { useEffect, useRef, useState } from "react";
import type { ThirdwebClient } from "thirdweb";
+import type { Team } from "@/api/team";
import { MarkdownRenderer } from "@/components/blocks/markdown-renderer";
+import { Button } from "@/components/ui/button";
import { ScrollShadow } from "@/components/ui/ScrollShadow/ScrollShadow";
import { cn } from "@/lib/utils";
+import { SupportTicketForm } from "../../../app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm";
import { Reasoning } from "./Reasoning";
// Define local types
@@ -47,10 +51,18 @@ export function CustomChats(props: {
useSmallText?: boolean;
sendMessage: (message: UserMessage) => void;
onFeedback?: (messageIndex: number, feedback: 1 | -1) => void;
+ // New props for support form
+ showSupportForm: boolean;
+ setShowSupportForm: (v: boolean) => void;
+ productLabel: string;
+ setProductLabel: (v: string) => void;
+ team: Team;
}) {
const { messages, setEnableAutoScroll, enableAutoScroll } = props;
const scrollAnchorRef = useRef(null);
const chatContainerRef = useRef(null);
+ // Add state to track if a support ticket was created
+ const [supportTicketCreated, setSupportTicketCreated] = useState(false);
// auto scroll to bottom when messages change
// eslint-disable-next-line no-restricted-syntax
@@ -123,6 +135,68 @@ export function CustomChats(props: {
sendMessage={props.sendMessage}
sessionId={props.sessionId}
/>
+ {/* Support Case Button/Form in last assistant message */}
+ {message.type === "assistant" &&
+ index === props.messages.length - 1 && (
+ <>
+ {/* Only show button/form if ticket not created */}
+ {!props.showSupportForm && !supportTicketCreated && (
+
+
props.setShowSupportForm(true)}
+ size="sm"
+ className="bg-[#2663EB] hover:bg-[#2663EB]/80 text-white transition-opacity"
+ >
+ Create Support Case
+
+
+
+ )}
+ {/* Show form if open and ticket not created */}
+ {props.showSupportForm && !supportTicketCreated && (
+
+ {
+ props.setShowSupportForm(false);
+ props.setProductLabel("");
+ setSupportTicketCreated(true);
+ }}
+ />
+
+ )}
+ {/* Show success message if ticket created */}
+ {supportTicketCreated && (
+
+
+ Your support ticket has been created! Our team
+ will get back to you soon.
+
+
+
+ )}
+ >
+ )}
);
})}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx
index bce10cb8765..6aade8d8afc 100644
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx
+++ b/apps/dashboard/src/app/(app)/(dashboard)/support/page.tsx
@@ -71,7 +71,7 @@ export default async function SupportPage() {
]);
const teams = await getTeams();
- const teamId = teams?.[0]?.id ?? undefined;
+ const team = teams?.[0];
return (
@@ -92,17 +92,19 @@ export default async function SupportPage() {
team.
-
+ {team && (
+
+ )}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx
index 02cf04df252..4e4f3574827 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/layout.tsx
@@ -103,19 +103,21 @@ export default async function TeamLayout(props: {
>
{props.children}
-
-
-
+ {team && (
+
+
+
+ )}
);
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx
index 1d786a70add..76fc4b4b5d9 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx
@@ -1,141 +1,21 @@
"use client";
import { format } from "date-fns";
-import {
- ArrowRightIcon,
- BotIcon,
- LoaderCircleIcon,
- SendIcon,
-} from "lucide-react";
-import dynamic from "next/dynamic";
+import { ArrowRightIcon, BotIcon, SendIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import type { SupportTicket } from "@/api/support";
-import {
- createSupportTicket,
- getSupportTicket,
- sendMessageToTicket,
-} from "@/api/support";
+import { getSupportTicket, sendMessageToTicket } from "@/api/support";
import type { Team } from "@/api/team";
import { MarkdownRenderer } from "@/components/blocks/markdown-renderer";
import { Reasoning } from "@/components/chat/Reasoning";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
-import { Skeleton } from "@/components/ui/skeleton";
import { Textarea } from "@/components/ui/textarea";
import { useDashboardRouter } from "@/lib/DashboardRouter";
import { SupportAIChatCard } from "./SupportAIChatCard";
import { SupportTabs } from "./SupportTabs";
-import { SupportForm_SelectInput } from "./shared/SupportForm_SelectInput";
-
-// Dynamic imports for contact forms using named exports
-const ConnectSupportForm = dynamic(
- () => import("./contact-forms/connect").then((mod) => mod.ConnectSupportForm),
- {
- loading: () => ,
- ssr: false,
- },
-);
-const EngineSupportForm = dynamic(
- () => import("./contact-forms/engine").then((mod) => mod.EngineSupportForm),
- {
- loading: () => ,
- ssr: false,
- },
-);
-const ContractSupportForm = dynamic(
- () =>
- import("./contact-forms/contracts").then((mod) => mod.ContractSupportForm),
- {
- loading: () => ,
- ssr: false,
- },
-);
-const AccountSupportForm = dynamic(
- () => import("./contact-forms/account").then((mod) => mod.AccountSupportForm),
- {
- loading: () => ,
- ssr: false,
- },
-);
-const OtherSupportForm = dynamic(
- () => import("./contact-forms/other").then((mod) => mod.OtherSupportForm),
- {
- loading: () => ,
- ssr: false,
- },
-);
-const PaymentsSupportForm = dynamic(
- () =>
- import("./contact-forms/payments").then((mod) => mod.PaymentsSupportForm),
- {
- loading: () => ,
- ssr: false,
- },
-);
-const TokensMarketplaceSupportForm = dynamic(
- () =>
- import("./contact-forms/tokens-marketplace").then(
- (mod) => mod.TokensMarketplaceSupportForm,
- ),
- {
- loading: () => ,
- ssr: false,
- },
-);
-
-const productOptions = [
- {
- component: ,
- label: "Wallets",
- },
- {
- component: ,
- label: "Transactions",
- },
- {
- component: ,
- label: "Payments",
- },
- {
- component: ,
- label: "Contracts",
- },
- {
- component: ,
- label: "Tokens / Marketplace",
- },
- {
- component: ,
- label: "Account",
- },
- {
- component: ,
- label: "Other",
- },
-];
-
-function ProductAreaSelection(props: {
- productLabel: string;
- setProductLabel: (val: string) => void;
-}) {
- const { productLabel, setProductLabel } = props;
-
- return (
-
- o.label)}
- promptText="Brief description of your issue"
- required={true}
- value={productLabel}
- />
- {productOptions.find((o) => o.label === productLabel)?.component}
-
- );
-}
+import { SupportTicketForm } from "./SupportTicketForm";
interface SupportCasesClientProps {
tickets: SupportTicket[];
@@ -179,9 +59,7 @@ export default function SupportCasesClient({
// Form states
const [showCreateForm, setShowCreateForm] = useState(false);
const [productLabel, setProductLabel] = useState("");
- const [isSubmittingForm, setIsSubmittingForm] = useState(false);
- const formRef = useRef(null);
- const _formContainerRef = useRef(null);
+ const [_isSubmittingForm, _setIsSubmittingFormm] = useState(false);
const selectedCase =
selectedCaseDetails || tickets.find((c) => c.id === selectedCaseId);
@@ -282,79 +160,6 @@ export default function SupportCasesClient({
}
}, [showAIChat]);
- const handleFormSubmit = async (e: React.FormEvent) => {
- e.preventDefault();
-
- if (!productLabel) {
- toast.error("Please select what you need help with");
- return;
- }
-
- if (!formRef.current) return;
- const formData = new FormData(formRef.current);
- const description = formData.get("markdown") as string;
-
- if (!description?.trim()) {
- toast.error("Please provide a description");
- return;
- }
-
- setIsSubmittingForm(true);
-
- try {
- // Get all extra fields from the form
- const extraFields = Array.from(formData.entries()).filter(([key]) =>
- key.startsWith("extraInfo_"),
- );
-
- // Format the message
- let formattedMessage = `Email: ${String(team.billingEmail ?? "-")}\nName: ${String(team.name ?? "-")}\nProduct: ${String(productLabel ?? "-")}`;
-
- // Add all extra fields above the message
- if (extraFields.length > 0) {
- extraFields.forEach(([key, value]) => {
- if (value) {
- const fieldName = key.replace("extraInfo_", "").replace(/_/g, " ");
- formattedMessage += `\n${fieldName}: ${String(value)}`;
- }
- });
- }
-
- formattedMessage += `\nMessage:\n${String(description ?? "-")}`;
-
- if (conversationId) {
- formattedMessage += `\n\n---\nAI Conversation ID: ${conversationId}`;
- }
-
- await createSupportTicket({
- message: formattedMessage,
- teamSlug: team.slug,
- title: `${productLabel} Issue - ${team.billingEmail} (${team.billingPlan})`,
- });
-
- // Add success message to chat
- const successMsg = {
- content:
- "Great! Your support case has been created successfully. Our technical team will review it and get back to you soon. You can continue chatting with me if you have any other questions.",
- id: Date.now(),
- isUser: false,
- timestamp: new Date().toISOString(),
- isSuccessMessage: true,
- };
-
- setChatMessages((msgs) => [...msgs, successMsg]);
- setShowCreateForm(false);
- setProductLabel("");
-
- toast.success("Support ticket created successfully!");
- } catch (error) {
- console.error("Error creating support ticket:", error);
- toast.error("Failed to create support ticket. Please try again.");
- } finally {
- setIsSubmittingForm(false);
- }
- };
-
const handleSelectCase = async (ticketId: string) => {
setSelectedCaseId(ticketId);
setSelectedCaseDetails(null);
@@ -772,45 +577,26 @@ export default function SupportCasesClient({
-
+ {
+ setShowCreateForm(false);
+ setProductLabel("");
+ setChatMessages((prev) => [
+ ...prev,
+ {
+ id: Date.now(),
+ content: `✅ **Support case created successfully!**\n\nYour case has been submitted to our technical team. You'll receive updates via email at ${team.billingEmail}.\n\nYou can track your case in the support portal above.`,
+ isUser: false,
+ timestamp: new Date().toISOString(),
+ isSuccessMessage: true,
+ },
+ ]);
+ }}
+ />
)}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm.tsx
new file mode 100644
index 00000000000..1cf57b7887a
--- /dev/null
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm.tsx
@@ -0,0 +1,208 @@
+import { LoaderCircleIcon } from "lucide-react";
+import dynamic from "next/dynamic";
+import { useRef, useState } from "react";
+import { toast } from "sonner";
+import { createSupportTicket } from "@/api/support";
+import { Button } from "@/components/ui/button";
+import { Skeleton } from "@/components/ui/skeleton";
+import { SupportForm_SelectInput } from "./shared/SupportForm_SelectInput";
+
+// Dynamic imports for contact forms using named exports
+const ConnectSupportForm = dynamic(
+ () => import("./contact-forms/connect").then((mod) => mod.ConnectSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const EngineSupportForm = dynamic(
+ () => import("./contact-forms/engine").then((mod) => mod.EngineSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const ContractSupportForm = dynamic(
+ () =>
+ import("./contact-forms/contracts").then((mod) => mod.ContractSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const AccountSupportForm = dynamic(
+ () => import("./contact-forms/account").then((mod) => mod.AccountSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const OtherSupportForm = dynamic(
+ () => import("./contact-forms/other").then((mod) => mod.OtherSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const PaymentsSupportForm = dynamic(
+ () =>
+ import("./contact-forms/payments").then((mod) => mod.PaymentsSupportForm),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+const TokensMarketplaceSupportForm = dynamic(
+ () =>
+ import("./contact-forms/tokens-marketplace").then(
+ (mod) => mod.TokensMarketplaceSupportForm,
+ ),
+ {
+ loading: () => ,
+ ssr: false,
+ },
+);
+
+const productOptions = [
+ { component: , label: "Wallets" },
+ { component: , label: "Transactions" },
+ { component: , label: "Payments" },
+ { component: , label: "Contracts" },
+ {
+ component: ,
+ label: "Tokens / Marketplace",
+ },
+ { component: , label: "Account" },
+ { component: , label: "Other" },
+];
+
+function ProductAreaSelection(props: {
+ productLabel: string;
+ setProductLabel: (val: string) => void;
+}) {
+ const { productLabel, setProductLabel } = props;
+ return (
+
+ o.label)}
+ promptText="Brief description of your issue"
+ required={true}
+ value={productLabel}
+ />
+ {productOptions.find((o) => o.label === productLabel)?.component}
+
+ );
+}
+
+interface SupportTicketFormProps {
+ team: any;
+ productLabel: string;
+ setProductLabel: (val: string) => void;
+ onSuccess?: () => void;
+ conversationId?: string;
+}
+
+export function SupportTicketForm({
+ team,
+ productLabel,
+ setProductLabel,
+ onSuccess,
+ conversationId,
+}: SupportTicketFormProps) {
+ const [isSubmittingForm, setIsSubmittingForm] = useState(false);
+ const formRef = useRef(null);
+
+ const handleFormSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!productLabel) {
+ toast.error("Please select what you need help with");
+ return;
+ }
+
+ if (!formRef.current) return;
+ const formData = new FormData(formRef.current);
+ const description = formData.get("markdown") as string;
+
+ if (!description?.trim()) {
+ toast.error("Please provide a description");
+ return;
+ }
+
+ setIsSubmittingForm(true);
+
+ try {
+ // Get all extra fields from the form
+ const extraFields = Array.from(formData.entries()).filter(([key]) =>
+ key.startsWith("extraInfo_"),
+ );
+
+ // Format the message
+ let formattedMessage = `Email: ${String(team.billingEmail ?? "-")}`;
+ formattedMessage += `\nName: ${String(team.name ?? "-")}`;
+ formattedMessage += `\nProduct: ${String(productLabel ?? "-")}`;
+
+ // Add all extra fields above the message
+ if (extraFields.length > 0) {
+ extraFields.forEach(([key, value]) => {
+ if (value) {
+ const fieldName = key.replace("extraInfo_", "").replace(/_/g, " ");
+ formattedMessage += `\n${fieldName}: ${String(value)}`;
+ }
+ });
+ }
+
+ formattedMessage += `\nMessage:\n${String(description ?? "-")}`;
+
+ if (conversationId) {
+ formattedMessage += `\n\n---\nAI Conversation ID: ${conversationId}`;
+ }
+
+ await createSupportTicket({
+ message: formattedMessage,
+ teamSlug: team.slug,
+ title: `${productLabel} Issue - ${team.billingEmail} (${team.billingPlan})`,
+ });
+
+ if (onSuccess) onSuccess();
+ if (formRef.current) formRef.current.reset();
+ setProductLabel("");
+ toast.success("Support ticket created successfully!");
+ } catch (error) {
+ console.error("Error creating support ticket:", error);
+ toast.error("Failed to create support ticket. Please try again.");
+ } finally {
+ setIsSubmittingForm(false);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx
index 0e13ff01443..e356c629e94 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/[project_slug]/(sidebar)/layout.tsx
@@ -107,7 +107,7 @@ export default async function ProjectLayout(props: {
label="Ask AI Assistant"
networks="all"
pageType="support"
- teamId={team.id}
+ team={team}
/>
From 4504a56c66f72ac2ffc371b48c7307f2268947da Mon Sep 17 00:00:00 2001
From: Yash <67926590+Yash094@users.noreply.github.com>
Date: Mon, 14 Jul 2025 18:38:08 +0530
Subject: [PATCH 04/18] coderabbit review fix
---
apps/dashboard/src/@/api/support.ts | 3 +-
.../support/_components/SupportAIChatCard.tsx | 15 +++---
.../_components/SupportCasesClient.tsx | 16 +++----
.../~/support/_components/SupportTabs.tsx | 10 ++--
.../support/_components/SupportTicketForm.tsx | 10 ++--
.../contact-forms/account/index.tsx | 7 ++-
.../connect/AffectedAreaInput.tsx | 12 ++++-
.../contact-forms/connect/index.tsx | 11 +++--
.../contact-forms/contracts/index.tsx | 15 ++++--
.../contact-forms/engine/index.tsx | 4 +-
.../_components/contact-forms/other/index.tsx | 47 ++++++-------------
.../contact-forms/payments/index.tsx | 7 ++-
.../tokens-marketplace/index.tsx | 5 +-
.../shared/SupportForm_DescriptionInput.tsx | 22 +++++----
.../shared/SupportForm_SelectInput.tsx | 8 ++--
.../shared/SupportForm_TextInput.tsx | 10 ++--
.../shared/SupportForm_UnityInput.tsx | 2 +
17 files changed, 113 insertions(+), 91 deletions(-)
diff --git a/apps/dashboard/src/@/api/support.ts b/apps/dashboard/src/@/api/support.ts
index c71eaedb58d..dca2cb6ddad 100644
--- a/apps/dashboard/src/@/api/support.ts
+++ b/apps/dashboard/src/@/api/support.ts
@@ -220,7 +220,8 @@ export async function getSupportTicket(
conversation.messages = messages;
} else {
// Don't throw error, just leave messages empty
- const _errorText = await messagesResponse.text();
+ const errorText = await messagesResponse.text();
+ console.error("Failed to fetch messages:", errorText);
conversation.messages = [];
}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportAIChatCard.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportAIChatCard.tsx
index a398c715f1d..ed0b4743fb3 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportAIChatCard.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportAIChatCard.tsx
@@ -1,6 +1,7 @@
"use client";
import { BotIcon } from "lucide-react";
import { useState } from "react";
+import { Input } from "@/components/ui/input";
export function SupportAIChatCard({
_authToken,
@@ -16,8 +17,8 @@ export function SupportAIChatCard({
return (
-
-
+
+
Ask AI for support
@@ -28,14 +29,14 @@ export function SupportAIChatCard({
-
- I’ll help you troubleshoot. If I can’t fix it, I’ll pass it to our
+
+ I'll help you troubleshoot. If I can't fix it, I'll pass it to our
support team.
-
setMessage(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey && message.trim()) {
@@ -47,7 +48,7 @@ export function SupportAIChatCard({
value={message}
/>
onStartChat(message)}
type="button"
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx
index 76fc4b4b5d9..9391c9adbf2 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportCasesClient.tsx
@@ -12,7 +12,6 @@ import { Reasoning } from "@/components/chat/Reasoning";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
-import { useDashboardRouter } from "@/lib/DashboardRouter";
import { SupportAIChatCard } from "./SupportAIChatCard";
import { SupportTabs } from "./SupportTabs";
import { SupportTicketForm } from "./SupportTicketForm";
@@ -52,14 +51,11 @@ export default function SupportCasesClient({
undefined,
);
const chatContainerRef = useRef(null);
- const [_inputFocused, _setInputFocusedd] = useState(false);
- const _router = useDashboardRouter();
const replySectionRef = useRef(null);
// Form states
const [showCreateForm, setShowCreateForm] = useState(false);
const [productLabel, setProductLabel] = useState("");
- const [_isSubmittingForm, _setIsSubmittingFormm] = useState(false);
const selectedCase =
selectedCaseDetails || tickets.find((c) => c.id === selectedCaseId);
@@ -174,7 +170,8 @@ export default function SupportCasesClient({
if (ticketDetails) {
setSelectedCaseDetails(ticketDetails);
}
- } catch (_error) {
+ } catch (error) {
+ console.error("Failed to load ticket details:", error);
toast.error("Failed to load ticket details");
} finally {
setIsLoadingCaseDetails(false);
@@ -229,7 +226,8 @@ export default function SupportCasesClient({
} finally {
setIsLoadingCaseDetails(false);
}
- } catch (_error) {
+ } catch (error) {
+ console.error("Failed to send reply:", error);
toast.error("Failed to send reply. Please try again.");
} finally {
setIsSubmittingReply(false);
@@ -394,7 +392,8 @@ export default function SupportCasesClient({
timestamp: new Date().toISOString(),
},
]);
- } catch (_error) {
+ } catch (error) {
+ console.error("Error in handleStartChat:", error);
setChatMessages([
initialAIMsg,
userMsg,
@@ -438,7 +437,8 @@ export default function SupportCasesClient({
timestamp: new Date().toISOString(),
},
]);
- } catch (_error) {
+ } catch (error) {
+ console.error("Error in handleChatSend:", error);
setChatMessages((msgs) => [
...msgs.slice(0, -1),
{
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTabs.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTabs.tsx
index c08de6a0e47..96401d5acb4 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTabs.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTabs.tsx
@@ -32,9 +32,9 @@ export function SupportTabs({
{/* Search Bar */}
-
+
onSearchChange(e.target.value)}
placeholder="Search cases..."
value={searchQuery}
@@ -42,14 +42,14 @@ export function SupportTabs({
{/* Tab Buttons */}
-
+
{tabs.map((tab) => (
onTabChange(tab.id)}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm.tsx
index 1cf57b7887a..badbfa02704 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportTicketForm.tsx
@@ -3,6 +3,7 @@ import dynamic from "next/dynamic";
import { useRef, useState } from "react";
import { toast } from "sonner";
import { createSupportTicket } from "@/api/support";
+import type { Team } from "@/api/team";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { SupportForm_SelectInput } from "./shared/SupportForm_SelectInput";
@@ -98,7 +99,7 @@ function ProductAreaSelection(props: {
}
interface SupportTicketFormProps {
- team: any;
+ team: Team;
productLabel: string;
setProductLabel: (val: string) => void;
onSuccess?: () => void;
@@ -187,12 +188,7 @@ export function SupportTicketForm({
/>
{/* Submit Buttons */}
-
+
{isSubmittingForm ? (
<>
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/account/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/account/index.tsx
index f7fb8fde39a..f88f8dc479f 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/account/index.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/account/index.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import { useState } from "react";
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
@@ -11,6 +13,7 @@ const ACCOUNT_PROBLEM_AREAS = [
export function AccountSupportForm() {
const [problemArea, setProblemArea] = useState("");
+ const [description, setDescription] = useState("");
return (
<>
@@ -23,7 +26,9 @@ export function AccountSupportForm() {
required={true}
value={problemArea}
/>
- {problemArea && }
+ {problemArea && (
+
+ )}
>
);
}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/AffectedAreaInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/AffectedAreaInput.tsx
index 5c086920bc7..9f0d8170098 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/AffectedAreaInput.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/AffectedAreaInput.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import { useState } from "react";
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
@@ -9,6 +11,9 @@ const AFFECTED_AREAS = ["Dashboard", "Application"];
export const AffectedAreaInput = () => {
const [selectedAffectedArea, setSelectedAffectedArea] = useState("");
const [selectedSDK, setSelectedSDK] = useState("");
+ const [description, setDescription] = useState("");
+ const [sdkDescription, setSdkDescription] = useState("");
+
return (
<>
{
inputType="url"
required={false}
/>
-
+
>
)}
>
) : (
-
+
))}
>
);
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/index.tsx
index ea1a3dce64f..e49dc786840 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/index.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/connect/index.tsx
@@ -34,6 +34,11 @@ const OSSelect = () => {
);
};
+const DescriptionInputWrapper = () => {
+ const [description, setDescription] = useState("");
+ return ;
+};
+
const PROBLEM_AREAS: ProblemAreaItem[] = [
{
component: ,
@@ -65,7 +70,7 @@ const PROBLEM_AREAS: ProblemAreaItem[] = [
inputType="url"
required={false}
/>
-
+
>
),
label: "Connect SDKs",
@@ -75,7 +80,7 @@ const PROBLEM_AREAS: ProblemAreaItem[] = [
<>
-
+
>
),
label: "Unity SDK",
@@ -98,7 +103,7 @@ const PROBLEM_AREAS: ProblemAreaItem[] = [
inputType="text"
required={false}
/>
-
+
>
),
label: ".NET SDK",
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/contracts/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/contracts/index.tsx
index 7e3ac2f4e0f..d9413073ed0 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/contracts/index.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/contracts/index.tsx
@@ -61,6 +61,11 @@ const ContractAffectedAreaInput = () => {
);
};
+const DescriptionInputWrapper = () => {
+ const [description, setDescription] = useState("");
+ return ;
+};
+
const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
{
component: (
@@ -68,7 +73,7 @@ const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
-
+
>
),
label: "Deploying a contract",
@@ -79,7 +84,7 @@ const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
-
+
>
),
label: "Contract verification",
@@ -91,7 +96,7 @@ const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
-
+
>
),
label: "Calling a function in my contract",
@@ -99,7 +104,7 @@ const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
{
component: (
<>
-
+
>
),
label: "Developing a custom contract",
@@ -107,7 +112,7 @@ const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
{
component: (
<>
-
+
>
),
label: "Other",
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/engine/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/engine/index.tsx
index 4ed24f4caee..d97a59ab380 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/engine/index.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/engine/index.tsx
@@ -15,6 +15,8 @@ const ENGINE_PROBLEM_AREAS = [
export function EngineSupportForm() {
const [selectedEngineType, setSelectedEngineType] = useState("");
const [problemArea, setProblemArea] = useState("");
+ const [description, setDescription] = useState("");
+
return (
<>
-
+
>
)}
>
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/other/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/other/index.tsx
index 0708928027d..075b94f55a1 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/other/index.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/other/index.tsx
@@ -1,53 +1,36 @@
-import { type ReactElement, useState } from "react";
+import { useState } from "react";
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
-const SharedOtherProblemComponent = (
- <>
-
- >
-);
+const SharedOtherProblemComponent = () => {
+ const [description, setDescription] = useState("");
-type ProblemAreaItem = {
- label: string;
- component: ReactElement;
+ return ;
};
-const OTHER_PROBLEM_AREAS: ProblemAreaItem[] = [
- {
- component: SharedOtherProblemComponent,
- label: "General inquiry",
- },
- {
- component: SharedOtherProblemComponent,
- label: "Security",
- },
- {
- component: SharedOtherProblemComponent,
- label: "Feedback",
- },
- {
- component: SharedOtherProblemComponent,
- label: "Other",
- },
+const OTHER_PROBLEM_AREAS = [
+ "General inquiry",
+ "Feature request",
+ "Bug report",
+ "Documentation",
+ "Integration help",
+ "Other",
];
-
-function OtherSupportForm() {
+export function OtherSupportForm() {
const [problemArea, setProblemArea] = useState("");
+
return (
<>
o.label)}
+ options={OTHER_PROBLEM_AREAS}
promptText="Select a problem area"
required={true}
value={problemArea}
/>
- {OTHER_PROBLEM_AREAS.find((o) => o.label === problemArea)?.component}
+ {problemArea && }
>
);
}
-
-export { OtherSupportForm };
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/payments/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/payments/index.tsx
index ce5d54770dc..052245261b1 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/payments/index.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/payments/index.tsx
@@ -1,3 +1,5 @@
+"use client";
+
import { useState } from "react";
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
import { SupportForm_SelectInput } from "../../shared/SupportForm_SelectInput";
@@ -7,6 +9,7 @@ const PAYMENT_AREAS = ["Dashboard", "Application"];
export function PaymentsSupportForm() {
const [area, setArea] = useState("");
+ const [description, setDescription] = useState("");
return (
<>
@@ -44,7 +47,9 @@ export function PaymentsSupportForm() {
/>
>
)}
- {(area === "Application" || area === "Dashboard") && }
+ {(area === "Application" || area === "Dashboard") && (
+
+ )}
>
);
}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/tokens-marketplace/index.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/tokens-marketplace/index.tsx
index 0693a8b5f5f..89d80267649 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/tokens-marketplace/index.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/contact-forms/tokens-marketplace/index.tsx
@@ -1,7 +1,10 @@
+import { useState } from "react";
import { DescriptionInput } from "../../shared/SupportForm_DescriptionInput";
import { SupportForm_TextInput } from "../../shared/SupportForm_TextInput";
export function TokensMarketplaceSupportForm() {
+ const [description, setDescription] = useState("");
+
return (
<>
-
+
>
);
}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_DescriptionInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_DescriptionInput.tsx
index 6465b752885..b8de8f429eb 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_DescriptionInput.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_DescriptionInput.tsx
@@ -1,18 +1,22 @@
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
-
-type Props = {
- placeholder?: string;
-};
+import { cn } from "@/lib/utils";
const defaultDescription =
"Please describe the issue you're encountering in detail, including steps that led to the error, any error messages, troubleshooting steps you've already taken, and the product(s), dashboard, or SDKs involved.";
-export const DescriptionInput = (props: Props) => {
+interface Props {
+ value: string;
+ onChange: (value: string) => void;
+ className?: string;
+ placeholder?: string;
+}
+
+export function DescriptionInput(props: Props) {
return (
-
+
Description
@@ -21,7 +25,7 @@ export const DescriptionInput = (props: Props) => {
);
-};
+}
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_SelectInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_SelectInput.tsx
index 3221368a1d0..5a00fa72085 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_SelectInput.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_SelectInput.tsx
@@ -23,7 +23,7 @@ export const SupportForm_SelectInput = (props: Props) => {
return (
{formLabel}
@@ -40,13 +40,13 @@ export const SupportForm_SelectInput = (props: Props) => {
required={required}
value={props.value}
>
-
+
{props.value}
-
+
{options.map((option) => (
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_TextInput.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_TextInput.tsx
index 143bdbf96a9..5656534e6d9 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_TextInput.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/shared/SupportForm_TextInput.tsx
@@ -8,14 +8,16 @@ type Props = {
required: boolean;
inputType: HTMLInputTypeAttribute | undefined;
placeholder?: string;
+ className?: string;
};
export const SupportForm_TextInput = (props: Props) => {
- const { formLabel, formValue, required, placeholder, inputType } = props;
+ const { formLabel, formValue, required, placeholder, inputType, className } =
+ props;
return (
-
+
{formLabel}
@@ -25,7 +27,7 @@ export const SupportForm_TextInput = (props: Props) => {
Date: Mon, 14 Jul 2025 19:00:43 +0530
Subject: [PATCH 05/18] small fix
---
.../(team)/~/support/_components/SupportAIChatCard.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportAIChatCard.tsx b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportAIChatCard.tsx
index ed0b4743fb3..ead148a4e69 100644
--- a/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportAIChatCard.tsx
+++ b/apps/dashboard/src/app/(app)/team/[team_slug]/(team)/~/support/_components/SupportAIChatCard.tsx
@@ -22,8 +22,8 @@ export function SupportAIChatCard({
Ask AI for support
-
From 23d29397edd821a16e9e80a4e13e41e1dc8152e1 Mon Sep 17 00:00:00 2001
From: Yash <67926590+Yash094@users.noreply.github.com>
Date: Tue, 15 Jul 2025 13:34:19 +0530
Subject: [PATCH 06/18] fixes
---
apps/dashboard/src/@/api/team.ts | 6 +-
.../src/@/components/chat/CustomChats.tsx | 10 +-
.../contact-forms/account/index.tsx | 67 -----
.../connect/AffectedAreaInput.tsx | 64 -----
.../contact-forms/connect/index.tsx | 137 ----------
.../contact-forms/contracts/index.tsx | 139 ----------
.../components/contact-forms/engine/index.tsx | 59 -----
.../components/contact-forms/other/index.tsx | 66 -----
.../shared/SupportForm_AttachmentUploader.tsx | 17 --
.../shared/SupportForm_DescriptionInput.tsx | 29 ---
.../shared/SupportForm_SelectInput.tsx | 53 ----
.../shared/SupportForm_TeamSelection.tsx | 58 -----
.../shared/SupportForm_TelegramInput.tsx | 27 --
.../shared/SupportForm_TextInput.tsx | 31 ---
.../shared/SupportForm_UnityInput.tsx | 48 ----
.../components/create-ticket.action.ts | 208 ---------------
.../components/create-ticket.client.tsx | 183 -------------
.../support/create-ticket/page.tsx | 48 ----
.../(dashboard)/support/opengraph-image.png | Bin 869261 -> 0 bytes
.../app/(app)/(dashboard)/support/page.tsx | 4 +-
.../[team_slug]/(team)/TeamSidebarLayout.tsx | 10 +-
.../(team)/~/support/SupportLayout.tsx | 14 +-
.../support/_components/SupportAIChatCard.tsx | 8 +-
.../_components/SupportCasesClient.tsx | 240 ++++++++++--------
.../~/support/_components/SupportTabs.tsx | 15 +-
.../support/_components/SupportTicketForm.tsx | 4 +
.../contact-forms/connect/index.tsx | 10 +-
.../contact-forms/engine/index.tsx | 4 +-
.../shared/SupportForm_DescriptionInput.tsx | 4 +-
29 files changed, 173 insertions(+), 1390 deletions(-)
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/account/index.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/AffectedAreaInput.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/index.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/contracts/index.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/engine/index.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/other/index.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_AttachmentUploader.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_DescriptionInput.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_SelectInput.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TeamSelection.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TelegramInput.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TextInput.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_UnityInput.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.action.ts
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.client.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/page.tsx
delete mode 100644 apps/dashboard/src/app/(app)/(dashboard)/support/opengraph-image.png
diff --git a/apps/dashboard/src/@/api/team.ts b/apps/dashboard/src/@/api/team.ts
index f8eeb3f43da..e30cf78095d 100644
--- a/apps/dashboard/src/@/api/team.ts
+++ b/apps/dashboard/src/@/api/team.ts
@@ -48,10 +48,6 @@ export async function service_getTeamBySlug(slug: string) {
return null;
}
-export function getTeamById(id: string) {
- return getTeamBySlug(id);
-}
-
export async function getTeams() {
const token = await getAuthToken();
if (!token) {
@@ -97,7 +93,7 @@ export async function getLastVisitedTeam() {
const lastVisitedTeamId = cookiesStore.get(LAST_USED_TEAM_ID)?.value;
if (lastVisitedTeamId) {
- const team = await getTeamById(lastVisitedTeamId);
+ const team = await getTeamBySlug(lastVisitedTeamId);
if (team) {
return team;
}
diff --git a/apps/dashboard/src/@/components/chat/CustomChats.tsx b/apps/dashboard/src/@/components/chat/CustomChats.tsx
index ab7bccafb4d..65018b471b5 100644
--- a/apps/dashboard/src/@/components/chat/CustomChats.tsx
+++ b/apps/dashboard/src/@/components/chat/CustomChats.tsx
@@ -141,11 +141,11 @@ export function CustomChats(props: {
<>
{/* Only show button/form if ticket not created */}
{!props.showSupportForm && !supportTicketCreated && (
-
+
props.setShowSupportForm(true)}
size="sm"
- className="bg-[#2663EB] hover:bg-[#2663EB]/80 text-white transition-opacity"
+ className="bg-primary hover:bg-primary/80 text-primary-foreground transition-opacity"
>
Create Support Case
@@ -154,7 +154,7 @@ export function CustomChats(props: {
)}
{/* Show form if open and ticket not created */}
{props.showSupportForm && !supportTicketCreated && (
-
+
+
Your support ticket has been created! Our team
will get back to you soon.
@@ -187,7 +187,7 @@ export function CustomChats(props: {
>
Go to Support Portal
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/account/index.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/account/index.tsx
deleted file mode 100644
index 28d52421d3e..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/account/index.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { type ReactElement, useState } from "react";
-import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
-import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
-import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
-
-type ProblemAreaItem = {
- label: string;
- component: ReactElement;
-};
-
-const ACCOUNT_PROBLEM_AREAS: ProblemAreaItem[] = [
- {
- component: (
- <>
-
-
- >
- ),
- label: "Pricing inquiry",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Billing inquiry",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Usage inquiry",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Other",
- },
-];
-
-export default function AccountSupportForm() {
- const [problemArea, setProblemArea] = useState
("");
-
- return (
- <>
- o.label)}
- promptText="Select a problem area"
- required={true}
- value={problemArea}
- />
- {ACCOUNT_PROBLEM_AREAS.find((o) => o.label === problemArea)?.component}
- >
- );
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/AffectedAreaInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/AffectedAreaInput.tsx
deleted file mode 100644
index a7b2485451e..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/AffectedAreaInput.tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { useState } from "react";
-import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
-import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
-import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
-import { SupportForm_TextInput } from "../shared/SupportForm_TextInput";
-import { UnitySupportForm } from "../shared/SupportForm_UnityInput";
-
-const AFFECTED_AREAS = ["Dashboard", "Application"];
-
-export const AffectedAreaInput = () => {
- const [selectedAffectedArea, setSelectedAffectedArea] = useState("");
- const [selectedSDK, setSelectedSDK] = useState("");
- return (
- <>
-
- {selectedAffectedArea &&
- (selectedAffectedArea === "Application" ? (
- <>
-
- {selectedSDK && (
- <>
- {selectedSDK === "Unity" && }
-
-
-
-
- >
- )}
- >
- ) : (
- <>
-
-
- >
- ))}
- >
- );
-};
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/index.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/index.tsx
deleted file mode 100644
index a47f4f6a6ad..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/connect/index.tsx
+++ /dev/null
@@ -1,137 +0,0 @@
-import { type ReactElement, useState } from "react";
-import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
-import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
-import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
-import { SupportForm_TextInput } from "../shared/SupportForm_TextInput";
-import { UnitySupportForm } from "../shared/SupportForm_UnityInput";
-import { AffectedAreaInput } from "./AffectedAreaInput";
-
-type ProblemAreaItem = {
- label: string;
- component: ReactElement;
-};
-
-const SDKVersionInput = () => (
-
-);
-
-const OSSelect = () => {
- const [selectedOS, setSelectedOS] = useState("");
- return (
-
- );
-};
-
-const PROBLEM_AREAS: ProblemAreaItem[] = [
- {
- component: ,
- label: "Embedded wallet login issues",
- },
- {
- component: ,
- label: "Embedded wallet transaction issues",
- },
- {
- component: ,
- label: "Embedded wallet Custom Auth",
- },
- {
- component: ,
- label: "Account Abstraction",
- },
- {
- component: ,
- label: "In-app wallet",
- },
- {
- component: (
- <>
-
-
-
-
- >
- ),
- label: "Connect SDKs",
- },
- {
- component: (
- <>
-
-
-
-
- >
- ),
- label: "Unity SDK",
- },
- {
- component: (
- <>
-
-
-
-
-
-
- >
- ),
- label: ".NET SDK",
- },
- {
- component: ,
- label: "Pay",
- },
- {
- component: ,
- label: "Auth",
- },
-];
-
-export default function ConnectSupportForm() {
- const [selectedProblemArea, setSelectedProblemArea] = useState("");
-
- return (
- <>
- o.label)}
- promptText="Select a problem area"
- required={true}
- value={selectedProblemArea}
- />
- {PROBLEM_AREAS.find((o) => o.label === selectedProblemArea)?.component}
- >
- );
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/contracts/index.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/contracts/index.tsx
deleted file mode 100644
index c06eeea8077..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/contracts/index.tsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import { type ReactElement, useState } from "react";
-import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
-import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
-import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
-import { SupportForm_TextInput } from "../shared/SupportForm_TextInput";
-
-type ProblemAreaItem = {
- label: string;
- component: ReactElement;
-};
-
-const NetworkInput = () => (
-
-);
-
-const ContractAddressInput = () => (
-
-);
-
-const ContractFunctionInput = () => (
-
-);
-
-const ContractTypeInput = () => (
-
-);
-
-const ContractAffectedAreaInput = () => {
- const [selectedAffectedArea, setSelectedAffectedArea] = useState("");
- return (
-
- );
-};
-
-const CONTRACT_PROBLEM_AREAS: ProblemAreaItem[] = [
- {
- component: (
- <>
-
-
-
-
-
- >
- ),
- label: "Deploying a contract",
- },
- {
- component: (
- <>
-
-
-
-
-
- >
- ),
- label: "Contract verification",
- },
- {
- component: (
- <>
-
-
-
-
-
-
- >
- ),
- label: "Calling a function in my contract",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Developing a custom contract",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Other",
- },
-];
-
-export default function ContractSupportForm() {
- const [problemArea, setProblemArea] = useState("");
- return (
- <>
- o.label)}
- promptText="Select a problem area"
- required={true}
- value={problemArea}
- />
- {CONTRACT_PROBLEM_AREAS.find((o) => o.label === problemArea)?.component}
- >
- );
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/engine/index.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/engine/index.tsx
deleted file mode 100644
index 860765bccfc..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/engine/index.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { useState } from "react";
-import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
-import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
-import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
-import { SupportForm_TextInput } from "../shared/SupportForm_TextInput";
-
-const ENGINE_TYPES = ["Cloud-Hosted", "Self-Hosted"];
-const ENGINE_PROBLEM_AREAS = [
- "SSL Issues",
- "Transaction queueing issues",
- "401 - Unauthorized",
- "404 - Endpoint Not Found",
- "Other",
-];
-
-export default function EngineSupportForm() {
- const [selectedEngineType, setSelectedEngineType] = useState("");
- const [problemArea, setProblemArea] = useState("");
- return (
- <>
-
- {selectedEngineType && (
- <>
-
-
- {problemArea && (
- <>
-
-
-
- >
- )}
- >
- )}
- >
- );
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/other/index.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/other/index.tsx
deleted file mode 100644
index fcbc0440670..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/other/index.tsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { type ReactElement, useState } from "react";
-import { AttachmentForm } from "../shared/SupportForm_AttachmentUploader";
-import { DescriptionInput } from "../shared/SupportForm_DescriptionInput";
-import { SupportForm_SelectInput } from "../shared/SupportForm_SelectInput";
-
-type ProblemAreaItem = {
- label: string;
- component: ReactElement;
-};
-
-const OTHER_PROBLEM_AREAS: ProblemAreaItem[] = [
- {
- component: (
- <>
-
-
- >
- ),
- label: "General inquiry",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Security",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Feedback",
- },
- {
- component: (
- <>
-
-
- >
- ),
- label: "Other",
- },
-];
-
-export default function OtherSupportForm() {
- const [problemArea, setProblemArea] = useState("");
- return (
- <>
- o.label)}
- promptText="Select a problem area"
- required={true}
- value={problemArea}
- />
- {OTHER_PROBLEM_AREAS.find((o) => o.label === problemArea)?.component}
- >
- );
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_AttachmentUploader.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_AttachmentUploader.tsx
deleted file mode 100644
index 9cd3479f633..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_AttachmentUploader.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import { Label } from "@/components/ui/label";
-
-export const AttachmentForm = () => {
- return (
-
-
- Attachments
-
-
-
- );
-};
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_DescriptionInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_DescriptionInput.tsx
deleted file mode 100644
index 24bed8dd13f..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_DescriptionInput.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { Label } from "@/components/ui/label";
-import { Textarea } from "@/components/ui/textarea";
-
-type Props = {
- placeholder?: string;
-};
-
-const defaultDescription =
- "Please describe the issue you're encountering in detail, including steps that led to the error, any error messages, troubleshooting steps you've already taken, and the product(s), dashboard, or SDKs involved.";
-
-export const DescriptionInput = (props: Props) => {
- return (
-
-
- Description
- •
-
-
-
-
- );
-};
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_SelectInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_SelectInput.tsx
deleted file mode 100644
index fdea3a49fbb..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_SelectInput.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { Label } from "@/components/ui/label";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-
-type Props = {
- options: string[];
- promptText: string;
- formLabel: string;
- name: string;
- required: boolean;
- value: string | undefined;
- onValueChange: (value: string) => void;
-};
-
-export const SupportForm_SelectInput = (props: Props) => {
- const { options, formLabel, name, required, promptText } = props;
-
- return (
-
-
- {formLabel}
- {required && (
- •
- )}
-
-
- {
- props.onValueChange(val);
- }}
- required={required}
- value={props.value}
- >
-
- {props.value}
-
-
- {options.map((option) => (
-
- {option}
-
- ))}
-
-
-
- );
-};
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TeamSelection.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TeamSelection.tsx
deleted file mode 100644
index 32db19a1465..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TeamSelection.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { useId } from "react";
-import { Label } from "@/components/ui/label";
-import {
- Select,
- SelectContent,
- SelectItem,
- SelectTrigger,
- SelectValue,
-} from "@/components/ui/select";
-
-type MinimalTeam = {
- name: string;
- id: string;
-};
-
-type Props = {
- teams: MinimalTeam[];
- selectedTeamId: string | undefined;
- onChange: (teamId: string) => void;
-};
-
-export const SupportForm_TeamSelection = (props: Props) => {
- const selectedTeamName = props.teams.find(
- (t) => t.id === props.selectedTeamId,
- )?.name;
-
- const teamId = useId();
-
- return (
-
-
- Select Team
- •
-
-
- {
- props.onChange(selectedId);
- }}
- value={props.selectedTeamId}
- >
-
-
- {selectedTeamName}
-
-
-
- {props.teams.map((team) => (
-
- {team.name}
-
- ))}
-
-
-
- );
-};
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TelegramInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TelegramInput.tsx
deleted file mode 100644
index fc79ad23bd3..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TelegramInput.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { Label } from "@/components/ui/label";
-
-type Props = {
- placeholder?: string;
-};
-
-const defaultDescription = "@YourHandle";
-
-export const SupportForm_TelegramInput = (props: Props) => {
- return (
-
-
- Telegram
- •
-
-
-
-
- );
-};
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TextInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TextInput.tsx
deleted file mode 100644
index 4fea62f0a99..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TextInput.tsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { HTMLInputTypeAttribute } from "react";
-import { Input } from "@/components/ui/input";
-import { Label } from "@/components/ui/label";
-
-type Props = {
- formLabel: string;
- formValue: `extraInfo_${string}`;
- required: boolean;
- inputType: HTMLInputTypeAttribute | undefined;
- placeholder?: string;
-};
-export const SupportForm_TextInput = (props: Props) => {
- const { formLabel, formValue, required, placeholder, inputType } = props;
- return (
-
-
- {formLabel}
- {required && (
- •
- )}
-
-
-
-
- );
-};
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_UnityInput.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_UnityInput.tsx
deleted file mode 100644
index bd68eda89ea..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_UnityInput.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { useState } from "react";
-import { SupportForm_SelectInput } from "./SupportForm_SelectInput";
-import { SupportForm_TextInput } from "./SupportForm_TextInput";
-
-const OPERATING_SYSTEMS = ["Windows", "MacOS", "Linux", "Other"];
-const TARGET_PLATFORMS = [
- "Windows Standalone",
- "MacOS Standalone",
- "Linux Standalone",
- "WebGL",
- "Android",
- "iOS",
- "Other",
-];
-
-export const UnitySupportForm = () => {
- const [selectedOS, setSelectedOS] = useState("");
- const [selectedTargetPlatform, setSelectedTargetPlatform] =
- useState("");
- return (
- <>
-
-
-
- >
- );
-};
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.action.ts b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.action.ts
deleted file mode 100644
index 8019ae07d18..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.action.ts
+++ /dev/null
@@ -1,208 +0,0 @@
-"use server";
-import "server-only";
-
-import { getTeamById } from "@/api/team";
-import { getAuthTokenWalletAddress } from "../../../../../../@/api/auth-token";
-import { getRawAccount } from "../../../../account/settings/getAccount";
-import { loginRedirect } from "../../../../login/loginRedirect";
-
-type State = {
- success: boolean;
- message: string;
-};
-
-const UNTHREAD_API_KEY = process.env.UNTHREAD_API_KEY || "";
-
-const planToCustomerId = {
- accelerate: process.env.UNTHREAD_ACCELERATE_TIER_ID as string,
- free: process.env.UNTHREAD_FREE_TIER_ID as string,
- growth: process.env.UNTHREAD_GROWTH_TIER_ID as string,
- pro: process.env.UNTHREAD_PRO_TIER_ID as string,
- scale: process.env.UNTHREAD_SCALE_TIER_ID as string,
- // treat starter as free
- starter: process.env.UNTHREAD_FREE_TIER_ID as string,
-} as const;
-
-function isValidPlan(plan: string): plan is keyof typeof planToCustomerId {
- return plan in planToCustomerId;
-}
-
-function prepareEmailTitle(
- product: string,
- problemArea: string,
- email: string,
- name: string,
-) {
- const title =
- product && problemArea
- ? `${product}: ${problemArea} (${email})`
- : `New ticket from ${name} (${email})`;
- return title;
-}
-
-function prepareEmailBody(props: {
- product: string;
- markdownInput: string;
- email: string;
- name: string;
- extraInfoInput: Record;
- walletAddress: string;
- telegramHandle: string;
-}) {
- const {
- extraInfoInput,
- email,
- walletAddress,
- product,
- name,
- markdownInput,
- telegramHandle,
- } = props;
- // Update `markdown` to include the infos from the form
- const extraInfo = Object.keys(extraInfoInput)
- .filter((key) => key.startsWith("extraInfo_"))
- .map((key) => {
- const prettifiedKey = `# ${key
- .replace("extraInfo_", "")
- .replaceAll("_", " ")}`;
- return `${prettifiedKey}: ${extraInfoInput[key] ?? "N/A"}\n`;
- })
- .join("");
- const markdown = `# Email: ${email}
- # Name: ${name}
- # Telegram: ${telegramHandle}
- # Wallet address: ${walletAddress}
- # Product: ${product}
- ${extraInfo}
- # Message:
- ${markdownInput}
- `;
- return markdown;
-}
-
-export async function createTicketAction(
- _previousState: State,
- formData: FormData,
-) {
- const teamId = formData.get("teamId")?.toString();
-
- if (!teamId) {
- return {
- message: "teamId is required",
- success: false,
- };
- }
-
- const team = await getTeamById(teamId);
-
- if (!team) {
- return {
- message: `Team with id "${teamId}" not found`,
- success: false,
- };
- }
-
- const [walletAddress, account] = await Promise.all([
- getAuthTokenWalletAddress(),
- getRawAccount(),
- ]);
-
- if (!walletAddress || !account) {
- loginRedirect("/support");
- }
-
- const customerId = isValidPlan(team.supportPlan)
- ? // fall back to "free" tier
- planToCustomerId[team.supportPlan] || planToCustomerId.free
- : // fallback to "free" tier
- planToCustomerId.free;
-
- const product = formData.get("product")?.toString() || "";
- const problemArea = formData.get("extraInfo_Problem_Area")?.toString() || "";
- const telegramHandle = formData.get("telegram")?.toString() || "";
-
- const title = prepareEmailTitle(
- product,
- problemArea,
- account.email || "",
- account.name || "",
- );
-
- const keyVal: Record = {};
- for (const key of formData.keys()) {
- keyVal[key] = formData.get(key)?.toString() || "";
- }
-
- const markdown = prepareEmailBody({
- email: account.email || "",
- extraInfoInput: keyVal,
- markdownInput: keyVal.markdown || "",
- name: account.name || "",
- product,
- telegramHandle: telegramHandle,
- walletAddress: walletAddress,
- });
-
- const content = {
- customerId,
- emailInboxId: process.env.UNTHREAD_EMAIL_INBOX_ID,
- markdown,
- onBehalfOf: {
- email: account.email,
- id: account.id,
- name: account.name,
- },
- status: "open",
- title,
- triageChannelId: process.env.UNTHREAD_TRIAGE_CHANNEL_ID,
- type: "email",
- };
-
- // check files
- const files = formData.getAll("files") as File[];
-
- if (files.length > 10) {
- return { message: "You can only attach 10 files at once.", success: false };
- }
- if (files.some((file) => file.size > 10 * 1024 * 1024)) {
- return { message: "The max file size is 20MB.", success: false };
- }
-
- // add the content
- formData.append("json", JSON.stringify(content));
-
- const KEEP_FIELDS = ["attachments", "json"];
- const keys = [...formData.keys()];
- // delete everything except attachments off of the form data
- for (const key of keys) {
- if (!KEEP_FIELDS.includes(key)) {
- formData.delete(key);
- }
- }
-
- // actually create the ticket
- const res = await fetch("https://api.unthread.io/api/conversations", {
- body: formData,
- headers: {
- "X-Api-Key": UNTHREAD_API_KEY,
- },
- method: "POST",
- });
- if (!res.ok) {
- console.error(
- "Failed to create ticket",
- res.status,
- res.statusText,
- await res.text(),
- );
- return {
- message: "Failed to create ticket, please try again later.",
- success: false,
- };
- }
-
- return {
- message: "Ticket created successfully",
- success: true,
- };
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.client.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.client.tsx
deleted file mode 100644
index 6b34630770f..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/components/create-ticket.client.tsx
+++ /dev/null
@@ -1,183 +0,0 @@
-"use client";
-
-import { SupportForm_SelectInput } from "@app/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_SelectInput";
-import { SupportForm_TeamSelection } from "@app/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TeamSelection";
-import { SupportForm_TelegramInput } from "@app/(dashboard)/support/create-ticket/components/contact-forms/shared/SupportForm_TelegramInput";
-import dynamic from "next/dynamic";
-import {
- type ReactElement,
- useActionState,
- useEffect,
- useRef,
- useState,
-} from "react";
-import { useFormStatus } from "react-dom";
-import { toast } from "sonner";
-import { Button } from "@/components/ui/button";
-import { Spinner } from "@/components/ui/Spinner/Spinner";
-import { Skeleton } from "@/components/ui/skeleton";
-import { cn } from "@/lib/utils";
-import { createTicketAction } from "./create-ticket.action";
-
-const ConnectSupportForm = dynamic(() => import("./contact-forms/connect"), {
- loading: () => ,
- ssr: false,
-});
-const EngineSupportForm = dynamic(() => import("./contact-forms/engine"), {
- loading: () => ,
- ssr: false,
-});
-const ContractSupportForm = dynamic(() => import("./contact-forms/contracts"), {
- loading: () => ,
- ssr: false,
-});
-const AccountSupportForm = dynamic(() => import("./contact-forms/account"), {
- loading: () => ,
- ssr: false,
-});
-const OtherSupportForm = dynamic(() => import("./contact-forms/other"), {
- loading: () => ,
- ssr: false,
-});
-
-const productOptions: { label: string; component: ReactElement }[] = [
- {
- component: ,
- label: "Connect",
- },
- {
- component: ,
- label: "Engine",
- },
- {
- component: ,
- label: "Contracts",
- },
- {
- component: ,
- label: "Account",
- },
- {
- component: ,
- label: "Other",
- },
-];
-
-function ProductAreaSelection(props: {
- productLabel: string;
- setProductLabel: (val: string) => void;
-}) {
- const { productLabel, setProductLabel } = props;
-
- return (
-
- o.label)}
- promptText="Select a product"
- required={true}
- value={productLabel}
- />
- {productOptions.find((o) => o.label === productLabel)?.component}
-
- );
-}
-
-export function CreateTicket(props: {
- teams: {
- name: string;
- id: string;
- }[];
-}) {
- const formRef = useRef(null);
- const [selectedTeamId, setSelectedTeamId] = useState(
- props.teams[0]?.id,
- );
-
- const [productLabel, setProductLabel] = useState("");
-
- const [state, formAction] = useActionState(createTicketAction, {
- message: "",
- success: false,
- });
-
- // needed here
- // eslint-disable-next-line no-restricted-syntax
- useEffect(() => {
- if (!state.message) {
- return;
- }
- if (state.success) {
- toast.success(state.message);
- } else {
- toast.error(state.message);
- }
- }, [state.success, state.message]);
-
- return (
-
- );
-}
-
-function SubmitButton() {
- const { pending } = useFormStatus();
- return (
-
- {pending && }
- {pending ? "Submitting" : "Submit"}
-
- );
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/page.tsx b/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/page.tsx
deleted file mode 100644
index 20d150e8513..00000000000
--- a/apps/dashboard/src/app/(app)/(dashboard)/support/create-ticket/page.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import Link from "next/link";
-import { getTeams } from "@/api/team";
-import {
- Breadcrumb,
- BreadcrumbItem,
- BreadcrumbLink,
- BreadcrumbList,
- BreadcrumbPage,
- BreadcrumbSeparator,
-} from "@/components/ui/breadcrumb";
-import { loginRedirect } from "../../../login/loginRedirect";
-import { CreateTicket } from "./components/create-ticket.client";
-
-export default async function Page() {
- const teams = await getTeams();
-
- const pagePath = "/support/create-ticket";
-
- if (!teams || teams.length === 0) {
- loginRedirect(pagePath);
- }
-
- return (
-
-
-
-
-
- Support
-
-
-
-
- Get Support
-
-
-
-
- ({
- id: t.id,
- name: t.name,
- }))}
- />
-
-
- );
-}
diff --git a/apps/dashboard/src/app/(app)/(dashboard)/support/opengraph-image.png b/apps/dashboard/src/app/(app)/(dashboard)/support/opengraph-image.png
deleted file mode 100644
index b41aca75a60e8b54af592af12ba561bed89bec04..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 869261
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