Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bbd44f0107 | |||
| c65a44c2c7 | |||
| 715d0113ca | |||
| 0f15fb82cb | |||
| 320a7dc342 | |||
| 42ba9e88ad | |||
| 4a002333d4 | |||
| 6384237782 | |||
| 557c5cdc29 | |||
| f9d7736e48 | |||
| 5b64389544 | |||
| 23833b47d1 | |||
| c3eff0a0e7 | |||
| 6f7bc37233 | |||
| b2e8fadaf3 | |||
| 315c8f9afe |
+57
-4
@@ -1,19 +1,72 @@
|
||||
<a name="readme-top"></a>
|
||||
# Changelog
|
||||
|
||||
# [1.0.0-beta.14](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.13...v1.0.0-beta.14) (2025-02-21)
|
||||
# [1.0.0-beta.20](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.19...v1.0.0-beta.20) (2025-02-23)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **locales**: Fixed description in multilingual files, updated text related to email registration functionality ([c356bc2](https://github.com/perfect-panel/ppanel-web/commit/c356bc2))
|
||||
* **auth**: Add error handling to form submission and reset Turnstile on failure ([715d011](https://github.com/perfect-panel/ppanel-web/commit/715d011))
|
||||
* **subscribe**: Update subscription domain placeholder to include examples; improve site name retrieval in global store ([c65a44c](https://github.com/perfect-panel/ppanel-web/commit/c65a44c))
|
||||
|
||||
# [1.0.0-beta.19](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.18...v1.0.0-beta.19) (2025-02-23)
|
||||
|
||||
|
||||
### ✨ Features
|
||||
|
||||
* **form**: Make version field optional and set default value; update site domain placeholder for clarity ([42ba9e8](https://github.com/perfect-panel/ppanel-web/commit/42ba9e8))
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **auth**: Refactor forms to use Turnstile ref for reset functionality ([320a7dc](https://github.com/perfect-panel/ppanel-web/commit/320a7dc))
|
||||
|
||||
# [1.0.0-beta.18](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.17...v1.0.0-beta.18) (2025-02-22)
|
||||
|
||||
|
||||
### ✨ Features
|
||||
|
||||
* **platform**: Update platform naming and add keywords and custom HTML fields ([6384237](https://github.com/perfect-panel/ppanel-web/commit/6384237))
|
||||
* **site**: Added localization support for custom HTML and keyword fields ([f9d7736](https://github.com/perfect-panel/ppanel-web/commit/f9d7736))
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **locales**: Update custom HTML description for clarity across multiple languages ([557c5cd](https://github.com/perfect-panel/ppanel-web/commit/557c5cd))
|
||||
|
||||
<a name="readme-top"></a>
|
||||
|
||||
# Changelog
|
||||
|
||||
# [1.0.0-beta.17](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.16...v1.0.0-beta.17) (2025-02-21)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **auth**: Refactor reset password form to simplify code input and update placeholder text ([23833b4](https://github.com/perfect-panel/ppanel-web/commit/23833b4))
|
||||
|
||||
# [1.0.0-beta.16](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.15...v1.0.0-beta.16) (2025-02-21)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **auth**: Simplify email verification code input rendering ([6f7bc37](https://github.com/perfect-panel/ppanel-web/commit/6f7bc37))
|
||||
|
||||
# [1.0.0-beta.15](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.14...v1.0.0-beta.15) (2025-02-21)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **profile**: Restore filter to ensure only valid OAuth accounts are shown ([315c8f9](https://github.com/perfect-panel/ppanel-web/commit/315c8f9))
|
||||
|
||||
# [1.0.0-beta.14](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.13...v1.0.0-beta.14) (2025-02-21)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **locales**: Fixed description in multilingual files, updated text related to email registration functionality ([c356bc2](https://github.com/perfect-panel/ppanel-web/commit/c356bc2))
|
||||
|
||||
# [1.0.0-beta.13](https://github.com/perfect-panel/ppanel-web/compare/v1.0.0-beta.12...v1.0.0-beta.13) (2025-02-21)
|
||||
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
* **api**: Fix type error in API request and add return URL parameter ([ee286dd](https://github.com/perfect-panel/ppanel-web/commit/ee286dd))
|
||||
- **api**: Fix type error in API request and add return URL parameter ([ee286dd](https://github.com/perfect-panel/ppanel-web/commit/ee286dd))
|
||||
|
||||
<a name="readme-top"></a>
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/
|
||||
import { Input } from '@workspace/ui/components/input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Dispatch, SetStateAction, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import CloudFlareTurnstile from '../turnstile';
|
||||
import CloudFlareTurnstile, { TurnstileRef } from '../turnstile';
|
||||
|
||||
export default function LoginForm({
|
||||
loading,
|
||||
@@ -38,10 +38,19 @@ export default function LoginForm({
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const turnstile = useRef<TurnstileRef>(null);
|
||||
const handleSubmit = form.handleSubmit((data) => {
|
||||
try {
|
||||
onSubmit(data);
|
||||
} catch (error) {
|
||||
turnstile.current?.reset();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
||||
<form onSubmit={handleSubmit} className='grid gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
@@ -73,7 +82,7 @@ export default function LoginForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CloudFlareTurnstile id='login' {...field} />
|
||||
<CloudFlareTurnstile id='login' {...field} ref={turnstile} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -6,11 +6,11 @@ import { Input } from '@workspace/ui/components/input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { Markdown } from '@workspace/ui/custom-components/markdown';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Dispatch, SetStateAction, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import SendCode from '../send-code';
|
||||
import CloudFlareTurnstile from '../turnstile';
|
||||
import CloudFlareTurnstile, { TurnstileRef } from '../turnstile';
|
||||
|
||||
export default function RegisterForm({
|
||||
loading,
|
||||
@@ -31,11 +31,10 @@ export default function RegisterForm({
|
||||
|
||||
const handleCheckUser = async (email: string) => {
|
||||
try {
|
||||
if (!auth.email.enable_verify) return true;
|
||||
const domain = email.split('@')[1];
|
||||
const isValid =
|
||||
!auth.email.enable_verify ||
|
||||
auth.email?.domain_suffix_list.split('\n').includes(domain || '');
|
||||
return !isValid;
|
||||
const isValid = auth.email?.domain_suffix_list.split('\n').includes(domain || '');
|
||||
return isValid;
|
||||
} catch (error) {
|
||||
console.log('Error checking user:', error);
|
||||
return false;
|
||||
@@ -53,7 +52,7 @@ export default function RegisterForm({
|
||||
password: z.string(),
|
||||
repeat_password: z.string(),
|
||||
code: auth.email.enable_verify ? z.string() : z.string().nullish(),
|
||||
invite: invite.forced_invite ? z.string() : z.string().nullish(),
|
||||
invite: invite.forced_invite ? z.string().min(1) : z.string().nullish(),
|
||||
cf_token:
|
||||
verify.enable_register_verify && verify.turnstile_site_key
|
||||
? z.string()
|
||||
@@ -77,13 +76,22 @@ export default function RegisterForm({
|
||||
},
|
||||
});
|
||||
|
||||
const turnstile = useRef<TurnstileRef>(null);
|
||||
const handleSubmit = form.handleSubmit((data) => {
|
||||
try {
|
||||
onSubmit(data);
|
||||
} catch (error) {
|
||||
turnstile.current?.reset();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{auth.register.stop_register ? (
|
||||
<Markdown>{t('message')}</Markdown>
|
||||
) : (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
||||
<form onSubmit={handleSubmit} className='grid gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
@@ -114,7 +122,12 @@ export default function RegisterForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input placeholder='Enter password again...' type='password' {...field} />
|
||||
<Input
|
||||
disabled={loading}
|
||||
placeholder='Enter password again...'
|
||||
type='password'
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
@@ -129,6 +142,7 @@ export default function RegisterForm({
|
||||
<FormControl>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
disabled={loading}
|
||||
placeholder='Enter code...'
|
||||
type='text'
|
||||
{...field}
|
||||
@@ -172,7 +186,7 @@ export default function RegisterForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CloudFlareTurnstile id='register' {...field} />
|
||||
<CloudFlareTurnstile id='register' {...field} ref={turnstile} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -5,11 +5,11 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/
|
||||
import { Input } from '@workspace/ui/components/input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Dispatch, SetStateAction, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import SendCode from '../send-code';
|
||||
import CloudFlareTurnstile from '../turnstile';
|
||||
import CloudFlareTurnstile, { TurnstileRef } from '../turnstile';
|
||||
|
||||
export default function ResetForm({
|
||||
loading,
|
||||
@@ -43,10 +43,19 @@ export default function ResetForm({
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const turnstile = useRef<TurnstileRef>(null);
|
||||
const handleSubmit = form.handleSubmit((data) => {
|
||||
try {
|
||||
onSubmit(data);
|
||||
} catch (error) {
|
||||
turnstile.current?.reset();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
||||
<form onSubmit={handleSubmit} className='grid gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
@@ -61,45 +70,43 @@ export default function ResetForm({
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
name='code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input placeholder='Enter your password...' type='password' {...field} />
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
disabled={loading}
|
||||
placeholder='Enter code...'
|
||||
type='text'
|
||||
{...field}
|
||||
value={field.value as string}
|
||||
/>
|
||||
<SendCode
|
||||
type='email'
|
||||
params={{
|
||||
...form.getValues(),
|
||||
type: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<Input placeholder='Enter your new password...' type='password' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{auth?.email?.enable_verify && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
placeholder='Enter code...'
|
||||
type='text'
|
||||
{...field}
|
||||
value={field.value as string}
|
||||
/>
|
||||
<SendCode
|
||||
type='email'
|
||||
params={{
|
||||
...form.getValues(),
|
||||
type: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{verify.enable_reset_password_verify && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
@@ -107,7 +114,7 @@ export default function ResetForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CloudFlareTurnstile id='reset' {...field} />
|
||||
<CloudFlareTurnstile id='reset' {...field} ref={turnstile} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -1,26 +1,38 @@
|
||||
'use client';
|
||||
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
import { useLocale } from 'next-intl';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect } from 'react';
|
||||
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||
import Turnstile, { useTurnstile } from 'react-turnstile';
|
||||
|
||||
export default function CloudFlareTurnstile({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
id?: string;
|
||||
value?: null | string;
|
||||
onChange: (value?: string) => void;
|
||||
}) {
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
|
||||
export type TurnstileRef = {
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
const CloudFlareTurnstile = forwardRef<
|
||||
TurnstileRef,
|
||||
{
|
||||
id?: string;
|
||||
value?: null | string;
|
||||
onChange: (value?: string) => void;
|
||||
}
|
||||
>(function CloudFlareTurnstile({ id, value, onChange }, ref) {
|
||||
const { common } = useGlobalStore();
|
||||
const { verify } = common;
|
||||
const { resolvedTheme } = useTheme();
|
||||
const locale = useLocale();
|
||||
const turnstile = useTurnstile();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
reset: () => turnstile.reset(),
|
||||
}),
|
||||
[turnstile],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (value === '') {
|
||||
turnstile.reset();
|
||||
@@ -51,4 +63,6 @@ export default function CloudFlareTurnstile({
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default CloudFlareTurnstile;
|
||||
|
||||
@@ -50,7 +50,7 @@ const defaultValues = {
|
||||
|
||||
const versionSchema = z.object({
|
||||
url: z.string(),
|
||||
version: z.string(),
|
||||
version: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
is_default: z.boolean().optional(),
|
||||
});
|
||||
@@ -221,6 +221,7 @@ export default function SubscribeAppForm<
|
||||
name: 'version',
|
||||
type: 'text',
|
||||
placeholder: t('version'),
|
||||
defaultValue: '1.0.0',
|
||||
},
|
||||
{
|
||||
name: 'description',
|
||||
@@ -251,7 +252,10 @@ export default function SubscribeAppForm<
|
||||
]}
|
||||
value={field.value}
|
||||
onChange={(value) => {
|
||||
form.setValue(field.name, value);
|
||||
form.setValue(
|
||||
field.name,
|
||||
value.filter((item) => item.url),
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
@@ -92,7 +92,7 @@ export default function SubscribeConfig() {
|
||||
<TableCell className='text-right'>
|
||||
<Textarea
|
||||
className='h-52'
|
||||
placeholder={t('subscriptionDomainPlaceholder')}
|
||||
placeholder={`${t('subscriptionDomainPlaceholder')}\nexample.com\nwww.example.com`}
|
||||
defaultValue={data?.subscribe_domain}
|
||||
onBlur={(e) => {
|
||||
updateConfig('subscribe_domain', e.target.value);
|
||||
|
||||
@@ -85,6 +85,35 @@ export default function Site() {
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<Label>{t('keywords')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('keywordsDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<EnhancedInput
|
||||
placeholder={t('keywordsDescription')}
|
||||
value={data?.keywords}
|
||||
onValueBlur={(value) => updateConfig('keywords', value)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className='align-top'>
|
||||
<Label>{t('customHtml')}</Label>
|
||||
<p className='text-muted-foreground text-xs'>{t('customHtmlDescription')}</p>
|
||||
</TableCell>
|
||||
<TableCell className='text-right'>
|
||||
<Textarea
|
||||
className='h-52'
|
||||
placeholder={t('customHtmlDescription')}
|
||||
defaultValue={data?.custom_html}
|
||||
onBlur={(e) => {
|
||||
updateConfig('custom_html', e.target.value);
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell className='align-top'>
|
||||
<Label>{t('siteDomain')}</Label>
|
||||
@@ -93,7 +122,7 @@ export default function Site() {
|
||||
<TableCell className='text-right'>
|
||||
<Textarea
|
||||
className='h-52'
|
||||
placeholder={t('siteDomainPlaceholder')}
|
||||
placeholder={`${t('siteDomainPlaceholder')}\nexample.com\nwww.example.com`}
|
||||
defaultValue={data?.host}
|
||||
onBlur={(e) => {
|
||||
updateConfig('host', e.target.value);
|
||||
|
||||
@@ -14,6 +14,8 @@ export const useGlobalStore = create<GlobalStore>((set) => ({
|
||||
site_name: '',
|
||||
site_desc: '',
|
||||
site_logo: '',
|
||||
keywords: '',
|
||||
custom_html: '',
|
||||
},
|
||||
verify: {
|
||||
turnstile_site_key: '',
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Úspěšně uloženo"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Vlastní HTML",
|
||||
"customHtmlDescription": "Vlastní HTML kód, který bude vložen na konec tagu <body> na stránce.",
|
||||
"keywords": "Klíčová slova",
|
||||
"keywordsDescription": "Používá se pro SEO účely",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Používá se k zobrazení místa, kde je potřeba zobrazit LOGO",
|
||||
"logoPlaceholder": "Zadejte URL adresu LOGA, nekončete '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Erfolgreich gespeichert"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Benutzerdefiniertes HTML",
|
||||
"customHtmlDescription": "Benutzerdefinierter HTML-Code, der am Ende des <body>-Tags der Seite eingefügt werden soll.",
|
||||
"keywords": "Schlüsselwörter",
|
||||
"keywordsDescription": "Für SEO-Zwecke verwendet",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Position zur Anzeige des LOGOs",
|
||||
"logoPlaceholder": "Bitte geben Sie die URL-Adresse des LOGOs ein, ohne '/' am Ende",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Save Successful"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Custom HTML",
|
||||
"customHtmlDescription": "Custom HTML code to be injected into the bottom of the site's <body> tag.",
|
||||
"keywords": "Keywords",
|
||||
"keywordsDescription": "Used for SEO purposes",
|
||||
"logo": "Logo",
|
||||
"logoDescription": "Used for displaying the logo in designated locations",
|
||||
"logoPlaceholder": "Enter the URL of the logo, without ending with '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Guardado exitosamente"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "HTML personalizado",
|
||||
"customHtmlDescription": "Código HTML personalizado que se inyectará en la parte inferior de la etiqueta <body> del sitio.",
|
||||
"keywords": "Palabras clave",
|
||||
"keywordsDescription": "Utilizado para fines de SEO",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Se utiliza para mostrar la ubicación donde se debe mostrar el LOGO",
|
||||
"logoPlaceholder": "Por favor, introduce la URL del LOGO, no termines con '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Guardado exitosamente"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "HTML personalizado",
|
||||
"customHtmlDescription": "Código HTML personalizado que se inyectará al final de la etiqueta <body> del sitio.",
|
||||
"keywords": "Palabras clave",
|
||||
"keywordsDescription": "Utilizado para propósitos de SEO",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Usado para mostrar la ubicación donde se debe exhibir el LOGO",
|
||||
"logoPlaceholder": "Por favor, ingresa la URL del LOGO, no termines con '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "ذخیرهسازی موفقیتآمیز"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "HTML سفارشی",
|
||||
"customHtmlDescription": "کد HTML سفارشی که باید در انتهای تگ <body> سایت وارد شود.",
|
||||
"keywords": "کلمات کلیدی",
|
||||
"keywordsDescription": "برای اهداف سئو استفاده میشود",
|
||||
"logo": "لوگو",
|
||||
"logoDescription": "برای نمایش لوگو در مکانهای تعیینشده استفاده میشود",
|
||||
"logoPlaceholder": "آدرس URL لوگو را وارد کنید، بدون '/' در انتها",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Tallennus onnistui"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Mukautettu HTML",
|
||||
"customHtmlDescription": "Mukautettu HTML-koodi, joka lisätään sivuston <body> -tagin loppuun.",
|
||||
"keywords": "Avainsanat",
|
||||
"keywordsDescription": "Käytetään SEO-tarkoituksiin",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Näyttääksesi paikan, jossa LOGO tulee näkyä",
|
||||
"logoPlaceholder": "Syötä LOGO:n URL-osoite, älä lopeta '/'-merkillä",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Enregistrement réussi"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "HTML personnalisé",
|
||||
"customHtmlDescription": "Code HTML personnalisé à injecter dans le bas de la balise <body> du site.",
|
||||
"keywords": "Mots-clés",
|
||||
"keywordsDescription": "Utilisé à des fins de référencement",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Utilisé pour afficher l'emplacement où le LOGO doit être montré",
|
||||
"logoPlaceholder": "Veuillez entrer l'URL du LOGO, ne pas terminer par '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "सफलतापूर्वक सहेजा गया"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "कस्टम HTML",
|
||||
"customHtmlDescription": "साइट के <body> टैग के नीचे इंजेक्ट करने के लिए कस्टम HTML कोड।",
|
||||
"keywords": "कीवर्ड",
|
||||
"keywordsDescription": "SEO उद्देश्यों के लिए उपयोग किया जाता है",
|
||||
"logo": "लोगो",
|
||||
"logoDescription": "लोगो प्रदर्शित करने के लिए आवश्यक स्थान",
|
||||
"logoPlaceholder": "कृपया लोगो का URL पता दर्ज करें, '/' के साथ समाप्त न करें",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Sikeres mentés"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Egyedi HTML",
|
||||
"customHtmlDescription": "Egyedi HTML kód, amelyet a webhely <body> címkéjének aljára kell beilleszteni.",
|
||||
"keywords": "Kulcsszavak",
|
||||
"keywordsDescription": "SEO célokra használva",
|
||||
"logo": "LOGÓ",
|
||||
"logoDescription": "A LOGÓ megjelenítésére szolgáló hely",
|
||||
"logoPlaceholder": "Kérjük, adja meg a LOGÓ URL-címét, ne végződjön '/' jellel",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "保存に成功しました"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "カスタムHTML",
|
||||
"customHtmlDescription": "サイトの <body> タグの下部に挿入されるカスタム HTML コード。",
|
||||
"keywords": "キーワード",
|
||||
"keywordsDescription": "SEO目的で使用されます",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "LOGOを表示する必要がある場所に使用されます",
|
||||
"logoPlaceholder": "LOGOのURLアドレスを入力してください。末尾に'/'を付けないでください",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "저장 성공"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "사용자 정의 HTML",
|
||||
"customHtmlDescription": "사이트의 <body> 태그 하단에 삽입될 사용자 정의 HTML 코드입니다.",
|
||||
"keywords": "키워드",
|
||||
"keywordsDescription": "SEO 목적으로 사용됨",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "LOGO를 표시해야 하는 위치에 사용됩니다",
|
||||
"logoPlaceholder": "LOGO의 URL 주소를 입력하세요. '/'로 끝나지 않도록 하세요",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Lagring vellykket"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Egendefinert HTML",
|
||||
"customHtmlDescription": "Egendefinert HTML-kode som skal settes inn i bunnen av nettstedets <body>-tag.",
|
||||
"keywords": "Nøkkelord",
|
||||
"keywordsDescription": "Brukes til SEO-formål",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Brukes til å vise hvor LOGO skal vises",
|
||||
"logoPlaceholder": "Vennligst skriv inn URL-adressen til LOGO, ikke avslutt med '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Zapisano pomyślnie"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Niestandardowy HTML",
|
||||
"customHtmlDescription": "Niestandardowy kod HTML, który ma być wstrzyknięty na końcu tagu <body> witryny.",
|
||||
"keywords": "Słowa kluczowe",
|
||||
"keywordsDescription": "Używane do celów SEO",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Miejsce do wyświetlania LOGO",
|
||||
"logoPlaceholder": "Wprowadź adres URL LOGO, nie kończ na '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Salvo com sucesso"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "HTML Personalizado",
|
||||
"customHtmlDescription": "Código HTML personalizado a ser inserido no final da tag <body> do site.",
|
||||
"keywords": "Palavras-chave",
|
||||
"keywordsDescription": "Usado para fins de SEO",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Usado para exibir a posição onde o LOGO precisa ser mostrado",
|
||||
"logoPlaceholder": "Por favor, insira o URL do LOGO, não termine com '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Salvare reușită"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "HTML personalizat",
|
||||
"customHtmlDescription": "Cod HTML personalizat care va fi injectat în partea de jos a etichetei <body> a site-ului.",
|
||||
"keywords": "Cuvinte cheie",
|
||||
"keywordsDescription": "Utilizate în scopuri SEO",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Utilizat pentru a afișa locația unde trebuie să fie prezentat LOGO-ul",
|
||||
"logoPlaceholder": "Vă rugăm să introduceți adresa URL a LOGO-ului, fără să se termine cu '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Успешно сохранено"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Пользовательский HTML",
|
||||
"customHtmlDescription": "Пользовательский HTML-код, который будет вставлен в конец тега <body> сайта.",
|
||||
"keywords": "Ключевые слова",
|
||||
"keywordsDescription": "Используется для SEO",
|
||||
"logo": "ЛОГОТИП",
|
||||
"logoDescription": "Используется для отображения места, где необходимо показать ЛОГОТИП",
|
||||
"logoPlaceholder": "Введите URL-адрес ЛОГОТИПА, не заканчивайте на '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "บันทึกสำเร็จ"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "HTML ที่กำหนดเอง",
|
||||
"customHtmlDescription": "รหัส HTML ที่กำหนดเองเพื่อแทรกลงในส่วนล่างของแท็ก <body> ของเว็บไซต์.",
|
||||
"keywords": "คำสำคัญ",
|
||||
"keywordsDescription": "ใช้สำหรับวัตถุประสงค์ SEO",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "ใช้สำหรับแสดงตำแหน่งที่ต้องการแสดง LOGO",
|
||||
"logoPlaceholder": "กรุณาใส่ URL ของ LOGO โดยไม่ต้องลงท้ายด้วย '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Başarıyla kaydedildi"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Özel HTML",
|
||||
"customHtmlDescription": "Site'in <body> etiketinin altına yerleştirilecek özel HTML kodu.",
|
||||
"keywords": "Anahtar Kelimeler",
|
||||
"keywordsDescription": "SEO amaçları için kullanılır",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "LOGO'nun gösterilmesi gereken yeri belirtir",
|
||||
"logoPlaceholder": "Lütfen LOGO'nun URL adresini girin, '/' ile bitmemelidir",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Успішно збережено"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "Користувацький HTML",
|
||||
"customHtmlDescription": "Користувацький HTML-код, який буде вставлено в кінець тегу <body> сайту.",
|
||||
"keywords": "Ключові слова",
|
||||
"keywordsDescription": "Використовується для SEO",
|
||||
"logo": "ЛОГО",
|
||||
"logoDescription": "Використовується для відображення місця, де потрібно показати ЛОГО",
|
||||
"logoPlaceholder": "Будь ласка, введіть URL-адресу ЛОГО, не закінчуйте на '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "Lưu thành công"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "HTML tùy chỉnh",
|
||||
"customHtmlDescription": "Mã HTML tùy chỉnh để được chèn vào cuối thẻ <body> của trang.",
|
||||
"keywords": "Từ khóa",
|
||||
"keywordsDescription": "Dùng cho mục đích SEO",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "Dùng để hiển thị vị trí cần hiển thị LOGO",
|
||||
"logoPlaceholder": "Vui lòng nhập địa chỉ URL của LOGO, không kết thúc bằng '/'",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "保存成功"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "自定义HTML",
|
||||
"customHtmlDescription": "要注入到网站 <body> 标签底部的自定义 HTML 代码。",
|
||||
"keywords": "关键词",
|
||||
"keywordsDescription": "用于搜索引擎优化",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "用于显示需要展示 LOGO 的位置",
|
||||
"logoPlaceholder": "请输入 LOGO 的 URL 地址,不要以 '/' 结尾",
|
||||
|
||||
@@ -9,6 +9,10 @@
|
||||
"saveSuccess": "保存成功"
|
||||
},
|
||||
"site": {
|
||||
"customHtml": "自定義HTML",
|
||||
"customHtmlDescription": "自定義 HTML 代碼,將注入到網站 <body> 標籤的底部。",
|
||||
"keywords": "關鍵字",
|
||||
"keywordsDescription": "用於SEO目的",
|
||||
"logo": "LOGO",
|
||||
"logoDescription": "用於顯示需要展示 LOGO 的位置",
|
||||
"logoPlaceholder": "請輸入 LOGO 的 URL 地址,不要以 '/' 結尾",
|
||||
|
||||
+3
-1
@@ -37,7 +37,7 @@ declare namespace API {
|
||||
|
||||
type ApplicationPlatform = {
|
||||
ios?: ApplicationVersion[];
|
||||
mac?: ApplicationVersion[];
|
||||
macos?: ApplicationVersion[];
|
||||
linux?: ApplicationVersion[];
|
||||
android?: ApplicationVersion[];
|
||||
windows?: ApplicationVersion[];
|
||||
@@ -1010,6 +1010,8 @@ declare namespace API {
|
||||
site_name: string;
|
||||
site_desc: string;
|
||||
site_logo: string;
|
||||
keywords: string;
|
||||
custom_html: string;
|
||||
};
|
||||
|
||||
type SortItem = {
|
||||
|
||||
+3
-1
@@ -35,7 +35,7 @@ declare namespace API {
|
||||
|
||||
type ApplicationPlatform = {
|
||||
ios?: ApplicationVersion[];
|
||||
mac?: ApplicationVersion[];
|
||||
macos?: ApplicationVersion[];
|
||||
linux?: ApplicationVersion[];
|
||||
android?: ApplicationVersion[];
|
||||
windows?: ApplicationVersion[];
|
||||
@@ -431,6 +431,8 @@ declare namespace API {
|
||||
site_name: string;
|
||||
site_desc: string;
|
||||
site_logo: string;
|
||||
keywords: string;
|
||||
custom_html: string;
|
||||
};
|
||||
|
||||
type SortItem = {
|
||||
|
||||
@@ -45,7 +45,7 @@ import Subscribe from '../subscribe/page';
|
||||
|
||||
const platforms: (keyof API.ApplicationPlatform)[] = [
|
||||
'windows',
|
||||
'mac',
|
||||
'macos',
|
||||
'linux',
|
||||
'ios',
|
||||
'android',
|
||||
@@ -106,7 +106,7 @@ export default function Content() {
|
||||
icon={`${
|
||||
{
|
||||
windows: 'mdi:microsoft-windows',
|
||||
mac: 'uil:apple',
|
||||
macos: 'uil:apple',
|
||||
linux: 'uil:linux',
|
||||
ios: 'simple-icons:ios',
|
||||
android: 'uil:android',
|
||||
@@ -261,6 +261,7 @@ export default function Content() {
|
||||
toast.success(
|
||||
<>
|
||||
<p>{t('copySuccess')}</p>
|
||||
<br />
|
||||
<p>{t('manualImportMessage')}</p>
|
||||
</>,
|
||||
);
|
||||
@@ -307,7 +308,10 @@ export default function Content() {
|
||||
<Link href={app.url}>{t('download')}</Link>
|
||||
</Button>
|
||||
|
||||
<CopyToClipboard text={url} onCopy={handleCopy}>
|
||||
<CopyToClipboard
|
||||
text={getAppSubLink(application.subscribe_type, url) || url}
|
||||
onCopy={handleCopy}
|
||||
>
|
||||
<Button size='sm' className='rounded-l-none p-2'>
|
||||
{t('import')}
|
||||
</Button>
|
||||
|
||||
@@ -202,8 +202,7 @@ export default function ThirdPartyAccounts() {
|
||||
name: 'Device',
|
||||
type: 'OAuth',
|
||||
},
|
||||
];
|
||||
// .filter((account) => oauth_methods?.includes(account.id));
|
||||
].filter((account) => oauth_methods?.includes(account.id));
|
||||
|
||||
const [editValues, setEditValues] = useState<Record<string, any>>({});
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/
|
||||
import { Input } from '@workspace/ui/components/input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Dispatch, SetStateAction, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import CloudFlareTurnstile from '../turnstile';
|
||||
import CloudFlareTurnstile, { TurnstileRef } from '../turnstile';
|
||||
|
||||
export default function LoginForm({
|
||||
loading,
|
||||
@@ -38,10 +38,19 @@ export default function LoginForm({
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const turnstile = useRef<TurnstileRef>(null);
|
||||
const handleSubmit = form.handleSubmit((data) => {
|
||||
try {
|
||||
onSubmit(data);
|
||||
} catch (error) {
|
||||
turnstile.current?.reset();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
||||
<form onSubmit={handleSubmit} className='grid gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
@@ -73,7 +82,7 @@ export default function LoginForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CloudFlareTurnstile id='login' {...field} />
|
||||
<CloudFlareTurnstile id='login' {...field} ref={turnstile} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -6,11 +6,11 @@ import { Input } from '@workspace/ui/components/input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { Markdown } from '@workspace/ui/custom-components/markdown';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Dispatch, SetStateAction, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import SendCode from '../send-code';
|
||||
import CloudFlareTurnstile from '../turnstile';
|
||||
import CloudFlareTurnstile, { TurnstileRef } from '../turnstile';
|
||||
|
||||
export default function RegisterForm({
|
||||
loading,
|
||||
@@ -76,13 +76,22 @@ export default function RegisterForm({
|
||||
},
|
||||
});
|
||||
|
||||
const turnstile = useRef<TurnstileRef>(null);
|
||||
const handleSubmit = form.handleSubmit((data) => {
|
||||
try {
|
||||
onSubmit(data);
|
||||
} catch (error) {
|
||||
turnstile.current?.reset();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{auth.register.stop_register ? (
|
||||
<Markdown>{t('message')}</Markdown>
|
||||
) : (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
||||
<form onSubmit={handleSubmit} className='grid gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
@@ -177,7 +186,7 @@ export default function RegisterForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CloudFlareTurnstile id='register' {...field} />
|
||||
<CloudFlareTurnstile id='register' {...field} ref={turnstile} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -5,11 +5,11 @@ import { Form, FormControl, FormField, FormItem, FormMessage } from '@workspace/
|
||||
import { Input } from '@workspace/ui/components/input';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Dispatch, SetStateAction, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import SendCode from '../send-code';
|
||||
import CloudFlareTurnstile from '../turnstile';
|
||||
import CloudFlareTurnstile, { TurnstileRef } from '../turnstile';
|
||||
|
||||
export default function ResetForm({
|
||||
loading,
|
||||
@@ -43,10 +43,19 @@ export default function ResetForm({
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const turnstile = useRef<TurnstileRef>(null);
|
||||
const handleSubmit = form.handleSubmit((data) => {
|
||||
try {
|
||||
onSubmit(data);
|
||||
} catch (error) {
|
||||
turnstile.current?.reset();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
||||
<form onSubmit={handleSubmit} className='grid gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='email'
|
||||
@@ -59,36 +68,33 @@ export default function ResetForm({
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{auth?.email?.enable_verify && (
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
disabled={loading}
|
||||
placeholder='Enter code...'
|
||||
type='text'
|
||||
{...field}
|
||||
value={field.value as string}
|
||||
/>
|
||||
<SendCode
|
||||
type='email'
|
||||
params={{
|
||||
...form.getValues(),
|
||||
type: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='code'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Input
|
||||
disabled={loading}
|
||||
placeholder='Enter code...'
|
||||
type='text'
|
||||
{...field}
|
||||
value={field.value as string}
|
||||
/>
|
||||
<SendCode
|
||||
type='email'
|
||||
params={{
|
||||
...form.getValues(),
|
||||
type: 2,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='password'
|
||||
@@ -108,7 +114,7 @@ export default function ResetForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CloudFlareTurnstile id='reset' {...field} />
|
||||
<CloudFlareTurnstile id='reset' {...field} ref={turnstile} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -7,11 +7,11 @@ import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { Dispatch, SetStateAction, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import SendCode from '../send-code';
|
||||
import CloudFlareTurnstile from '../turnstile';
|
||||
import CloudFlareTurnstile, { TurnstileRef } from '../turnstile';
|
||||
|
||||
export default function LoginForm({
|
||||
loading,
|
||||
@@ -45,10 +45,19 @@ export default function LoginForm({
|
||||
|
||||
const [mode, setMode] = useState<'password' | 'code'>('password');
|
||||
|
||||
const turnstile = useRef<TurnstileRef>(null);
|
||||
const handleSubmit = form.handleSubmit((data) => {
|
||||
try {
|
||||
onSubmit(data);
|
||||
} catch (error) {
|
||||
turnstile.current?.reset();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
||||
<form onSubmit={handleSubmit} className='grid gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='telephone'
|
||||
@@ -138,7 +147,7 @@ export default function LoginForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CloudFlareTurnstile id='login' {...field} />
|
||||
<CloudFlareTurnstile id='login' {...field} ref={turnstile} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -7,11 +7,11 @@ import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { Markdown } from '@workspace/ui/custom-components/markdown';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Dispatch, SetStateAction, useRef } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import SendCode from '../send-code';
|
||||
import CloudFlareTurnstile from '../turnstile';
|
||||
import CloudFlareTurnstile, { TurnstileRef } from '../turnstile';
|
||||
|
||||
export default function RegisterForm({
|
||||
loading,
|
||||
@@ -63,13 +63,22 @@ export default function RegisterForm({
|
||||
},
|
||||
});
|
||||
|
||||
const turnstile = useRef<TurnstileRef>(null);
|
||||
const handleSubmit = form.handleSubmit((data) => {
|
||||
try {
|
||||
onSubmit(data);
|
||||
} catch (error) {
|
||||
turnstile.current?.reset();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{auth.register.stop_register ? (
|
||||
<Markdown>{t('message')}</Markdown>
|
||||
) : (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
||||
<form onSubmit={handleSubmit} className='grid gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='telephone'
|
||||
@@ -193,7 +202,7 @@ export default function RegisterForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CloudFlareTurnstile id='register' {...field} />
|
||||
<CloudFlareTurnstile id='register' {...field} ref={turnstile} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -6,11 +6,11 @@ import { Input } from '@workspace/ui/components/input';
|
||||
import { AreaCodeSelect } from '@workspace/ui/custom-components/area-code-select';
|
||||
import { Icon } from '@workspace/ui/custom-components/icon';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { Dispatch, SetStateAction, useState } from 'react';
|
||||
import { Dispatch, SetStateAction, useRef, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
import SendCode from '../send-code';
|
||||
import CloudFlareTurnstile from '../turnstile';
|
||||
import CloudFlareTurnstile, { TurnstileRef } from '../turnstile';
|
||||
|
||||
export default function ResetForm({
|
||||
loading,
|
||||
@@ -46,10 +46,19 @@ export default function ResetForm({
|
||||
defaultValues: initialValues,
|
||||
});
|
||||
|
||||
const turnstile = useRef<TurnstileRef>(null);
|
||||
const handleSubmit = form.handleSubmit((data) => {
|
||||
try {
|
||||
onSubmit(data);
|
||||
} catch (error) {
|
||||
turnstile.current?.reset();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className='grid gap-6'>
|
||||
<form onSubmit={handleSubmit} className='grid gap-6'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='telephone'
|
||||
@@ -136,7 +145,7 @@ export default function ResetForm({
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormControl>
|
||||
<CloudFlareTurnstile id='reset' {...field} />
|
||||
<CloudFlareTurnstile id='reset' {...field} ref={turnstile} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
@@ -2,26 +2,37 @@
|
||||
|
||||
import { useLocale } from 'next-intl';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect } from 'react';
|
||||
import { forwardRef, useEffect, useImperativeHandle } from 'react';
|
||||
import Turnstile, { useTurnstile } from 'react-turnstile';
|
||||
|
||||
import useGlobalStore from '@/config/use-global';
|
||||
|
||||
export default function CloudFlareTurnstile({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
id?: string;
|
||||
value?: null | string;
|
||||
onChange: (value?: string) => void;
|
||||
}) {
|
||||
export type TurnstileRef = {
|
||||
reset: () => void;
|
||||
};
|
||||
|
||||
const CloudFlareTurnstile = forwardRef<
|
||||
TurnstileRef,
|
||||
{
|
||||
id?: string;
|
||||
value?: null | string;
|
||||
onChange: (value?: string) => void;
|
||||
}
|
||||
>(function CloudFlareTurnstile({ id, value, onChange }, ref) {
|
||||
const { common } = useGlobalStore();
|
||||
const { verify } = common;
|
||||
const { resolvedTheme } = useTheme();
|
||||
const locale = useLocale();
|
||||
const turnstile = useTurnstile();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
() => ({
|
||||
reset: () => turnstile.reset(),
|
||||
}),
|
||||
[turnstile],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (value === '') {
|
||||
turnstile.reset();
|
||||
@@ -52,4 +63,6 @@ export default function CloudFlareTurnstile({
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
export default CloudFlareTurnstile;
|
||||
|
||||
@@ -43,6 +43,7 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
template: `%s | ${site?.site_name || 'PPanel'}`,
|
||||
},
|
||||
description: site?.site_desc || '',
|
||||
keywords: site?.keywords || '',
|
||||
icons: {
|
||||
icon: site?.site_logo
|
||||
? [
|
||||
@@ -114,6 +115,10 @@ export default async function RootLayout({
|
||||
{children}
|
||||
</Providers>
|
||||
</NextIntlClientProvider>
|
||||
<div
|
||||
id='custom_html'
|
||||
dangerouslySetInnerHTML={{ __html: config?.site.custom_html || '' }}
|
||||
/>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -20,6 +20,8 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
||||
site_name: '',
|
||||
site_desc: '',
|
||||
site_logo: '',
|
||||
keywords: '',
|
||||
custom_html: '',
|
||||
},
|
||||
verify: {
|
||||
turnstile_site_key: '',
|
||||
@@ -102,7 +104,7 @@ export const useGlobalStore = create<GlobalStore>((set, get) => ({
|
||||
});
|
||||
},
|
||||
getAppSubLink: (type: string, url: string) => {
|
||||
const name = get().common.site.site_name || '';
|
||||
const name = get().common?.site?.site_name || '';
|
||||
switch (type) {
|
||||
case 'Clash':
|
||||
return `clash://install-config?url=${url}&name=${name}`;
|
||||
|
||||
+3
-1
@@ -35,7 +35,7 @@ declare namespace API {
|
||||
|
||||
type ApplicationPlatform = {
|
||||
ios?: ApplicationVersion[];
|
||||
mac?: ApplicationVersion[];
|
||||
macos?: ApplicationVersion[];
|
||||
linux?: ApplicationVersion[];
|
||||
android?: ApplicationVersion[];
|
||||
windows?: ApplicationVersion[];
|
||||
@@ -431,6 +431,8 @@ declare namespace API {
|
||||
site_name: string;
|
||||
site_desc: string;
|
||||
site_logo: string;
|
||||
keywords: string;
|
||||
custom_html: string;
|
||||
};
|
||||
|
||||
type SortItem = {
|
||||
|
||||
Vendored
+3
-1
@@ -29,7 +29,7 @@ declare namespace API {
|
||||
|
||||
type ApplicationPlatform = {
|
||||
ios?: ApplicationVersion[];
|
||||
mac?: ApplicationVersion[];
|
||||
macos?: ApplicationVersion[];
|
||||
linux?: ApplicationVersion[];
|
||||
android?: ApplicationVersion[];
|
||||
windows?: ApplicationVersion[];
|
||||
@@ -625,6 +625,8 @@ declare namespace API {
|
||||
site_name: string;
|
||||
site_desc: string;
|
||||
site_logo: string;
|
||||
keywords: string;
|
||||
custom_html: string;
|
||||
};
|
||||
|
||||
type SortItem = {
|
||||
|
||||
@@ -56,13 +56,13 @@ export function Logout() {
|
||||
}
|
||||
}
|
||||
|
||||
export function getPlatform(): 'windows' | 'mac' | 'linux' | 'android' | 'ios' | 'harmony' {
|
||||
export function getPlatform(): 'windows' | 'macos' | 'linux' | 'android' | 'ios' | 'harmony' {
|
||||
const parser = new UAParser();
|
||||
const os = parser.getOS();
|
||||
const osName = os.name?.toLowerCase() || '';
|
||||
|
||||
if (osName.includes('windows')) return 'windows';
|
||||
if (osName.includes('mac')) return 'mac';
|
||||
if (osName.includes('mac')) return 'macos';
|
||||
if (
|
||||
osName.includes('linux') ||
|
||||
osName.includes('ubuntu') ||
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ppanel-web",
|
||||
"version": "1.0.0-beta.14",
|
||||
"version": "1.0.0-beta.20",
|
||||
"private": true,
|
||||
"homepage": "https://github.com/perfect-panel/ppanel-web",
|
||||
"bugs": {
|
||||
|
||||
Reference in New Issue
Block a user