feat: integrate sonner for toast notifications and add ToastInner component

This commit is contained in:
micha 2025-06-19 03:46:44 +02:00
parent 70a819f525
commit 0caddb59d8
6 changed files with 205 additions and 1 deletions

View file

@ -51,6 +51,7 @@
"react-day-picker": "^9.7.0", "react-day-picker": "^9.7.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-hook-form": "^7.56.4", "react-hook-form": "^7.56.4",
"sonner": "^2.0.5",
"swagger-ui-react": "^5.24.1", "swagger-ui-react": "^5.24.1",
"tailwind-merge": "^3.2.0", "tailwind-merge": "^3.2.0",
"zod": "^3.25.60" "zod": "^3.25.60"

View file

@ -3,6 +3,7 @@ import { ThemeProvider } from '@/components/wrappers/theme-provider';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import './globals.css'; import './globals.css';
import { QueryProvider } from '@/components/query-provider'; import { QueryProvider } from '@/components/query-provider';
import { Toaster } from '@/components/ui/sonner';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'MeetUp', title: 'MeetUp',
@ -58,6 +59,7 @@ export default function RootLayout({
> >
<QueryProvider>{children}</QueryProvider> <QueryProvider>{children}</QueryProvider>
</ThemeProvider> </ThemeProvider>
<Toaster />
</body> </body>
</html> </html>
); );

View file

@ -13,6 +13,8 @@ import {
} from '@/generated/api/event/event'; } from '@/generated/api/event/event';
import ParticipantListEntry from '@/components/custom-ui/participantListEntry'; import ParticipantListEntry from '@/components/custom-ui/participantListEntry';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import { CalendarCheck } from 'lucide-react';
interface EventFormProps { interface EventFormProps {
type: 'create' | 'edit'; type: 'create' | 'edit';
@ -129,9 +131,14 @@ const EventForm: React.FC<EventFormProps> = (props) => {
console.log('Updating event with data:', data); console.log('Updating event with data:', data);
} else { } else {
console.log('Creating event with data:', data); console.log('Creating event with data:', data);
createEvent({ data }); createEvent({ data });
} }
toast('Event saved successfully', {
description: `Your event "${data.title}" has been saved.`,
icon: <CalendarCheck />,
});
router.back(); router.back();
} }

View file

@ -0,0 +1,146 @@
/*
USAGE:
import { toast } from 'sonner';
import { ToastInner } from '@/components/misc/toast-inner';
import { Button } from '@/components/ui/button';
<Button
variant='outline_primary'
onClick={() =>
toast.custom(
(t) => (
<ToastInner
toastId={t}
title=''
description=''
onAction={() => console.log('on Action')} //No Button shown if this is null
variant=''default' | 'success' | 'error' | 'info' | 'warning' | 'notification''
buttonText=[No Button shown if this is null]
iconName=[Any Icon Name from Lucide in UpperCamelCase or default if null]
/>
),
{
duration: 5000,
},
)
}
>
Show Toast
</Button>
*/
'use client';
import { toast } from 'sonner';
import { X } from 'lucide-react';
import React from 'react';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import * as Icons from 'lucide-react';
interface ToastInnerProps {
title: string;
description?: string;
buttonText?: string;
onAction?: () => void;
toastId: string | number;
variant?:
| 'default'
| 'success'
| 'error'
| 'info'
| 'warning'
| 'notification';
iconName?: keyof typeof Icons;
}
const variantConfig = {
default: {
bgColor: 'bg-green-150',
defaultIcon: 'Info',
},
success: {
bgColor: 'bg-green-200',
defaultIcon: 'CheckCircle',
},
error: {
bgColor: 'bg-red-200',
defaultIcon: 'XCircle',
},
info: {
bgColor: 'bg-blue-200',
defaultIcon: 'Info',
},
warning: {
bgColor: 'bg-yellow-200',
defaultIcon: 'AlertTriangle',
},
notification: {
bgColor: 'bg-neutral-150',
defaultIcon: 'BellRing',
},
};
export const ToastInner: React.FC<ToastInnerProps> = ({
title,
description,
buttonText,
onAction,
toastId,
variant = 'default',
iconName,
}) => {
const bgColor = variantConfig[variant].bgColor;
// fallback to variant's default icon if iconName is not provided
const iconKey = (iconName ||
variantConfig[variant].defaultIcon) as keyof typeof Icons;
const Icon = Icons[iconKey] as React.ComponentType<Icons.LucideProps>;
return (
<div className={`relative w-120 rounded p-4 ${bgColor} select-none`}>
{/* Close Button */}
<button
onClick={() => toast.dismiss(toastId)}
className='absolute top-2 right-2 cursor-pointer'
aria-label='Close notification'
>
<X className='h-4 w-4 text-neutral-600' />
</button>
<div className='grid grid-cols-[40px_auto_auto] gap-4 items-center'>
{/* Icon */}
<div className='flex items-center justify-center'>
<Icon size={40} />
</div>
{/* Text Content */}
<div className='grid gap-1'>
<h6>{title}</h6>
{description && <Label>{description}</Label>}
</div>
{/* Action Button */}
<div className='flex justify-center'>
{onAction && buttonText && (
<Button
variant={'secondary'}
className='w-100px w-full mr-2'
onClick={onAction}
>
{buttonText}
</Button>
)}
</div>
</div>
</div>
);
};

View file

@ -0,0 +1,37 @@
'use client';
import { useTheme } from 'next-themes';
import { Toaster as Sonner, ToasterProps } from 'sonner';
const Toaster = ({ ...props }: ToasterProps) => {
const { theme = 'system' } = useTheme();
return (
<Sonner
theme={theme as ToasterProps['theme']}
richColors={true}
className='toaster group'
toastOptions={{
style: {
backgroundColor: 'var(--color-neutral-150)',
color: 'var(--color-text-alt)',
borderRadius: 'var(--radius)',
},
cancelButtonStyle: {
backgroundColor: 'var(--color-secondary)',
color: 'var(--color-text-alt)',
},
actionButtonStyle: {
backgroundColor: 'var(--color-secondary)',
color: 'var(--color-text-alt)',
},
}}
swipeDirections={['left', 'right']}
closeButton={true}
expand={true}
{...props}
/>
);
};
export { Toaster };

View file

@ -6761,6 +6761,7 @@ __metadata:
react-day-picker: "npm:^9.7.0" react-day-picker: "npm:^9.7.0"
react-dom: "npm:^19.0.0" react-dom: "npm:^19.0.0"
react-hook-form: "npm:^7.56.4" react-hook-form: "npm:^7.56.4"
sonner: "npm:^2.0.5"
swagger-ui-react: "npm:^5.24.1" swagger-ui-react: "npm:^5.24.1"
tailwind-merge: "npm:^3.2.0" tailwind-merge: "npm:^3.2.0"
tailwindcss: "npm:4.1.10" tailwindcss: "npm:4.1.10"
@ -8661,6 +8662,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"sonner@npm:^2.0.5":
version: 2.0.5
resolution: "sonner@npm:2.0.5"
peerDependencies:
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
checksum: 10c0/38ec98e2f5d7e086825307f737a90bdc8639182d184e002719c2368bf3a9259c340f41afda731716d2b78c40e5e3aa9165058375be42f6a93bda0876b9b433ba
languageName: node
linkType: hard
"source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.1": "source-map-js@npm:^1.0.2, source-map-js@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "source-map-js@npm:1.2.1" resolution: "source-map-js@npm:1.2.1"