mirror of
https://github.com/R0m1k3/Priceflow.git
synced 2026-10-24 15:48:11 +02:00
Merge pull request #45 from R0m1k3/antigravity
feat: Introduce a comprehensive settings page for managing notificati…
This commit is contained in:
6 files changed
+138
-39
No files matched your search
+1
-6
@@ -3,12 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<!-- FAVICON: Remplacer les fichiers dans /frontend/public/ -->
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||||
<!-- - favicon.ico (32x32 ou 64x64) -->
|
|
||||||
<!-- - apple-touch-icon.png (180x180) -->
|
|
||||||
<!-- - logo.png (logo principal de l'application) -->
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
||||||
<link rel="alternate icon" type="image/x-icon" href="/favicon.ico" />
|
|
||||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>PriceFlow</title>
|
<title>PriceFlow</title>
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 39 KiB After Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 104 KiB |
@@ -37,7 +37,7 @@ const Layout = ({ children, theme, toggleTheme }) => {
|
|||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
<div className="flex h-14 items-center border-b px-6">
|
<div className="flex h-14 items-center border-b px-6">
|
||||||
<Link to="/" className="flex items-center gap-2 font-semibold">
|
<Link to="/" className="flex items-center gap-2 font-semibold">
|
||||||
<img src="/logo.svg" alt="PriceFlow Logo" className="h-8 w-8 rounded-lg" />
|
<img src="/logo.png" alt="PriceFlow Logo" className="h-8 w-8 rounded-lg" />
|
||||||
<span className="text-xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
|
<span className="text-xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent">
|
||||||
PriceFlow
|
PriceFlow
|
||||||
</span>
|
</span>
|
||||||
@@ -166,7 +166,7 @@ const Layout = ({ children, theme, toggleTheme }) => {
|
|||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
<Link to="/" className="flex items-center gap-2 font-semibold">
|
<Link to="/" className="flex items-center gap-2 font-semibold">
|
||||||
<img src="/logo.svg" alt="PriceFlow Logo" className="h-6 w-6 rounded" />
|
<img src="/logo.png" alt="PriceFlow Logo" className="h-6 w-6 rounded" />
|
||||||
PriceFlow
|
PriceFlow
|
||||||
</Link>
|
</Link>
|
||||||
<div className="ml-auto flex items-center gap-2">
|
<div className="ml-auto flex items-center gap-2">
|
||||||
|
|||||||
+135
-31
@@ -173,17 +173,43 @@ export default function Settings() {
|
|||||||
const handleProfileSubmit = async (e) => {
|
const handleProfileSubmit = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
try {
|
try {
|
||||||
|
let finalAppriseUrl = newProfile.apprise_url;
|
||||||
|
|
||||||
|
// Construct Apprise URL based on provider
|
||||||
|
if (newProfile.provider === 'discord' && newProfile.discord_webhook) {
|
||||||
|
// Parse Discord Webhook: https://discord.com/api/webhooks/{id}/{token} -> discord://{id}/{token}
|
||||||
|
const match = newProfile.discord_webhook.match(/webhooks\/(\d+)\/(.+)/);
|
||||||
|
if (match) {
|
||||||
|
finalAppriseUrl = `discord://${match[1]}/${match[2]}`;
|
||||||
|
} else {
|
||||||
|
toast.error('Invalid Discord Webhook URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else if (newProfile.provider === 'telegram' && newProfile.telegram_token && newProfile.telegram_chat_id) {
|
||||||
|
// Construct Telegram URL: tgram://{token}/{chat_id}
|
||||||
|
finalAppriseUrl = `tgram://${newProfile.telegram_token}/${newProfile.telegram_chat_id}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const profileData = {
|
||||||
|
...newProfile,
|
||||||
|
apprise_url: finalAppriseUrl
|
||||||
|
};
|
||||||
|
|
||||||
if (editingProfileId) {
|
if (editingProfileId) {
|
||||||
await axios.put(`${API_URL}/notification-profiles/${editingProfileId}`, newProfile);
|
await axios.put(`${API_URL}/notification-profiles/${editingProfileId}`, profileData);
|
||||||
toast.success('Profile updated');
|
toast.success('Profile updated');
|
||||||
} else {
|
} else {
|
||||||
await axios.post(`${API_URL}/notification-profiles`, newProfile);
|
await axios.post(`${API_URL}/notification-profiles`, profileData);
|
||||||
toast.success('Profile created');
|
toast.success('Profile created');
|
||||||
}
|
}
|
||||||
|
|
||||||
setNewProfile({
|
setNewProfile({
|
||||||
name: '',
|
name: '',
|
||||||
apprise_url: '',
|
apprise_url: '',
|
||||||
|
discord_webhook: '',
|
||||||
|
telegram_token: '',
|
||||||
|
telegram_chat_id: '',
|
||||||
|
provider: 'custom',
|
||||||
check_interval_minutes: 60,
|
check_interval_minutes: 60,
|
||||||
notify_on_price_drop: true,
|
notify_on_price_drop: true,
|
||||||
notify_on_target_price: true,
|
notify_on_target_price: true,
|
||||||
@@ -198,9 +224,36 @@ export default function Settings() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const editProfile = (profile) => {
|
const editProfile = (profile) => {
|
||||||
|
let provider = 'custom';
|
||||||
|
let discord_webhook = '';
|
||||||
|
let telegram_token = '';
|
||||||
|
let telegram_chat_id = '';
|
||||||
|
|
||||||
|
// Deconstruct Apprise URL
|
||||||
|
if (profile.apprise_url.startsWith('discord://')) {
|
||||||
|
provider = 'discord';
|
||||||
|
// discord://{id}/{token} -> https://discord.com/api/webhooks/{id}/{token}
|
||||||
|
const parts = profile.apprise_url.replace('discord://', '').split('/');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
discord_webhook = `https://discord.com/api/webhooks/${parts[0]}/${parts[1]}`;
|
||||||
|
}
|
||||||
|
} else if (profile.apprise_url.startsWith('tgram://')) {
|
||||||
|
provider = 'telegram';
|
||||||
|
// tgram://{token}/{chat_id}
|
||||||
|
const parts = profile.apprise_url.replace('tgram://', '').split('/');
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
telegram_token = parts[0];
|
||||||
|
telegram_chat_id = parts[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setNewProfile({
|
setNewProfile({
|
||||||
name: profile.name,
|
name: profile.name,
|
||||||
apprise_url: profile.apprise_url,
|
apprise_url: profile.apprise_url,
|
||||||
|
provider,
|
||||||
|
discord_webhook,
|
||||||
|
telegram_token,
|
||||||
|
telegram_chat_id,
|
||||||
check_interval_minutes: profile.check_interval_minutes,
|
check_interval_minutes: profile.check_interval_minutes,
|
||||||
notify_on_price_drop: profile.notify_on_price_drop,
|
notify_on_price_drop: profile.notify_on_price_drop,
|
||||||
notify_on_target_price: profile.notify_on_target_price,
|
notify_on_target_price: profile.notify_on_target_price,
|
||||||
@@ -639,36 +692,87 @@ export default function Settings() {
|
|||||||
<Label>Name</Label>
|
<Label>Name</Label>
|
||||||
<Input required value={newProfile.name} onChange={(e) => setNewProfile({ ...newProfile, name: e.target.value })} placeholder="e.g., Email Alerts" />
|
<Input required value={newProfile.name} onChange={(e) => setNewProfile({ ...newProfile, name: e.target.value })} placeholder="e.g., Email Alerts" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2 md:col-span-2">
|
<div className="space-y-4 md:col-span-2 border p-4 rounded-lg bg-muted/20">
|
||||||
<Label>Apprise URL</Label>
|
<div className="space-y-2">
|
||||||
<Input
|
<Label>Notification Provider</Label>
|
||||||
required
|
<Select
|
||||||
value={newProfile.apprise_url}
|
value={newProfile.provider || 'custom'}
|
||||||
onChange={(e) => setNewProfile({ ...newProfile, apprise_url: e.target.value })}
|
onValueChange={(val) => {
|
||||||
placeholder="mailto://user:pass@gmail.com"
|
setNewProfile(prev => ({
|
||||||
className="font-mono text-sm"
|
...prev,
|
||||||
/>
|
provider: val,
|
||||||
<div className="space-y-1 text-xs text-muted-foreground">
|
// Reset fields when switching provider
|
||||||
<p className="font-medium">Exemples d'URLs Apprise :</p>
|
discord_webhook: '',
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-1 pl-3">
|
telegram_token: '',
|
||||||
<p>• <span className="font-mono">mailto://user:pass@gmail.com</span> - Email</p>
|
telegram_chat_id: '',
|
||||||
<p>• <span className="font-mono">tgram://bottoken/ChatID</span> - Telegram</p>
|
apprise_url: val === 'custom' ? prev.apprise_url : ''
|
||||||
<p>• <span className="font-mono">discord://webhook_id/webhook_token</span> - Discord</p>
|
}));
|
||||||
<p>• <span className="font-mono">slack://tokenA/tokenB/tokenC</span> - Slack</p>
|
}}
|
||||||
<p>• <span className="font-mono">ntfy://topic</span> - Ntfy</p>
|
>
|
||||||
<p>• <span className="font-mono">gotify://hostname/token</span> - Gotify</p>
|
<SelectTrigger>
|
||||||
</div>
|
<SelectValue />
|
||||||
<p className="pt-1">
|
</SelectTrigger>
|
||||||
<a
|
<SelectContent>
|
||||||
href="https://github.com/caronc/apprise/wiki"
|
<SelectItem value="custom">Custom (Apprise URL)</SelectItem>
|
||||||
target="_blank"
|
<SelectItem value="discord">Discord</SelectItem>
|
||||||
rel="noopener noreferrer"
|
<SelectItem value="telegram">Telegram</SelectItem>
|
||||||
className="text-primary hover:underline"
|
</SelectContent>
|
||||||
>
|
</Select>
|
||||||
📚 Documentation complète Apprise
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{(!newProfile.provider || newProfile.provider === 'custom') && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Apprise URL</Label>
|
||||||
|
<Input
|
||||||
|
required
|
||||||
|
value={newProfile.apprise_url}
|
||||||
|
onChange={(e) => setNewProfile({ ...newProfile, apprise_url: e.target.value })}
|
||||||
|
placeholder="mailto://user:pass@gmail.com"
|
||||||
|
className="font-mono text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
See <a href="https://github.com/caronc/apprise/wiki" target="_blank" rel="noopener noreferrer" className="text-primary hover:underline">Apprise documentation</a> for supported services.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{newProfile.provider === 'discord' && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Discord Webhook URL</Label>
|
||||||
|
<Input
|
||||||
|
value={newProfile.discord_webhook}
|
||||||
|
onChange={(e) => setNewProfile({ ...newProfile, discord_webhook: e.target.value })}
|
||||||
|
placeholder="https://discord.com/api/webhooks/..."
|
||||||
|
className="font-mono text-sm"
|
||||||
|
/>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
Paste the full Webhook URL from Discord Server Settings > Integrations > Webhooks.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{newProfile.provider === 'telegram' && (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Bot Token</Label>
|
||||||
|
<Input
|
||||||
|
value={newProfile.telegram_token}
|
||||||
|
onChange={(e) => setNewProfile({ ...newProfile, telegram_token: e.target.value })}
|
||||||
|
placeholder="123456789:ABCdef..."
|
||||||
|
className="font-mono text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label>Chat ID</Label>
|
||||||
|
<Input
|
||||||
|
value={newProfile.telegram_chat_id}
|
||||||
|
onChange={(e) => setNewProfile({ ...newProfile, telegram_chat_id: e.target.value })}
|
||||||
|
placeholder="-100123456789"
|
||||||
|
className="font-mono text-sm"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
|||||||
Reference in new issue
Block a user