From f48cd212b9a1f35ceb42530efe27f1ffde864d15 Mon Sep 17 00:00:00 2001
From: chev
Date: Mon, 15 Jun 2026 03:37:05 -0500
Subject: [PATCH 1/2] chore: ignore design system extracts
---
.gitignore | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.gitignore b/.gitignore
index 7aa1b0a..c1fbb57 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,3 +48,7 @@ next-env.d.ts
# Notion sync cache
.notion-db-id
+
+# Local design-system extracts
+/Polycord Design System/
+/polycord-design-system/
From a57c5d723ae4f77cb1fa1b740a90b9d715d55d06 Mon Sep 17 00:00:00 2001
From: chev
Date: Mon, 15 Jun 2026 03:37:14 -0500
Subject: [PATCH 2/2] feat: add premium-aware notifications
---
src/features/Inbox/Inbox.stories.tsx | 147 +++++++++++++++---
src/features/Inbox/Inbox.tsx | 27 +++-
.../Inbox/NotificationEntry.stories.tsx | 12 +-
src/features/Inbox/NotificationEntry.tsx | 27 +++-
src/features/Navbar/Navbar.stories.tsx | 13 +-
src/features/Navbar/Navbar.tsx | 4 +-
src/locales/en.json | 4 +
src/locales/ja.json | 4 +
src/types/notification.ts | 8 +-
9 files changed, 204 insertions(+), 42 deletions(-)
diff --git a/src/features/Inbox/Inbox.stories.tsx b/src/features/Inbox/Inbox.stories.tsx
index 117f43d..f20ea79 100644
--- a/src/features/Inbox/Inbox.stories.tsx
+++ b/src/features/Inbox/Inbox.stories.tsx
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react';
-import { expect, screen, userEvent, within } from '@storybook/test';
+import { expect, userEvent, within } from '@storybook/test';
import { useTranslations } from 'next-intl';
import React, { useState } from 'react';
import { Button } from '@/components/Button';
@@ -24,25 +24,29 @@ const meta: Meta = {
export default meta;
type Story = StoryObj;
-const NotificationsStory = () => {
+const FreeNotificationsStory = () => {
const t = useTranslations('Inbox');
return (
{
);
};
-export const WithNotifications: Story = {
- render: () => ,
+export const FreeNotifications: Story = {
+ render: () => ,
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const trigger = await canvas.findByRole('button', {
name: 'Notifications',
});
- await expect(canvas.getByText('3')).toBeInTheDocument();
+ await expect(canvas.getByText('2')).toBeInTheDocument();
await userEvent.click(trigger);
+ const portal = within(document.body);
+
await expect(
- await screen.findByRole('heading', { name: 'Notifications' }),
+ await portal.findByRole('heading', { name: 'Notifications' }),
).toBeInTheDocument();
+ await expect(
+ portal.getAllByText('A user copied your username'),
+ ).toHaveLength(2);
+ await expect(
+ portal.getByText('See who it was with Premium'),
+ ).toHaveAttribute('href', '/en/settings#premium');
+ await expect(
+ portal.queryByText('Sophie Laurent viewed your profile'),
+ ).not.toBeInTheDocument();
+ },
+};
+
+const PremiumNotificationsStory = () => {
+ const t = useTranslations('Inbox');
+ return (
+
+ );
+};
+
+export const PremiumNotifications: Story = {
+ render: () => ,
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = await canvas.findByRole('button', {
+ name: 'Notifications',
+ });
+
+ await expect(canvas.getByText('4')).toBeInTheDocument();
+
+ await userEvent.click(trigger);
+
+ const portal = within(document.body);
+
+ await expect(
+ await portal.findByText('Mina Park copied your username'),
+ ).toBeInTheDocument();
+ await expect(
+ portal.getByText('Sophie Laurent viewed your profile'),
+ ).toBeInTheDocument();
+ await expect(
+ portal.getByText('A guest viewed your profile'),
+ ).toBeInTheDocument();
+ await expect(
+ portal.getByText('An anonymous user copied your username'),
+ ).toBeInTheDocument();
+ await expect(
+ portal.queryByText('See who it was with Premium'),
+ ).not.toBeInTheDocument();
},
};
@@ -72,6 +156,25 @@ export const Empty: Story = {
args: {
notifications: [],
},
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = await canvas.findByRole('button', {
+ name: 'Notifications',
+ });
+
+ await userEvent.click(trigger);
+
+ const portal = within(document.body);
+
+ await expect(
+ await portal.findByText('No notifications yet'),
+ ).toBeInTheDocument();
+ await expect(
+ portal.getByText(
+ "You'll see new notifications here when something happens!",
+ ),
+ ).toBeInTheDocument();
+ },
};
const ToastComponent = React.memo(
@@ -108,26 +211,24 @@ const LiveUpdateStory = () => {
const { toasts, addToast, dismissToast } = useToastStack();
const createNotificationAndToast = (
- messageKey: 'anonymousUserCopied' | 'userCopied',
- iconUrl?: string,
+ actorName?: string,
+ actorAvatarUrl?: string,
) => {
- const message =
- messageKey === 'userCopied'
- ? t('userCopied', { user: 'xhev' })
- : t('anonymousUserCopied');
-
const newNotification: Notification = {
id: Date.now().toString(),
- message: message,
+ kind: 'copy',
+ actorName,
timestamp: t('minutesAgo', { count: 0 }),
- iconUrl: iconUrl,
+ actorAvatarUrl,
};
const newToast: Omit = {
title: t('newNotification'),
- description: newNotification.message,
+ description: actorName
+ ? t('userCopied', { user: actorName })
+ : t('anonymousCopyAlert'),
duration: 5000,
- iconUrl: iconUrl,
+ iconUrl: actorAvatarUrl,
};
setNotifications((prev) => [newNotification, ...prev]);
@@ -135,11 +236,11 @@ const LiveUpdateStory = () => {
};
const simulateAnonNotification = () => {
- createNotificationAndToast('anonymousUserCopied');
+ createNotificationAndToast();
};
const simulateUserNotification = () => {
- createNotificationAndToast('userCopied', MOCK_USER_AVATAR_URL);
+ createNotificationAndToast('xhev', MOCK_USER_AVATAR_URL);
};
return (
diff --git a/src/features/Inbox/Inbox.tsx b/src/features/Inbox/Inbox.tsx
index 22ff906..8e7f7ed 100644
--- a/src/features/Inbox/Inbox.tsx
+++ b/src/features/Inbox/Inbox.tsx
@@ -1,5 +1,6 @@
import * as Popover from '@radix-ui/react-popover';
-import { useTranslations } from 'next-intl';
+import Link from 'next/link';
+import { useLocale, useTranslations } from 'next-intl';
import { useEffect, useState } from 'react';
import {
MdOutlineInbox,
@@ -10,18 +11,23 @@ import { Button } from '@/components/Button';
import type { Notifications } from '@/types';
import { NotificationEntry } from './NotificationEntry';
-const initializeNotifications = (initial: Notifications) =>
- initial.map((n) => ({ ...n, read: false, isDeleting: false }));
+const initializeNotifications = (initial: Notifications, premium: boolean) =>
+ initial
+ .filter((notification) => premium || notification.kind === 'copy')
+ .map((n) => ({ ...n, read: false, isDeleting: false }));
export const Inbox = ({
notifications: initialNotifications,
+ premium = false,
}: {
notifications: Notifications;
+ premium?: boolean;
}) => {
const t = useTranslations('Inbox');
+ const locale = useLocale();
const [notifications, setNotifications] = useState(() =>
- initializeNotifications(initialNotifications),
+ initializeNotifications(initialNotifications, premium),
);
const [currentPage, setCurrentPage] = useState(1);
const [isMounted, setIsMounted] = useState(false);
@@ -32,9 +38,9 @@ export const Inbox = ({
}, []);
useEffect(() => {
- setNotifications(initializeNotifications(initialNotifications));
+ setNotifications(initializeNotifications(initialNotifications, premium));
setCurrentPage(1);
- }, [initialNotifications]);
+ }, [initialNotifications, premium]);
const unreadCount = notifications.filter((n) => !n.read).length;
@@ -161,6 +167,7 @@ export const Inbox = ({
currentNotifications.map((notification, index) => (
handleMarkAsRead(notification.id)}
onDelete={() => handleDelete(notification.id)}
@@ -186,6 +193,14 @@ export const Inbox = ({
)}
+ {!premium && currentNotifications.length > 0 && (
+
+ {t('seeWhoWithPremium')}
+
+ )}
{totalPages > 1 && (
diff --git a/src/features/Inbox/NotificationEntry.stories.tsx b/src/features/Inbox/NotificationEntry.stories.tsx
index 167ceff..e7a6609 100644
--- a/src/features/Inbox/NotificationEntry.stories.tsx
+++ b/src/features/Inbox/NotificationEntry.stories.tsx
@@ -36,11 +36,13 @@ const DefaultNotificationStory = () => {
{}}
onDelete={() => {}}
/>
@@ -57,11 +59,13 @@ const ReadNotificationStory = () => {
{}}
onDelete={() => {}}
/>
diff --git a/src/features/Inbox/NotificationEntry.tsx b/src/features/Inbox/NotificationEntry.tsx
index 0ab1d5f..c139106 100644
--- a/src/features/Inbox/NotificationEntry.tsx
+++ b/src/features/Inbox/NotificationEntry.tsx
@@ -10,12 +10,32 @@ import type { Notification } from '@/types';
type NotificationEntryProps = {
notification: Notification & { read: boolean; isDeleting?: boolean };
+ premium?: boolean;
onMarkAsRead: () => void;
onDelete: () => void;
} & HTMLAttributes;
+const getNotificationMessage = (
+ notification: Notification,
+ premium: boolean,
+ t: ReturnType>,
+) => {
+ if (!premium) return t('anonymousCopyAlert');
+
+ if (notification.kind === 'view') {
+ return notification.actorName
+ ? t('userViewed', { user: notification.actorName })
+ : t('guestViewed');
+ }
+
+ return notification.actorName
+ ? t('userCopied', { user: notification.actorName })
+ : t('anonymousUserCopied');
+};
+
export const NotificationEntry: React.FC = ({
notification,
+ premium = false,
onMarkAsRead,
onDelete,
className,
@@ -23,6 +43,9 @@ export const NotificationEntry: React.FC = ({
...props
}) => {
const t = useTranslations('Inbox');
+ const avatarUrl = premium ? notification.actorAvatarUrl : undefined;
+ const message = getNotificationMessage(notification, premium, t);
+
return (
= ({
}`}
/>
-
+
-
{notification.message}
+
{message}
{notification.timestamp}
diff --git a/src/features/Navbar/Navbar.stories.tsx b/src/features/Navbar/Navbar.stories.tsx
index bb364d5..ceaf87f 100644
--- a/src/features/Navbar/Navbar.stories.tsx
+++ b/src/features/Navbar/Navbar.stories.tsx
@@ -37,21 +37,26 @@ export const LoggedIn: Story = {
{...args}
iconUrl={MOCK_USER_AVATAR_URL}
isLoggedIn
+ premium
notifications={[
{
id: '1',
- message: t('anonymousUserCopied'),
+ kind: 'copy',
+ actorName: 'Mina Park',
+ actorAvatarUrl: MOCK_USER_AVATAR_URL,
timestamp: t('minutesAgo', { count: 2 }),
},
{
id: '2',
- message: t('userCopied', { user: 'xhev' }),
+ kind: 'view',
+ actorName: 'Sophie Laurent',
+ actorAvatarUrl: MOCK_USER_AVATAR_URL,
timestamp: t('hoursAgo', { count: 1 }),
- iconUrl: MOCK_USER_AVATAR_URL,
},
{
id: '3',
- message: t('anonymousUserCopied'),
+ kind: 'copy',
+ isGuest: true,
timestamp: t('hoursAgo', { count: 2 }),
},
]}
diff --git a/src/features/Navbar/Navbar.tsx b/src/features/Navbar/Navbar.tsx
index f7b167d..98027c8 100644
--- a/src/features/Navbar/Navbar.tsx
+++ b/src/features/Navbar/Navbar.tsx
@@ -11,6 +11,7 @@ type NavbarProps = {
iconUrl?: string;
isLoggedIn: boolean;
notifications: Notifications;
+ premium?: boolean;
onHomeClick?: () => void;
onLoginClick: () => void;
onProfileClick: () => void;
@@ -25,6 +26,7 @@ export const Navbar: React.FC
= ({
onHomeClick,
onLoginClick,
notifications,
+ premium = false,
onProfileClick,
onBumpProfileClick,
onSettingsClick,
@@ -57,7 +59,7 @@ export const Navbar: React.FC = ({
{isLoggedIn ? (
<>
-
+