Skip to content

Commit

Permalink
[IVD-28] Notifications (#71)
Browse files Browse the repository at this point in the history
* feat(notification): init novu

* [IVD-57] Migrate to radix themes (#72)

* feat(UI): setup radix-theme

* feat(UI): use custom radix theme color

* feat(UI): migrate highscore pages to radix-theme

* chore(ui): init base layout from shadcn block 'dashboard-05'

* chore(deps): update vaul drawer module to latest minor

* feat: try to implement custom Spinner with css only

* chore(WIP): better invader loader + help page + layout

* feat: tricky but working custom spinner

* feat(UI): migrate help page to radix theme

* feat(UI): migrate help/thanks page to radix theme

* feat(UI): migrate legal pages to radix-ui

* feat(UI): root layout/drawer fixes w/ map page + better styles

* fix(UI): header top margin in PWA mode

* feat: move root drawer into its own component

* fix: breaking oklch colors handling on old devices

* feat(UI): use RootDrawer in root layout + add global scroll handling

* feat(UI): use new Spinner in root loading page

* feat(UI): use radix-theme classes for Drawer + remove overlay

* feat(UI): add padding for scrollbar in legal pages

* fix(UI): map page sizing + new loader

* perf: reduce costs by disabling /list image optimization

* fix(UI): add 'content' id to root layout for map sheet

* chore(UI): fix styles + add /help link in RootDrawer

* feat(UI): migrate most of /list to radix-theme + pwa scroll fixes

* feat(UI): migrate /map/[invaderName] to radix-themes

* chore(UI): use radix-theme for carousel buttons

* chore(UI): use radix-theme for SliderActions

* feat(UI): use radix-theme in MapSheet

* chore(UI): migrate UserMarker to radix-theme

* chore(UI): migrate InvaderHit to radix-themes

* fix(UI): bring scroll snap back for /list

* feat(UI): use DropdownMenu radix-themes classes on radix MenuBar

* feat(UI): migrate /list MenuBar to radix-themes

* feat(UI): migrate SkeletonHit to radix-themes + random string ssr function

* chore(UI): use new skeleton prop + radius fix + few ui tweaks

* feat(UI): migrate /map page to radix-themes

* fix: tweak MapSheet Header padding

* chore(UI): migrate Card to radix-theme

* feat: add shadow to Card component + better padding

* chore(UI): migrate CardForm to radix-themes

* feat(UI): migrate /account to radix-theme + FileInput component

* feat: rework AuthButton for RootHeader

* chore: add elevation to /list cards

* feat: add shadow to root header + better AuthButton skeleton

* feat(UI): revamp QR code drawer

* chore(UI): migrate ReferralLink section to radix-theme

* chore: ui fixes on RootDrawer and Drawer + LoginButton

* feat(UI): migrate /account header to radix-theme

* refactor: move /account header to its own component

* chore: complete Carousel migration to radix-theme

* feat(UI): migrate ReviewsSection to radix-ui

* feat(UI): /account review section skeleton

* feat: migrate ReviewCard to radix-theme

* feat(UI): migrate EditModal to radix-theme

* feat(UI): migrate invader history modal to radix-theme

* perf: reduce root drawer open delay

* feat(UI): added new desktop navigation menu (UI regression on iOS /list on drawer open)

* fix(GPU): [temp] iOS Safari crash when opening drawer on /list

* Upgrade React, Nextjs, NextAuth, Drizzle, vercel-postgres + fix local DB timeout + remove daisyUI (#73)

* fix: AuthButton new props on instances

* feat: use stable version for nextjs14 + react 19

* chore: update browserlist-lite

* refacto: remove unused imports

* refactor: remove unused components + move Single instance component closer to their page

* feat: invader history skeleton

* refacto: move Spinner image to public

* feat: get rid of daisy-ui classes + many small ui fixs and tweaks

* chore: remove daisyui from deps

* fix: build warnings about css nesting

* fix(/account): move server actions to proper files

* fix: missing name for StateForm

* fix(/map/[invaderName]): move server actions to proper files

* chore: allow api access from browser in local

* feat: upgrade auth.js drizzle @vercel/postgres + fix local db timeout + migration for users

* [FEAT] Custom Theme Panel + Themed Map styles (#74)

* feat(/map): [WIP] change map colors with theme ones

* fix: wrong bg on map IconButtons

* feat: mix grays and accent colors for dynamic map styles

* feat(map): bigger gap between gray/accent colors

* feat: added custom ThemePanel

* fix: tricky fix for map untouchable after dialog close

* fix: revert gap on RootDrawer items

* fix: tweak dirty fix timeout

* [FEATURE] map styles from current theme + custom theme panel (#75)

* feat(/map): [WIP] change map colors with theme ones

* fix: wrong bg on map IconButtons

* feat: mix grays and accent colors for dynamic map styles

* feat(map): bigger gap between gray/accent colors

* feat: added custom ThemePanel

* fix: tricky fix for map untouchable after dialog close

* fix: revert gap on RootDrawer items

* fix: tweak dirty fix timeout

* [CHORE] use our radix-themes fork + Nextjs 15 upgrade (#76)

* fix: use radix themes fork + upgrade next/react/vaul

* chore: migrate import to use radix theme fork

* chore: keep GET route handlers cached

* fix: keep our app requests cached by defautlt

* fix: keep client cache as it was in next14

* fix: migrate to @vercel/functions for ip geolocation

* fix: wrong naming for route handlers config option

* fix: remove deprecated Geo type

* fix: use drizzle directly for sitemap generation + handle errors

* fix: switch trustHost to true, don't really know why

* feat: enable react-compiler + eslint plugin

* fix: map types by avoiding using google global

* fix: dark grays are no longer the light ones

* fix: edge function timeout test

* Revert "fix: edge function timeout test"

This reverts commit 30ee7a6.

* feat(notification): init novu

* feat: add novu react basic implementation

* fix: URL.canParse polyfill for nextjs dev overlay

- the error only happen on safari <17 (iOS, macOS)
- it is triggered by vercel/next.js@a5e2a02#diff-daba055a3db9875013f17e3c82552acb4a81cf3cabd60ca168a7263285afaf3dR18-R21
- don't know why nextjs default browserslist doesn't make the official polyfill load

* feat: init notification center

* fix: notification refresh by updating novu/react

* feat: added basic notification center that works

* feat: use real props for novu client provider

* feat(notification): identify user to novu on login

* feat(notification): added contribution review workflow

* fix(edge): @novu/framework using crypto...

* fix: weird random error about $$typeof

it seems to be this one: vercel/next.js#69545
as suggested [here](vercel/next.js#69545 (comment))
a possible workaround could be to turn svgr components into client components

* fix: another unknown bug about file upload

could be JPG => AVIF conversion that fails or take too long
  • Loading branch information
v1s10n-4 authored Sep 21, 2024
1 parent 1b47b48 commit 25d3056
Show file tree
Hide file tree
Showing 96 changed files with 3,208 additions and 449 deletions.
6 changes: 5 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
{
"extends": "next/core-web-vitals"
"extends": "next/core-web-vitals",
"plugins": ["eslint-plugin-react-compiler"],
"rules": {
"react-compiler/react-compiler": "error"
}
}
27 changes: 27 additions & 0 deletions app/ClientProviders.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"use client";

import { NovuProvider } from "@novu/react";
import React, { ComponentProps, FC, PropsWithChildren } from "react";
import NotificationFilterStatusProvider from "@/app/NotificationFilterStatus";

const ClientProviders: FC<
PropsWithChildren<
Pick<
ComponentProps<typeof NovuProvider>,
"applicationIdentifier" | "subscriberId"
>
>
> = ({ applicationIdentifier, subscriberId, children }) => {
return (
<NovuProvider
applicationIdentifier={applicationIdentifier}
subscriberId={subscriberId}
>
<NotificationFilterStatusProvider>
{children}
</NotificationFilterStatusProvider>
</NovuProvider>
);
};

export default ClientProviders;
9 changes: 6 additions & 3 deletions app/CustomThemeForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,17 @@ import {
Theme,
Tooltip,
useThemeContext,
} from "@radix-ui/themes";
} from "@v1s10n_4/radix-ui-themes";
import React, { FC } from "react";
import { radiusPropDef, themePropDefs } from "@radix-ui/themes/dist/esm/props";
import {
radiusPropDef,
themePropDefs,
} from "@v1s10n_4/radix-ui-themes/dist/esm/props";
import InvertIcon from "pixelarticons/svg/invert.svg";
import ChessIcon from "pixelarticons/svg/chess.svg";
import MoonIcon from "pixelarticons/svg/moon.svg";
import SunIcon from "pixelarticons/svg/sun.svg";
import { getMatchingGrayColor } from "@radix-ui/themes/dist/esm/helpers";
import { getMatchingGrayColor } from "@v1s10n_4/radix-ui-themes/dist/esm/helpers";

export const CustomThemeForm: FC<ThemeContext> = ({
appearance,
Expand Down
2 changes: 1 addition & 1 deletion app/CustomThemePopover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
IconButtonProps,
Popover,
useThemeContext,
} from "@radix-ui/themes";
} from "@v1s10n_4/radix-ui-themes";
import Fill from "pixelarticons/svg/fill.svg";
import FillHalf from "pixelarticons/svg/fill-half.svg";
import CloseIcon from "pixelarticons/svg/close.svg";
Expand Down
102 changes: 102 additions & 0 deletions app/NotificationCenter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
"use client";
import {
Badge,
Button,
Card,
IconButton,
Inset,
ScrollArea,
Separator,
VisuallyHidden,
} from "@v1s10n_4/radix-ui-themes";
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@/components/Drawer";
import BellIcon from "@/app/bell.svg";
import BellRingIcon from "@/app/bell-ring.svg";
import React, { ComponentProps, FC } from "react";
import { useCounts } from "@novu/react";
import NotificationList from "@/app/NotificationList";

const NotificationTrigger: FC<ComponentProps<typeof IconButton>> = (props) => {
const { counts, isFetching, isLoading } = useCounts({
filters: [{ read: false }],
});
return (
<IconButton
size="3"
variant="ghost"
color="gray"
mr="2"
className="relative"
{...props}
>
<div>
{counts && counts[0].count > 0 && (isLoading || isFetching) ? (
<BellRingIcon className="h-8 w-8" />
) : (
<BellIcon className="h-8 w-8" />
)}

{counts && counts[0].count > 0 && (
<Badge color="red" className="absolute right-0 top-0">
{counts[0].count}
</Badge>
)}
<VisuallyHidden>
Notification Center ({counts && counts[0].count} unread)
</VisuallyHidden>
</div>
</IconButton>
);
};

const NotificationCenter = () => {
return (
<Drawer direction="top" modal={false}>
<DrawerTrigger asChild>
<NotificationTrigger />
</DrawerTrigger>
<DrawerContent
className="fixed right-0 top-2 max-h-[98dvh] focus-visible:outline-0 sm:right-2 sm:top-24"
asChild
>
<Card className="mx-2 flex max-w-screen-xs flex-col rounded-t-none pb-[env(safe-area-inset-top)] [box-shadow:--shadow-5] before:rounded-b-none after:!inset-[--base-card-border-width] after:rounded-t-none">
<DrawerHeader pt="2">
<DrawerTitle align="center" size="1" mb="0">
Notifications
</DrawerTitle>
<VisuallyHidden>
<DrawerDescription></DrawerDescription>
</VisuallyHidden>
</DrawerHeader>
<Inset side="x">
<Separator size="4" mb="3" />
</Inset>
<ScrollArea>
<NotificationList />
</ScrollArea>
<Inset side="x">
<Separator size="4" mt="3" />
</Inset>
<DrawerFooter px="0" py="3">
<DrawerClose asChild>
<Button variant="surface" color="gray">
Close
</Button>
</DrawerClose>
</DrawerFooter>
</Card>
</DrawerContent>
</Drawer>
);
};

export default NotificationCenter;
38 changes: 38 additions & 0 deletions app/NotificationFilterStatus.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"use client";
import React, { ReactNode, useState } from "react";

type StatusContextProps = {
status: "all" | "unread" | "archived";
setStatus: (status: "all" | "unread" | "archived") => void;
};

const StatusContext = React.createContext<StatusContextProps | undefined>(
undefined
);

export const useNotificationFilterStatus = () => {
const context = React.useContext(StatusContext);
if (context === undefined) {
throw new Error(
"useNotificationFilterStatus must be used within a StatusProvider"
);
}

return context;
};

export const NotificationFilterStatusProvider = ({
children,
}: {
children: ReactNode;
}) => {
const [status, setStatus] = useState<StatusContextProps["status"]>("all");

return (
<StatusContext.Provider value={{ status, setStatus }}>
{children}
</StatusContext.Provider>
);
};

export default NotificationFilterStatusProvider;
57 changes: 57 additions & 0 deletions app/NotificationItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"use client";
import { Avatar, Box, Card, IconButton, Text } from "@v1s10n_4/radix-ui-themes";
import type { Notification } from "@novu/react";
import MailDeleteIcon from "pixelarticons/svg/mail-delete.svg";
import MailCheckIcon from "pixelarticons/svg/mail-check.svg";
import React, { FC } from "react";
import { timeAgo } from "@/lib/utils";

const NotificationItem: FC<{ notification: Notification }> = ({
notification,
}) => {
return (
<Card
key={notification.id}
className="flex gap-3"
variant="classic"
style={{
backgroundColor: notification.isRead ? "" : "var(--color-panel-solid)",
}}
>
{notification.avatar && (
<Avatar
size="3"
src={notification.avatar}
fallback={notification.body[0] || "N"}
/>
)}
<Box style={{ flex: 1 }}>
<Text size="2" weight={notification.isRead ? "regular" : "bold"}>
{notification.body}
</Text>
<Text size="1" color="gray" asChild>
<time> ({timeAgo(notification.createdAt)})</time>
</Text>
</Box>
{notification.isRead ? (
<IconButton
size="1"
variant="outline"
onClick={() => notification.unread()}
>
<MailDeleteIcon />
</IconButton>
) : (
<IconButton
size="1"
variant="outline"
onClick={() => notification.read()}
>
<MailCheckIcon />
</IconButton>
)}
</Card>
);
};

export default NotificationItem;
59 changes: 59 additions & 0 deletions app/NotificationList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"use client";
import React, { useMemo } from "react";
import { useNotifications } from "@novu/react";
import {
Button,
Flex,
ScrollArea,
Skeleton,
Text,
} from "@v1s10n_4/radix-ui-themes";
import { useNotificationFilterStatus } from "@/app/NotificationFilterStatus";
import NotificationItem from "@/app/NotificationItem";

export default function NotificationList() {
const { status } = useNotificationFilterStatus();
const filter = useMemo(() => {
if (status === "unread") {
return { read: false };
} else if (status === "archived") {
return { archived: true };
}

return { archived: false };
}, [status]);
const { notifications, isLoading, isFetching, hasMore, fetchMore, error } =
useNotifications(filter);

const handleLoadMore = async () => {
if (hasMore && !isLoading) {
await fetchMore();
}
};

return (
<ScrollArea style={{ flex: 1 }}>
<Flex direction="column" gap="1">
{isLoading &&
Array(4)
.fill(0)
.map((_, i) => <Skeleton key={i} height="56px" />)}
{notifications?.map((notification) => (
<NotificationItem key={notification.id} notification={notification} />
))}
{!notifications?.length && (
<Text size="1" m="2" align="center">
You’re all caught up! Check back soon for new alerts.
</Text>
)}
{hasMore && (
<Flex justify="center" p="4">
<Button onClick={handleLoadMore} disabled={isLoading}>
{isLoading ? "Loading..." : "Load More"}
</Button>
</Flex>
)}
</Flex>
</ScrollArea>
);
}
28 changes: 28 additions & 0 deletions app/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import React from "react";
import { auth } from "@/auth";
import dynamic from "next/dynamic";
import { Spinner } from "@v1s10n_4/radix-ui-themes";

const ClientProviders = dynamic(() => import("@/app/ClientProviders"), {
ssr: false,
loading: () => <Spinner className="h-8 w-8" />,
});

const NotificationCenter = dynamic(() => import("@/app/NotificationCenter"), {
ssr: false,
loading: () => <Spinner className="h-8 w-8" />,
});

const Notifications = async () => {
const session = await auth();
return (
<ClientProviders
applicationIdentifier={process.env.NEXT_PUBLIC_NOVU_APP_ID!}
subscriberId={session?.user?.id || "publicSubscriberId"}
>
<NotificationCenter />
</ClientProviders>
);
};

export default Notifications;
2 changes: 1 addition & 1 deletion app/RootDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import {
RadioCards,
Text,
useThemeContext,
} from "@radix-ui/themes";
} from "@v1s10n_4/radix-ui-themes";
import { usePathname, useRouter } from "next/navigation";
import ChatIcon from "pixelarticons/svg/chat.svg";
import ListIcon from "pixelarticons/svg/list.svg";
Expand Down
2 changes: 1 addition & 1 deletion app/RootNav.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import { Card } from "@/components/Card";
import Icon from "@/components/Icon/Icon";
import { Box, Flex, RadioCards, Tooltip } from "@radix-ui/themes";
import { Box, Flex, RadioCards, Tooltip } from "@v1s10n_4/radix-ui-themes";
import { usePathname, useRouter } from "next/navigation";
import ChatIcon from "pixelarticons/svg/chat.svg";
import ListIcon from "pixelarticons/svg/list.svg";
Expand Down
2 changes: 1 addition & 1 deletion app/account/CardForm.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import { CardContent, CardFooter } from "@/components/Card";
import SubmitButton from "@/components/SubmitButton";
import { Separator, Text } from "@radix-ui/themes";
import { Separator, Text } from "@v1s10n_4/radix-ui-themes";
import React, { FC, PropsWithChildren, useActionState } from "react";
import { SafeParseSuccess } from "zod";
import { typeToFlattenedError } from "zod/lib/ZodError";
Expand Down
2 changes: 1 addition & 1 deletion app/account/ProfileHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { signOutAction } from "@/app/actions";
import { Card } from "@/components/Card";
import { HitPlaceholder } from "@/components/Placeholder";
import { User } from "@/db";
import { Avatar, Button, Flex, Heading, Text } from "@radix-ui/themes";
import { Avatar, Button, Flex, Heading, Text } from "@v1s10n_4/radix-ui-themes";
import Image from "next/image";
import LogOutIcon from "pixelarticons/svg/logout.svg";
import React, { FC } from "react";
Expand Down
Loading

0 comments on commit 25d3056

Please sign in to comment.