Merge pull request #130 from SimplyBrandon/linked-notifications
feat: notifications link to relevant pages when available.
This commit is contained in:
commit
2501648d31
12 changed files with 64 additions and 10 deletions
|
|
@ -205,6 +205,7 @@
|
||||||
<div class="flex items-center justify-center gap-2">
|
<div class="flex items-center justify-center gap-2">
|
||||||
{#each tips as tip, index}
|
{#each tips as tip, index}
|
||||||
<button
|
<button
|
||||||
|
aria-label={`Go to page ${index + 1}`}
|
||||||
onclick={() => goToPage(index)}
|
onclick={() => goToPage(index)}
|
||||||
class="h-2 w-2 rounded-full transition-colors {index === currentPage
|
class="h-2 w-2 rounded-full transition-colors {index === currentPage
|
||||||
? 'bg-primary'
|
? 'bg-primary'
|
||||||
|
|
|
||||||
|
|
@ -265,6 +265,7 @@ export const notifications = pgTable("notification", {
|
||||||
type: notificationTypeEnum("type").notNull(),
|
type: notificationTypeEnum("type").notNull(),
|
||||||
title: varchar("title", { length: 200 }).notNull(),
|
title: varchar("title", { length: 200 }).notNull(),
|
||||||
message: text("message").notNull(),
|
message: text("message").notNull(),
|
||||||
|
link: text("link"),
|
||||||
isRead: boolean("is_read").notNull().default(false),
|
isRead: boolean("is_read").notNull().default(false),
|
||||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||||
}, (table) => {
|
}, (table) => {
|
||||||
|
|
|
||||||
|
|
@ -120,6 +120,7 @@ export async function resolveExpiredQuestions() {
|
||||||
'HOPIUM',
|
'HOPIUM',
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
`/hopium/${question.id}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -219,6 +220,7 @@ export async function resolveExpiredQuestions() {
|
||||||
'HOPIUM',
|
'HOPIUM',
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
`/hopium/${question.id}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -9,12 +9,14 @@ export async function createNotification(
|
||||||
type: NotificationType,
|
type: NotificationType,
|
||||||
title: string,
|
title: string,
|
||||||
message: string,
|
message: string,
|
||||||
|
link?: string,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await db.insert(notifications).values({
|
await db.insert(notifications).values({
|
||||||
userId: parseInt(userId),
|
userId: parseInt(userId),
|
||||||
type,
|
type,
|
||||||
title,
|
title,
|
||||||
message
|
message,
|
||||||
|
link
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -27,6 +29,7 @@ export async function createNotification(
|
||||||
notificationType: type,
|
notificationType: type,
|
||||||
title,
|
title,
|
||||||
message,
|
message,
|
||||||
|
link
|
||||||
};
|
};
|
||||||
|
|
||||||
await redis.publish(channel, JSON.stringify(payload));
|
await redis.publish(channel, JSON.stringify(payload));
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ export interface Notification {
|
||||||
type: string;
|
type: string;
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message: string;
|
||||||
|
link?: string;
|
||||||
data: any;
|
data: any;
|
||||||
isRead: boolean;
|
isRead: boolean;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
|
|
|
||||||
|
|
@ -197,6 +197,7 @@ function handleWebSocketMessage(event: MessageEvent): void {
|
||||||
type: message.notificationType,
|
type: message.notificationType,
|
||||||
title: message.title,
|
title: message.title,
|
||||||
message: message.message,
|
message: message.message,
|
||||||
|
link: message.link,
|
||||||
isRead: false,
|
isRead: false,
|
||||||
createdAt: message.timestamp,
|
createdAt: message.timestamp,
|
||||||
data: message.amount ? { amount: message.amount } : null
|
data: message.amount ? { amount: message.amount } : null
|
||||||
|
|
|
||||||
|
|
@ -354,6 +354,7 @@ export async function POST({ params, request }) {
|
||||||
'RUG_PULL',
|
'RUG_PULL',
|
||||||
'Coin rugpulled!',
|
'Coin rugpulled!',
|
||||||
`A coin you owned, ${coinData.name} (*${normalizedSymbol}), crashed ${Math.abs(priceImpact).toFixed(1)}%!`,
|
`A coin you owned, ${coinData.name} (*${normalizedSymbol}), crashed ${Math.abs(priceImpact).toFixed(1)}%!`,
|
||||||
|
`/coin/${normalizedSymbol}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ export const GET: RequestHandler = async ({ url, request }) => {
|
||||||
type: notifications.type,
|
type: notifications.type,
|
||||||
title: notifications.title,
|
title: notifications.title,
|
||||||
message: notifications.message,
|
message: notifications.message,
|
||||||
|
link: notifications.link,
|
||||||
isRead: notifications.isRead,
|
isRead: notifications.isRead,
|
||||||
createdAt: notifications.createdAt,
|
createdAt: notifications.createdAt,
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -94,6 +94,7 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
||||||
type: 'SYSTEM',
|
type: 'SYSTEM',
|
||||||
title: `${prestigeName} Achieved!`,
|
title: `${prestigeName} Achieved!`,
|
||||||
message: `Congratulations! You have successfully reached ${prestigeName}. Your portfolio has been reset, daily reward cooldown has been cleared, and you can now start fresh with your new prestige badge and enhanced daily rewards.`,
|
message: `Congratulations! You have successfully reached ${prestigeName}. Your portfolio has been reset, daily reward cooldown has been cleared, and you can now start fresh with your new prestige badge and enhanced daily rewards.`,
|
||||||
|
link: `/user/${userId}`
|
||||||
});
|
});
|
||||||
|
|
||||||
return json({
|
return json({
|
||||||
|
|
|
||||||
|
|
@ -131,6 +131,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
'TRANSFER',
|
'TRANSFER',
|
||||||
'Money received!',
|
'Money received!',
|
||||||
`You received ${formatValue(amount)} from @${senderData.username}`,
|
`You received ${formatValue(amount)} from @${senderData.username}`,
|
||||||
|
`/user/${senderData.id}`
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
@ -264,6 +265,7 @@ export const POST: RequestHandler = async ({ request }) => {
|
||||||
'TRANSFER',
|
'TRANSFER',
|
||||||
'Coins received!',
|
'Coins received!',
|
||||||
`You received ${amount.toFixed(6)} *${coinData.symbol} from @${senderData.username}`,
|
`You received ${amount.toFixed(6)} *${coinData.symbol} from @${senderData.username}`,
|
||||||
|
`/coin/${normalizedSymbol}`
|
||||||
);
|
);
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@
|
||||||
import { formatTimeAgo, formatValue } from '$lib/utils';
|
import { formatTimeAgo, formatValue } from '$lib/utils';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
import NotificationItem from './NotificationItem.svelte';
|
||||||
|
|
||||||
let loading = $state(true);
|
let loading = $state(true);
|
||||||
let newNotificationIds = $state<number[]>([]);
|
let newNotificationIds = $state<number[]>([]);
|
||||||
|
|
@ -132,13 +133,7 @@
|
||||||
{#each $NOTIFICATIONS as notification, index (notification.id)}
|
{#each $NOTIFICATIONS as notification, index (notification.id)}
|
||||||
{@const IconComponent = getNotificationIcon(notification.type)}
|
{@const IconComponent = getNotificationIcon(notification.type)}
|
||||||
{@const isNewNotification = newNotificationIds.includes(notification.id)}
|
{@const isNewNotification = newNotificationIds.includes(notification.id)}
|
||||||
<button
|
<NotificationItem {notification} isNew={isNewNotification}>
|
||||||
class={getNotificationColorClasses(
|
|
||||||
notification.type,
|
|
||||||
isNewNotification,
|
|
||||||
notification.isRead
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full {getNotificationIconColorClasses(
|
class="flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full {getNotificationIconColorClasses(
|
||||||
notification.type
|
notification.type
|
||||||
|
|
@ -146,7 +141,6 @@
|
||||||
>
|
>
|
||||||
<IconComponent class="h-4 w-4" />
|
<IconComponent class="h-4 w-4" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="min-w-0 flex-1">
|
<div class="min-w-0 flex-1">
|
||||||
<div class="mb-1 flex items-center gap-2">
|
<div class="mb-1 flex items-center gap-2">
|
||||||
<h3 class="truncate text-sm font-medium">{notification.title}</h3>
|
<h3 class="truncate text-sm font-medium">{notification.title}</h3>
|
||||||
|
|
@ -188,7 +182,7 @@
|
||||||
{formatTimeAgo(notification.createdAt)}
|
{formatTimeAgo(notification.createdAt)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</NotificationItem>
|
||||||
|
|
||||||
{#if index < $NOTIFICATIONS.length - 1}
|
{#if index < $NOTIFICATIONS.length - 1}
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
|
||||||
46
website/src/routes/notifications/NotificationItem.svelte
Normal file
46
website/src/routes/notifications/NotificationItem.svelte
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<script lang="ts">
|
||||||
|
interface Notification {
|
||||||
|
type: 'HOPIUM' | 'TRANSFER' | 'RUG_PULL' | 'SYSTEM';
|
||||||
|
link?: string;
|
||||||
|
isRead: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let notification: Notification;
|
||||||
|
export let isNew = false;
|
||||||
|
|
||||||
|
function getNotificationColorClasses(type: string, isNew: boolean, isRead: boolean) {
|
||||||
|
const base =
|
||||||
|
'hover:bg-muted/50 flex w-full items-start gap-4 rounded-md p-3 text-left transition-all duration-200';
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
return `${base} bg-primary/10`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isRead) {
|
||||||
|
const colors = {
|
||||||
|
HOPIUM: 'bg-blue-50/50 dark:bg-blue-950/10',
|
||||||
|
TRANSFER: 'bg-green-50/50 dark:bg-green-950/10',
|
||||||
|
RUG_PULL: 'bg-red-50/50 dark:bg-red-950/10',
|
||||||
|
SYSTEM: 'bg-purple-50/50 dark:bg-purple-950/10'
|
||||||
|
};
|
||||||
|
return `${base} ${colors[type as keyof typeof colors] || 'bg-muted/20'}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if notification.link}
|
||||||
|
<a
|
||||||
|
href={notification.link}
|
||||||
|
class={getNotificationColorClasses(notification.type, isNew, notification.isRead)}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class={getNotificationColorClasses(notification.type, isNew, notification.isRead)}
|
||||||
|
>
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
Reference in a new issue