feat(login): Refactor the login page and add dynamic background and highlight effects #9869 (#10482)

### What problem does this PR solve?

Refactor(login): Refactor the login page and add dynamic background and
highlight effects #9869

### Type of change
- [x] Refactoring
This commit is contained in:
chanx
2025-10-11 15:24:42 +08:00
committed by GitHub
parent 313e92dd9b
commit 52f26f4643
6 changed files with 568 additions and 386 deletions

View File

@ -0,0 +1,159 @@
import './index.less';
const aspectRatio = {
top: 240,
middle: 466,
bottom: 704,
};
export const BgSvg = () => {
const def = (
path: string,
id: number | string = '',
type: keyof typeof aspectRatio,
) => {
return (
<svg
className="w-full h-full"
// style={{ aspectRatio: `1440/${aspectRatio[type]}` }}
// preserveAspectRatio="xMinYMid meet"
preserveAspectRatio="none"
// viewBox={`${getPathBounds(path).minX} 0 ${
// getPathBounds(path).width
// } ${height}`}
viewBox={`0 0 1440 ${aspectRatio[type]}`}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient id={`glow${id}`} x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#80FFF8" stopOpacity="0" />
<stop offset="50%" stopColor="#80FFF8" stopOpacity="1" />
<stop offset="100%" stopColor="#80FFF8" stopOpacity="0" />
</linearGradient>
<linearGradient
id="strokeWidthGradient"
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<stop offset="0%" stopColor="#000" />
<stop offset="10%" stopColor="#fff" />
<stop offset="50%" stopColor="#fff" />
<stop offset="90%" stopColor="#fff" />
<stop offset="100%" stopColor="#000" />
</linearGradient>
<linearGradient
id={`highlight${id}`}
x1="0%"
y1="0%"
x2="100%"
y2="0%"
>
<stop offset="45%" stopColor="#FFF" stopOpacity="0.2" />
<stop offset="48%" stopColor="#FFD700" stopOpacity="0.3" />
</linearGradient>
<filter
id={`glowFilter${id}`}
x="-10%"
y="-10%"
width="120%"
height="120%"
>
<feGaussianBlur in="SourceGraphic" stdDeviation="5.2" />
{/* <feBlend
in="blur"
in2="SourceGraphic"
mode="screen"
result="glow"
/> */}
</filter>
<filter
id={`highlightFilter${id}`}
x="-5%"
y="-5%"
width="110%"
height="110%"
>
<feGaussianBlur in="SourceGraphic" stdDeviation="5.5" />
</filter>
<mask id={`glowMask${id}`}>
<rect width="100%" height="100%" fill="transparent" />
<path
d={path}
fill="none"
stroke="url(#strokeWidthGradient)"
strokeWidth="1"
strokeDasharray="50,600"
strokeDashoffset="0"
filter={`url(#glowFilter${id})`}
className="animate-glow mask-path"
/>
<path
d={path}
fill="none"
stroke={`url(#highlight${id})`}
strokeWidth="0.5"
strokeDasharray="50,600"
strokeDashoffset="16"
filter={`url(#highlightFilter${id})`}
className="animate-highlight mask-path"
/>
</mask>
</defs>
<path
d={path}
stroke="#00BEB4"
strokeWidth="1"
fill="none"
opacity="0.1"
/>
<path
d={path}
stroke={`url(#glow${id})`}
strokeWidth="2"
fill="none"
opacity="0.8"
mask={`url(#glowMask${id})`}
/>
</svg>
);
};
return (
<div className="absolute inset-0 overflow-hidden pointer-events-none ">
<div className="absolute top-0 left-0 right-0 w-full">
<div
className={`w-full ml-10`}
style={{ height: aspectRatio['top'] + 'px' }}
>
{def(
'M1282.81 -45L999.839 147.611C988.681 155.206 975.496 159.267 961.999 159.267H746.504H330.429C317.253 159.267 304.368 155.397 293.373 148.137L0.88623 -45',
0,
'top',
)}
</div>
<div
className={`w-full -mt-40`}
style={{ height: aspectRatio['middle'] + 'px' }}
>
{def(
'M0 1L203.392 203.181C215.992 215.705 233.036 222.736 250.802 222.736H287.103C305.94 222.736 323.913 230.636 336.649 244.514L425.401 341.222C438.137 355.1 456.11 363 474.947 363H976.902C996.333 363 1014.81 354.595 1027.59 339.95L1104.79 251.424C1116.14 238.4 1132.08 230.248 1149.29 228.659L1191.13 224.796C1205.62 223.458 1219.28 217.461 1230.06 207.704L1440 17.7981',
1,
'middle',
)}
</div>
<div
className={`w-full -mt-52`}
style={{ height: aspectRatio['bottom'] + 'px' }}
>
{def(
'M-10 1L57.1932 71.1509C67.7929 82.2171 74.2953 96.5714 75.6239 111.837L79.8042 159.87C81.3312 177.416 89.68 193.662 103.057 205.117L399.311 458.829C411.497 469.265 427.011 475 443.054 475H972.606C988.463 475 1003.81 469.396 1015.94 459.179L1310.78 210.75C1323.01 200.451 1331.16 186.136 1333.79 170.369L1341.87 121.837C1344.06 108.691 1350.11 96.492 1359.24 86.7885L1440 1',
2,
'bottom',
)}
</div>
</div>
</div>
);
};

