This commit is contained in:
Face 2025-07-15 18:28:31 +03:00
commit 0f13b74ddf
12 changed files with 64 additions and 10 deletions

View file

@ -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'

View file

@ -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) => {

View file

@ -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}`
); );
} }
}); });

View file

@ -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));

View file

@ -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;

View file

@ -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

View file

@ -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}`
); );
} }
} }

View file

@ -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,
}) })

View file

@ -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({

View file

@ -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}`
); );
})(); })();

View file

@ -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 />

View 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}