Dashboard Redesign (#48)

* feat: add user_template setting

* style: header

* style: page padding

* style: header

* feat: header now time

* style: login page

* feat: nav indicator

* style: button inset shadow

* style: footer text size

* feat: header show login_ip

* fix: error toast

* fix: frontend_templates setting

* fix: lint

* feat: pr auto format

* chore: auto-fix linting and formatting issues

---------

Co-authored-by: hamster1963 <hamster1963@users.noreply.github.com>
This commit is contained in:
仓鼠
2024-12-13 23:51:33 +08:00
committed by GitHub
co-authored by hamster1963
parent 811835ffe5
commit 540bb0185d
132 changed files with 13241 additions and 12877 deletions
+230 -87
View File
@@ -1,11 +1,8 @@
import { useEffect, useState } from "react";
import { toast } from "sonner";
import { ModelSettingResponse, settingCoverageTypes, nezhaLang } from "@/types";
import { SettingsTab } from "@/components/settings-tab";
import { z } from "zod";
import { asOptionalField } from "@/lib/utils";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { getSettings, updateSettings } from "@/api/settings"
import { SettingsTab } from "@/components/settings-tab"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import {
Form,
FormControl,
@@ -13,23 +10,25 @@ import {
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { getSettings, updateSettings } from "@/api/settings";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import { Card, CardContent } from "@/components/ui/card";
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useTranslation } from "react-i18next";
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { asOptionalField } from "@/lib/utils"
import { ModelSettingResponse, nezhaLang, settingCoverageTypes } from "@/types"
import { zodResolver } from "@hookform/resolvers/zod"
import { useEffect, useState } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
import { z } from "zod"
const settingFormSchema = z.object({
dns_servers: asOptionalField(z.string()),
@@ -47,77 +46,82 @@ const settingFormSchema = z.object({
tls: asOptionalField(z.boolean()),
enable_ip_change_notification: asOptionalField(z.boolean()),
enable_plain_ip_in_notification: asOptionalField(z.boolean()),
});
})
export default function SettingsPage() {
const { t, i18n } = useTranslation();
const [config, setConfig] = useState<ModelSettingResponse>();
const [error, setError] = useState<Error>();
const { t, i18n } = useTranslation()
const [config, setConfig] = useState<ModelSettingResponse>()
const [error, setError] = useState<Error>()
useEffect(() => {
if (error)
toast(t("Error"), {
description: t("Results.ErrorFetchingResource", { error: error.message }),
});
description: t("Results.ErrorFetchingResource", {
error: error.message,
}),
})
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [error]);
}, [error])
useEffect(() => {
(async () => {
;(async () => {
try {
const c = await getSettings();
setConfig(c);
const c = await getSettings()
setConfig(c)
} catch (e) {
if (e instanceof Error) setError(e);
if (e instanceof Error) setError(e)
}
})();
}, []);
})()
}, [])
const form = useForm<z.infer<typeof settingFormSchema>>({
resolver: zodResolver(settingFormSchema),
defaultValues: config
? {
...config,
site_name: config.site_name || "",
user_template: config.user_template || Object.keys(config.frontend_templates || {})[0] || "user-dist",
}
...config,
site_name: config.site_name || "",
user_template:
config.user_template ||
Object.keys(config.frontend_templates.filter((t) => !t.is_admin) || {})[0] ||
"user-dist",
}
: {
ip_change_notification_group_id: 0,
cover: 1,
site_name: "",
language: "",
user_template: "user-dist",
},
ip_change_notification_group_id: 0,
cover: 1,
site_name: "",
language: "",
user_template: "user-dist",
},
resetOptions: {
keepDefaultValues: false,
},
});
})
useEffect(() => {
if (config) {
form.reset(config);
form.reset(config)
}
}, [config, form]);
}, [config, form])
const onSubmit = async (values: z.infer<typeof settingFormSchema>) => {
try {
await updateSettings(values);
const newConfig = await getSettings();
setConfig(newConfig);
form.reset();
await updateSettings(values)
const newConfig = await getSettings()
setConfig(newConfig)
form.reset()
} catch (e) {
if (e instanceof Error) setError(e);
return;
if (e instanceof Error) setError(e)
return
} finally {
if (values.language != i18n.language) {
i18n.changeLanguage(values.language);
i18n.changeLanguage(values.language)
}
toast(t("Success"));
toast(t("Success"))
}
};
}
return (
<div className="px-8">
<div className="px-3">
<SettingsTab className="mt-6 mb-4 w-full" />
<div>
<Form {...form}>
@@ -171,9 +175,11 @@ export default function SettingsPage() {
<Select
value={field.value}
onValueChange={(value) => {
const template = config?.frontend_templates?.find(t => t.path === value);
const template = config?.frontend_templates?.find(
(t) => t.path === value,
)
if (template) {
form.setValue("user_template", template.path ?? '');
form.setValue("user_template", template.path)
}
}}
>
@@ -183,13 +189,22 @@ export default function SettingsPage() {
</SelectTrigger>
</FormControl>
<SelectContent>
{(config?.frontend_templates?.filter(t => !t.is_admin) || []).map((template) => (
{(
config?.frontend_templates.filter(
(t) => !t.is_admin,
) || []
).map((template) => (
<div key={template.path}>
<SelectItem value={template.path!}>
<SelectItem value={template.path}>
<div className="flex flex-col items-start gap-1">
<div className="font-medium">{template.name}</div>
<div className="font-medium">
{template.name}
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>{t("Author")}: {template.author}</span>
<span>
{t("Author")}:{" "}
{template.author}
</span>
{!template.is_official ? (
<span className="px-1.5 py-0.5 rounded-md bg-red-100 text-red-800 text-xs">
{t("Community")}
@@ -218,10 +233,102 @@ export default function SettingsPage() {
</Select>
</FormControl>
<FormMessage />
{!config?.frontend_templates?.find(t => t.path === field.value)?.is_official && (
{!config?.frontend_templates?.find(
(t) => t.path === field.value,
)?.is_official && (
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-md p-2">
<div className="font-medium text-lg mb-1">{t("CommunityThemeWarning")}</div>
<div className="text-yellow-700 dark:text-yellow-200">{t("CommunityThemeDescription")}</div>
<div className="font-medium text-lg mb-1">
{t("CommunityThemeWarning")}
</div>
<div className="text-yellow-700 dark:text-yellow-200">
{t("CommunityThemeDescription")}
</div>
</div>
)}
</FormItem>
)}
/>
<FormField
control={form.control}
name="user_template"
render={({ field }) => (
<FormItem>
<FormLabel>{t("Theme")}</FormLabel>
<FormControl>
<Select
value={field.value}
onValueChange={(value) => {
const template = config?.frontend_templates?.find(
(t) => t.path === value,
)
if (template) {
form.setValue(
"user_template",
template.path ?? "",
)
}
}}
>
<FormControl>
<SelectTrigger>
<SelectValue placeholder={t("SelectTheme")} />
</SelectTrigger>
</FormControl>
<SelectContent>
{(
config?.frontend_templates?.filter(
(t) => !t.is_admin,
) || []
).map((template) => (
<div key={template.path}>
<SelectItem value={template.path!}>
<div className="flex flex-col items-start gap-1">
<div className="font-medium">
{template.name}
</div>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span>
{t("Author")}:{" "}
{template.author}
</span>
{!template.is_official ? (
<span className="px-1.5 py-0.5 rounded-md bg-red-100 text-red-800 text-xs">
{t("Community")}
</span>
) : (
<span className="px-1.5 py-0.5 rounded-md bg-blue-100 text-blue-800 text-xs">
{t("Official")}
</span>
)}
</div>
</div>
</SelectItem>
<div className="px-8 py-1">
<a
href={template.repository}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-blue-600 hover:text-blue-800 hover:underline"
>
{template.repository}
</a>
</div>
</div>
))}
</SelectContent>
</Select>
</FormControl>
<FormMessage />
{!config?.frontend_templates?.find(
(t) => t.path === field.value,
)?.is_official && (
<div className="mt-2 text-sm text-yellow-700 dark:text-yellow-200 bg-yellow-100 dark:bg-yellow-900 border border-yellow-200 dark:border-yellow-700 rounded-md p-2">
<div className="font-medium text-lg mb-1">
{t("CommunityThemeWarning")}
</div>
<div className="text-yellow-700 dark:text-yellow-200">
{t("CommunityThemeDescription")}
</div>
</div>
)}
</FormItem>
@@ -273,10 +380,11 @@ export default function SettingsPage() {
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
<Label className="text-sm">
{t("ConfigTLS")}
</Label>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">{t("ConfigTLS")}</Label>
</div>
</FormControl>
<FormMessage />
@@ -289,7 +397,9 @@ export default function SettingsPage() {
render={({ field }) => (
<FormItem>
<FormLabel>
{t("CustomPublicDNSNameserversforDDNS") + " " + t("SeparateWithComma")}
{t("CustomPublicDNSNameserversforDDNS") +
" " +
t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input {...field} />
@@ -306,16 +416,28 @@ export default function SettingsPage() {
<FormLabel>{t("RealIPHeader")}</FormLabel>
<FormControl>
<div className="flex items-center">
<Input disabled={field.value == 'NZ::Use-Peer-IP'} className="w-1/2" placeholder="CF-Connecting-IP" {...field} />
<Checkbox checked={field.value == 'NZ::Use-Peer-IP'} className="ml-2" onCheckedChange={(checked) => {
if (checked) {
field.disabled = true;
form.setValue("real_ip_header", "NZ::Use-Peer-IP");
} else {
field.disabled = false;
form.setValue("real_ip_header", "");
}
}} />
<Input
disabled={field.value == "NZ::Use-Peer-IP"}
className="w-1/2"
placeholder="CF-Connecting-IP"
{...field}
/>
<Checkbox
checked={field.value == "NZ::Use-Peer-IP"}
className="ml-2"
onCheckedChange={(checked) => {
if (checked) {
field.disabled = true
form.setValue(
"real_ip_header",
"NZ::Use-Peer-IP",
)
} else {
field.disabled = false
form.setValue("real_ip_header", "")
}
}}
/>
<FormLabel className="font-normal ml-2">
{t("UseDirectConnectingIP")}
</FormLabel>
@@ -336,14 +458,19 @@ export default function SettingsPage() {
render={({ field }) => (
<FormItem>
<FormLabel>{t("Coverage")}</FormLabel>
<Select onValueChange={field.onChange} value={`${field.value}`}>
<Select
onValueChange={field.onChange}
value={`${field.value}`}
>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
{Object.entries(settingCoverageTypes).map(([k, v]) => (
{Object.entries(
settingCoverageTypes,
).map(([k, v]) => (
<SelectItem key={k} value={k}>
{v}
</SelectItem>
@@ -359,7 +486,11 @@ export default function SettingsPage() {
name="ignored_ip_notification"
render={({ field }) => (
<FormItem>
<FormLabel>{t("SpecificServers") + " " + t("SeparateWithComma")}</FormLabel>
<FormLabel>
{t("SpecificServers") +
" " +
t("SeparateWithComma")}
</FormLabel>
<FormControl>
<Input placeholder="1,2,3" {...field} />
</FormControl>
@@ -374,7 +505,11 @@ export default function SettingsPage() {
<FormItem>
<FormLabel>{t("NotifierGroupID")}</FormLabel>
<FormControl>
<Input placeholder="0" type="number" {...field} />
<Input
placeholder="0"
type="number"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -387,8 +522,13 @@ export default function SettingsPage() {
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
<Label className="text-sm">{t("Enable")}</Label>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
{t("Enable")}
</Label>
</div>
</FormControl>
<FormMessage />
@@ -406,7 +546,10 @@ export default function SettingsPage() {
<FormItem className="flex items-center space-x-2">
<FormControl>
<div className="flex items-center gap-2">
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
<Label className="text-sm">
{t("FullIPNotification")}
</Label>
@@ -421,5 +564,5 @@ export default function SettingsPage() {
</Form>
</div>
</div>
);
)
}