mirror of
https://github.com/Buriburizaem0n/admin-frontend-domain.git
synced 2026-09-20 10:10:14 +00:00
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:
committed by
GitHub
co-authored by
hamster1963
parent
811835ffe5
commit
540bb0185d
+230
-87
@@ -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>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user