B2B-88: add starter kit structure and elements
This commit is contained in:
3
packages/features/notifications/eslint.config.mjs
Normal file
3
packages/features/notifications/eslint.config.mjs
Normal file
@@ -0,0 +1,3 @@
|
||||
import eslintConfigBase from '@kit/eslint-config/base.js';
|
||||
|
||||
export default eslintConfigBase;
|
||||
17
packages/features/notifications/node_modules/.bin/tsc
generated
vendored
Executable file
17
packages/features/notifications/node_modules/.bin/tsc
generated
vendored
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
||||
esac
|
||||
|
||||
if [ -z "$NODE_PATH" ]; then
|
||||
export NODE_PATH="/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/typescript@5.8.3/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/node_modules"
|
||||
else
|
||||
export NODE_PATH="/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/typescript@5.8.3/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/node_modules:$NODE_PATH"
|
||||
fi
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../../../../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/tsc" "$@"
|
||||
else
|
||||
exec node "$basedir/../../../../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/tsc" "$@"
|
||||
fi
|
||||
17
packages/features/notifications/node_modules/.bin/tsserver
generated
vendored
Executable file
17
packages/features/notifications/node_modules/.bin/tsserver
generated
vendored
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
|
||||
|
||||
case `uname` in
|
||||
*CYGWIN*) basedir=`cygpath -w "$basedir"`;;
|
||||
esac
|
||||
|
||||
if [ -z "$NODE_PATH" ]; then
|
||||
export NODE_PATH="/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/typescript@5.8.3/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/node_modules"
|
||||
else
|
||||
export NODE_PATH="/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/typescript@5.8.3/node_modules:/Users/devmcee/dev/mountbirch/MRB2B/node_modules/.pnpm/node_modules:$NODE_PATH"
|
||||
fi
|
||||
if [ -x "$basedir/node" ]; then
|
||||
exec "$basedir/node" "$basedir/../../../../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/tsserver" "$@"
|
||||
else
|
||||
exec node "$basedir/../../../../../node_modules/.pnpm/typescript@5.8.3/node_modules/typescript/bin/tsserver" "$@"
|
||||
fi
|
||||
1
packages/features/notifications/node_modules/@kit/eslint-config
generated
vendored
Symbolic link
1
packages/features/notifications/node_modules/@kit/eslint-config
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../tooling/eslint
|
||||
1
packages/features/notifications/node_modules/@kit/prettier-config
generated
vendored
Symbolic link
1
packages/features/notifications/node_modules/@kit/prettier-config
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../tooling/prettier
|
||||
1
packages/features/notifications/node_modules/@kit/supabase
generated
vendored
Symbolic link
1
packages/features/notifications/node_modules/@kit/supabase
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../supabase
|
||||
1
packages/features/notifications/node_modules/@kit/tsconfig
generated
vendored
Symbolic link
1
packages/features/notifications/node_modules/@kit/tsconfig
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../tooling/typescript
|
||||
1
packages/features/notifications/node_modules/@kit/ui
generated
vendored
Symbolic link
1
packages/features/notifications/node_modules/@kit/ui
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../ui
|
||||
1
packages/features/notifications/node_modules/@supabase/supabase-js
generated
vendored
Symbolic link
1
packages/features/notifications/node_modules/@supabase/supabase-js
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../node_modules/.pnpm/@supabase+supabase-js@2.49.4/node_modules/@supabase/supabase-js
|
||||
1
packages/features/notifications/node_modules/@tanstack/react-query
generated
vendored
Symbolic link
1
packages/features/notifications/node_modules/@tanstack/react-query
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../node_modules/.pnpm/@tanstack+react-query@5.76.1_react@19.1.0/node_modules/@tanstack/react-query
|
||||
1
packages/features/notifications/node_modules/@types/react
generated
vendored
Symbolic link
1
packages/features/notifications/node_modules/@types/react
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../../node_modules/.pnpm/@types+react@19.1.4/node_modules/@types/react
|
||||
1
packages/features/notifications/node_modules/lucide-react
generated
vendored
Symbolic link
1
packages/features/notifications/node_modules/lucide-react
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../node_modules/.pnpm/lucide-react@0.510.0_react@19.1.0/node_modules/lucide-react
|
||||
1
packages/features/notifications/node_modules/react
generated
vendored
Symbolic link
1
packages/features/notifications/node_modules/react
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../node_modules/.pnpm/react@19.1.0/node_modules/react
|
||||
1
packages/features/notifications/node_modules/react-dom
generated
vendored
Symbolic link
1
packages/features/notifications/node_modules/react-dom
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../node_modules/.pnpm/react-dom@19.1.0_react@19.1.0/node_modules/react-dom
|
||||
1
packages/features/notifications/node_modules/react-i18next
generated
vendored
Symbolic link
1
packages/features/notifications/node_modules/react-i18next
generated
vendored
Symbolic link
@@ -0,0 +1 @@
|
||||
../../../../node_modules/.pnpm/react-i18next@15.5.2_i18next@25.1.3_typescript@5.8.3__react-dom@19.1.0_react@19.1.0__react@19.1.0_typescript@5.8.3/node_modules/react-i18next
|
||||
38
packages/features/notifications/package.json
Normal file
38
packages/features/notifications/package.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"name": "@kit/notifications",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"scripts": {
|
||||
"clean": "git clean -xdf .turbo node_modules",
|
||||
"format": "prettier --check \"**/*.{ts,tsx}\"",
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"exports": {
|
||||
"./api": "./src/server/api.ts",
|
||||
"./components": "./src/components/index.ts",
|
||||
"./hooks": "./src/hooks/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@kit/eslint-config": "workspace:*",
|
||||
"@kit/prettier-config": "workspace:*",
|
||||
"@kit/supabase": "workspace:*",
|
||||
"@kit/tsconfig": "workspace:*",
|
||||
"@kit/ui": "workspace:*",
|
||||
"@supabase/supabase-js": "2.49.4",
|
||||
"@tanstack/react-query": "5.76.1",
|
||||
"@types/react": "19.1.4",
|
||||
"lucide-react": "^0.510.0",
|
||||
"react": "19.1.0",
|
||||
"react-dom": "19.1.0",
|
||||
"react-i18next": "^15.5.1"
|
||||
},
|
||||
"prettier": "@kit/prettier-config",
|
||||
"typesVersions": {
|
||||
"*": {
|
||||
"*": [
|
||||
"src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
1
packages/features/notifications/src/components/index.ts
Normal file
1
packages/features/notifications/src/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './notifications-popover';
|
||||
@@ -0,0 +1,249 @@
|
||||
'use client';
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { Bell, CircleAlert, Info, TriangleAlert, XIcon } from 'lucide-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
import { Button } from '@kit/ui/button';
|
||||
import { If } from '@kit/ui/if';
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@kit/ui/popover';
|
||||
import { Separator } from '@kit/ui/separator';
|
||||
import { cn } from '@kit/ui/utils';
|
||||
|
||||
import { useDismissNotification, useFetchNotifications } from '../hooks';
|
||||
|
||||
type Notification = Database['public']['Tables']['notifications']['Row'];
|
||||
|
||||
type PartialNotification = Pick<
|
||||
Notification,
|
||||
'id' | 'body' | 'dismissed' | 'type' | 'created_at' | 'link'
|
||||
>;
|
||||
|
||||
export function NotificationsPopover(params: {
|
||||
realtime: boolean;
|
||||
accountIds: string[];
|
||||
onClick?: (notification: PartialNotification) => void;
|
||||
}) {
|
||||
const { i18n, t } = useTranslation();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [notifications, setNotifications] = useState<PartialNotification[]>([]);
|
||||
|
||||
const onNotifications = useCallback(
|
||||
(notifications: PartialNotification[]) => {
|
||||
setNotifications((existing) => {
|
||||
const unique = new Set(existing.map((notification) => notification.id));
|
||||
|
||||
const notificationsFiltered = notifications.filter(
|
||||
(notification) => !unique.has(notification.id),
|
||||
);
|
||||
|
||||
return [...notificationsFiltered, ...existing];
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const dismissNotification = useDismissNotification();
|
||||
|
||||
useFetchNotifications({
|
||||
onNotifications,
|
||||
accountIds: params.accountIds,
|
||||
realtime: params.realtime,
|
||||
});
|
||||
|
||||
const timeAgo = (createdAt: string) => {
|
||||
const date = new Date(createdAt);
|
||||
|
||||
let time: number;
|
||||
|
||||
const daysAgo = Math.floor(
|
||||
(new Date().getTime() - date.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
|
||||
const formatter = new Intl.RelativeTimeFormat(i18n.language, {
|
||||
numeric: 'auto',
|
||||
});
|
||||
|
||||
if (daysAgo < 1) {
|
||||
time = Math.floor((new Date().getTime() - date.getTime()) / (1000 * 60));
|
||||
|
||||
if (time < 5) {
|
||||
return t('common:justNow');
|
||||
}
|
||||
|
||||
if (time < 60) {
|
||||
return formatter.format(-time, 'minute');
|
||||
}
|
||||
|
||||
const hours = Math.floor(time / 60);
|
||||
|
||||
return formatter.format(-hours, 'hour');
|
||||
}
|
||||
|
||||
const unit = (() => {
|
||||
const minutesAgo = Math.floor(
|
||||
(new Date().getTime() - date.getTime()) / (1000 * 60),
|
||||
);
|
||||
|
||||
if (minutesAgo <= 60) {
|
||||
return 'minute';
|
||||
}
|
||||
|
||||
if (daysAgo <= 1) {
|
||||
return 'hour';
|
||||
}
|
||||
|
||||
if (daysAgo <= 30) {
|
||||
return 'day';
|
||||
}
|
||||
|
||||
if (daysAgo <= 365) {
|
||||
return 'month';
|
||||
}
|
||||
|
||||
return 'year';
|
||||
})();
|
||||
|
||||
const text = formatter.format(-daysAgo, unit);
|
||||
|
||||
return text.slice(0, 1).toUpperCase() + text.slice(1);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
setNotifications([]);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Popover modal open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button className={'relative h-9 w-9'} variant={'ghost'}>
|
||||
<Bell className={'min-h-4 min-w-4'} />
|
||||
|
||||
<span
|
||||
className={cn(
|
||||
`fade-in animate-in zoom-in absolute right-1 top-1 mt-0 flex h-3.5 w-3.5 items-center justify-center rounded-full bg-red-500 text-[0.65rem] text-white`,
|
||||
{
|
||||
hidden: !notifications.length,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{notifications.length}
|
||||
</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent
|
||||
className={'flex w-full max-w-96 flex-col p-0 lg:min-w-64'}
|
||||
align={'start'}
|
||||
collisionPadding={20}
|
||||
sideOffset={10}
|
||||
>
|
||||
<div className={'flex items-center px-3 py-2 text-sm font-semibold'}>
|
||||
{t('common:notifications')}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<If condition={!notifications.length}>
|
||||
<div className={'px-3 py-2 text-sm'}>
|
||||
{t('common:noNotifications')}
|
||||
</div>
|
||||
</If>
|
||||
|
||||
<div
|
||||
className={
|
||||
'flex max-h-[60vh] flex-col divide-y divide-gray-100 overflow-y-auto dark:divide-gray-800'
|
||||
}
|
||||
>
|
||||
{notifications.map((notification) => {
|
||||
const maxChars = 100;
|
||||
|
||||
let body = t(notification.body, {
|
||||
defaultValue: notification.body,
|
||||
});
|
||||
|
||||
if (body.length > maxChars) {
|
||||
body = body.substring(0, maxChars) + '...';
|
||||
}
|
||||
|
||||
const Icon = () => {
|
||||
switch (notification.type) {
|
||||
case 'warning':
|
||||
return <TriangleAlert className={'h-4 text-yellow-500'} />;
|
||||
case 'error':
|
||||
return <CircleAlert className={'text-destructive h-4'} />;
|
||||
default:
|
||||
return <Info className={'h-4 text-blue-500'} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
key={notification.id.toString()}
|
||||
className={cn(
|
||||
'min-h-18 flex flex-col items-start justify-center gap-y-1 px-3 py-2',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (params.onClick) {
|
||||
params.onClick(notification);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className={'flex w-full items-start justify-between'}>
|
||||
<div
|
||||
className={'flex items-start justify-start gap-x-3 py-2'}
|
||||
>
|
||||
<div className={'py-0.5'}>
|
||||
<Icon />
|
||||
</div>
|
||||
|
||||
<div className={'flex flex-col space-y-1'}>
|
||||
<div className={'text-sm'}>
|
||||
<If condition={notification.link} fallback={body}>
|
||||
{(link) => (
|
||||
<a href={link} className={'hover:underline'}>
|
||||
{body}
|
||||
</a>
|
||||
)}
|
||||
</If>
|
||||
</div>
|
||||
|
||||
<span className={'text-muted-foreground text-xs'}>
|
||||
{timeAgo(notification.created_at)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={'py-2'}>
|
||||
<Button
|
||||
className={'max-h-6 max-w-6'}
|
||||
size={'icon'}
|
||||
variant={'ghost'}
|
||||
onClick={() => {
|
||||
setNotifications((existing) => {
|
||||
return existing.filter(
|
||||
(existingNotification) =>
|
||||
existingNotification.id !== notification.id,
|
||||
);
|
||||
});
|
||||
|
||||
return dismissNotification(notification.id);
|
||||
}}
|
||||
>
|
||||
<XIcon className={'h-3'} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
2
packages/features/notifications/src/hooks/index.ts
Normal file
2
packages/features/notifications/src/hooks/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './use-fetch-notifications';
|
||||
export * from './use-dismiss-notification';
|
||||
@@ -0,0 +1,21 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
|
||||
export function useDismissNotification() {
|
||||
const client = useSupabase();
|
||||
|
||||
return useCallback(
|
||||
async (notification: number) => {
|
||||
const { error } = await client
|
||||
.from('notifications')
|
||||
.update({ dismissed: true })
|
||||
.eq('id', notification);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[client],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
|
||||
import { useNotificationsStream } from './use-notifications-stream';
|
||||
|
||||
type Notification = {
|
||||
id: number;
|
||||
body: string;
|
||||
dismissed: boolean;
|
||||
type: 'info' | 'warning' | 'error';
|
||||
created_at: string;
|
||||
link: string | null;
|
||||
};
|
||||
|
||||
export function useFetchNotifications({
|
||||
onNotifications,
|
||||
accountIds,
|
||||
realtime,
|
||||
}: {
|
||||
onNotifications: (notifications: Notification[]) => unknown;
|
||||
accountIds: string[];
|
||||
realtime: boolean;
|
||||
}) {
|
||||
const { data: initialNotifications } = useFetchInitialNotifications({
|
||||
accountIds,
|
||||
});
|
||||
|
||||
useNotificationsStream({
|
||||
onNotifications,
|
||||
accountIds,
|
||||
enabled: realtime,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (initialNotifications) {
|
||||
onNotifications(initialNotifications);
|
||||
}
|
||||
}, [initialNotifications, onNotifications]);
|
||||
}
|
||||
|
||||
function useFetchInitialNotifications(props: { accountIds: string[] }) {
|
||||
const client = useSupabase();
|
||||
const now = new Date().toISOString();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ['notifications', ...props.accountIds],
|
||||
queryFn: async () => {
|
||||
const { data } = await client
|
||||
.from('notifications')
|
||||
.select(
|
||||
`id,
|
||||
body,
|
||||
dismissed,
|
||||
type,
|
||||
created_at,
|
||||
link
|
||||
`,
|
||||
)
|
||||
.in('account_id', props.accountIds)
|
||||
.eq('dismissed', false)
|
||||
.gt('expires_at', now)
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(10);
|
||||
|
||||
return data;
|
||||
},
|
||||
refetchOnMount: false,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||
|
||||
type Notification = {
|
||||
id: number;
|
||||
body: string;
|
||||
dismissed: boolean;
|
||||
type: 'info' | 'warning' | 'error';
|
||||
created_at: string;
|
||||
link: string | null;
|
||||
};
|
||||
|
||||
export function useNotificationsStream(params: {
|
||||
onNotifications: (notifications: Notification[]) => void;
|
||||
accountIds: string[];
|
||||
enabled: boolean;
|
||||
}) {
|
||||
const client = useSupabase();
|
||||
|
||||
const { data: subscription } = useQuery({
|
||||
enabled: params.enabled,
|
||||
queryKey: ['realtime-notifications', ...params.accountIds],
|
||||
queryFn: () => {
|
||||
const channel = client.channel('notifications-channel');
|
||||
|
||||
return channel
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: 'public',
|
||||
filter: `account_id=in.(${params.accountIds.join(', ')})`,
|
||||
table: 'notifications',
|
||||
},
|
||||
(payload) => {
|
||||
params.onNotifications([payload.new as Notification]);
|
||||
},
|
||||
)
|
||||
.subscribe();
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
void subscription?.unsubscribe();
|
||||
};
|
||||
}, [subscription]);
|
||||
}
|
||||
53
packages/features/notifications/src/server/api.ts
Normal file
53
packages/features/notifications/src/server/api.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @file API for notifications
|
||||
*
|
||||
* Usage
|
||||
*
|
||||
* ```typescript
|
||||
* import { createNotificationsApi } from '@kit/notifications/api';
|
||||
*
|
||||
* const api = createNotificationsApi(client);
|
||||
*
|
||||
* await api.createNotification({
|
||||
* body: 'Hello, world!',
|
||||
* account_id: '123',
|
||||
* type: 'info',
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
import { createNotificationsService } from './notifications.service';
|
||||
|
||||
type Notification = Database['public']['Tables']['notifications'];
|
||||
|
||||
/**
|
||||
* @name createNotificationsApi
|
||||
* @param client
|
||||
*/
|
||||
export function createNotificationsApi(client: SupabaseClient<Database>) {
|
||||
return new NotificationsApi(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @name NotificationsApi
|
||||
*/
|
||||
class NotificationsApi {
|
||||
private readonly service: ReturnType<typeof createNotificationsService>;
|
||||
|
||||
constructor(private readonly client: SupabaseClient<Database>) {
|
||||
this.service = createNotificationsService(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* @name createNotification
|
||||
* @description Create a new notification in the database
|
||||
* @param params
|
||||
*/
|
||||
createNotification(params: Notification['Insert']) {
|
||||
return this.service.createNotification(params);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import 'server-only';
|
||||
|
||||
import { SupabaseClient } from '@supabase/supabase-js';
|
||||
|
||||
import { Database } from '@kit/supabase/database';
|
||||
|
||||
type Notification = Database['public']['Tables']['notifications'];
|
||||
|
||||
export function createNotificationsService(client: SupabaseClient<Database>) {
|
||||
return new NotificationsService(client);
|
||||
}
|
||||
|
||||
class NotificationsService {
|
||||
constructor(private readonly client: SupabaseClient<Database>) {}
|
||||
|
||||
async createNotification(params: Notification['Insert']) {
|
||||
const { error } = await this.client.from('notifications').insert(params);
|
||||
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
8
packages/features/notifications/tsconfig.json
Normal file
8
packages/features/notifications/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "@kit/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
|
||||
},
|
||||
"include": ["*.ts", "*.tsx", "src"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user