mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
feat(notifications): add build failed and invitation emails from react-email
This commit is contained in:
@@ -99,7 +99,7 @@ export const AddRegistry = () => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Button className="max-sm:w-full">
|
||||
<Container className="h-4 w-4" />
|
||||
Create Registry
|
||||
</Button>
|
||||
|
||||
@@ -88,7 +88,7 @@ export const AddSelfHostedRegistry = () => {
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Button className="max-sm:w-full">
|
||||
<Container className="h-4 w-4" />
|
||||
Enable Self Hosted Registry
|
||||
</Button>
|
||||
|
||||
@@ -42,11 +42,11 @@ export const ShowRegistry = () => {
|
||||
{data?.length === 0 ? (
|
||||
<div className="flex flex-col items-center gap-3">
|
||||
<Server className="size-8 self-center text-muted-foreground" />
|
||||
<span className="text-base text-muted-foreground">
|
||||
<span className="text-base text-muted-foreground text-center">
|
||||
To create a cluster is required to set a registry.
|
||||
</span>
|
||||
|
||||
<div className="flex flex-row gap-2">
|
||||
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
||||
<AddSelfHostedRegistry />
|
||||
<AddRegistry />
|
||||
</div>
|
||||
|
||||
@@ -34,8 +34,10 @@ import {
|
||||
} from "@/components/icons/notification-icons";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
|
||||
const baseDatabaseSchema = z.object({
|
||||
name: z.string().min(1, "Name required"),
|
||||
const notificationBaseSchema = z.object({
|
||||
name: z.string().min(1, {
|
||||
message: "Name is required",
|
||||
}),
|
||||
appDeploy: z.boolean().default(false),
|
||||
userJoin: z.boolean().default(false),
|
||||
appBuilderError: z.boolean().default(false),
|
||||
@@ -43,41 +45,47 @@ const baseDatabaseSchema = z.object({
|
||||
dokployRestart: z.boolean().default(false),
|
||||
});
|
||||
|
||||
const mySchema = z.discriminatedUnion("type", [
|
||||
export const notificationSchema = z.discriminatedUnion("type", [
|
||||
z
|
||||
.object({
|
||||
type: z.literal("slack"),
|
||||
webhookUrl: z.string().min(1),
|
||||
channel: z.string().min(1),
|
||||
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
||||
channel: z.string(),
|
||||
})
|
||||
.merge(baseDatabaseSchema),
|
||||
.merge(notificationBaseSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("telegram"),
|
||||
botToken: z.string().min(1),
|
||||
chatId: z.string().min(1),
|
||||
botToken: z.string().min(1, { message: "Bot Token is required" }),
|
||||
chatId: z.string().min(1, { message: "Chat ID is required" }),
|
||||
})
|
||||
.merge(baseDatabaseSchema),
|
||||
.merge(notificationBaseSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("discord"),
|
||||
webhookUrl: z.string().min(1),
|
||||
webhookUrl: z.string().min(1, { message: "Webhook URL is required" }),
|
||||
})
|
||||
.merge(baseDatabaseSchema),
|
||||
.merge(notificationBaseSchema),
|
||||
z
|
||||
.object({
|
||||
type: z.literal("email"),
|
||||
smtpServer: z.string().min(1),
|
||||
smtpPort: z.string().min(1),
|
||||
username: z.string().min(1),
|
||||
password: z.string().min(1),
|
||||
fromAddress: z.string().min(1),
|
||||
toAddresses: z.array(z.string()).min(1),
|
||||
smtpServer: z.string().min(1, { message: "SMTP Server is required" }),
|
||||
smtpPort: z.number().min(1, { message: "SMTP Port is required" }),
|
||||
username: z.string().min(1, { message: "Username is required" }),
|
||||
password: z.string().min(1, { message: "Password is required" }),
|
||||
fromAddress: z.string().min(1, { message: "From Address is required" }),
|
||||
toAddresses: z
|
||||
.array(
|
||||
z.string().min(1, { message: "Email is required" }).email({
|
||||
message: "Email is invalid",
|
||||
}),
|
||||
)
|
||||
.min(1, { message: "At least one email is required" }),
|
||||
})
|
||||
.merge(baseDatabaseSchema),
|
||||
.merge(notificationBaseSchema),
|
||||
]);
|
||||
|
||||
const notificationsMap = {
|
||||
export const notificationsMap = {
|
||||
slack: {
|
||||
icon: <SlackIcon />,
|
||||
label: "Slack",
|
||||
@@ -96,31 +104,40 @@ const notificationsMap = {
|
||||
},
|
||||
};
|
||||
|
||||
type AddNotification = z.infer<typeof mySchema>;
|
||||
export type NotificationSchema = z.infer<typeof notificationSchema>;
|
||||
|
||||
export const AddNotification = () => {
|
||||
const utils = api.useUtils();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const { mutateAsync: testConnection, isLoading: isLoadingConnection } =
|
||||
api.notification.testConnection.useMutation();
|
||||
|
||||
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
||||
api.notification.testSlackConnection.useMutation();
|
||||
|
||||
const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } =
|
||||
api.notification.testTelegramConnection.useMutation();
|
||||
const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } =
|
||||
api.notification.testDiscordConnection.useMutation();
|
||||
const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } =
|
||||
api.notification.testEmailConnection.useMutation();
|
||||
const slackMutation = api.notification.createSlack.useMutation();
|
||||
const telegramMutation = api.notification.createTelegram.useMutation();
|
||||
const discordMutation = api.notification.createDiscord.useMutation();
|
||||
const emailMutation = api.notification.createEmail.useMutation();
|
||||
|
||||
const form = useForm<AddNotification>({
|
||||
const form = useForm<NotificationSchema>({
|
||||
defaultValues: {
|
||||
type: "slack",
|
||||
webhookUrl: "",
|
||||
channel: "",
|
||||
name: "",
|
||||
},
|
||||
resolver: zodResolver(mySchema),
|
||||
resolver: zodResolver(notificationSchema),
|
||||
});
|
||||
const type = form.watch("type");
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "toAddresses",
|
||||
name: "toAddresses" as never,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -140,7 +157,7 @@ export const AddNotification = () => {
|
||||
email: emailMutation,
|
||||
};
|
||||
|
||||
const onSubmit = async (data: AddNotification) => {
|
||||
const onSubmit = async (data: NotificationSchema) => {
|
||||
const {
|
||||
appBuilderError,
|
||||
appDeploy,
|
||||
@@ -226,8 +243,6 @@ export const AddNotification = () => {
|
||||
Create new notifications providers for multiple
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{/* {isError && <AlertBlock type="error">{error?.message}</AlertBlock>} */}
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
@@ -405,68 +420,86 @@ export const AddNotification = () => {
|
||||
|
||||
{type === "email" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpServer"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>SMTP Server</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="smtp.gmail.com" {...field} />
|
||||
</FormControl>
|
||||
<div className="flex md:flex-row flex-col gap-2 w-full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpServer"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>SMTP Server</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="smtp.gmail.com" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpPort"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>SMTP Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="587" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpPort"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>SMTP Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="587"
|
||||
{...field}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
if (value) {
|
||||
const port = Number.parseInt(value);
|
||||
if (port > 0 && port < 65536) {
|
||||
field.onChange(port);
|
||||
}
|
||||
}
|
||||
}}
|
||||
type="number"
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="username" {...field} />
|
||||
</FormControl>
|
||||
<div className="flex md:flex-row flex-col gap-2 w-full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="username" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="******************"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="******************"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fromAddress"
|
||||
@@ -480,21 +513,6 @@ export const AddNotification = () => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* <FormField
|
||||
control={form.control}
|
||||
name="toAddresses"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>To Addresses</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="email@example.com" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/> */}
|
||||
<div className="flex flex-col gap-2 pt-2">
|
||||
<FormLabel>To Addresses</FormLabel>
|
||||
|
||||
@@ -531,6 +549,12 @@ export const AddNotification = () => {
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{type === "email" &&
|
||||
"toAddresses" in form.formState.errors && (
|
||||
<div className="text-sm font-medium text-destructive">
|
||||
{form.formState?.errors?.toAddresses?.root?.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
@@ -546,18 +570,18 @@ export const AddNotification = () => {
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormLabel className="text-lg font-semibold leading-none tracking-tight">
|
||||
Select the actions.
|
||||
</FormLabel>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appDeploy"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<div className="space-y-0.5">
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="">
|
||||
<FormLabel>App Deploy</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a app is deployed.
|
||||
@@ -576,7 +600,7 @@ export const AddNotification = () => {
|
||||
control={form.control}
|
||||
name="userJoin"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>User Join</FormLabel>
|
||||
<FormDescription>
|
||||
@@ -596,7 +620,7 @@ export const AddNotification = () => {
|
||||
control={form.control}
|
||||
name="databaseBackup"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Database Backup</FormLabel>
|
||||
<FormDescription>
|
||||
@@ -612,11 +636,12 @@ export const AddNotification = () => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="dokployRestart"
|
||||
render={({ field }) => (
|
||||
<FormItem className="mt-4 flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Deploy Restart</FormLabel>
|
||||
<FormDescription>
|
||||
@@ -632,38 +657,72 @@ export const AddNotification = () => {
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="appBuilderError"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>App Builder Error</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when the build fails.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter className="flex flex-row gap-2 !justify-between w-full">
|
||||
<Button
|
||||
isLoading={isLoadingConnection}
|
||||
isLoading={
|
||||
isLoadingSlack ||
|
||||
isLoadingTelegram ||
|
||||
isLoadingDiscord ||
|
||||
isLoadingEmail
|
||||
}
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
await testConnection({
|
||||
webhookUrl: form.getValues("webhookUrl"),
|
||||
channel: form.getValues("channel"),
|
||||
notificationType: type,
|
||||
botToken: form.getValues("botToken"),
|
||||
chatId: form.getValues("chatId"),
|
||||
//
|
||||
smtpPort: form.getValues("smtpPort"),
|
||||
smtpServer: form.getValues("smtpServer"),
|
||||
username: form.getValues("username"),
|
||||
password: form.getValues("password"),
|
||||
toAddresses: form.getValues("toAddresses"),
|
||||
fromAddress: form.getValues("fromAddress"),
|
||||
})
|
||||
.then(async () => {
|
||||
toast.success("Connection Success");
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to connect the provider");
|
||||
});
|
||||
try {
|
||||
if (type === "slack") {
|
||||
await testSlackConnection({
|
||||
webhookUrl: form.getValues("webhookUrl"),
|
||||
channel: form.getValues("channel"),
|
||||
});
|
||||
} else if (type === "telegram") {
|
||||
await testTelegramConnection({
|
||||
botToken: form.getValues("botToken"),
|
||||
chatId: form.getValues("chatId"),
|
||||
});
|
||||
} else if (type === "discord") {
|
||||
await testDiscordConnection({
|
||||
webhookUrl: form.getValues("webhookUrl"),
|
||||
});
|
||||
} else if (type === "email") {
|
||||
await testEmailConnection({
|
||||
smtpServer: form.getValues("smtpServer"),
|
||||
smtpPort: form.getValues("smtpPort"),
|
||||
username: form.getValues("username"),
|
||||
password: form.getValues("password"),
|
||||
toAddresses: form.getValues("toAddresses"),
|
||||
fromAddress: form.getValues("fromAddress"),
|
||||
});
|
||||
}
|
||||
toast.success("Connection Success");
|
||||
} catch (err) {
|
||||
toast.error("Error to test the provider");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Send Test
|
||||
Test Notification
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
|
||||
@@ -6,9 +6,15 @@ import {
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { api } from "@/utils/api";
|
||||
import { BellRing } from "lucide-react";
|
||||
import { BellRing, Mail } from "lucide-react";
|
||||
import { AddNotification } from "./add-notification";
|
||||
import { DeleteNotification } from "./delete-notification";
|
||||
import {
|
||||
DiscordIcon,
|
||||
SlackIcon,
|
||||
TelegramIcon,
|
||||
} from "@/components/icons/notification-icons";
|
||||
import { UpdateNotification } from "./update-notification";
|
||||
|
||||
export const ShowNotifications = () => {
|
||||
const { data } = api.notification.all.useQuery();
|
||||
@@ -20,7 +26,7 @@ export const ShowNotifications = () => {
|
||||
<CardTitle className="text-xl">Notifications</CardTitle>
|
||||
<CardDescription>
|
||||
Add your providers to receive notifications, like Discord, Slack,
|
||||
Telegram, Email, etc.
|
||||
Telegram, Email.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-2 pt-4">
|
||||
@@ -34,25 +40,45 @@ export const ShowNotifications = () => {
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col gap-4">
|
||||
{data?.map((destination, index) => (
|
||||
<div
|
||||
key={destination.notificationId}
|
||||
className="flex items-center justify-between border p-3.5 rounded-lg"
|
||||
>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{index + 1}. {destination.name}
|
||||
</span>
|
||||
<div className="flex flex-row gap-1">
|
||||
{/* <UpdateDestination
|
||||
destinationId={destination.destinationId}
|
||||
/> */}
|
||||
<DeleteNotification
|
||||
notificationId={destination.notificationId}
|
||||
/>
|
||||
<div className="grid lg:grid-cols-2 xl:grid-cols-3 gap-4">
|
||||
{data?.map((notification, index) => (
|
||||
<div
|
||||
key={notification.notificationId}
|
||||
className="flex items-center justify-between border gap-2 p-3.5 rounded-lg"
|
||||
>
|
||||
<div className="flex flex-row gap-2 items-center w-full ">
|
||||
{notification.notificationType === "slack" && (
|
||||
<SlackIcon className="text-muted-foreground size-6 flex-shrink-0" />
|
||||
)}
|
||||
{notification.notificationType === "telegram" && (
|
||||
<TelegramIcon className="text-muted-foreground size-8 flex-shrink-0" />
|
||||
)}
|
||||
{notification.notificationType === "discord" && (
|
||||
<DiscordIcon className="text-muted-foreground size-7 flex-shrink-0" />
|
||||
)}
|
||||
{notification.notificationType === "email" && (
|
||||
<Mail
|
||||
size={29}
|
||||
className="text-muted-foreground size-6 flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{notification.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-1 w-fit">
|
||||
<UpdateNotification
|
||||
notificationId={notification.notificationId}
|
||||
/>
|
||||
<DeleteNotification
|
||||
notificationId={notification.notificationId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 justify-end w-full items-end">
|
||||
<AddNotification />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,686 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { api } from "@/utils/api";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Mail, PenBoxIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { FieldErrors, useFieldArray, useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
TelegramIcon,
|
||||
DiscordIcon,
|
||||
SlackIcon,
|
||||
} from "@/components/icons/notification-icons";
|
||||
import {
|
||||
notificationSchema,
|
||||
type NotificationSchema,
|
||||
} from "./add-notification";
|
||||
|
||||
interface Props {
|
||||
notificationId: string;
|
||||
}
|
||||
|
||||
export const UpdateNotification = ({ notificationId }: Props) => {
|
||||
const utils = api.useUtils();
|
||||
const { data, refetch } = api.notification.one.useQuery(
|
||||
{
|
||||
notificationId,
|
||||
},
|
||||
{
|
||||
enabled: !!notificationId,
|
||||
},
|
||||
);
|
||||
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
||||
api.notification.testSlackConnection.useMutation();
|
||||
|
||||
const { mutateAsync: testTelegramConnection, isLoading: isLoadingTelegram } =
|
||||
api.notification.testTelegramConnection.useMutation();
|
||||
const { mutateAsync: testDiscordConnection, isLoading: isLoadingDiscord } =
|
||||
api.notification.testDiscordConnection.useMutation();
|
||||
const { mutateAsync: testEmailConnection, isLoading: isLoadingEmail } =
|
||||
api.notification.testEmailConnection.useMutation();
|
||||
const slackMutation = api.notification.updateSlack.useMutation();
|
||||
const telegramMutation = api.notification.updateTelegram.useMutation();
|
||||
const discordMutation = api.notification.updateDiscord.useMutation();
|
||||
const emailMutation = api.notification.updateEmail.useMutation();
|
||||
|
||||
const form = useForm<NotificationSchema>({
|
||||
defaultValues: {
|
||||
type: "slack",
|
||||
webhookUrl: "",
|
||||
channel: "",
|
||||
},
|
||||
resolver: zodResolver(notificationSchema),
|
||||
});
|
||||
const type = form.watch("type");
|
||||
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
control: form.control,
|
||||
name: "toAddresses" as never,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (data) {
|
||||
if (data.notificationType === "slack") {
|
||||
form.reset({
|
||||
appBuilderError: data.appBuildError,
|
||||
appDeploy: data.appDeploy,
|
||||
dokployRestart: data.dokployRestart,
|
||||
databaseBackup: data.databaseBackup,
|
||||
userJoin: data.userJoin,
|
||||
webhookUrl: data.slack?.webhookUrl,
|
||||
channel: data.slack?.channel || "",
|
||||
name: data.name,
|
||||
type: data.notificationType,
|
||||
});
|
||||
} else if (data.notificationType === "telegram") {
|
||||
form.reset({
|
||||
appBuilderError: data.appBuildError,
|
||||
appDeploy: data.appDeploy,
|
||||
dokployRestart: data.dokployRestart,
|
||||
databaseBackup: data.databaseBackup,
|
||||
userJoin: data.userJoin,
|
||||
botToken: data.telegram?.botToken,
|
||||
chatId: data.telegram?.chatId,
|
||||
type: data.notificationType,
|
||||
name: data.name,
|
||||
});
|
||||
} else if (data.notificationType === "discord") {
|
||||
form.reset({
|
||||
appBuilderError: data.appBuildError,
|
||||
appDeploy: data.appDeploy,
|
||||
dokployRestart: data.dokployRestart,
|
||||
databaseBackup: data.databaseBackup,
|
||||
userJoin: data.userJoin,
|
||||
type: data.notificationType,
|
||||
webhookUrl: data.discord?.webhookUrl,
|
||||
name: data.name,
|
||||
});
|
||||
} else if (data.notificationType === "email") {
|
||||
form.reset({
|
||||
appBuilderError: data.appBuildError,
|
||||
appDeploy: data.appDeploy,
|
||||
dokployRestart: data.dokployRestart,
|
||||
databaseBackup: data.databaseBackup,
|
||||
type: data.notificationType,
|
||||
userJoin: data.userJoin,
|
||||
smtpServer: data.email?.smtpServer,
|
||||
smtpPort: data.email?.smtpPort,
|
||||
username: data.email?.username,
|
||||
password: data.email?.password,
|
||||
toAddresses: data.email?.toAddresses,
|
||||
fromAddress: data.email?.fromAddress,
|
||||
name: data.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [form, form.reset, data]);
|
||||
|
||||
const onSubmit = async (formData: NotificationSchema) => {
|
||||
const {
|
||||
appBuilderError,
|
||||
appDeploy,
|
||||
dokployRestart,
|
||||
databaseBackup,
|
||||
userJoin,
|
||||
} = formData;
|
||||
let promise: Promise<unknown> | null = null;
|
||||
if (formData?.type === "slack" && data?.slackId) {
|
||||
promise = slackMutation.mutateAsync({
|
||||
appBuildError: appBuilderError,
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
userJoin: userJoin,
|
||||
webhookUrl: formData.webhookUrl,
|
||||
channel: formData.channel,
|
||||
name: formData.name,
|
||||
notificationId: notificationId,
|
||||
slackId: data?.slackId,
|
||||
});
|
||||
} else if (formData.type === "telegram" && data?.telegramId) {
|
||||
promise = telegramMutation.mutateAsync({
|
||||
appBuildError: appBuilderError,
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
userJoin: userJoin,
|
||||
botToken: formData.botToken,
|
||||
chatId: formData.chatId,
|
||||
name: formData.name,
|
||||
notificationId: notificationId,
|
||||
telegramId: data?.telegramId,
|
||||
});
|
||||
} else if (formData.type === "discord" && data?.discordId) {
|
||||
promise = discordMutation.mutateAsync({
|
||||
appBuildError: appBuilderError,
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
userJoin: userJoin,
|
||||
webhookUrl: formData.webhookUrl,
|
||||
name: formData.name,
|
||||
notificationId: notificationId,
|
||||
discordId: data?.discordId,
|
||||
});
|
||||
} else if (formData.type === "email" && data?.emailId) {
|
||||
promise = emailMutation.mutateAsync({
|
||||
appBuildError: appBuilderError,
|
||||
appDeploy: appDeploy,
|
||||
dokployRestart: dokployRestart,
|
||||
databaseBackup: databaseBackup,
|
||||
userJoin: userJoin,
|
||||
smtpServer: formData.smtpServer,
|
||||
smtpPort: formData.smtpPort,
|
||||
username: formData.username,
|
||||
password: formData.password,
|
||||
fromAddress: formData.fromAddress,
|
||||
toAddresses: formData.toAddresses,
|
||||
name: formData.name,
|
||||
notificationId: notificationId,
|
||||
emailId: data?.emailId,
|
||||
});
|
||||
}
|
||||
|
||||
if (promise) {
|
||||
await promise
|
||||
.then(async () => {
|
||||
toast.success("Notification Updated");
|
||||
await utils.notification.all.invalidate();
|
||||
refetch();
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error to update a notification");
|
||||
});
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger className="" asChild>
|
||||
<Button variant="ghost">
|
||||
<PenBoxIcon className="size-4 text-muted-foreground" />
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Update Notification</DialogTitle>
|
||||
<DialogDescription>
|
||||
Update the current notification config
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
id="hook-form"
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="grid w-full gap-8 "
|
||||
>
|
||||
<div className="flex flex-col gap-4 ">
|
||||
<div className="flex flex-row gap-2 w-full items-center">
|
||||
<div className="flex flex-row gap-2 items-center w-full ">
|
||||
<FormLabel className="text-lg font-semibold leading-none tracking-tight flex">
|
||||
{data?.notificationType === "slack"
|
||||
? "Slack"
|
||||
: data?.notificationType === "telegram"
|
||||
? "Telegram"
|
||||
: data?.notificationType === "discord"
|
||||
? "Discord"
|
||||
: "Email"}
|
||||
</FormLabel>
|
||||
</div>
|
||||
{data?.notificationType === "slack" && (
|
||||
<SlackIcon className="text-muted-foreground size-6 flex-shrink-0" />
|
||||
)}
|
||||
{data?.notificationType === "telegram" && (
|
||||
<TelegramIcon className="text-muted-foreground size-8 flex-shrink-0" />
|
||||
)}
|
||||
{data?.notificationType === "discord" && (
|
||||
<DiscordIcon className="text-muted-foreground size-7 flex-shrink-0" />
|
||||
)}
|
||||
{data?.notificationType === "email" && (
|
||||
<Mail
|
||||
size={29}
|
||||
className="text-muted-foreground size-6 flex-shrink-0"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Name" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{type === "slack" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="channel"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Channel</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Channel" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "telegram" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="botToken"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Bot Token</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="6660491268:AAFMGmajZOVewpMNZCgJr5H7cpXpoZPgvXw"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="chatId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Chat ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="431231869" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{type === "discord" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="webhookUrl"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Webhook URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="https://discord.com/api/webhooks/123456789/ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{type === "email" && (
|
||||
<>
|
||||
<div className="flex md:flex-row flex-col gap-2 w-full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpServer"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>SMTP Server</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="smtp.gmail.com" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="smtpPort"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>SMTP Port</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="587" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex md:flex-row flex-col gap-2 w-full">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="username"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Username</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="username" {...field} />
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormLabel>Password</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
type="password"
|
||||
placeholder="******************"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="fromAddress"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>From Address</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="from@example.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 pt-2">
|
||||
<FormLabel>To Addresses</FormLabel>
|
||||
|
||||
{fields.map((field, index) => (
|
||||
<div
|
||||
key={field.id}
|
||||
className="flex flex-row gap-2 w-full"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name={`toAddresses.${index}`}
|
||||
render={({ field }) => (
|
||||
<FormItem className="w-full">
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder="email@example.com"
|
||||
className="w-full"
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
{type === "email" &&
|
||||
"toAddresses" in form.formState.errors && (
|
||||
<div className="text-sm font-medium text-destructive">
|
||||
{form.formState?.errors?.toAddresses?.root?.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
append("");
|
||||
}}
|
||||
>
|
||||
Add
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
<FormLabel className="text-lg font-semibold leading-none tracking-tight">
|
||||
Select the actions.
|
||||
</FormLabel>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
defaultValue={form.control._defaultValues.appDeploy}
|
||||
name="appDeploy"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>App Deploy</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a app is deployed.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
defaultValue={form.control._defaultValues.userJoin}
|
||||
name="userJoin"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>User Join</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a user joins the app.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="databaseBackup"
|
||||
defaultValue={form.control._defaultValues.databaseBackup}
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Database Backup</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a database backup is created.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
defaultValue={form.control._defaultValues.dokployRestart}
|
||||
name="dokployRestart"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>Deploy Restart</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when a deploy is restarted.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
defaultValue={form.control._defaultValues.appBuilderError}
|
||||
name="appBuilderError"
|
||||
render={({ field }) => (
|
||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel>App Builder Error</FormLabel>
|
||||
<FormDescription>
|
||||
Trigger the action when the build fails.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<DialogFooter className="flex flex-row gap-2 !justify-between w-full">
|
||||
<Button
|
||||
isLoading={
|
||||
isLoadingSlack ||
|
||||
isLoadingTelegram ||
|
||||
isLoadingDiscord ||
|
||||
isLoadingEmail
|
||||
}
|
||||
variant="secondary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
if (type === "slack") {
|
||||
await testSlackConnection({
|
||||
webhookUrl: form.getValues("webhookUrl"),
|
||||
channel: form.getValues("channel"),
|
||||
});
|
||||
} else if (type === "telegram") {
|
||||
await testTelegramConnection({
|
||||
botToken: form.getValues("botToken"),
|
||||
chatId: form.getValues("chatId"),
|
||||
});
|
||||
} else if (type === "discord") {
|
||||
await testDiscordConnection({
|
||||
webhookUrl: form.getValues("webhookUrl"),
|
||||
});
|
||||
} else if (type === "email") {
|
||||
await testEmailConnection({
|
||||
smtpServer: form.getValues("smtpServer"),
|
||||
smtpPort: form.getValues("smtpPort"),
|
||||
username: form.getValues("username"),
|
||||
password: form.getValues("password"),
|
||||
toAddresses: form.getValues("toAddresses"),
|
||||
fromAddress: form.getValues("fromAddress"),
|
||||
});
|
||||
}
|
||||
toast.success("Connection Success");
|
||||
} catch (err) {
|
||||
toast.error("Error to test the provider");
|
||||
}
|
||||
}}
|
||||
>
|
||||
Test Notification
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
form="hook-form"
|
||||
type="submit"
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -1,34 +1,34 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
export const SlackIcon = ({ className }: Props) => {
|
||||
return (
|
||||
<>
|
||||
<svg
|
||||
viewBox="0 0 2447.6 2452.5"
|
||||
className="size-8"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clip-rule="evenodd" fill-rule="evenodd">
|
||||
<path
|
||||
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
|
||||
fill="#36c5f0"
|
||||
/>
|
||||
<path
|
||||
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
|
||||
fill="#2eb67d"
|
||||
/>
|
||||
<path
|
||||
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
|
||||
fill="#ecb22e"
|
||||
/>
|
||||
<path
|
||||
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
|
||||
fill="#e01e5a"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</>
|
||||
<svg
|
||||
viewBox="0 0 2447.6 2452.5"
|
||||
className={cn("size-8", className)}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g clipRule="evenodd" fillRule="evenodd">
|
||||
<path
|
||||
d="m897.4 0c-135.3.1-244.8 109.9-244.7 245.2-.1 135.3 109.5 245.1 244.8 245.2h244.8v-245.1c.1-135.3-109.5-245.1-244.9-245.3.1 0 .1 0 0 0m0 654h-652.6c-135.3.1-244.9 109.9-244.8 245.2-.2 135.3 109.4 245.1 244.7 245.3h652.7c135.3-.1 244.9-109.9 244.8-245.2.1-135.4-109.5-245.2-244.8-245.3z"
|
||||
fill="#36c5f0"
|
||||
/>
|
||||
<path
|
||||
d="m2447.6 899.2c.1-135.3-109.5-245.1-244.8-245.2-135.3.1-244.9 109.9-244.8 245.2v245.3h244.8c135.3-.1 244.9-109.9 244.8-245.3zm-652.7 0v-654c.1-135.2-109.4-245-244.7-245.2-135.3.1-244.9 109.9-244.8 245.2v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.3z"
|
||||
fill="#2eb67d"
|
||||
/>
|
||||
<path
|
||||
d="m1550.1 2452.5c135.3-.1 244.9-109.9 244.8-245.2.1-135.3-109.5-245.1-244.8-245.2h-244.8v245.2c-.1 135.2 109.5 245 244.8 245.2zm0-654.1h652.7c135.3-.1 244.9-109.9 244.8-245.2.2-135.3-109.4-245.1-244.7-245.3h-652.7c-135.3.1-244.9 109.9-244.8 245.2-.1 135.4 109.4 245.2 244.7 245.3z"
|
||||
fill="#ecb22e"
|
||||
/>
|
||||
<path
|
||||
d="m0 1553.2c-.1 135.3 109.5 245.1 244.8 245.2 135.3-.1 244.9-109.9 244.8-245.2v-245.2h-244.8c-135.3.1-244.9 109.9-244.8 245.2zm652.7 0v654c-.2 135.3 109.4 245.1 244.7 245.3 135.3-.1 244.9-109.9 244.8-245.2v-653.9c.2-135.3-109.4-245.1-244.7-245.3-135.4 0-244.9 109.8-244.8 245.1 0 0 0 .1 0 0"
|
||||
fill="#e01e5a"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -39,7 +39,7 @@ export const TelegramIcon = ({ className }: Props) => {
|
||||
viewBox="0 0 48 48"
|
||||
width="48px"
|
||||
height="48px"
|
||||
className="size-9"
|
||||
className={cn("size-9", className)}
|
||||
>
|
||||
<linearGradient
|
||||
id="BiF7D16UlC0RZ_VqXJHnXa"
|
||||
@@ -49,8 +49,8 @@ export const TelegramIcon = ({ className }: Props) => {
|
||||
y2="38.142"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stop-color="#33bef0" />
|
||||
<stop offset="1" stop-color="#0a85d9" />
|
||||
<stop offset="0" stopColor="#33bef0" />
|
||||
<stop offset="1" stopColor="#0a85d9" />
|
||||
</linearGradient>
|
||||
<path
|
||||
fill="url(#BiF7D16UlC0RZ_VqXJHnXa)"
|
||||
@@ -79,7 +79,7 @@ export const DiscordIcon = ({ className }: Props) => {
|
||||
viewBox="0 0 48 48"
|
||||
width="48px"
|
||||
height="48px"
|
||||
className="size-9"
|
||||
className={cn("size-9", className)}
|
||||
>
|
||||
<path
|
||||
fill="#536dfe"
|
||||
|
||||
Reference in New Issue
Block a user