mirror of
https://github.com/clucraft/PriceGhost.git
synced 2026-05-10 08:12:45 +02:00
Add refresh controls and notification support
- Add refresh button to product list items with spinning animation - Add editable refresh interval dropdown on product detail page - Add user profile dropdown with settings link in navbar - Create Settings page for Telegram and Discord configuration - Add per-product notification options (price drop threshold, back in stock) - Integrate notifications into scheduler for automatic alerts - Add notification service supporting Telegram Bot API and Discord webhooks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
8c5d20707d
commit
a6928a0c17
13 changed files with 1373 additions and 21 deletions
137
backend/src/services/notifications.ts
Normal file
137
backend/src/services/notifications.ts
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
import axios from 'axios';
|
||||
|
||||
export interface NotificationPayload {
|
||||
productName: string;
|
||||
productUrl: string;
|
||||
type: 'price_drop' | 'back_in_stock';
|
||||
oldPrice?: number;
|
||||
newPrice?: number;
|
||||
currency?: string;
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
function formatMessage(payload: NotificationPayload): string {
|
||||
const currencySymbol = payload.currency === 'EUR' ? '€' : payload.currency === 'GBP' ? '£' : '$';
|
||||
|
||||
if (payload.type === 'price_drop') {
|
||||
const oldPriceStr = payload.oldPrice ? `${currencySymbol}${payload.oldPrice.toFixed(2)}` : 'N/A';
|
||||
const newPriceStr = payload.newPrice ? `${currencySymbol}${payload.newPrice.toFixed(2)}` : 'N/A';
|
||||
const dropAmount = payload.oldPrice && payload.newPrice
|
||||
? `${currencySymbol}${(payload.oldPrice - payload.newPrice).toFixed(2)}`
|
||||
: '';
|
||||
|
||||
return `🔔 Price Drop Alert!\n\n` +
|
||||
`📦 ${payload.productName}\n\n` +
|
||||
`💰 Price dropped from ${oldPriceStr} to ${newPriceStr}` +
|
||||
(dropAmount ? ` (-${dropAmount})` : '') + `\n\n` +
|
||||
`🔗 ${payload.productUrl}`;
|
||||
}
|
||||
|
||||
if (payload.type === 'back_in_stock') {
|
||||
const priceStr = payload.newPrice ? ` at ${currencySymbol}${payload.newPrice.toFixed(2)}` : '';
|
||||
return `🎉 Back in Stock!\n\n` +
|
||||
`📦 ${payload.productName}\n\n` +
|
||||
`✅ This item is now available${priceStr}\n\n` +
|
||||
`🔗 ${payload.productUrl}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
export async function sendTelegramNotification(
|
||||
botToken: string,
|
||||
chatId: string,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const message = formatMessage(payload);
|
||||
const url = `https://api.telegram.org/bot${botToken}/sendMessage`;
|
||||
|
||||
await axios.post(url, {
|
||||
chat_id: chatId,
|
||||
text: message,
|
||||
parse_mode: 'HTML',
|
||||
disable_web_page_preview: false,
|
||||
});
|
||||
|
||||
console.log(`Telegram notification sent to chat ${chatId}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to send Telegram notification:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendDiscordNotification(
|
||||
webhookUrl: string,
|
||||
payload: NotificationPayload
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const currencySymbol = payload.currency === 'EUR' ? '€' : payload.currency === 'GBP' ? '£' : '$';
|
||||
|
||||
let embed;
|
||||
if (payload.type === 'price_drop') {
|
||||
const oldPriceStr = payload.oldPrice ? `${currencySymbol}${payload.oldPrice.toFixed(2)}` : 'N/A';
|
||||
const newPriceStr = payload.newPrice ? `${currencySymbol}${payload.newPrice.toFixed(2)}` : 'N/A';
|
||||
|
||||
embed = {
|
||||
title: '🔔 Price Drop Alert!',
|
||||
description: payload.productName,
|
||||
color: 0x10b981, // Green
|
||||
fields: [
|
||||
{ name: 'Old Price', value: oldPriceStr, inline: true },
|
||||
{ name: 'New Price', value: newPriceStr, inline: true },
|
||||
],
|
||||
url: payload.productUrl,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
} else {
|
||||
const priceStr = payload.newPrice ? `${currencySymbol}${payload.newPrice.toFixed(2)}` : 'Check link';
|
||||
|
||||
embed = {
|
||||
title: '🎉 Back in Stock!',
|
||||
description: payload.productName,
|
||||
color: 0x6366f1, // Indigo
|
||||
fields: [
|
||||
{ name: 'Price', value: priceStr, inline: true },
|
||||
{ name: 'Status', value: '✅ Available', inline: true },
|
||||
],
|
||||
url: payload.productUrl,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
await axios.post(webhookUrl, {
|
||||
embeds: [embed],
|
||||
});
|
||||
|
||||
console.log('Discord notification sent');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to send Discord notification:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendNotifications(
|
||||
settings: {
|
||||
telegram_bot_token: string | null;
|
||||
telegram_chat_id: string | null;
|
||||
discord_webhook_url: string | null;
|
||||
},
|
||||
payload: NotificationPayload
|
||||
): Promise<void> {
|
||||
const promises: Promise<boolean>[] = [];
|
||||
|
||||
if (settings.telegram_bot_token && settings.telegram_chat_id) {
|
||||
promises.push(
|
||||
sendTelegramNotification(settings.telegram_bot_token, settings.telegram_chat_id, payload)
|
||||
);
|
||||
}
|
||||
|
||||
if (settings.discord_webhook_url) {
|
||||
promises.push(sendDiscordNotification(settings.discord_webhook_url, payload));
|
||||
}
|
||||
|
||||
await Promise.allSettled(promises);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue