feat(ui): new Layout UI using antd

This commit is contained in:
Fu Diwei
2024-12-07 13:47:02 +08:00
parent 2facb160aa
commit d6ddf8e9f4
18 changed files with 229 additions and 204 deletions

View File

@@ -0,0 +1,101 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useToast } from "@/components/ui/use-toast";
import { getErrMessage } from "@/lib/error";
import { getPocketBase } from "@/repository/pocketbase";
const formSchema = z.object({
email: z.string().email("settings.account.email.errmsg.invalid"),
});
const Account = () => {
const { toast } = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const [changed, setChanged] = useState(false);
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
email: getPocketBase().authStore.model?.email,
},
});
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await getPocketBase().admins.update(getPocketBase().authStore.model?.id, {
email: values.email,
});
getPocketBase().authStore.clear();
toast({
title: t("settings.account.email.changed.message"),
description: t("settings.account.relogin.message"),
});
setTimeout(() => {
navigate("/login");
}, 500);
} catch (e) {
const message = getErrMessage(e);
toast({
title: t("settings.account.email.failed.message"),
description: message,
variant: "destructive",
});
}
};
return (
<>
<div className="w-full md:max-w-[35em]">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 dark:text-stone-200">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t("settings.account.email.label")}</FormLabel>
<FormControl>
<Input
placeholder={t("settings.account.email.placeholder")}
{...field}
type="email"
onChange={(e) => {
setChanged(true);
form.setValue("email", e.target.value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
{changed ? (
<Button type="submit">{t("common.update")}</Button>
) : (
<Button type="submit" disabled variant={"secondary"}>
{t("common.update")}
</Button>
)}
</div>
</form>
</Form>
</div>
</>
);
};
export default Account;

View File

@@ -0,0 +1,88 @@
import { useTranslation } from "react-i18next";
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion";
import DingTalk from "@/components/notify/DingTalk";
import Lark from "@/components/notify/Lark";
import NotifyTemplate from "@/components/notify/NotifyTemplate";
import Telegram from "@/components/notify/Telegram";
import Webhook from "@/components/notify/Webhook";
import ServerChan from "@/components/notify/ServerChan";
import Email from "@/components/notify/Email";
import Bark from "@/components/notify/Bark";
import { NotifyProvider } from "@/providers/notify";
const Notify = () => {
const { t } = useTranslation();
return (
<>
<NotifyProvider>
<div className="border rounded-sm p-5 shadow-lg">
<Accordion type={"multiple"} className="dark:text-stone-200">
<AccordionItem value="item-1" className="dark:border-stone-200">
<AccordionTrigger>{t("settings.notification.template.label")}</AccordionTrigger>
<AccordionContent>
<NotifyTemplate />
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
<div className="border rounded-md p-5 mt-7 shadow-lg">
<Accordion type={"single"} collapsible={true} className="dark:text-stone-200">
<AccordionItem value="item-email" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.email")}</AccordionTrigger>
<AccordionContent>
<Email />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-webhook" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.webhook")}</AccordionTrigger>
<AccordionContent>
<Webhook />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-dingtalk" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.dingtalk")}</AccordionTrigger>
<AccordionContent>
<DingTalk />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-lark" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.lark")}</AccordionTrigger>
<AccordionContent>
<Lark />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-telegram" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.telegram")}</AccordionTrigger>
<AccordionContent>
<Telegram />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-serverchan" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.serverchan")}</AccordionTrigger>
<AccordionContent>
<ServerChan />
</AccordionContent>
</AccordionItem>
<AccordionItem value="item-bark" className="dark:border-stone-200">
<AccordionTrigger>{t("common.provider.bark")}</AccordionTrigger>
<AccordionContent>
<Bark />
</AccordionContent>
</AccordionItem>
</Accordion>
</div>
</NotifyProvider>
</>
);
};
export default Notify;

View File

@@ -0,0 +1,136 @@
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { useToast } from "@/components/ui/use-toast";
import { getErrMessage } from "@/lib/error";
import { getPocketBase } from "@/repository/pocketbase";
const formSchema = z
.object({
oldPassword: z.string().min(10, {
message: "settings.password.password.errmsg.length",
}),
newPassword: z.string().min(10, {
message: "settings.password.password.errmsg.length",
}),
confirmPassword: z.string().min(10, {
message: "settings.password.password.errmsg.length",
}),
})
.refine((data) => data.newPassword === data.confirmPassword, {
message: "settings.password.password.errmsg.not_matched",
path: ["confirmPassword"],
});
const Password = () => {
const { toast } = useToast();
const navigate = useNavigate();
const { t } = useTranslation();
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
oldPassword: "",
newPassword: "",
confirmPassword: "",
},
});
const onSubmit = async (values: z.infer<typeof formSchema>) => {
try {
await getPocketBase().admins.authWithPassword(getPocketBase().authStore.model?.email, values.oldPassword);
} catch (e) {
const message = getErrMessage(e);
form.setError("oldPassword", { message });
}
try {
await getPocketBase().admins.update(getPocketBase().authStore.model?.id, {
password: values.newPassword,
passwordConfirm: values.confirmPassword,
});
getPocketBase().authStore.clear();
toast({
title: t("settings.password.changed.message"),
description: t("settings.account.relogin.message"),
});
setTimeout(() => {
navigate("/login");
}, 500);
} catch (e) {
const message = getErrMessage(e);
toast({
title: t("settings.password.failed.message"),
description: message,
variant: "destructive",
});
}
};
return (
<>
<div className="w-full md:max-w-[35em]">
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 dark:text-stone-200">
<FormField
control={form.control}
name="oldPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t("settings.password.current_password.label")}</FormLabel>
<FormControl>
<Input placeholder={t("settings.password.current_password.placeholder")} {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="newPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t("settings.password.new_password.label")}</FormLabel>
<FormControl>
<Input placeholder={t("settings.password.new_password.placeholder")} {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
render={({ field }) => (
<FormItem>
<FormLabel>{t("settings.password.confirm_password.label")}</FormLabel>
<FormControl>
<Input placeholder={t("settings.password.confirm_password.placeholder")} {...field} type="password" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-end">
<Button type="submit">{t("common.update")}</Button>
</div>
</form>
</Form>
</div>
</>
);
};
export default Password;

View File

@@ -0,0 +1,445 @@
import { useContext, useEffect, useState, createContext } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { cn } from "@/components/ui/utils";
import { Button } from "@/components/ui/button";
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { useToast } from "@/components/ui/use-toast";
import { getErrMessage } from "@/lib/error";
import { SSLProvider as SSLProviderType, SSLProviderSetting, Setting } from "@/domain/settings";
import { getSetting, update } from "@/repository/settings";
import { produce } from "immer";
type SSLProviderContext = {
setting: Setting<SSLProviderSetting>;
onSubmit: (data: Setting<SSLProviderSetting>) => void;
setConfig: (config: Setting<SSLProviderSetting>) => void;
};
const Context = createContext({} as SSLProviderContext);
export const useSSLProviderContext = () => {
return useContext(Context);
};
const SSLProvider = () => {
const { t } = useTranslation();
const [config, setConfig] = useState<Setting<SSLProviderSetting>>({
id: "",
content: {
provider: "letsencrypt",
config: {},
},
});
const { toast } = useToast();
useEffect(() => {
const fetchData = async () => {
const setting = await getSetting<SSLProviderSetting>("ssl-provider");
if (setting) {
setConfig(setting);
}
};
fetchData();
}, []);
const setProvider = (val: SSLProviderType) => {
const newData = produce(config, (draft) => {
if (draft.content) {
draft.content.provider = val;
} else {
draft.content = {
provider: val,
config: {},
};
}
});
setConfig(newData);
};
const getOptionCls = (val: string) => {
if (config.content?.provider === val) {
return "border-primary dark:border-primary";
}
return "";
};
const onSubmit = async (data: Setting<SSLProviderSetting>) => {
try {
console.log(data);
const resp = await update({ ...data });
setConfig(resp);
toast({
title: t("common.update.succeeded.message"),
description: t("common.update.succeeded.message"),
});
} catch (e) {
const message = getErrMessage(e);
toast({
title: t("common.update.failed.message"),
description: message,
variant: "destructive",
});
}
};
return (
<>
<Context.Provider value={{ onSubmit, setConfig, setting: config }}>
<div className="w-full md:max-w-[35em]">
<Label className="dark:text-stone-200">{t("common.text.ca")}</Label>
<RadioGroup
className="flex mt-3 dark:text-stone-200"
onValueChange={(val) => {
setProvider(val as SSLProviderType);
}}
value={config.content?.provider}
>
<div className="flex items-center space-x-2 ">
<RadioGroupItem value="letsencrypt" id="letsencrypt" />
<Label htmlFor="letsencrypt">
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer dark:border-stone-700", getOptionCls("letsencrypt"))}>
<img src={"/imgs/providers/letsencrypt.svg"} className="h-6" />
<div>{"Let's Encrypt"}</div>
</div>
</Label>
</div>
<div className="flex items-center space-x-2 ">
<RadioGroupItem value="zerossl" id="zerossl" />
<Label htmlFor="zerossl">
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer dark:border-stone-700", getOptionCls("zerossl"))}>
<img src={"/imgs/providers/zerossl.svg"} className="h-6" />
<div>{"ZeroSSL"}</div>
</div>
</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="gts" id="gts" />
<Label htmlFor="gts">
<div className={cn("flex items-center space-x-2 border p-2 rounded cursor-pointer dark:border-stone-700", getOptionCls("gts"))}>
<img src={"/imgs/providers/google.svg"} className="h-6" />
<div>{"Google Trust Services"}</div>
</div>
</Label>
</div>
</RadioGroup>
<SSLProviderForm kind={config.content?.provider ?? ""} />
</div>
</Context.Provider>
</>
);
};
const SSLProviderForm = ({ kind }: { kind: string }) => {
const getForm = () => {
switch (kind) {
case "zerossl":
return <SSLProviderZeroSSLForm />;
case "gts":
return <SSLProviderGtsForm />;
default:
return <SSLProviderLetsEncryptForm />;
}
};
return (
<>
<div className="mt-5">{getForm()}</div>
</>
);
};
const getConfigStr = (content: SSLProviderSetting, kind: string, key: string) => {
if (!content.config) {
return "";
}
if (!content.config[kind]) {
return "";
}
return content.config[kind][key] ?? "";
};
const SSLProviderLetsEncryptForm = () => {
const { t } = useTranslation();
const { setting, onSubmit } = useSSLProviderContext();
const formSchema = z.object({
kind: z.literal("letsencrypt"),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
kind: "letsencrypt",
},
});
const onLocalSubmit = async (data: z.infer<typeof formSchema>) => {
const newData = produce(setting, (draft) => {
if (!draft.content) {
draft.content = {
provider: data.kind,
config: {
letsencrypt: {},
},
};
}
});
onSubmit(newData);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onLocalSubmit)} className="space-y-8 dark:text-stone-200">
<FormField
control={form.control}
name="kind"
render={({ field }) => (
<FormItem hidden>
<FormLabel>kind</FormLabel>
<FormControl>
<Input {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.update")}</Button>
</div>
</form>
</Form>
);
};
const SSLProviderZeroSSLForm = () => {
const { t } = useTranslation();
const { setting, onSubmit } = useSSLProviderContext();
const formSchema = z.object({
kind: z.literal("zerossl"),
eabKid: z.string().min(1, { message: t("settings.ca.eab_kid_hmac_key.errmsg.empty") }),
eabHmacKey: z.string().min(1, { message: t("settings.ca.eab_kid_hmac_key.errmsg.empty") }),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
kind: "zerossl",
eabKid: "",
eabHmacKey: "",
},
});
useEffect(() => {
if (setting.content) {
const content = setting.content;
form.reset({
eabKid: getConfigStr(content, "zerossl", "eabKid"),
eabHmacKey: getConfigStr(content, "zerossl", "eabHmacKey"),
});
}
}, [setting]);
const onLocalSubmit = async (data: z.infer<typeof formSchema>) => {
const newData = produce(setting, (draft) => {
if (!draft.content) {
draft.content = {
provider: "zerossl",
config: {
zerossl: {},
},
};
}
draft.content.config.zerossl = {
eabKid: data.eabKid,
eabHmacKey: data.eabHmacKey,
};
});
onSubmit(newData);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onLocalSubmit)} className="space-y-8 dark:text-stone-200">
<FormField
control={form.control}
name="kind"
render={({ field }) => (
<FormItem hidden>
<FormLabel>kind</FormLabel>
<FormControl>
<Input {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabKid"
render={({ field }) => (
<FormItem>
<FormLabel>EAB_KID</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_kid.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabHmacKey"
render={({ field }) => (
<FormItem>
<FormLabel>EAB_HMAC_KEY</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_hmac_key.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.update")}</Button>
</div>
</form>
</Form>
);
};
const SSLProviderGtsForm = () => {
const { t } = useTranslation();
const { setting, onSubmit } = useSSLProviderContext();
const formSchema = z.object({
kind: z.literal("gts"),
eabKid: z.string().min(1, { message: t("settings.ca.eab_kid_hmac_key.errmsg.empty") }),
eabHmacKey: z.string().min(1, { message: t("settings.ca.eab_kid_hmac_key.errmsg.empty") }),
});
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
kind: "gts",
eabKid: "",
eabHmacKey: "",
},
});
useEffect(() => {
if (setting.content) {
const content = setting.content;
form.reset({
eabKid: getConfigStr(content, "gts", "eabKid"),
eabHmacKey: getConfigStr(content, "gts", "eabHmacKey"),
});
}
}, [setting]);
const onLocalSubmit = async (data: z.infer<typeof formSchema>) => {
const newData = produce(setting, (draft) => {
if (!draft.content) {
draft.content = {
provider: "gts",
config: {
zerossl: {},
},
};
}
draft.content.config.gts = {
eabKid: data.eabKid,
eabHmacKey: data.eabHmacKey,
};
});
onSubmit(newData);
};
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onLocalSubmit)} className="space-y-8 dark:text-stone-200">
<FormField
control={form.control}
name="kind"
render={({ field }) => (
<FormItem hidden>
<FormLabel>kind</FormLabel>
<FormControl>
<Input {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabKid"
render={({ field }) => (
<FormItem>
<FormLabel>EAB_KID</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_kid.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="eabHmacKey"
render={({ field }) => (
<FormItem>
<FormLabel>EAB_HMAC_KEY</FormLabel>
<FormControl>
<Input placeholder={t("settings.ca.eab_hmac_key.errmsg.empty")} {...field} type="text" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormMessage />
<div className="flex justify-end">
<Button type="submit">{t("common.update")}</Button>
</div>
</form>
</Form>
);
};
export default SSLProvider;