diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/dashboards/list-dashboards.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/dashboards/list-dashboards.tsx index eecc84cf..cf9c0d04 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/dashboards/list-dashboards.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/dashboards/list-dashboards.tsx @@ -24,25 +24,20 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) { const deletion = api.dashboard.delete.useMutation({ onError: (error, variables) => { return handleErrorToastOptions({ - action: ( - { - deletion.mutate({ - forceDelete: true, - id: variables.id, - }); - }} - > - Force delete - - ), + action: { + label: 'Force delete', + onClick: () => { + deletion.mutate({ + forceDelete: true, + id: variables.id, + }); + }, + }, })(error); }, onSuccess() { router.refresh(); - toast({ - title: 'Success', + toast('Success', { description: 'Dashboard deleted.', }); }, diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/settings/organization/edit-organization.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/settings/organization/edit-organization.tsx index 1e43609a..4b186136 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/settings/organization/edit-organization.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/settings/organization/edit-organization.tsx @@ -25,13 +25,12 @@ export default function EditOrganization({ const router = useRouter(); const { register, handleSubmit, formState, reset } = useForm({ - defaultValues: organization, + defaultValues: organization ?? undefined, }); const mutation = api.organization.update.useMutation({ onSuccess(res) { - toast({ - title: 'Organization updated', + toast('Organization updated', { description: 'Your organization has been updated.', }); reset(res); @@ -57,7 +56,7 @@ export default function EditOrganization({ diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/settings/organization/invite-user.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/settings/organization/invite-user.tsx index 2bae018f..6f8a56d3 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/settings/organization/invite-user.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/settings/organization/invite-user.tsx @@ -27,8 +27,7 @@ export function InviteUser() { const mutation = api.organization.inviteUser.useMutation({ onSuccess() { - toast({ - title: 'User invited!', + toast('User invited!', { description: 'The user has been invited to the organization.', }); reset(); diff --git a/apps/web/src/app/(app)/[organizationId]/[projectId]/settings/profile/edit-profile.tsx b/apps/web/src/app/(app)/[organizationId]/[projectId]/settings/profile/edit-profile.tsx index 292175cc..2d678a66 100644 --- a/apps/web/src/app/(app)/[organizationId]/[projectId]/settings/profile/edit-profile.tsx +++ b/apps/web/src/app/(app)/[organizationId]/[projectId]/settings/profile/edit-profile.tsx @@ -35,8 +35,7 @@ export default function EditProfile({ profile }: EditProfileProps) { const mutation = api.user.update.useMutation({ onSuccess(res) { - toast({ - title: 'Profile updated', + toast('Profile updated', { description: 'Your profile has been updated.', }); reset(res); diff --git a/apps/web/src/app/_trpc/client.tsx b/apps/web/src/app/_trpc/client.tsx index 7e664017..83d94d8c 100644 --- a/apps/web/src/app/_trpc/client.tsx +++ b/apps/web/src/app/_trpc/client.tsx @@ -1,8 +1,8 @@ -import type { Toast } from '@/components/ui/use-toast'; import type { AppRouter } from '@/server/api/root'; import type { TRPCClientErrorBase } from '@trpc/react-query'; import { createTRPCReact } from '@trpc/react-query'; import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'; +import type { ExternalToast } from 'sonner'; import { toast } from 'sonner'; export const api = createTRPCReact({}); @@ -24,16 +24,14 @@ export type IChartData = RouterOutputs['chart']['chart']; export type IChartSerieDataItem = IChartData['series'][number]['data'][number]; export function handleError(error: TRPCClientErrorBase) { - toast({ - title: 'Error', + toast('Error', { description: error.message, }); } -export function handleErrorToastOptions(options: Toast) { +export function handleErrorToastOptions(options: ExternalToast) { return function (error: TRPCClientErrorBase) { - toast({ - title: 'Error', + toast('Error', { description: error.message, ...options, }); diff --git a/apps/web/src/app/providers.tsx b/apps/web/src/app/providers.tsx index 9e401e08..91278087 100644 --- a/apps/web/src/app/providers.tsx +++ b/apps/web/src/app/providers.tsx @@ -2,7 +2,6 @@ import React, { useRef, useState } from 'react'; import { api } from '@/app/_trpc/client'; -import { Toaster } from '@/components/ui/toaster'; import { TooltipProvider } from '@/components/ui/tooltip'; import { ModalProvider } from '@/modals'; import type { AppStore } from '@/redux'; @@ -50,7 +49,6 @@ export default function Providers({ children }: { children: React.ReactNode }) { {children} - diff --git a/apps/web/src/components/clients/ClientActions.tsx b/apps/web/src/components/clients/ClientActions.tsx index ba02ff46..30e831d6 100644 --- a/apps/web/src/components/clients/ClientActions.tsx +++ b/apps/web/src/components/clients/ClientActions.tsx @@ -6,6 +6,7 @@ import type { IClientWithProject } from '@/types'; import { clipboard } from '@/utils/clipboard'; import { MoreHorizontal } from 'lucide-react'; import { useRouter } from 'next/navigation'; +import { toast } from 'sonner'; import { Button } from '../ui/button'; import { @@ -16,15 +17,13 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '../ui/dropdown-menu'; -import { toast } from '../ui/use-toast'; export function ClientActions(client: IClientWithProject) { const { id } = client; const router = useRouter(); const deletion = api.client.remove.useMutation({ onSuccess() { - toast({ - title: 'Success', + toast('Success', { description: 'Client revoked, incoming requests will be rejected.', }); router.refresh(); diff --git a/apps/web/src/components/projects/ProjectActions.tsx b/apps/web/src/components/projects/ProjectActions.tsx index 1b104d22..0bc3d8ef 100644 --- a/apps/web/src/components/projects/ProjectActions.tsx +++ b/apps/web/src/components/projects/ProjectActions.tsx @@ -6,6 +6,7 @@ import type { IProject } from '@/types'; import { clipboard } from '@/utils/clipboard'; import { MoreHorizontal } from 'lucide-react'; import { useRouter } from 'next/navigation'; +import { toast } from 'sonner'; import { Button } from '../ui/button'; import { @@ -16,15 +17,13 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '../ui/dropdown-menu'; -import { toast } from '../ui/use-toast'; export function ProjectActions(project: IProject) { const { id } = project; const router = useRouter(); const deletion = api.project.remove.useMutation({ onSuccess() { - toast({ - title: 'Success', + toast('Success', { description: 'Project deleted successfully.', }); router.refresh(); diff --git a/apps/web/src/components/report/ReportSaveButton.tsx b/apps/web/src/components/report/ReportSaveButton.tsx index fd7ce3bd..9fb874b9 100644 --- a/apps/web/src/components/report/ReportSaveButton.tsx +++ b/apps/web/src/components/report/ReportSaveButton.tsx @@ -19,8 +19,7 @@ export function ReportSaveButton({ className }: ReportSaveButtonProps) { const update = api.report.update.useMutation({ onSuccess() { dispatch(resetDirty()); - toast({ - title: 'Success', + toast('Success', { description: 'Report updated.', }); }, diff --git a/apps/web/src/components/ui/sonner.tsx b/apps/web/src/components/ui/sonner.tsx index 1128edfc..537e52a5 100644 --- a/apps/web/src/components/ui/sonner.tsx +++ b/apps/web/src/components/ui/sonner.tsx @@ -1,29 +1,29 @@ -import { useTheme } from "next-themes" -import { Toaster as Sonner } from "sonner" +import { useTheme } from 'next-themes'; +import { Toaster as Sonner } from 'sonner'; -type ToasterProps = React.ComponentProps +type ToasterProps = React.ComponentProps; const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme() + const { theme = 'system' } = useTheme(); return ( - ) -} + ); +}; -export { Toaster } +export { Toaster }; diff --git a/apps/web/src/components/ui/use-toast.ts b/apps/web/src/components/ui/use-toast.ts deleted file mode 100644 index e6807d07..00000000 --- a/apps/web/src/components/ui/use-toast.ts +++ /dev/null @@ -1,190 +0,0 @@ -'use client'; - -// Inspired by react-hot-toast library -import * as React from 'react'; -import type { ToastActionElement, ToastProps } from '@/components/ui/toast'; - -const TOAST_LIMIT = 1; -const TOAST_REMOVE_DELAY = 1000000; - -type ToasterToast = ToastProps & { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - action?: ToastActionElement; -}; - -const actionTypes = { - ADD_TOAST: 'ADD_TOAST', - UPDATE_TOAST: 'UPDATE_TOAST', - DISMISS_TOAST: 'DISMISS_TOAST', - REMOVE_TOAST: 'REMOVE_TOAST', -} as const; - -let count = 0; - -function genId() { - count = (count + 1) % Number.MAX_VALUE; - return count.toString(); -} - -type ActionType = typeof actionTypes; - -type Action = - | { - type: ActionType['ADD_TOAST']; - toast: ToasterToast; - } - | { - type: ActionType['UPDATE_TOAST']; - toast: Partial; - } - | { - type: ActionType['DISMISS_TOAST']; - toastId?: ToasterToast['id']; - } - | { - type: ActionType['REMOVE_TOAST']; - toastId?: ToasterToast['id']; - }; - -interface State { - toasts: ToasterToast[]; -} - -const toastTimeouts = new Map>(); - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return; - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId); - dispatch({ - type: 'REMOVE_TOAST', - toastId: toastId, - }); - }, TOAST_REMOVE_DELAY); - - toastTimeouts.set(toastId, timeout); -}; - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case 'ADD_TOAST': - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - }; - - case 'UPDATE_TOAST': - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - }; - - case 'DISMISS_TOAST': { - const { toastId } = action; - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId); - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id); - }); - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t - ), - }; - } - case 'REMOVE_TOAST': - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - }; - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - }; - } -}; - -const listeners: ((state: State) => void)[] = []; - -let memoryState: State = { toasts: [] }; - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action); - listeners.forEach((listener) => { - listener(memoryState); - }); -} - -export type Toast = Omit; - -function toast({ ...props }: Toast) { - const id = genId(); - - const update = (props: ToasterToast) => - dispatch({ - type: 'UPDATE_TOAST', - toast: { ...props, id }, - }); - const dismiss = () => dispatch({ type: 'DISMISS_TOAST', toastId: id }); - - dispatch({ - type: 'ADD_TOAST', - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss(); - }, - }, - }); - - return { - id: id, - dismiss, - update, - }; -} - -function useToast() { - const [state, setState] = React.useState(memoryState); - - React.useEffect(() => { - listeners.push(setState); - return () => { - const index = listeners.indexOf(setState); - if (index > -1) { - listeners.splice(index, 1); - } - }; - }, [state]); - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }), - }; -} - -export { useToast, toast }; diff --git a/apps/web/src/modals/AddClient.tsx b/apps/web/src/modals/AddClient.tsx index fa56d455..15d03ba9 100644 --- a/apps/web/src/modals/AddClient.tsx +++ b/apps/web/src/modals/AddClient.tsx @@ -40,8 +40,7 @@ export default function AddClient({ organizationId }: AddClientProps) { const mutation = api.client.create.useMutation({ onError: handleError, onSuccess() { - toast({ - title: 'Success', + toast('Success', { description: 'Client created!', }); router.refresh(); diff --git a/apps/web/src/modals/AddDashboard.tsx b/apps/web/src/modals/AddDashboard.tsx index af5412e6..fde7d54a 100644 --- a/apps/web/src/modals/AddDashboard.tsx +++ b/apps/web/src/modals/AddDashboard.tsx @@ -35,8 +35,7 @@ export default function AddDashboard() { onError: handleError, onSuccess() { router.refresh(); - toast({ - title: 'Success', + toast('Success', { description: 'Dashboard created.', }); popModal(); diff --git a/apps/web/src/modals/AddProject.tsx b/apps/web/src/modals/AddProject.tsx index d6f3c658..6ee2c799 100644 --- a/apps/web/src/modals/AddProject.tsx +++ b/apps/web/src/modals/AddProject.tsx @@ -27,8 +27,7 @@ export default function AddProject({ organizationId }: AddProjectProps) { onError: handleError, onSuccess() { router.refresh(); - toast({ - title: 'Success', + toast('Success', { description: 'Project created! Lets create a client for it 🤘', }); popModal(); diff --git a/apps/web/src/modals/EditClient.tsx b/apps/web/src/modals/EditClient.tsx index 37435372..dcebc6b1 100644 --- a/apps/web/src/modals/EditClient.tsx +++ b/apps/web/src/modals/EditClient.tsx @@ -39,8 +39,7 @@ export default function EditClient({ id, name, cors }: EditClientProps) { onError: handleError, onSuccess() { reset(); - toast({ - title: 'Success', + toast('Success', { description: 'Client updated.', }); popModal(); diff --git a/apps/web/src/modals/EditDashboard.tsx b/apps/web/src/modals/EditDashboard.tsx index b90634f5..b0f3b3a0 100644 --- a/apps/web/src/modals/EditDashboard.tsx +++ b/apps/web/src/modals/EditDashboard.tsx @@ -37,8 +37,7 @@ export default function EditDashboard({ id, name }: EditDashboardProps) { onError: handleError, onSuccess() { reset(); - toast({ - title: 'Success', + toast('Success', { description: 'Dashboard updated.', }); popModal(); diff --git a/apps/web/src/modals/EditProject.tsx b/apps/web/src/modals/EditProject.tsx index d25abfd7..37767c11 100644 --- a/apps/web/src/modals/EditProject.tsx +++ b/apps/web/src/modals/EditProject.tsx @@ -38,8 +38,7 @@ export default function EditProject({ id, name }: EditProjectProps) { onSuccess() { reset(); router.refresh(); - toast({ - title: 'Success', + toast('Success', { description: 'Project updated.', }); popModal(); diff --git a/apps/web/src/modals/SaveReport.tsx b/apps/web/src/modals/SaveReport.tsx index 0941a37e..ab1ab34d 100644 --- a/apps/web/src/modals/SaveReport.tsx +++ b/apps/web/src/modals/SaveReport.tsx @@ -38,8 +38,7 @@ export default function SaveReport({ report }: SaveReportProps) { const save = api.report.save.useMutation({ onError: handleError, onSuccess(res) { - toast({ - title: 'Success', + toast('Success', { description: 'Report saved.', }); popModal(); @@ -65,8 +64,7 @@ export default function SaveReport({ report }: SaveReportProps) { onSuccess(res) { setValue('dashboardId', res.id); dashboardQuery.refetch(); - toast({ - title: 'Success', + toast('Success', { description: 'Dashboard created.', }); }, diff --git a/apps/web/src/utils/clipboard.ts b/apps/web/src/utils/clipboard.ts index 171fc623..02bf3ba8 100644 --- a/apps/web/src/utils/clipboard.ts +++ b/apps/web/src/utils/clipboard.ts @@ -2,8 +2,7 @@ import { toast } from 'sonner'; export function clipboard(value: string | number) { navigator.clipboard.writeText(value.toString()); - toast({ - title: 'Copied to clipboard', + toast('Copied to clipboard', { description: value.toString(), }); } diff --git a/packages/queue/src/connection.ts b/packages/queue/src/connection.ts index d3d65c0d..60a50b38 100644 --- a/packages/queue/src/connection.ts +++ b/packages/queue/src/connection.ts @@ -1,12 +1,9 @@ const parse = (connectionString: string) => { - const match = connectionString.match(/redis:\/\/(.+?):(.+?)@(.+?):(.+)/); - if (!match) { - throw new Error('Invalid connection string'); - } + const url = new URL(connectionString); return { - host: match[3]!, - port: Number(match[4]), - password: match[2]!, + host: url.hostname, + port: Number(url.port), + password: url.password, } as const; };