Files
simple-react-app-kit/apps/web/src/pages/login-page.tsx
T

258 lines
7.6 KiB
TypeScript

import React, { useState } from "react"
import { useMutation } from "@tanstack/react-query"
import { Button } from "@workspace/ui/components/button"
import { Input } from "@workspace/ui/components/input"
import { EyeIcon, EyeOffIcon } from "@workspace/ui/components/icon"
import { cn } from "@workspace/ui/lib/utils"
import { ThemeToggleButton } from "@workspace/blocks/blocks/appearance"
import { Logo } from "@/components/logo"
export function LoginPage() {
return (
<div className="isolate h-svh min-h-148 w-svw lg:min-h-164.5">
<header className="relative z-10 h-0">
<div className="flex h-14 items-center justify-between px-6">
<div className="size-6 opacity-0 lg:opacity-100">
<Logo />
</div>
<div>
<ThemeToggleButton />
</div>
</div>
</header>
<main className="relative isolate flex size-full before:pointer-events-none before:absolute before:inset-0 before:-z-1 before:bg-[url(https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/background/background-3-blur.webp)] before:bg-cover before:bg-center before:bg-no-repeat before:opacity-20">
<div className="pointer-events-none hidden w-120 flex-col justify-center gap-16 bg-linear-to-r from-secondary/10 to-muted/20 p-6 pt-18 select-none lg:flex">
<div className="text-center">
<h2 className="text-3xl font-bold tracking-tight text-foreground">
欢迎回来 👋🏻
</h2>
<p className="mt-4 text-muted-foreground">
让商品、订单与运营管理更简单高效。
</p>
</div>
<img
src="https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/illustrations/illustration-dashboard.webp"
alt=""
className="aspect-4/3 w-full object-cover object-center"
/>
<Features />
</div>
<div className="flex flex-1 flex-col items-start gap-7 bg-sidebar/80 px-6 py-20 lg:justify-center lg:px-8">
<div className="mx-auto mb-10 w-full max-w-md lg:hidden">
<Logo className="size-12" />
</div>
<LoginForm />
<div className="lg:h-24" />
</div>
</main>
</div>
)
}
const features = [
{
label: "Jwt",
img: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/icons/platforms/ic-jwt.svg",
},
{
label: "Firebase",
img: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/icons/platforms/ic-firebase.svg",
},
{
label: "Amplify",
img: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/icons/platforms/ic-amplify.svg",
},
{
label: "Auth0",
img: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/icons/platforms/ic-auth0.svg",
},
{
label: "Supabase",
img: "https://pub-c5e31b5cdafb419fb247a8ac2e78df7a.r2.dev/public/assets/icons/platforms/ic-supabase.svg",
},
]
function Features() {
const [index, setIndex] = React.useState(0)
React.useEffect(() => {
const timerId = setInterval(() => {
setIndex((currentIndex) => (currentIndex + 1) % features.length)
}, 1000)
return () => {
clearInterval(timerId)
}
}, [])
return (
<ul className="flex items-center justify-center gap-4">
{features.map((f, i) => (
<li
key={f.label}
className={cn(
"transition-[filter] duration-1000 ease-linear",
i === index ? "grayscale-0" : "grayscale-100"
)}
>
<img className="size-8" alt={f.label} src={f.img} />
</li>
))}
</ul>
)
}
function LoginForm() {
// const navigate = useNavigate()
const [username, setUsername] = useState("")
const [password, setPassword] = useState("")
const mutation = useMutation({
// mutationFn: () => {
// return api.post<AdminAuthResponse>("/api/v1/admin/auth/login", {
// username,
// password,
// })
// },
// onSuccess: () => {
// login()
// Toast("登录成功", "success")
// navigate("/", { replace: true })
// },
// onError: (err: Error) => toast(err.message || "登录失败", "error"),
})
return (
<div className="mx-auto w-full max-w-md">
<h5 className="mb-3 text-2xl font-bold tracking-tight text-foreground">
账号登录
</h5>
<p className="lg:text-md text-sm text-muted-foreground">
<span className="text-muted-foreground">
请输入您的帐户信息以开始管理您的系统
</span>
</p>
<form
className="mt-8 space-y-8"
onSubmit={(e) => {
e.preventDefault()
mutation.mutate()
}}
>
<Field
label="用户名"
inputId="username"
Component={UsernameInput}
value={username}
setValue={setUsername}
/>
<Field
label="密码"
inputId="password"
Component={PasswordInput}
value={password}
setValue={setPassword}
/>
<Button
type="submit"
className="mt-2 h-12 w-full rounded-lg"
disabled={mutation.isPending}
>
{mutation.isPending ? "登录中..." : "登录"}
</Button>
</form>
</div>
)
}
type FieldInputProps = {
inputId: string
className?: string
value: string
setValue: React.Dispatch<React.SetStateAction<string>>
}
type FieldInputComponent = React.ComponentType<FieldInputProps>
function Field({
label,
Component,
...props
}: Omit<FieldInputProps, "className"> & {
label: string
Component: FieldInputComponent
}) {
return (
<fieldset className="relative flex h-12 items-center rounded-lg border-2 border-border p-0 focus-within:border-accent focus-within:[&_label]:opacity-100">
<legend className="ml-2 flex h-0 px-0">
<label
className="h-fit -translate-y-1/2 px-1 text-sm opacity-50"
htmlFor={props.inputId}
>
{label}
</label>
</legend>
<Component
className="h-full rounded-none border-none bg-transparent outline-none autofill:bg-transparent focus-visible:border-none focus-visible:ring-0 [&:-webkit-autofill]:[-webkit-background-clip:text] [&:-webkit-autofill]:[-webkit-text-fill-color:var(--foreground)] [&:-webkit-autofill]:[transition:background-color_9999s_ease-out]"
{...props}
/>
</fieldset>
)
}
function UsernameInput({
inputId,
className,
value,
setValue,
}: FieldInputProps) {
return (
<Input
id={inputId}
value={value}
className={className}
onChange={(e) => setValue(e.target.value)}
placeholder="请输入用户名"
required
/>
)
}
function PasswordInput({
inputId,
className,
value,
setValue,
}: FieldInputProps) {
const [isVisible, setVisile] = React.useState(false)
return (
<>
<Input
id={inputId}
type={isVisible ? "text" : "password"}
value={value}
className={cn("relative z-1 pe-24", className)}
onChange={(e) => setValue(e.target.value)}
placeholder="请输入登录密码"
required
/>
<div className="absolute top-0 right-0 z-2 flex h-11 w-11 items-center justify-center">
<Button
type="button"
variant="ghost"
size="icon"
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
setVisile((v) => !v)
}}
className="rounded-sm"
>
{isVisible ? <EyeIcon /> : <EyeOffIcon />}
</Button>
</div>
</>
)
}