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 { import {
Breadcrumb, Breadcrumb,
BreadcrumbItem, BreadcrumbItem,
BreadcrumbList, BreadcrumbList,
BreadcrumbPage, BreadcrumbPage,
BreadcrumbSeparator, BreadcrumbSeparator,
} from "@/components/ui/breadcrumb"; } from "@/components/ui/breadcrumb"
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator"
import { import { SidebarInset, SidebarProvider, SidebarTrigger } from "@/components/ui/sidebar"
SidebarInset, import { Card, CardContent, CardHeader } from "@/components/ui/card"
SidebarProvider, import { useTheme } from "next-themes"
SidebarTrigger, import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
} 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 { Input } from "@/components/ui/input"
import { useEffect, useState } from "react"; import { useEffect, useState } from "react"
import axios from "axios"; import axios from "axios"
import Cookies from "js-cookie"; import Cookies from "js-cookie"
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert" 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 { import {
AlertDialog, AlertDialog,
@ -39,19 +31,19 @@ import {
AlertDialogTitle, AlertDialogTitle,
AlertDialogTrigger, AlertDialogTrigger,
} from "@/components/ui/alert-dialog" } from "@/components/ui/alert-dialog"
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"; import { Checkbox } from "@/components/ui/checkbox"
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea"
interface NotificationsResponse { interface NotificationsResponse {
notifications: any[]; notifications: any[]
} }
interface NotificationResponse { interface NotificationResponse {
notification_text?: string; notification_text?: string
} }
export default function Settings() { export default function Settings() {
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme()
const [email, setEmail] = useState<string>("") const [email, setEmail] = useState<string>("")
const [password, setPassword] = useState<string>("") const [password, setPassword] = useState<string>("")
@ -83,89 +75,88 @@ export default function Settings() {
const [notificationText, setNotificationText] = useState<string>("") const [notificationText, setNotificationText] = useState<string>("")
const changeEmail = async () => { const changeEmail = async () => {
setEmailErrorVisible(false); setEmailErrorVisible(false)
setEmailSuccess(false); setEmailSuccess(false)
setEmailError(""); setEmailError("")
if (!email) { if (!email) {
setEmailError("Email is required"); setEmailError("Email is required")
setEmailErrorVisible(true); setEmailErrorVisible(true)
setTimeout(() => { setTimeout(() => {
setEmailErrorVisible(false); setEmailErrorVisible(false)
setEmailError(""); setEmailError("")
} }, 3000)
, 3000); return
return;
} }
try { try {
await axios.post('/api/auth/edit_email', { await axios.post("/api/auth/edit_email", {
newEmail: email, newEmail: email,
jwtToken: Cookies.get('token') jwtToken: Cookies.get("token"),
}); })
setEmailSuccess(true); setEmailSuccess(true)
setEmail(""); setEmail("")
setTimeout(() => { setTimeout(() => {
setEmailSuccess(false); setEmailSuccess(false)
}, 3000); }, 3000)
} catch (error: any) { } catch (error: any) {
setEmailError(error.response.data.error); setEmailError(error.response.data.error)
setEmailErrorVisible(true); setEmailErrorVisible(true)
setTimeout(() => { setTimeout(() => {
setEmailErrorVisible(false); setEmailErrorVisible(false)
setEmailError(""); setEmailError("")
}, 3000); }, 3000)
} }
} }
const changePassword = async () => { const changePassword = async () => {
try { try {
if (password !== confirmPassword) { if (password !== confirmPassword) {
setPasswordError("Passwords do not match"); setPasswordError("Passwords do not match")
setPasswordErrorVisible(true); setPasswordErrorVisible(true)
setTimeout(() => { setTimeout(() => {
setPasswordErrorVisible(false); setPasswordErrorVisible(false)
setPasswordError(""); setPasswordError("")
}, 3000); }, 3000)
return; return
} }
if (!oldPassword || !password || !confirmPassword) { if (!oldPassword || !password || !confirmPassword) {
setPasswordError("All fields are required"); setPasswordError("All fields are required")
setPasswordErrorVisible(true); setPasswordErrorVisible(true)
setTimeout(() => { setTimeout(() => {
setPasswordErrorVisible(false); setPasswordErrorVisible(false)
setPasswordError(""); setPasswordError("")
}, 3000); }, 3000)
return; return
} }
const response = await axios.post('/api/auth/edit_password', { const response = await axios.post("/api/auth/edit_password", {
oldPassword: oldPassword, oldPassword: oldPassword,
newPassword: password, newPassword: password,
jwtToken: Cookies.get('token') jwtToken: Cookies.get("token"),
}); })
if (response.status === 200) { if (response.status === 200) {
setPasswordSuccess(true); setPasswordSuccess(true)
setPassword(""); setPassword("")
setOldPassword(""); setOldPassword("")
setConfirmPassword(""); setConfirmPassword("")
setTimeout(() => { setTimeout(() => {
setPasswordSuccess(false); setPasswordSuccess(false)
}, 3000); }, 3000)
} }
} catch (error: any) { } catch (error: any) {
setPasswordErrorVisible(true); setPasswordErrorVisible(true)
setPasswordError(error.response.data.error); setPasswordError(error.response.data.error)
setTimeout(() => { setTimeout(() => {
setPasswordErrorVisible(false); setPasswordErrorVisible(false)
setPasswordError(""); setPasswordError("")
}, 3000); }, 3000)
} }
} }
const addNotification = async () => { const addNotification = async () => {
try { try {
const response = await axios.post('/api/notifications/add', { const response = await axios.post("/api/notifications/add", {
type: notificationType, type: notificationType,
smtpHost: smtpHost, smtpHost: smtpHost,
smtpPort: smtpPort, smtpPort: smtpPort,
@ -176,37 +167,35 @@ export default function Settings() {
smtpTo: smtpTo, smtpTo: smtpTo,
telegramToken: telegramToken, telegramToken: telegramToken,
telegramChatId: telegramChatId, telegramChatId: telegramChatId,
discordWebhook: discordWebhook discordWebhook: discordWebhook,
}); })
getNotifications(); getNotifications()
} } catch (error: any) {
catch (error: any) { alert(error.response.data.error)
alert(error.response.data.error);
} }
} }
const deleteNotification = async (id: number) => { const deleteNotification = async (id: number) => {
try { try {
const response = await axios.post('/api/notifications/delete', { const response = await axios.post("/api/notifications/delete", {
id: id id: id,
}); })
if (response.status === 200) { if (response.status === 200) {
getNotifications() getNotifications()
} }
} catch (error: any) { } catch (error: any) {
alert(error.response.data.error); alert(error.response.data.error)
} }
} }
const getNotifications = async () => { const getNotifications = async () => {
try { 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) { if (response.status === 200 && response.data) {
setNotifications(response.data.notifications); setNotifications(response.data.notifications)
} }
} } catch (error: any) {
catch (error: any) { alert(error.response.data.error)
alert(error.response.data.error);
} }
} }
@ -214,29 +203,28 @@ export default function Settings() {
getNotifications() getNotifications()
}, []) }, [])
const getNotificationText = async () => { const getNotificationText = async () => {
try { 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.status === 200) {
if (response.data.notification_text) { if (response.data.notification_text) {
setNotificationText(response.data.notification_text); setNotificationText(response.data.notification_text)
} else { } else {
setNotificationText("The application !name (!url) is now !status."); setNotificationText("The application !name (!url) is now !status.")
} }
} }
} catch (error: any) { } catch (error: any) {
alert(error.response.data.error); alert(error.response.data.error)
}
} }
};
const editNotificationText = async () => { const editNotificationText = async () => {
try { try {
const response = await axios.post('/api/settings/notification_text', { const response = await axios.post("/api/settings/notification_text", {
text: notificationText text: notificationText,
}); })
} catch (error: any) { } catch (error: any) {
alert(error.response.data.error); alert(error.response.data.error)
} }
} }
@ -403,23 +391,26 @@ export default function Settings() {
</CardContent> </CardContent>
</Card> </Card>
<Card className="overflow-hidden border-2 border-muted/20 shadow-sm"> <Card className="overflow-hidden border-2 border-muted/20 shadow-sm">
<CardHeader className="bg-muted/10 px-6 py-4 border-b"> <CardHeader className="bg-muted/10 px-6 py-4 border-b">
<div className="flex items-center gap-2"> <div className="flex items-center gap-3">
<div className="bg-muted/20 p-2 rounded-full">
<Bell className="h-5 w-5 text-primary" /> <Bell className="h-5 w-5 text-primary" />
</div>
<h2 className="text-xl font-semibold">Notifications</h2> <h2 className="text-xl font-semibold">Notifications</h2>
</div> </div>
</CardHeader> </CardHeader>
<CardContent className="pb-6"> <CardContent className="p-6">
<div className="text-sm text-muted-foreground mb-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> </div>
<div className="grid gap-4 md:grid-cols-2">
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<Button className="w-full"> <Button className="w-full h-11 flex items-center gap-2">
Add Notification <Bell className="h-4 w-4" />
Add Notification Channel
</Button> </Button>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
@ -459,10 +450,7 @@ export default function Settings() {
</div> </div>
<div className="flex items-center space-x-2 pt-2 pb-4"> <div className="flex items-center space-x-2 pt-2 pb-4">
<Checkbox <Checkbox id="smtpSecure" onCheckedChange={(checked: any) => setSmtpSecure(checked)} />
id="smtpSecure"
onCheckedChange={(checked: any) => setSmtpSecure(checked)}
/>
<Label htmlFor="smtpSecure" className="text-sm font-medium leading-none"> <Label htmlFor="smtpSecure" className="text-sm font-medium leading-none">
Secure Connection (TLS/SSL) Secure Connection (TLS/SSL)
</Label> </Label>
@ -518,11 +506,21 @@ export default function Settings() {
<div className="mt-4 space-y-2"> <div className="mt-4 space-y-2">
<div className="grid w-full items-center gap-1.5"> <div className="grid w-full items-center gap-1.5">
<Label htmlFor="telegramToken">Bot Token</Label> <Label htmlFor="telegramToken">Bot Token</Label>
<Input type="text" id="telegramToken" placeholder="" onChange={(e) => setTelegramToken(e.target.value)} /> <Input
type="text"
id="telegramToken"
placeholder=""
onChange={(e) => setTelegramToken(e.target.value)}
/>
</div> </div>
<div className="grid w-full items-center gap-1.5"> <div className="grid w-full items-center gap-1.5">
<Label htmlFor="telegramChatId">Chat ID</Label> <Label htmlFor="telegramChatId">Chat ID</Label>
<Input type="text" id="telegramChatId" placeholder="" onChange={(e) => setTelegramChatId(e.target.value)} /> <Input
type="text"
id="telegramChatId"
placeholder=""
onChange={(e) => setTelegramChatId(e.target.value)}
/>
</div> </div>
</div> </div>
)} )}
@ -531,29 +529,29 @@ export default function Settings() {
<div className="mt-4"> <div className="mt-4">
<div className="grid w-full items-center gap-1.5"> <div className="grid w-full items-center gap-1.5">
<Label htmlFor="discordWebhook">Webhook URL</Label> <Label htmlFor="discordWebhook">Webhook URL</Label>
<Input type="text" id="discordWebhook" placeholder="" onChange={(e) => setDiscordWebhook(e.target.value)} /> <Input
type="text"
id="discordWebhook"
placeholder=""
onChange={(e) => setDiscordWebhook(e.target.value)}
/>
</div> </div>
</div> </div>
)} )}
</Select> </Select>
</AlertDialogDescription> </AlertDialogDescription>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={addNotification}> <AlertDialogAction onClick={addNotification}>Add</AlertDialogAction>
Add
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
<AlertDialog> <AlertDialog>
<AlertDialogTrigger asChild> <AlertDialogTrigger asChild>
<div className="pt-4 pb-2"> <Button className="w-full h-11" variant="outline">
<Button className="w-full" variant="secondary">
Customize Notification Text Customize Notification Text
</Button> </Button>
</div>
</AlertDialogTrigger> </AlertDialogTrigger>
<AlertDialogContent> <AlertDialogContent>
<AlertDialogTitle>Customize Notification Text</AlertDialogTitle> <AlertDialogTitle>Customize Notification Text</AlertDialogTitle>
@ -561,52 +559,98 @@ export default function Settings() {
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-1.5"> <div className="space-y-1.5">
<Label htmlFor="text">Notification Text</Label> <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> </div>
<div className="pt-4 text-sm text-muted-foreground"> <div className="pt-4 text-sm text-muted-foreground">
You can use the following placeholders in the text: You can use the following placeholders in the text:
<ul className="list-disc list-inside space-y-1 pt-2"> <ul className="list-disc list-inside space-y-1 pt-2">
<li><strong>!name</strong> - Application name</li> <li>
<li><strong>!url</strong> - Application URL</li> <strong>!name</strong> - Application name
<li><strong>!status</strong> - Application status (online/offline)</li> </li>
<li>
<strong>!url</strong> - Application URL
</li>
<li>
<strong>!status</strong> - Application status (online/offline)
</li>
</ul> </ul>
</div> </div>
</AlertDialogDescription> </AlertDialogDescription>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel> <AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={editNotificationText}> <AlertDialogAction onClick={editNotificationText}>Save</AlertDialogAction>
Save
</AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
</AlertDialog> </AlertDialog>
</div>
<div className="mt-6 space-y-4"> <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.length > 0 ? (
notifications.map((notification) => ( notifications.map((notification) => (
<div <div
key={notification.id} key={notification.id}
className="flex items-center justify-between p-4 bg-muted/10 rounded-lg border" className="flex items-center justify-between p-4 rounded-lg border bg-card transition-all hover:shadow-sm"
> >
<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"> <div className="space-y-1">
<h3 className="font-medium capitalize">{notification.type}</h3> <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> </div>
<Button <Button
variant="destructive" variant="ghost"
size="sm" size="sm"
className="hover:bg-muted/20"
onClick={() => deleteNotification(notification.id)} onClick={() => deleteNotification(notification.id)}
> >
Delete <Trash2 className="h-4 w-4 mr-1" />
Remove
</Button> </Button>
</div> </div>
)) ))
) : ( ) : (
<div className="text-center text-muted-foreground py-6"> <div className="text-center py-12 border rounded-lg bg-muted/5">
No notifications configured <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>
)} )}
</div> </div>
</div>
</CardContent> </CardContent>
</Card> </Card>
</div> </div>

15
package-lock.json generated
View File

@ -4609,6 +4609,21 @@
"optional": true "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"
}
} }
} }
} }