mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-04-18 01:52:20 +00:00
Update notification service
This commit is contained in:
103
AppImage/components/channel-grid.tsx
Normal file
103
AppImage/components/channel-grid.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
const CHANNELS = [
|
||||
{ key: "telegram", label: "Telegram", icon: "/icons/telegram.svg", color: "blue", switchOn: "bg-blue-600" },
|
||||
{ key: "gotify", label: "Gotify", icon: "/icons/gotify.svg", color: "green", switchOn: "bg-green-600" },
|
||||
{ key: "discord", label: "Discord", icon: "/icons/discord.svg", color: "indigo", switchOn: "bg-indigo-600" },
|
||||
{ key: "email", label: "Email", icon: "/icons/mail.svg", color: "amber", switchOn: "bg-amber-600" },
|
||||
]
|
||||
|
||||
const SELECTED_BORDER = {
|
||||
blue: "border-blue-500/60 bg-blue-500/10",
|
||||
green: "border-green-500/60 bg-green-500/10",
|
||||
indigo: "border-indigo-500/60 bg-indigo-500/10",
|
||||
amber: "border-amber-500/60 bg-amber-500/10",
|
||||
}
|
||||
|
||||
interface ChannelGridProps {
|
||||
enabledChannels: { telegram: boolean; gotify: boolean; discord: boolean; email: boolean }
|
||||
onToggle: (channel: string, enabled: boolean) => void
|
||||
selectedChannel: string | null
|
||||
onSelect: (channel: string | null) => void
|
||||
}
|
||||
|
||||
export function ChannelGrid({ enabledChannels, onToggle, selectedChannel, onSelect }: ChannelGridProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{CHANNELS.map(ch => {
|
||||
const isEnabled = enabledChannels[ch.key as keyof typeof enabledChannels] || false
|
||||
const isSelected = selectedChannel === ch.key
|
||||
const selStyle = SELECTED_BORDER[ch.color as keyof typeof SELECTED_BORDER]
|
||||
|
||||
return (
|
||||
<button
|
||||
key={ch.key}
|
||||
type="button"
|
||||
onClick={() => onSelect(isSelected ? null : ch.key)}
|
||||
className={
|
||||
"group relative flex flex-col items-center justify-center gap-2 rounded-lg border p-4 transition-all cursor-pointer " +
|
||||
(isSelected
|
||||
? selStyle + " ring-1 ring-offset-0"
|
||||
: isEnabled
|
||||
? "border-border/60 bg-muted/30 hover:bg-muted/40"
|
||||
: "border-border/30 bg-muted/10 hover:border-border/50 hover:bg-muted/20")
|
||||
}
|
||||
>
|
||||
{/* Status dot */}
|
||||
{isEnabled && (
|
||||
<span className={"absolute top-2 right-2 h-1.5 w-1.5 rounded-full " + ch.switchOn} />
|
||||
)}
|
||||
|
||||
{/* Logo */}
|
||||
<img
|
||||
src={ch.icon}
|
||||
alt={ch.label}
|
||||
className={
|
||||
"h-7 w-7 transition-opacity " +
|
||||
(isEnabled || isSelected ? "opacity-100" : "opacity-30 group-hover:opacity-70")
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Label */}
|
||||
<span
|
||||
className={
|
||||
"text-[11px] font-medium transition-colors " +
|
||||
(isEnabled || isSelected ? "text-foreground" : "text-muted-foreground/60")
|
||||
}
|
||||
>
|
||||
{ch.label}
|
||||
</span>
|
||||
|
||||
{/* Hover overlay with switch */}
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-0 flex items-center justify-center py-1.5 rounded-b-lg bg-background/80 backdrop-blur-sm opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
onToggle(ch.key, !isEnabled)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"relative w-8 h-4 rounded-full transition-colors " +
|
||||
(isEnabled ? ch.switchOn : "bg-muted-foreground/30")
|
||||
}
|
||||
role="switch"
|
||||
aria-checked={isEnabled}
|
||||
aria-label={"Enable " + ch.label}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
"absolute top-[2px] left-[2px] h-3 w-3 rounded-full bg-white shadow transition-transform " +
|
||||
(isEnabled ? "translate-x-4" : "translate-x-0")
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from "react"
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
|
||||
|
||||
import { ChannelGrid } from "./channel-grid"
|
||||
import { Input } from "./ui/input"
|
||||
import { Label } from "./ui/label"
|
||||
import { Badge } from "./ui/badge"
|
||||
@@ -101,42 +101,6 @@ const AI_PROVIDERS = [
|
||||
{ value: "groq", label: "Groq" },
|
||||
]
|
||||
|
||||
// ── Channel visual definitions ──
|
||||
const CHANNEL_COLOR_MAP: Record<string, string> = {
|
||||
blue: "border-blue-500/60 bg-blue-500/10",
|
||||
green: "border-green-500/60 bg-green-500/10",
|
||||
indigo: "border-indigo-500/60 bg-indigo-500/10",
|
||||
amber: "border-amber-500/60 bg-amber-500/10",
|
||||
}
|
||||
|
||||
const CHANNEL_SWITCH_COLOR: Record<string, string> = {
|
||||
blue: "bg-blue-600",
|
||||
green: "bg-green-600",
|
||||
indigo: "bg-indigo-600",
|
||||
amber: "bg-amber-600",
|
||||
}
|
||||
|
||||
const CHANNEL_DEFS: ChannelDef[] = [
|
||||
{ key: "telegram", label: "Telegram", color: "blue", activeColor: "bg-blue-600 hover:bg-blue-700" },
|
||||
{ key: "gotify", label: "Gotify", color: "green", activeColor: "bg-green-600 hover:bg-green-700" },
|
||||
{ key: "discord", label: "Discord", color: "indigo", activeColor: "bg-indigo-600 hover:bg-indigo-700" },
|
||||
{ key: "email", label: "Email", color: "amber", activeColor: "bg-amber-600 hover:bg-amber-700" },
|
||||
]
|
||||
|
||||
interface ChannelDef {
|
||||
key: string
|
||||
label: string
|
||||
color: string
|
||||
activeColor: string
|
||||
}
|
||||
|
||||
const CHANNEL_ICONS: Record<string, string> = {
|
||||
telegram: "/icons/telegram.svg",
|
||||
gotify: "/icons/gotify.svg",
|
||||
discord: "/icons/discord.svg",
|
||||
email: "/icons/mail.svg",
|
||||
}
|
||||
|
||||
const DEFAULT_CONFIG: NotificationConfig = {
|
||||
enabled: false,
|
||||
channels: {
|
||||
@@ -707,64 +671,21 @@ matcher: proxmenux-pbs
|
||||
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Channels</span>
|
||||
</div>
|
||||
|
||||
{/* ── Channel Cards Grid ── */}
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
{CHANNEL_DEFS.map(ch => {
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const chConf = (config.channels || {})[ch.key]
|
||||
const isEnabled = !!(chConf && chConf.enabled)
|
||||
const isSelected = selectedChannel === ch.key
|
||||
<ChannelGrid
|
||||
enabledChannels={{
|
||||
telegram: config.channels.telegram?.enabled || false,
|
||||
gotify: config.channels.gotify?.enabled || false,
|
||||
discord: config.channels.discord?.enabled || false,
|
||||
email: config.channels.email?.enabled || false,
|
||||
}}
|
||||
onToggle={(ch, val) => updateChannel(ch, "enabled", val)}
|
||||
selectedChannel={selectedChannel}
|
||||
onSelect={setSelectedChannel}
|
||||
/>
|
||||
|
||||
return (
|
||||
<button
|
||||
key={ch.key}
|
||||
onClick={() => setSelectedChannel(isSelected ? null : ch.key)}
|
||||
className={`group relative flex flex-col items-center justify-center gap-2 rounded-lg border p-4 transition-all cursor-pointer ${
|
||||
isSelected
|
||||
? CHANNEL_COLOR_MAP[ch.color] + " ring-1 ring-offset-0"
|
||||
: isEnabled
|
||||
? "border-border/60 bg-muted/30 hover:bg-muted/40"
|
||||
: "border-border/30 bg-muted/10 hover:border-border/50 hover:bg-muted/20"
|
||||
}`}
|
||||
>
|
||||
{isEnabled && (
|
||||
<span className={"absolute top-2 right-2 h-1.5 w-1.5 rounded-full " + CHANNEL_SWITCH_COLOR[ch.color]} />
|
||||
)}
|
||||
|
||||
<img
|
||||
src={CHANNEL_ICONS[ch.key]}
|
||||
alt={ch.label}
|
||||
className={"h-7 w-7 transition-opacity " + (isEnabled || isSelected ? "opacity-100" : "opacity-30 group-hover:opacity-70")}
|
||||
/>
|
||||
|
||||
<span className={"text-[11px] font-medium transition-colors " + (isEnabled || isSelected ? "text-foreground" : "text-muted-foreground/60")}>
|
||||
{ch.label}
|
||||
</span>
|
||||
|
||||
<div
|
||||
className="absolute inset-x-0 bottom-0 flex items-center justify-center py-1.5 rounded-b-lg bg-background/80 backdrop-blur-sm opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
updateChannel(ch.key, "enabled", !isEnabled)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={"relative w-8 h-4 rounded-full transition-colors " + (isEnabled ? CHANNEL_SWITCH_COLOR[ch.color] : "bg-muted-foreground/30")}
|
||||
role="switch"
|
||||
aria-checked={isEnabled}
|
||||
aria-label={"Enable " + ch.label}
|
||||
>
|
||||
<span className={"absolute top-[2px] left-[2px] h-3 w-3 rounded-full bg-white shadow transition-transform " + (isEnabled ? "translate-x-4" : "translate-x-0")} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* ── Selected Channel Configuration Panel ── */}
|
||||
{/* ── Telegram Config ── */}
|
||||
{selectedChannel === "telegram" && (
|
||||
<div className="rounded-lg border border-blue-500/30 bg-blue-500/5 p-3 space-y-3">
|
||||
<div className="rounded-lg border border-blue-500/30 bg-blue-500/5 p-3 space-y-3 mt-3">
|
||||
{config.channels.telegram?.enabled ? (
|
||||
<>
|
||||
<div className="space-y-1.5">
|
||||
@@ -819,8 +740,9 @@ matcher: proxmenux-pbs
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Gotify Config ── */}
|
||||
{selectedChannel === "gotify" && (
|
||||
<div className="rounded-lg border border-green-500/30 bg-green-500/5 p-3 space-y-3">
|
||||
<div className="rounded-lg border border-green-500/30 bg-green-500/5 p-3 space-y-3 mt-3">
|
||||
{config.channels.gotify?.enabled ? (
|
||||
<>
|
||||
<div className="space-y-1.5">
|
||||
@@ -875,8 +797,9 @@ matcher: proxmenux-pbs
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Discord Config ── */}
|
||||
{selectedChannel === "discord" && (
|
||||
<div className="rounded-lg border border-indigo-500/30 bg-indigo-500/5 p-3 space-y-3">
|
||||
<div className="rounded-lg border border-indigo-500/30 bg-indigo-500/5 p-3 space-y-3 mt-3">
|
||||
{config.channels.discord?.enabled ? (
|
||||
<>
|
||||
<div className="space-y-1.5">
|
||||
@@ -922,8 +845,9 @@ matcher: proxmenux-pbs
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Email Config ── */}
|
||||
{selectedChannel === "email" && (
|
||||
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 space-y-3">
|
||||
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 p-3 space-y-3 mt-3">
|
||||
{config.channels.email?.enabled ? (
|
||||
<>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
@@ -1065,7 +989,6 @@ matcher: proxmenux-pbs
|
||||
<span>{testResult.message}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>{/* close bordered channel container */}
|
||||
</div>
|
||||
|
||||
{/* ── Filters ── */}
|
||||
|
||||
2
AppImage/public/icons/discord.svg
Normal file
2
AppImage/public/icons/discord.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><defs><style>.a{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;}</style></defs><path class="a" d="M17.59,34.1733c-.89,1.3069-1.8944,2.6152-2.91,3.8267C7.3,37.79,4.5,33,4.5,33A44.83,44.83,0,0,1,9.31,13.48,16.47,16.47,0,0,1,18.69,10l1,2.31A32.6875,32.6875,0,0,1,24,12a32.9643,32.9643,0,0,1,4.33.3l1-2.31a16.47,16.47,0,0,1,9.38,3.51A44.8292,44.8292,0,0,1,43.5,33s-2.8,4.79-10.18,5a47.4193,47.4193,0,0,1-2.86-3.81m6.46-2.9c-3.84,1.9454-7.5555,3.89-12.92,3.89s-9.08-1.9446-12.92-3.89"/><circle class="a" cx="17.847" cy="26.23" r="3.35"/><circle class="a" cx="30.153" cy="26.23" r="3.35"/></svg>
|
||||
|
After Width: | Height: | Size: 819 B |
2
AppImage/public/icons/gotify.svg
Normal file
2
AppImage/public/icons/gotify.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg"><defs><style>.a{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;}</style></defs><path class="a" d="M32.6319,35.4058c-1.59,2.4228-3.29,5.0352-7.0635,6.1748-3.7828,1.13-9.7585.9672-13.4072-.7183C8.503,39.1673,7.1718,35.94,7.8518,33.4405c.67-2.509,3.3422-4.3,4.5488-6.2247,1.1971-1.9345.929-4.003.6129-5.9183a27.9877,27.9877,0,0,1-.6129-5.0947A10.325,10.325,0,0,1,13.43,12.4655"/><path class="a" d="M14.5553,12.6116c-6.9429-.4788-1.4364-6.7036.9577-1.9153"/><path class="a" d="M15.1814,10.1307c4.5488-5.2671,21.4-6.7413,23.0758,6.6658"/><path class="a" d="M19.8224,7.2726c.1844-1.9982,1.2165-2.1541,2.616-.6717"/><path class="a" d="M38.2572,16.7965a1.214,1.214,0,0,1,1.312,1.0822,1.3446,1.3446,0,0,1-2.6335,0A1.2174,1.2174,0,0,1,38.2572,16.7965Z"/><path class="a" d="M37.06,18.3575c-5.5069,3.9549,5.3813,4.1232,2.509-.3855"/><path class="a" d="M28.6807,11.1751a5.7459,5.7459,0,1,1-5.7459,5.7459A5.7443,5.7443,0,0,1,28.6807,11.1751Z"/><path class="a" d="M30.596,15.724a1.3227,1.3227,0,1,1-1.197,1.3119A1.2566,1.2566,0,0,1,30.596,15.724Z"/><path class="a" d="M38.4967,21.148v1.7846"/><path class="a" d="M36.29,11.2939c2.2266-.7383,4.4755,2.8711,3.03,5.9511"/><path class="a" d="M26.9282,25.4537,40.0672,22.59a1.3982,1.3982,0,0,1,1.665,1.0666l.0013.006,1.7334,7.9485a1.3982,1.3982,0,0,1-1.063,1.6663L29.2553,36.1411a1.3981,1.3981,0,0,1-1.665-1.0665l-.0013-.0061L25.8556,27.12a1.3983,1.3983,0,0,1,1.0666-1.665Z"/><path class="a" d="M8.8844,31.3775c-5.77-2.4511-1.072-5.0228.8284-1.03"/><path class="a" d="M4.5,37.9894q3.1124,2.8729,5.0277-.7183"/><path class="a" d="M30.9815,37.7684c1.6762-.389,2.225.73,1.53,3.3333"/><path class="a" d="M23.1742,26.4975q1.1972,2.2889,3.8306.1341"/><path class="a" d="M26.0472,26.9764l9.0977,3.9647,6.2938-7.8819"/><path class="a" d="M28.4789,36.09l4.74-5.9877"/><path class="a" d="M36.56,29.1694l6.6911,3.5083"/></svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
2
AppImage/public/icons/mail.svg
Normal file
2
AppImage/public/icons/mail.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 48 48" id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;}</style></defs><path class="cls-1" d="M6.47,10.71a2,2,0,0,0-2,2h0V35.32a2,2,0,0,0,2,2H41.53a2,2,0,0,0,2-2h0V12.68a2,2,0,0,0-2-2H6.47Zm33.21,3.82L24,26.07,8.32,14.53"/></svg>
|
||||
|
After Width: | Height: | Size: 504 B |
2
AppImage/public/icons/telegram.svg
Normal file
2
AppImage/public/icons/telegram.svg
Normal file
@@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 48 48" id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg"><defs><style>.cls-1{fill:none;stroke:#000000;stroke-linecap:round;stroke-linejoin:round;}</style></defs><path class="cls-1" d="M40.83,8.48c1.14,0,2,1,1.54,2.86l-5.58,26.3c-.39,1.87-1.52,2.32-3.08,1.45L20.4,29.26a.4.4,0,0,1,0-.65L35.77,14.73c.7-.62-.15-.92-1.07-.36L15.41,26.54a.46.46,0,0,1-.4.05L6.82,24C5,23.47,5,22.22,7.23,21.33L40,8.69a2.16,2.16,0,0,1,.83-.21Z"/></svg>
|
||||
|
After Width: | Height: | Size: 614 B |
Reference in New Issue
Block a user