Notification UI and house invitation
This commit is contained in:
@@ -1,19 +1,7 @@
|
||||
import { Separator } from '@base-ui/react/separator';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { BellIcon } from '@phosphor-icons/react';
|
||||
import { Badge } from '@ui/badge';
|
||||
import { Button } from '@ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@ui/dropdown-menu';
|
||||
import { SidebarTrigger } from '@ui/sidebar';
|
||||
import { useAuth } from './auth/auth-provider';
|
||||
import Notification from './Notification';
|
||||
import RouterBreadcrumb from './sidebar/router-breadcrumb';
|
||||
|
||||
export default function Header() {
|
||||
@@ -30,48 +18,7 @@ export default function Header() {
|
||||
/>
|
||||
<RouterBreadcrumb />
|
||||
</div>
|
||||
<div className="flex mr-2">
|
||||
{session?.user && (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button size="lg" variant="ghost" className="relative">
|
||||
<BellIcon size={32} />
|
||||
{false && (
|
||||
<Badge
|
||||
variant="destructive"
|
||||
className="absolute -top-1 -right-1 h-5 w-5 flex items-center justify-center p-0 text-xs"
|
||||
>
|
||||
0
|
||||
</Badge>
|
||||
)}
|
||||
<span className="sr-only">Notifications</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-sm min-w-56 rounded-lg">
|
||||
<DropdownMenuLabel className="font-bold text-black">
|
||||
{m.ui_label_notifications()}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<div className="flex flex-col gap-1">
|
||||
<p className="text-sm font-medium">System</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
1 hour ago
|
||||
</p>
|
||||
</div>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
{m.ui_view_all_notifications()}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex mr-2">{session?.user && <Notification />}</div>
|
||||
</header>
|
||||
</>
|
||||
);
|
||||
|
||||
83
src/components/Notification.tsx
Normal file
83
src/components/Notification.tsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import { notificationQueries } from '@/service/queries';
|
||||
import { formatTimeAgo } from '@/utils/helper';
|
||||
import { cn } from '@lib/utils';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { BellIcon } from '@phosphor-icons/react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { Link } from '@tanstack/react-router';
|
||||
import { Button } from '@ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@ui/dropdown-menu';
|
||||
import { Item, ItemContent, ItemDescription, ItemTitle } from './ui/item';
|
||||
|
||||
const Notification = () => {
|
||||
const { data: notifications } = useQuery(notificationQueries.topFive());
|
||||
|
||||
if (!notifications) return null;
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
size="icon-lg"
|
||||
variant="ghost"
|
||||
className="relative rounded-full"
|
||||
>
|
||||
<BellIcon
|
||||
size={32}
|
||||
className={cn('origin-top', { 'animate-bell-ring': true })}
|
||||
/>
|
||||
{true && (
|
||||
<span className="absolute top-1 right-1 rounded-full w-2 h-2 bg-red-600"></span>
|
||||
)}
|
||||
<span className="sr-only">{m.ui_label_notifications()}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-sm min-w-56 rounded-lg">
|
||||
<DropdownMenuLabel className="font-bold text-black">
|
||||
{m.ui_label_notifications()}
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
{notifications.map((notify) => {
|
||||
return (
|
||||
<DropdownMenuItem key={notify.id}>
|
||||
<Item className="p-0">
|
||||
<ItemContent>
|
||||
<ItemTitle className="text-sm">
|
||||
{m.templates_title_notification({
|
||||
title: notify.title as Parameters<
|
||||
typeof m.templates_title_notification
|
||||
>[0]['title'],
|
||||
})}
|
||||
</ItemTitle>
|
||||
<ItemDescription>
|
||||
{formatTimeAgo(new Date(notify.createdAt))}
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</DropdownMenuItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link to="/management/notifications" className="cursor-pointer">
|
||||
{m.ui_view_all_notifications()}
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notification;
|
||||
@@ -38,7 +38,7 @@ const UserInviteMemberForm = ({ onSubmit }: FormProps) => {
|
||||
});
|
||||
onSubmit(false);
|
||||
refetch();
|
||||
toast.success(m.houses_page_message_create_house_success(), {
|
||||
toast.success(m.houses_page_message_invite_member_success(), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import useHasPermission from '@/hooks/use-has-permission';
|
||||
import { cancelInvitation } from '@/service/house.api';
|
||||
import { INVITE_STATUS } from '@/types/enum';
|
||||
import { authClient } from '@lib/auth-client';
|
||||
@@ -23,13 +24,17 @@ type InvitationListProps = {
|
||||
|
||||
const CurrentUserInvitationList = ({ activeHouse }: InvitationListProps) => {
|
||||
const { refetch } = authClient.useActiveOrganization();
|
||||
const { hasPermission, isLoading } = useHasPermission(
|
||||
'invitation',
|
||||
'cancel',
|
||||
true,
|
||||
);
|
||||
|
||||
const { mutate: cancelInvitationMutation } = useMutation({
|
||||
mutationFn: cancelInvitation,
|
||||
onSuccess: () => {
|
||||
refetch();
|
||||
// _setOpen(false);
|
||||
toast.success(m.houses_page_message_delete_house_success(), {
|
||||
toast.success(m.houses_page_message_cancel_invitation_success(), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
@@ -66,7 +71,7 @@ const CurrentUserInvitationList = ({ activeHouse }: InvitationListProps) => {
|
||||
<TableBody>
|
||||
{activeHouse.invitations.length > 0 ? (
|
||||
activeHouse.invitations.map((item) => (
|
||||
<TableRow>
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>
|
||||
<Item>
|
||||
<ItemContent>
|
||||
@@ -87,7 +92,7 @@ const CurrentUserInvitationList = ({ activeHouse }: InvitationListProps) => {
|
||||
</TableCell>
|
||||
<TableCell className="p-6">
|
||||
<div className="flex justify-end gap-2">
|
||||
{item.status !== INVITE_STATUS.CANCELED && (
|
||||
{item.status === INVITE_STATUS.PENDING && hasPermission && (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="cursor-pointer w-20 py-4"
|
||||
|
||||
@@ -18,7 +18,7 @@ type ActionProps = {};
|
||||
|
||||
const InviteUserAction = ({}: ActionProps) => {
|
||||
const { hasPermission, isLoading } = useHasPermission(
|
||||
'house',
|
||||
'invitation',
|
||||
'create',
|
||||
true,
|
||||
);
|
||||
|
||||
59
src/components/notification/notification-item.tsx
Normal file
59
src/components/notification/notification-item.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { NOTIFICATION_TYPE } from '@/types/enum';
|
||||
import { cn } from '@lib/utils';
|
||||
import { m } from '@paraglide/messages';
|
||||
import { Item, ItemHeader } from '@ui/item';
|
||||
import { cva, VariantProps } from 'class-variance-authority';
|
||||
import NotificationInvitation from './notification-type/invitation';
|
||||
|
||||
type NotifyProps = {
|
||||
notify: NotificationWithUser;
|
||||
};
|
||||
|
||||
const notifyVariants = cva('bg-linear-to-br shadow-xs', {
|
||||
variants: {
|
||||
variant: {
|
||||
invitation: 'to-gray-50 from-teal-50',
|
||||
system: '',
|
||||
error: '',
|
||||
house: '',
|
||||
expired: '',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'invitation',
|
||||
},
|
||||
});
|
||||
|
||||
const NotificationItem = ({
|
||||
className,
|
||||
notify,
|
||||
...props
|
||||
}: NotifyProps & React.ComponentProps<'div'>) => {
|
||||
return (
|
||||
<Item
|
||||
variant="outline"
|
||||
className={cn(
|
||||
notifyVariants({
|
||||
variant: notify.type as VariantProps<
|
||||
typeof notifyVariants
|
||||
>['variant'],
|
||||
className,
|
||||
}),
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ItemHeader>
|
||||
{m.templates_title_notification({
|
||||
title: notify.title as Parameters<
|
||||
typeof m.templates_title_notification
|
||||
>[0]['title'],
|
||||
})}
|
||||
</ItemHeader>
|
||||
{notify.type === NOTIFICATION_TYPE.INVITATION && (
|
||||
<NotificationInvitation notify={notify} />
|
||||
)}
|
||||
</Item>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationItem;
|
||||
110
src/components/notification/notification-type/invitation.tsx
Normal file
110
src/components/notification/notification-type/invitation.tsx
Normal file
@@ -0,0 +1,110 @@
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
ItemActions,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from '@/components/ui/item';
|
||||
import { m } from '@/paraglide/messages';
|
||||
import { acceptInvitation, rejectInvitation } from '@/service/house.api';
|
||||
import { notificationQueries } from '@/service/queries';
|
||||
import { formatTimeAgo } from '@/utils/helper';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
type NotifyProps = {
|
||||
notify: NotificationWithUser;
|
||||
};
|
||||
|
||||
const NotificationInvitation = ({ notify }: NotifyProps) => {
|
||||
const { house } = JSON.parse(notify.metadata || '');
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { mutate: acceptInvitationMutation } = useMutation({
|
||||
mutationFn: acceptInvitation,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...notificationQueries.all, 'list'],
|
||||
});
|
||||
toast.success(m.notification_page_message_invitation_success(), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
onError: (error: ReturnError) => {
|
||||
console.error(error);
|
||||
const code = error.code as Parameters<
|
||||
typeof m.backend_message
|
||||
>[0]['code'];
|
||||
toast.error(m.backend_message({ code }), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: rejectInvitationMutation } = useMutation({
|
||||
mutationFn: rejectInvitation,
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: [...notificationQueries.all, 'list'],
|
||||
});
|
||||
toast.success(m.notification_page_message_invitation_rejected(), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
onError: (error: ReturnError) => {
|
||||
console.error(error);
|
||||
const code = error.code as Parameters<
|
||||
typeof m.backend_message
|
||||
>[0]['code'];
|
||||
toast.error(m.backend_message({ code }), {
|
||||
richColors: true,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const handleAgreeAction = async () => {
|
||||
if (notify.link) {
|
||||
acceptInvitationMutation({
|
||||
data: { id: notify.link, notificationId: notify.id },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleRejectAction = () => {
|
||||
if (notify.link) {
|
||||
rejectInvitationMutation({
|
||||
data: { id: notify.link, notificationId: notify.id },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<ItemContent>
|
||||
<ItemTitle>
|
||||
{`${notify.user.name} (${notify.user.email})`}{' '}
|
||||
{m.templates_message_notification({
|
||||
message: notify.message as Parameters<
|
||||
typeof m.templates_message_notification
|
||||
>[0]['message'],
|
||||
name: house.name,
|
||||
})}
|
||||
</ItemTitle>
|
||||
<ItemDescription>{formatTimeAgo(notify.createdAt)}</ItemDescription>
|
||||
</ItemContent>
|
||||
{notify.link && (
|
||||
<ItemActions>
|
||||
<Button onClick={() => handleAgreeAction()}>
|
||||
{m.ui_agree_btn()}
|
||||
</Button>
|
||||
<Button variant="outline" onClick={() => handleRejectAction()}>
|
||||
{m.ui_reject_btn()}
|
||||
</Button>
|
||||
</ItemActions>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotificationInvitation;
|
||||
@@ -25,7 +25,7 @@ const AddNewUserButton = () => {
|
||||
return (
|
||||
<Dialog open={_open} onOpenChange={_setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button type="button" variant="default">
|
||||
<Button type="button" variant="default" className="cursor-pointer">
|
||||
<PlusIcon />
|
||||
{m.nav_add_new()}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user