From 595da3bbba12b04a28552b4308b512d0e499bdd0 Mon Sep 17 00:00:00 2001 From: itsjoxdev Date: Sat, 25 Jul 2026 16:40:40 +0300 Subject: [PATCH 1/2] feat: initialize web application with Next.js, Tailwind CSS, and core dependencies --- apps/web/package-lock.json | 11 +++++++++++ apps/web/package.json | 1 + 2 files changed, 12 insertions(+) diff --git a/apps/web/package-lock.json b/apps/web/package-lock.json index 73efd10..2b667cb 100644 --- a/apps/web/package-lock.json +++ b/apps/web/package-lock.json @@ -18,6 +18,7 @@ "react": "latest", "react-dom": "latest", "react-hook-form": "latest", + "sonner": "^2.0.7", "tailwind-merge": "latest", "zod": "latest", "zustand": "latest" @@ -5970,6 +5971,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/sonner": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-2.0.7.tgz", + "integrity": "sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", diff --git a/apps/web/package.json b/apps/web/package.json index f9cce7d..634d18a 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -20,6 +20,7 @@ "react": "latest", "react-dom": "latest", "react-hook-form": "latest", + "sonner": "^2.0.7", "tailwind-merge": "latest", "zod": "latest", "zustand": "latest" From 047609b9eed913a2b3e464a59ddf96ff3e2db433 Mon Sep 17 00:00:00 2001 From: itsjoxdev Date: Sat, 25 Jul 2026 19:42:06 +0300 Subject: [PATCH 2/2] feat(toast): implement production toast notification system with Sonner abstraction --- apps/web/components/ui/sonner.tsx | 35 ++++++++++++++++++ .../tickets/components/TicketAssignment.tsx | 23 +++++------- .../tickets/components/TicketWorkflow.tsx | 16 +++----- .../features/users/components/UserActions.tsx | 30 +++++++-------- .../features/users/components/UserForm.tsx | 37 ++++++++++--------- apps/web/providers/Providers.tsx | 2 + 6 files changed, 86 insertions(+), 57 deletions(-) create mode 100644 apps/web/components/ui/sonner.tsx diff --git a/apps/web/components/ui/sonner.tsx b/apps/web/components/ui/sonner.tsx new file mode 100644 index 0000000..2865367 --- /dev/null +++ b/apps/web/components/ui/sonner.tsx @@ -0,0 +1,35 @@ +"use client" + +import { useTheme } from "next-themes" +import { Toaster as Sonner } from "sonner" + +type ToasterProps = React.ComponentProps + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/apps/web/features/tickets/components/TicketAssignment.tsx b/apps/web/features/tickets/components/TicketAssignment.tsx index 964ee20..0dd8384 100644 --- a/apps/web/features/tickets/components/TicketAssignment.tsx +++ b/apps/web/features/tickets/components/TicketAssignment.tsx @@ -6,6 +6,8 @@ import { ticketService } from '../api/tickets.api'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { toast } from '@/lib/toast'; +import { MESSAGES } from '@/lib/toast/messages'; interface Props { ticketId: string; @@ -16,18 +18,18 @@ export function TicketAssignment({ ticketId, currentAssignee }: Props) { const router = useRouter(); const [isAssigning, setIsAssigning] = useState(false); const [technicianId, setTechnicianId] = useState(''); - const [error, setError] = useState(null); const handleAssign = async () => { if (!technicianId) return; setIsAssigning(true); - setError(null); try { - await ticketService.assignTicket(ticketId, technicianId); + const apiCall = ticketService.assignTicket(ticketId, technicianId); + toast.promise(apiCall, MESSAGES.TICKETS.ASSIGN); + await apiCall; setTechnicianId(''); router.refresh(); } catch (err: any) { - setError(err.message || 'Failed to assign ticket'); + // Error feedback is handled by toast } finally { setIsAssigning(false); } @@ -35,12 +37,13 @@ export function TicketAssignment({ ticketId, currentAssignee }: Props) { const handleUnassign = async () => { setIsAssigning(true); - setError(null); try { - await ticketService.unassignTicket(ticketId); + const apiCall = ticketService.unassignTicket(ticketId); + toast.promise(apiCall, MESSAGES.TICKETS.UNASSIGN); + await apiCall; router.refresh(); } catch (err: any) { - setError(err.message || 'Failed to unassign ticket'); + // Error feedback is handled by toast } finally { setIsAssigning(false); } @@ -52,12 +55,6 @@ export function TicketAssignment({ ticketId, currentAssignee }: Props) { Assignment - {error && ( -
- {error} -
- )} - {currentAssignee ? (

diff --git a/apps/web/features/tickets/components/TicketWorkflow.tsx b/apps/web/features/tickets/components/TicketWorkflow.tsx index a0b3c8e..fad3bf4 100644 --- a/apps/web/features/tickets/components/TicketWorkflow.tsx +++ b/apps/web/features/tickets/components/TicketWorkflow.tsx @@ -6,6 +6,8 @@ import { ticketService } from '../api/tickets.api'; import { TicketStatus } from '../types'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; +import { toast } from '@/lib/toast'; +import { MESSAGES } from '@/lib/toast/messages'; interface Props { ticketId: string; @@ -15,7 +17,6 @@ interface Props { export function TicketWorkflow({ ticketId, currentStatus }: Props) { const router = useRouter(); const [isUpdating, setIsUpdating] = useState(false); - const [error, setError] = useState(null); const getAvailableTransitions = () => { // This replicates the backend ALLOWED_TRANSITIONS logic temporarily for UI purposes @@ -33,12 +34,13 @@ export function TicketWorkflow({ ticketId, currentStatus }: Props) { const handleStatusChange = async (newStatus: TicketStatus) => { setIsUpdating(true); - setError(null); try { - await ticketService.updateTicketStatus(ticketId, newStatus); + const apiCall = ticketService.updateTicketStatus(ticketId, newStatus); + toast.promise(apiCall, MESSAGES.TICKETS.STATUS_UPDATE); + await apiCall; router.refresh(); } catch (err: any) { - setError(err.message || 'Failed to update status'); + // Error feedback is handled by toast } finally { setIsUpdating(false); } @@ -54,12 +56,6 @@ export function TicketWorkflow({ ticketId, currentStatus }: Props) { Workflow Actions - {error && ( -

- {error} -
- )} -
{availableTransitions.map((status) => (