View File

@ -1,267 +0,0 @@
'use client';
import { toast } from '@/components/hooks/use-toast';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import {
InputOTP,
InputOTPGroup,
InputOTPSlot,
} from '@/components/ui/input-otp';
import { useTranslate } from '@/hooks/common-hooks';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
export function SignUpForm() {
const { t } = useTranslate('login');
const FormSchema = z.object({
email: z.string().email({
message: t('emailPlaceholder'),
}),
nickname: z.string({ required_error: t('nicknamePlaceholder') }),
password: z.string({ required_error: t('passwordPlaceholder') }),
agree: z.boolean({ required_error: t('passwordPlaceholder') }),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
email: '',
},
});
function onSubmit(data: z.infer<typeof FormSchema>) {
console.log('🚀 ~ onSubmit ~ data:', data);
toast({
title: 'You submitted the following values:',
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t('emailLabel')}</FormLabel>
<FormControl>
<Input placeholder={t('emailPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="nickname"
render={({ field }) => (
<FormItem>
<FormLabel>{t('nicknameLabel')}</FormLabel>
<FormControl>
<Input placeholder={t('nicknamePlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('passwordLabel')}</FormLabel>
<FormControl>
<Input
type={'password'}
placeholder={t('passwordPlaceholder')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="agree"
render={({ field }) => (
<FormItem className="flex flex-row items-start space-x-3 space-y-0 rounded-md">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<div className="space-y-1 leading-none">
<FormLabel>
I understand and agree to the Terms of Service and Privacy
Policy.
</FormLabel>
</div>
</FormItem>
)}
/>
<Button type="submit" className="w-full">
{t('signUp')}
</Button>
</form>
</Form>
);
}
export function SignInForm() {
const { t } = useTranslate('login');
const FormSchema = z.object({
email: z.string().email({
message: t('emailPlaceholder'),
}),
password: z.string({ required_error: t('passwordPlaceholder') }),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
email: '',
},
});
function onSubmit(data: z.infer<typeof FormSchema>) {
console.log('🚀 ~ onSubmit ~ data:', data);
toast({
title: 'You submitted the following values:',
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>{t('emailLabel')}</FormLabel>
<FormControl>
<Input placeholder={t('emailPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>{t('passwordLabel')}</FormLabel>
<FormControl>
<Input
type={'password'}
placeholder={t('passwordPlaceholder')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex items-center space-x-2">
<Checkbox id="terms" />
<label
htmlFor="terms"
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
>
{t('rememberMe')}
</label>
</div>
<Button type="submit" className="w-full">
{t('login')}
</Button>
</form>
</Form>
);
}
export function VerifyEmailForm() {
const FormSchema = z.object({
pin: z.string().min(6, {
message: 'Your one-time password must be 6 characters.',
}),
});
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
pin: '',
},
});
function onSubmit(data: z.infer<typeof FormSchema>) {
console.log('🚀 ~ onSubmit ~ data:', data);
toast({
title: 'You submitted the following values:',
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
});
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="pin"
render={({ field }) => (
<FormItem>
<FormLabel>One-Time Password</FormLabel>
<FormControl>
<InputOTP maxLength={6} {...field}>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">
Verify
</Button>
</form>
</Form>
);
}

View File

@ -1,20 +0,0 @@
import { useCallback } from 'react';
import { useSearchParams } from 'umi';
export enum Step {
SignIn,
SignUp,
ForgotPassword,
ResetPassword,
VerifyEmail,
}
export const useSwitchStep = (step: Step) => {
const [_, setSearchParams] = useSearchParams();
console.log('🚀 ~ useSwitchStep ~ _:', _);
const switchStep = useCallback(() => {
setSearchParams(new URLSearchParams({ step: step.toString() }));
}, [setSearchParams, step]);
return { switchStep };
};

View File

@ -0,0 +1,42 @@
.animate-glow {
animation: glow 16s infinite linear;
}
.mask-path {
stroke-width: 8;
::after {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
stroke-dasharray: 660;
stroke-dashoffset: 0;
stroke: #d11818;
stroke-width: 8;
fill: none;
}
}
@keyframes glow {
0% {
stroke-dashoffset: 0;
}
100% {
stroke-dashoffset: -650;
}
}
@keyframes highlight-flow {
0% {
stroke-dashoffset: 50;
}
100% {
stroke-dashoffset: -600;
} /* 15+300-30=285 */
}
.animate-highlight {
animation: highlight-flow 16s linear infinite;
}
//////////////////////////////////////////////////////////////////////////

View File

@ -1,107 +1,305 @@
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { useTranslate } from '@/hooks/common-hooks';
import { DiscordLogoIcon, GitHubLogoIcon } from '@radix-ui/react-icons';
import { useSearchParams } from 'umi';
import { SignInForm, SignUpForm, VerifyEmailForm } from './form';
import { Step, useSwitchStep } from './hooks';
import SvgIcon from '@/components/svg-icon';
import { useAuth } from '@/hooks/auth-hooks';
import {
useLogin,
useLoginChannels,
useLoginWithChannel,
useRegister,
} from '@/hooks/login-hooks';
import { useSystemConfig } from '@/hooks/system-hooks';
import { rsaPsw } from '@/utils';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'umi';
function LoginFooter() {
return (
<section className="pt-4">
<Separator />
<p className="text-center pt-4">or continue with</p>
<div className="flex gap-4 justify-center pt-[20px]">
<GitHubLogoIcon className="w-8 h-8"></GitHubLogoIcon>
<DiscordLogoIcon className="w-8 h-8"></DiscordLogoIcon>
</div>
</section>
);
}
export function SignUpCard() {
const { t } = useTranslate('login');
const { switchStep } = useSwitchStep(Step.SignIn);
return (
<Card className="w-[400px]">
<CardHeader>
<CardTitle>{t('signUp')}</CardTitle>
</CardHeader>
<CardContent>
<SignUpForm></SignUpForm>
<div className="text-center">
<Button variant={'link'} className="pt-6" onClick={switchStep}>
Already have an account? Log In
</Button>
</div>
<LoginFooter></LoginFooter>
</CardContent>
</Card>
);
}
export function SignInCard() {
const { t } = useTranslate('login');
const { switchStep } = useSwitchStep(Step.SignUp);
return (
<Card className="w-[400px]">
<CardHeader>
<CardTitle>{t('login')}</CardTitle>
</CardHeader>
<CardContent>
<SignInForm></SignInForm>
<Button
className="w-full mt-2"
onClick={switchStep}
variant={'secondary'}
>
{t('signUp')}
</Button>
</CardContent>
</Card>
);
}
export function VerifyEmailCard() {
// const { t } = useTranslate('login');
return (
<Card className="w-[400px]">
<CardHeader>
<CardTitle>Verify email</CardTitle>
</CardHeader>
<CardContent>
<section className="flex gap-y-6 flex-col">
<div className="flex items-center space-x-4">
<div className="flex-1 space-y-1">
<p className="text-sm font-medium leading-none">
Weve sent a 6-digit code to
</p>
<p className="text-sm text-blue-500">yifanwu92@gmail.com.</p>
</div>
<Button>Resend</Button>
</div>
<VerifyEmailForm></VerifyEmailForm>
</section>
</CardContent>
</Card>
);
}
import Spotlight from '@/components/spotlight';
import { Button, ButtonLoading } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { Input } from '@/components/ui/input';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import { BgSvg } from './bg';
import './index.less';
import { SpotlightTopLeft, SpotlightTopRight } from './spotlight-top';
const Login = () => {
const [searchParams] = useSearchParams();
const step = Number((searchParams.get('step') ?? Step.SignIn) as Step);
const [title, setTitle] = useState('login');
const navigate = useNavigate();
const { login, loading: signLoading } = useLogin();
const { register, loading: registerLoading } = useRegister();
const { channels, loading: channelsLoading } = useLoginChannels();
const { login: loginWithChannel, loading: loginWithChannelLoading } =
useLoginWithChannel();
const { t } = useTranslation('translation', { keyPrefix: 'login' });
const loading =
signLoading ||
registerLoading ||
channelsLoading ||
loginWithChannelLoading;
const { config } = useSystemConfig();
const registerEnabled = config?.registerEnabled !== 0;
const { isLogin } = useAuth();
useEffect(() => {
if (isLogin) {
navigate('/');
}
}, [isLogin, navigate]);
const handleLoginWithChannel = async (channel: string) => {
await loginWithChannel(channel);
};
const changeTitle = () => {
if (title === 'login' && !registerEnabled) {
return;
}
setTitle((title) => (title === 'login' ? 'register' : 'login'));
};
const FormSchema = z
.object({
nickname: z.string().optional(),
email: z
.string()
.email()
.min(1, { message: t('emailPlaceholder') }),
password: z.string().min(1, { message: t('passwordPlaceholder') }),
remember: z.boolean().optional(),
})
.superRefine((data, ctx) => {
if (title === 'register' && !data.nickname) {
ctx.addIssue({
path: ['nickname'],
message: 'nicknamePlaceholder',
code: z.ZodIssueCode.custom,
});
}
});
const form = useForm({
defaultValues: {
nickname: '',
email: '',
password: '',
confirmPassword: '',
remember: false,
},
resolver: zodResolver(FormSchema),
});
const onCheck = async (params) => {
console.log('params', params);
try {
// const params = await form.validateFields();
const rsaPassWord = rsaPsw(params.password) as string;
if (title === 'login') {
const code = await login({
email: `${params.email}`.trim(),
password: rsaPassWord,
});
if (code === 0) {
navigate('/');
}
} else {
const code = await register({
nickname: params.nickname,
email: params.email,
password: rsaPassWord,
});
if (code === 0) {
setTitle('login');
}
}
} catch (errorInfo) {
console.log('Failed:', errorInfo);
}
};
return (
<div className="w-full h-full flex items-center pl-[15%] bg-[url('@/assets/svg/next-login-bg.svg')] bg-cover bg-center">
<div className="inline-block bg-colors-background-neutral-standard rounded-lg">
{step === Step.SignIn && <SignInCard></SignInCard>}
{step === Step.SignUp && <SignUpCard></SignUpCard>}
{step === Step.VerifyEmail && <VerifyEmailCard></VerifyEmailCard>}
<div className="min-h-screen relative overflow-hidden">
<BgSvg />
<Spotlight opcity={0.6} coverage={60} />
<SpotlightTopLeft opcity={0.2} coverage={20} />
<SpotlightTopRight opcity={0.2} coverage={20} />
<div className="absolute top-3 flex flex-col items-center mb-12 w-full text-text-primary">
<div className="flex items-center mb-4 w-full pl-10 pt-10 ">
<div className="w-10 h-10 rounded-lg border flex items-center justify-center mr-3">
<img
src={'/logo.svg'}
alt="logo"
className="size-10 mr-[12] cursor-pointer"
/>
</div>
<span className="text-xl font-bold self-end">RAGFlow</span>
</div>
<h1 className="text-2xl font-bold text-center mb-2">
A Leading RAG engine with Agent for superior LLM context.
</h1>
<div className="mt-4 px-6 py-1 text-sm font-medium text-cyan-600 border border-accent-primary rounded-full hover:bg-cyan-50 transition-colors duration-200 border-glow relative overflow-hidden">
Let's get started
</div>
</div>
<div className="relative z-10 flex flex-col items-center justify-center min-h-screen px-4 sm:px-6 lg:px-8">
{/* Logo and Header */}
{/* Login Form */}
<div className="text-center mb-8">
<h2 className="text-xl font-semibold text-text-primary">
{title === 'login'
? 'Sign in to Your Account'
: 'Create an Account'}
</h2>
</div>
<div className="w-full max-w-md bg-bg-base backdrop-blur-sm rounded-2xl shadow-xl p-8 border border-border-button">
<Form {...form}>
<form
className="space-y-6"
onSubmit={form.handleSubmit((data) => onCheck(data))}
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('emailLabel')}</FormLabel>
<FormControl>
<Input placeholder={t('emailPlaceholder')} {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{title === 'register' && (
<FormField
control={form.control}
name="nickname"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('nicknameLabel')}</FormLabel>
<FormControl>
<Input
placeholder={t('nicknamePlaceholder')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel required>{t('passwordLabel')}</FormLabel>
<FormControl>
<Input
type="password"
placeholder={t('passwordPlaceholder')}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{title === 'login' && (
<FormField
control={form.control}
name="remember"
render={({ field }) => (
<FormItem>
<FormControl>
<div className="flex gap-2">
<Checkbox
checked={field.value}
onCheckedChange={(checked) => {
field.onChange(checked);
}}
/>
<FormLabel>{t('rememberMe')}</FormLabel>
</div>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<ButtonLoading
type="submit"
loading={loading}
className="bg-metallic-gradient border-b-[#00BEB4] border-b-2 hover:bg-metallic-gradient hover:border-b-[#02bcdd] w-full"
>
{title === 'login' ? t('login') : t('continue')}
</ButtonLoading>
{title === 'login' && channels && channels.length > 0 && (
<div className="mt-3 border">
{channels.map((item) => (
<Button
variant={'transparent'}
key={item.channel}
onClick={() => handleLoginWithChannel(item.channel)}
style={{ marginTop: 10 }}
>
<div className="flex items-center">
<SvgIcon
name={item.icon || 'sso'}
width={20}
height={20}
style={{ marginRight: 5 }}
/>
Sign in with {item.display_name}
</div>
</Button>
))}
</div>
)}
</form>
</Form>
{title === 'login' && registerEnabled && (
<div className="mt-6 text-right">
<p className="text-text-disabled text-sm">
{t('signInTip')}
<Button
variant={'transparent'}
onClick={changeTitle}
className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
>
{t('signUp')}
</Button>
</p>
</div>
)}
{title === 'register' && (
<div className="mt-6 text-right">
<p className="text-text-disabled text-sm">
{t('signUpTip')}
<Button
variant={'transparent'}
onClick={changeTitle}
className="text-cyan-600 hover:text-cyan-800 font-medium border-none transition-colors duration-200"
>
{t('login')}
</Button>
</p>
</div>
)}
</div>
</div>
</div>
);

View File

@ -0,0 +1,70 @@
import { useIsDarkTheme } from '@/components/theme-provider';
import React from 'react';
interface SpotlightProps {
className?: string;
opcity?: number;
coverage?: number;
}
/**
*
* @param opcity 0~1 default 0.5
* @param coverage 0~100 default 60
* @returns
*/
export const SpotlightTopLeft: React.FC<SpotlightProps> = ({
className,
opcity = 0.5,
coverage = 60,
}) => {
const isDark = useIsDarkTheme();
const rgb = isDark ? '255, 255, 255' : '194, 221, 243';
return (
<div
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
style={{
backdropFilter: 'blur(30px)',
zIndex: -1,
}}
>
<div
className="absolute inset-0"
style={{
background: `radial-gradient(circle at 10% -10%, rgba(${rgb},${opcity}) 0%, rgba(${rgb},0) ${coverage}%)`,
pointerEvents: 'none',
}}
></div>
</div>
);
};
/**
*
* @param opcity 0~1 default 0.5
* @param coverage 0~100 default 60
* @returns
*/
export const SpotlightTopRight: React.FC<SpotlightProps> = ({
className,
opcity = 0.5,
coverage = 60,
}) => {
const isDark = useIsDarkTheme();
const rgb = isDark ? '255, 255, 255' : '194, 221, 243';
return (
<div
className={`absolute inset-0 opacity-80 ${className} rounded-lg`}
style={{
backdropFilter: 'blur(30px)',
zIndex: -1,
}}
>
<div
className="absolute inset-0"
style={{
background: `radial-gradient(circle at 90% -10%, rgba(${rgb},${opcity}) 0%, rgba(${rgb},0) ${coverage}%)`,
pointerEvents: 'none',
}}
></div>
</div>
);
};