feat: integrate sonner for toast notifications and add ToastInner component
This commit is contained in:
parent
70a819f525
commit
0caddb59d8
6 changed files with 205 additions and 1 deletions
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
146
src/components/misc/toast-inner.tsx
Normal file
146
src/components/misc/toast-inner.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
37
src/components/ui/sonner.tsx
Normal file
37
src/components/ui/sonner.tsx
Normal 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 };
|
11
yarn.lock
11
yarn.lock
|
@ -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"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue