improved ui for setting notifications card

This commit is contained in:
headlessdev 2025-04-18 23:03:05 +02:00
parent c3fe3bc03d
commit 0e1f9edaab
2 changed files with 337 additions and 278 deletions

View File

@ -1,33 +1,25 @@
import { AppSidebar } from "@/components/app-sidebar";
"use client"
import { AppSidebar } from "@/components/app-sidebar"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@/components/ui/breadcrumb";
import { Separator } from "@/components/ui/separator";
import {
SidebarInset,
SidebarProvider,
SidebarTrigger,
} from "@/components/ui/sidebar";
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { useTheme } from "next-themes";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
} from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"
import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
import { Card, CardContent, CardHeader } from "@/components/ui/card"
import { useTheme } from "next-themes"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Input } from "@/components/ui/input"
import { useEffect, useState } from "react";
import axios from "axios";
import Cookies from "js-cookie";
import { Button } from "@/components/ui/button";
import { useEffect, useState } from "react"
import axios from "axios"
import Cookies from "js-cookie"
import { Button } from "@/components/ui/button"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { AlertCircle, Check, Palette, User, Bell } from "lucide-react";
import { AlertCircle, Check, Palette, User, Bell, AtSign, Send, MessageSquare, Trash2 } from "lucide-react"
import {
AlertDialog,
@ -39,19 +31,19 @@ import {
AlertDialogTitle,
AlertDialogTrigger,
} from "@/components/ui/alert-dialog"
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
import { Textarea } from "@/components/ui/textarea"
interface NotificationsResponse {
notifications: any[];
notifications: any[]
}
interface NotificationResponse {
notification_text?: string;
notification_text?: string
}
export default function Settings() {
const { theme, setTheme } = useTheme();
const { theme, setTheme } = useTheme()
const [email, setEmail] = useState<string>("")
const [password, setPassword] = useState<string>("")
@ -83,89 +75,88 @@ export default function Settings() {
const [notificationText, setNotificationText] = useState<string>("")
const changeEmail = async () => {
setEmailErrorVisible(false);
setEmailSuccess(false);
setEmailError("");
setEmailErrorVisible(false)
setEmailSuccess(false)
setEmailError("")
if (!email) {
setEmailError("Email is required");
setEmailErrorVisible(true);
setEmailError("Email is required")
setEmailErrorVisible(true)
setTimeout(() => {
setEmailErrorVisible(false);
setEmailError("");
}
, 3000);
return;
setEmailErrorVisible(false)
setEmailError("")
}, 3000)
return
}
try {
await axios.post('/api/auth/edit_email', {
await axios.post("/api/auth/edit_email", {
newEmail: email,
jwtToken: Cookies.get('token')
});
setEmailSuccess(true);
setEmail("");
jwtToken: Cookies.get("token"),
})
setEmailSuccess(true)
setEmail("")
setTimeout(() => {
setEmailSuccess(false);
}, 3000);
setEmailSuccess(false)
}, 3000)
} catch (error: any) {
setEmailError(error.response.data.error);
setEmailErrorVisible(true);
setEmailError(error.response.data.error)
setEmailErrorVisible(true)
setTimeout(() => {
setEmailErrorVisible(false);
setEmailError("");
}, 3000);
setEmailErrorVisible(false)
setEmailError("")
}, 3000)
}
}
const changePassword = async () => {
try {
if (password !== confirmPassword) {
setPasswordError("Passwords do not match");
setPasswordErrorVisible(true);
setPasswordError("Passwords do not match")
setPasswordErrorVisible(true)
setTimeout(() => {
setPasswordErrorVisible(false);
setPasswordError("");
}, 3000);
return;
setPasswordErrorVisible(false)
setPasswordError("")
}, 3000)
return
}
if (!oldPassword || !password || !confirmPassword) {
setPasswordError("All fields are required");
setPasswordErrorVisible(true);
setPasswordError("All fields are required")
setPasswordErrorVisible(true)
setTimeout(() => {
setPasswordErrorVisible(false);
setPasswordError("");
}, 3000);
return;
setPasswordErrorVisible(false)
setPasswordError("")
}, 3000)
return
}
const response = await axios.post('/api/auth/edit_password', {
const response = await axios.post("/api/auth/edit_password", {
oldPassword: oldPassword,
newPassword: password,
jwtToken: Cookies.get('token')
});
jwtToken: Cookies.get("token"),
})
if (response.status === 200) {
setPasswordSuccess(true);
setPassword("");
setOldPassword("");
setConfirmPassword("");
setPasswordSuccess(true)
setPassword("")
setOldPassword("")
setConfirmPassword("")
setTimeout(() => {
setPasswordSuccess(false);
}, 3000);
setPasswordSuccess(false)
}, 3000)
}
} catch (error: any) {
setPasswordErrorVisible(true);
setPasswordError(error.response.data.error);
setPasswordErrorVisible(true)
setPasswordError(error.response.data.error)
setTimeout(() => {
setPasswordErrorVisible(false);
setPasswordError("");
}, 3000);
setPasswordErrorVisible(false)
setPasswordError("")
}, 3000)
}
}
const addNotification = async () => {
try {
const response = await axios.post('/api/notifications/add', {
const response = await axios.post("/api/notifications/add", {
type: notificationType,
smtpHost: smtpHost,
smtpPort: smtpPort,
@ -176,37 +167,35 @@ export default function Settings() {
smtpTo: smtpTo,
telegramToken: telegramToken,
telegramChatId: telegramChatId,
discordWebhook: discordWebhook
});
getNotifications();
}
catch (error: any) {
alert(error.response.data.error);
discordWebhook: discordWebhook,
})
getNotifications()
} catch (error: any) {
alert(error.response.data.error)
}
}
const deleteNotification = async (id: number) => {
try {
const response = await axios.post('/api/notifications/delete', {
id: id
});
const response = await axios.post("/api/notifications/delete", {
id: id,
})
if (response.status === 200) {
getNotifications()
}
} catch (error: any) {
alert(error.response.data.error);
alert(error.response.data.error)
}
}
const getNotifications = async () => {
try {
const response = await axios.post<NotificationsResponse>('/api/notifications/get', {});
const response = await axios.post<NotificationsResponse>("/api/notifications/get", {})
if (response.status === 200 && response.data) {
setNotifications(response.data.notifications);
setNotifications(response.data.notifications)
}
}
catch (error: any) {
alert(error.response.data.error);
} catch (error: any) {
alert(error.response.data.error)
}
}
@ -214,29 +203,28 @@ export default function Settings() {
getNotifications()
}, [])
const getNotificationText = async () => {
try {
const response = await axios.post<NotificationResponse>('/api/settings/get_notification_text', {});
const response = await axios.post<NotificationResponse>("/api/settings/get_notification_text", {})
if (response.status === 200) {
if (response.data.notification_text) {
setNotificationText(response.data.notification_text);
setNotificationText(response.data.notification_text)
} else {
setNotificationText("The application !name (!url) is now !status.");
setNotificationText("The application !name (!url) is now !status.")
}
}
} catch (error: any) {
alert(error.response.data.error);
alert(error.response.data.error)
}
};
}
const editNotificationText = async () => {
try {
const response = await axios.post('/api/settings/notification_text', {
text: notificationText
});
const response = await axios.post("/api/settings/notification_text", {
text: notificationText,
})
} catch (error: any) {
alert(error.response.data.error);
alert(error.response.data.error)
}
}
@ -403,209 +391,265 @@ export default function Settings() {
</CardContent>
</Card>
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
<CardHeader className="bg-muted/10 px-6 py-4 border-b">
<div className="flex items-center gap-2">
<Bell className="h-5 w-5 text-primary" />
<div className="flex items-center gap-3">
<div className="bg-muted/20 p-2 rounded-full">
<Bell className="h-5 w-5 text-primary" />
</div>
<h2 className="text-xl font-semibold">Notifications</h2>
</div>
</CardHeader>
<CardContent className="pb-6">
<CardContent className="p-6">
<div className="text-sm text-muted-foreground mb-6">
Set up Notifications to get notified when an application goes offline or online.
Set up notifications to get instantly alerted when an application changes status.
</div>
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="w-full">
Add Notification
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Add Notification</AlertDialogTitle>
<AlertDialogDescription>
<Select value={notificationType} onValueChange={(value: string) => setNotificationType(value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Notification Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="smtp">SMTP</SelectItem>
<SelectItem value="telegram">Telegram</SelectItem>
<SelectItem value="discord">Discord</SelectItem>
</SelectContent>
<div className="grid gap-4 md:grid-cols-2">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="w-full h-11 flex items-center gap-2">
<Bell className="h-4 w-4" />
Add Notification Channel
</Button>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Add Notification</AlertDialogTitle>
<AlertDialogDescription>
<Select value={notificationType} onValueChange={(value: string) => setNotificationType(value)}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Notification Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="smtp">SMTP</SelectItem>
<SelectItem value="telegram">Telegram</SelectItem>
<SelectItem value="discord">Discord</SelectItem>
</SelectContent>
{notificationType === "smtp" && (
<div className="mt-4 space-y-4">
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="smtpHost">SMTP Host</Label>
<Input
type="text"
id="smtpHost"
placeholder="smtp.example.com"
onChange={(e) => setSmtpHost(e.target.value)}
/>
{notificationType === "smtp" && (
<div className="mt-4 space-y-4">
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="smtpHost">SMTP Host</Label>
<Input
type="text"
id="smtpHost"
placeholder="smtp.example.com"
onChange={(e) => setSmtpHost(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtpPort">SMTP Port</Label>
<Input
type="number"
id="smtpPort"
placeholder="587"
onChange={(e) => setSmtpPort(Number(e.target.value))}
/>
</div>
</div>
<div className="flex items-center space-x-2 pt-2 pb-4">
<Checkbox id="smtpSecure" onCheckedChange={(checked: any) => setSmtpSecure(checked)} />
<Label htmlFor="smtpSecure" className="text-sm font-medium leading-none">
Secure Connection (TLS/SSL)
</Label>
</div>
<div className="grid gap-4">
<div className="space-y-1.5">
<Label htmlFor="smtpUser">SMTP Username</Label>
<Input
type="text"
id="smtpUser"
placeholder="user@example.com"
onChange={(e) => setSmtpUsername(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtpPass">SMTP Password</Label>
<Input
type="password"
id="smtpPass"
placeholder="••••••••"
onChange={(e) => setSmtpPassword(e.target.value)}
/>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="smtpFrom">From Address</Label>
<Input
type="email"
id="smtpFrom"
placeholder="noreply@example.com"
onChange={(e) => setSmtpFrom(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtpTo">To Address</Label>
<Input
type="email"
id="smtpTo"
placeholder="admin@example.com"
onChange={(e) => setSmtpTo(e.target.value)}
/>
</div>
</div>
</div>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtpPort">SMTP Port</Label>
<Input
type="number"
id="smtpPort"
placeholder="587"
onChange={(e) => setSmtpPort(Number(e.target.value))}
/>
</div>
</div>
<div className="flex items-center space-x-2 pt-2 pb-4">
<Checkbox
id="smtpSecure"
onCheckedChange={(checked: any) => setSmtpSecure(checked)}
/>
<Label htmlFor="smtpSecure" className="text-sm font-medium leading-none">
Secure Connection (TLS/SSL)
</Label>
</div>
<div className="grid gap-4">
<div className="space-y-1.5">
<Label htmlFor="smtpUser">SMTP Username</Label>
<Input
type="text"
id="smtpUser"
placeholder="user@example.com"
onChange={(e) => setSmtpUsername(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtpPass">SMTP Password</Label>
<Input
type="password"
id="smtpPass"
placeholder="••••••••"
onChange={(e) => setSmtpPassword(e.target.value)}
/>
</div>
<div className="grid md:grid-cols-2 gap-4">
<div className="space-y-1.5">
<Label htmlFor="smtpFrom">From Address</Label>
)}
{notificationType === "telegram" && (
<div className="mt-4 space-y-2">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="telegramToken">Bot Token</Label>
<Input
type="email"
id="smtpFrom"
placeholder="noreply@example.com"
onChange={(e) => setSmtpFrom(e.target.value)}
type="text"
id="telegramToken"
placeholder=""
onChange={(e) => setTelegramToken(e.target.value)}
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="smtpTo">To Address</Label>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="telegramChatId">Chat ID</Label>
<Input
type="email"
id="smtpTo"
placeholder="admin@example.com"
onChange={(e) => setSmtpTo(e.target.value)}
type="text"
id="telegramChatId"
placeholder=""
onChange={(e) => setTelegramChatId(e.target.value)}
/>
</div>
</div>
</div>
</div>
)}
)}
{notificationType === "telegram" && (
<div className="mt-4 space-y-2">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="telegramToken">Bot Token</Label>
<Input type="text" id="telegramToken" placeholder="" onChange={(e) => setTelegramToken(e.target.value)} />
{notificationType === "discord" && (
<div className="mt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="discordWebhook">Webhook URL</Label>
<Input
type="text"
id="discordWebhook"
placeholder=""
onChange={(e) => setDiscordWebhook(e.target.value)}
/>
</div>
</div>
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="telegramChatId">Chat ID</Label>
<Input type="text" id="telegramChatId" placeholder="" onChange={(e) => setTelegramChatId(e.target.value)} />
</div>
</div>
)}
)}
</Select>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={addNotification}>Add</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
{notificationType === "discord" && (
<div className="mt-4">
<div className="grid w-full items-center gap-1.5">
<Label htmlFor="discordWebhook">Webhook URL</Label>
<Input type="text" id="discordWebhook" placeholder="" onChange={(e) => setDiscordWebhook(e.target.value)} />
</div>
</div>
)}
</Select>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={addNotification}>
Add
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog>
<AlertDialogTrigger asChild>
<div className="pt-4 pb-2">
<Button className="w-full" variant="secondary">
<AlertDialog>
<AlertDialogTrigger asChild>
<Button className="w-full h-11" variant="outline">
Customize Notification Text
</Button>
</div>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Customize Notification Text</AlertDialogTitle>
<AlertDialogDescription>
<div className="space-y-4">
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogTitle>Customize Notification Text</AlertDialogTitle>
<AlertDialogDescription>
<div className="space-y-4">
<div className="space-y-1.5">
<Label htmlFor="text">Notification Text</Label>
<Textarea id="text" placeholder="Type here..." value={notificationText} onChange={(e) => setNotificationText(e.target.value)} rows={4} />
<Textarea
id="text"
placeholder="Type here..."
value={notificationText}
onChange={(e) => setNotificationText(e.target.value)}
rows={4}
/>
</div>
</div>
<div className="pt-4 text-sm text-muted-foreground">
You can use the following placeholders in the text:
<ul className="list-disc list-inside space-y-1 pt-2">
<li><strong>!name</strong> - Application name</li>
<li><strong>!url</strong> - Application URL</li>
<li><strong>!status</strong> - Application status (online/offline)</li>
</ul>
</div>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={editNotificationText}>
Save
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<div className="mt-6 space-y-4">
{notifications.length > 0 ? (
notifications.map((notification) => (
<div
key={notification.id}
className="flex items-center justify-between p-4 bg-muted/10 rounded-lg border"
>
<div className="space-y-1">
<h3 className="font-medium capitalize">{notification.type}</h3>
</div>
<Button
variant="destructive"
size="sm"
onClick={() => deleteNotification(notification.id)}
<div className="pt-4 text-sm text-muted-foreground">
You can use the following placeholders in the text:
<ul className="list-disc list-inside space-y-1 pt-2">
<li>
<strong>!name</strong> - Application name
</li>
<li>
<strong>!url</strong> - Application URL
</li>
<li>
<strong>!status</strong> - Application status (online/offline)
</li>
</ul>
</div>
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={editNotificationText}>Save</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
<div className="mt-8">
<h3 className="text-lg font-medium mb-4">Active Notification Channels</h3>
<div className="space-y-3">
{notifications.length > 0 ? (
notifications.map((notification) => (
<div
key={notification.id}
className="flex items-center justify-between p-4 rounded-lg border bg-card transition-all hover:shadow-sm"
>
Delete
</Button>
<div className="flex items-center gap-3">
{notification.type === "smtp" && (
<div className="bg-muted/20 p-2 rounded-full">
<AtSign className="h-5 w-5 text-primary" />
</div>
)}
{notification.type === "telegram" && (
<div className="bg-muted/20 p-2 rounded-full">
<Send className="h-5 w-5 text-primary" />
</div>
)}
{notification.type === "discord" && (
<div className="bg-muted/20 p-2 rounded-full">
<MessageSquare className="h-5 w-5 text-primary" />
</div>
)}
<div className="space-y-1">
<h3 className="font-medium capitalize">{notification.type}</h3>
<p className="text-xs text-muted-foreground">
{notification.type === "smtp" && "Email notifications"}
{notification.type === "telegram" && "Telegram bot alerts"}
{notification.type === "discord" && "Discord webhook alerts"}
</p>
</div>
</div>
<Button
variant="ghost"
size="sm"
className="hover:bg-muted/20"
onClick={() => deleteNotification(notification.id)}
>
<Trash2 className="h-4 w-4 mr-1" />
Remove
</Button>
</div>
))
) : (
<div className="text-center py-12 border rounded-lg bg-muted/5">
<div className="flex justify-center mb-3">
<div className="bg-muted/20 p-3 rounded-full">
<Bell className="h-6 w-6 text-muted-foreground" />
</div>
</div>
<h3 className="text-lg font-medium mb-1">No notifications configured</h3>
<p className="text-sm text-muted-foreground max-w-md mx-auto">
Add a notification channel to get alerted when your applications change status.
</p>
</div>
))
) : (
<div className="text-center text-muted-foreground py-6">
No notifications configured
</div>
)}
)}
</div>
</div>
</CardContent>
</Card>

15
package-lock.json generated
View File

@ -4609,6 +4609,21 @@
"optional": true
}
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "15.3.0",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.0.tgz",
"integrity": "sha512-vHUQS4YVGJPmpjn7r5lEZuMhK5UQBNBRSB+iGDvJjaNk649pTIcRluDWNb9siunyLLiu/LDPHfvxBtNamyuLTw==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">= 10"
}
}
}
}