From 09d6255bd5294b9adff5e48dc7d983eea0acdd7f Mon Sep 17 00:00:00 2001 From: luorijun Date: Tue, 29 Apr 2025 18:47:36 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=B8=AA=E4=BA=BA=E4=B8=AD?= =?UTF-8?q?=E5=BF=83=E9=A1=B5=E9=9D=A2=E5=8A=9F=E8=83=BD=EF=BC=8C=E5=85=AC?= =?UTF-8?q?=E5=85=B1=E7=BB=84=E4=BB=B6=E9=83=A8=E5=88=86=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 7 + src/actions/base.ts | 1 + src/actions/user.ts | 18 +- src/app/admin/identify/page.tsx | 44 +- src/app/admin/profile/page.tsx | 994 +++++++----------- .../composites/purchase/_client/right.tsx | 2 +- .../recharge.tsx => recharge/index.tsx} | 4 +- src/components/ui/button.tsx | 32 +- src/components/ui/card.tsx | 50 +- src/components/ui/dialog.tsx | 12 +- src/components/ui/form.tsx | 29 +- src/components/ui/input.tsx | 2 +- src/middleware.ts | 2 + 13 files changed, 531 insertions(+), 666 deletions(-) rename src/components/composites/{purchase/_client/recharge.tsx => recharge/index.tsx} (98%) diff --git a/README.md b/README.md index 5f9fbfa..4fc0636 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ ## TODO +验证码读秒用 store 保存到本地,(全局共享读秒时间)? + +网页标题根据实际页面变化 + 检查时间范围选择,限定到一定范围内 将翻页操作反映在路由历史中,可以通过后退返回到上一个翻页状态 @@ -21,3 +25,6 @@ - [ ] 提取记录 - [ ] 使用记录 +检查扩大服务端组件边界 + +检查 Card 替换 section 或 div diff --git a/src/actions/base.ts b/src/actions/base.ts index 8b408d4..ac5d0b5 100644 --- a/src/actions/base.ts +++ b/src/actions/base.ts @@ -163,6 +163,7 @@ async function postCall(rawResp: Promise>) { ].some(item => item.test(pathname)) if (match && !resp.success && resp.status === 401) { + console.log("!!!!!!!!!redirect", resp.message) redirect(pathname === '/' ? '/login' : `/login?redirect=${pathname}`) } diff --git a/src/actions/user.ts b/src/actions/user.ts index 81cb59b..282b0ec 100644 --- a/src/actions/user.ts +++ b/src/actions/user.ts @@ -1,5 +1,4 @@ 'use server' - import {callByUser, callPublic} from '@/actions/base' export async function RechargeByAlipay(props: { @@ -51,3 +50,20 @@ export async function IdentifyCallback(props: { message: string }>('/api/user/identify/callback', props) } + +export async function update(props: { + username: string + email: string + contact_qq: string + contact_wechat: string +}) { + return await callByUser('/api/user/update', props) +} + +export async function updatePassword(props: { + phone: string + code: string + password: string +}) { + return await callByUser('/api/user/update/password', props) +} diff --git a/src/app/admin/identify/page.tsx b/src/app/admin/identify/page.tsx index a8bba07..107961f 100644 --- a/src/app/admin/identify/page.tsx +++ b/src/app/admin/identify/page.tsx @@ -18,6 +18,8 @@ import personal from './_assets/personal.webp' import step1 from './_assets/step1.webp' import step2 from './_assets/step2.webp' import step3 from './_assets/step3.webp' +import {Card, CardContent, CardDescription, CardHeader, CardTitle} from '@/components/ui/card' +import {WorkflowIcon} from 'lucide-react' export type IdentifyPageProps = {} @@ -180,15 +182,21 @@ export default function IdentifyPage(props: IdentifyPageProps) { -
-

操作引导

-

为响应国家相关规定,使用HTTP代理需完成实名认证。认证服务由支付宝提供,您的个人信息将受到严格保护,仅用于账户安全认证

-
+ + + + 操作引导 + + + +

+ 为响应国家相关规定,使用HTTP代理需完成实名认证。认证服务由支付宝提供,您的个人信息将受到严格保护,仅用于账户安全认证 +

-

- 01 - 注册账号 -
+ + 01 + 注册账号 + {`步骤配图`}

@@ -198,10 +206,10 @@ export default function IdentifyPage(props: IdentifyPageProps) { )}>

-

- 02 - 实名认证 -
+ + 02 + 实名认证 + {`步骤配图`}

@@ -211,14 +219,14 @@ export default function IdentifyPage(props: IdentifyPageProps) { )}>

-

- 03 - 充值、支付 -
+ + 03 + 充值、支付 + {`步骤配图`}

- -
+ + ) } diff --git a/src/app/admin/profile/page.tsx b/src/app/admin/profile/page.tsx index e853db0..6a808b6 100644 --- a/src/app/admin/profile/page.tsx +++ b/src/app/admin/profile/page.tsx @@ -1,651 +1,461 @@ 'use client' -import {useEffect, useState, useContext, useRef} from 'react' +import {useEffect, useRef, useState} from 'react' import Page from '@/components/page' -import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs' -import {Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle} from '@/components/ui/card' -import {Form, FormField, FormLabel} from '@/components/ui/form' +import {Card, CardContent, CardHeader, CardTitle} from '@/components/ui/card' +import {Form, FormField} from '@/components/ui/form' import {Button} from '@/components/ui/button' -import {Input} from '@/components/ui/input' import {useForm} from 'react-hook-form' import {zodResolver} from '@hookform/resolvers/zod' import * as z from 'zod' -import {StoreContext, useProfileStore} from '@/components/providers/StoreProvider' -import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog' +import {useProfileStore} from '@/components/providers/StoreProvider' import {toast} from 'sonner' -import {Alert, AlertDescription, AlertTitle} from '@/components/ui/alert' -import {CheckCircle, AlertCircle, Shield, Wallet, CreditCard, QrCode, UserIcon} from 'lucide-react' +import {CheckCircle, QrCodeIcon} from 'lucide-react' import * as qrcode from 'qrcode' -import Link from 'next/link' -import RechargeModal from '@/components/composites/purchase/_client/recharge' +import Image from 'next/image' +import banner from '@/app/admin/identify/_assets/banner.webp' +import {Input} from '@/components/ui/input' +import {merge} from '@/lib/utils' import {User} from '@/lib/models' -import { Label } from '@/components/ui/label' +import {update, updatePassword} from '@/actions/user' +import { + Dialog, DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog' +import {sendSMS} from '@/actions/verify' +import RechargeModal from '@/components/composites/recharge' export type ProfilePageProps = {} export default function ProfilePage(props: ProfilePageProps) { - const profile = useProfileStore(store => store.profile) - const refreshProfile = useProfileStore(store => store.refreshProfile) - // 默认选中的Tab - const [activeTab, setActiveTab] = useState('basic') + const profile = useProfileStore(store => store.profile) + + // ====================== + // render + // ====================== if (!profile) { return ( -
- - - 加载中 - 正在加载个人信息,请稍候... - +
+

加载中...

+

请稍等片刻

) } return ( - -
-
- - - 基本信息 - 安全设置 - 余额管理 - 实名认证 - + +
+ {/* banner */} +
+ {`背景图`} +

蓝狐HTTP邀请您参与【先测后买】服务

+

为了保障您的账户安全,请先完成实名认证,即可获取福利套餐测试资格

+
- - - + {/* 块信息 */} +
- - - - - - - - - - - - -
- - {/* 侧边栏:客服经理信息 */} -
- + - - 账户概览 - + 帐号余额(元) - -
-

用户名

-

{profile.name || profile.username}

-
- -
-

手机号

-

{profile.phone ? maskPhone(profile.phone) : '未设置'}

-
- -
-

账户余额

-

¥{profile.balance || 0}

-
- -
-

实名认证

-

- {profile.id_token ? ( - <> 已认证 - ) : ( - <> 未认证 - )} -

-
+ +

{profile?.balance}

+
- {profile.contact_wechat && ( - - - - 专属客服经理 - - - -
- -

- 扫描上方二维码添加客服经理微信
获取更多帮助与支持 + + + 实名认证 + + + {!profile?.id_token + ? <> +

为了保障您的账户安全和正常使用服务,请您尽快完成实名认证

+ + + : <> +

+ {profile.name} + {profile.id_no}

-
-
-
- )} +

+ + 已认证 +

+ } + +
+
+ +
+ + {/* 安全信息 */} +
+

安全信息

+
+

{profile.phone}

+ +
+
+ + {/* 基本信息 */} +
+ + {/* 侧边栏:客服经理信息 */} + +
) } -// 基本信息组件 -function BasicInfoTab(props: { +function Aftersale(props: { profile: User - refreshProfile: () => Promise }) { - const {profile, refreshProfile} = props - - const basicInfoSchema = z.object({ - name: z.string().min(2, '名称至少需要2个字符'), - email: z.string().email('请输入有效的邮箱地址').or(z.string().length(0)), - }) - - type BasicInfoShema = z.infer - - const form = useForm({ - resolver: zodResolver(basicInfoSchema), - defaultValues: { - name: profile.name || '', - email: profile.email || '', - }, - }) - - const [isSubmitting, setIsSubmitting] = useState(false) - - const onSubmit = async (data: BasicInfoShema) => { - setIsSubmitting(true) - try { - // 这里会调用更新用户信息的API - // 示例: await updateUserProfile(data) - - toast.success('基本信息更新成功') - await refreshProfile() - } - catch (error) { - toast.error('更新失败', { - description: (error as Error).message || '请稍后重试', - }) - } - finally { - setIsSubmitting(false) - } - } - - return ( -
- - {({id, field}) => ( - - )} - - - - {({id, field}) => ( - - )} - - -
- -
-
- ) -} - -// 安全设置组件 -function SecurityTab(props: { - profile: User - refreshProfile: () => Promise -}) { - const {profile, refreshProfile} = props - - const [showPhoneDialog, setShowPhoneDialog] = useState(false) - const [showPasswordDialog, setShowPasswordDialog] = useState(false) - - return ( -
-
-
-

手机号

-

- {profile.phone ? maskPhone(profile.phone) : '未设置手机号'} -

-
- -
- -
-
-

密码

-

定期修改密码可以保障您的账户安全

-
- -
- -
-
-

账号保护

-

开启登录保护,提升账号安全

-
- -
- - {/* 修改手机号对话框 */} - - - {/* 修改密码对话框 */} - -
- ) -} - -// 余额管理组件 -function BalanceTab(props: { - profile: User - refreshProfile: () => Promise -}) { - const {profile} = props - - // 交易历史记录示例数据 - const transactions = [ - {id: 1, type: '充值', amount: 100, date: '2025-04-18', status: '成功'}, - {id: 2, type: '购买套餐', amount: -50, date: '2025-04-15', status: '成功'}, - {id: 3, type: '系统赠送', amount: 10, date: '2025-04-10', status: '成功'}, - ] - - return ( -
-
-
-

当前余额: ¥{profile.balance || 0}

-

您可以随时充值或查看交易记录

-
- -
- -
-

近期交易记录

-
-
-
类型
-
金额
-
日期
-
状态
-
- -
- {transactions.map(tx => ( -
-
{tx.type}
-
= 0 ? 'text-green-600' : 'text-red-600'}> - {tx.amount >= 0 ? `+${tx.amount}` : tx.amount} -
-
{tx.date}
-
{tx.status}
-
- ))} -
-
- -
- -
-
-
- ) -} - -// 实名认证组件 -function IdentifyTab(props: { - profile: User - refreshProfile: () => Promise -}) { - const {profile} = props - - return ( -
- {profile.id_token ? ( -
- -

您已完成实名认证

-

认证信息已通过验证,可以正常使用所有功能

- -
-
-

认证姓名

-

{maskName(profile.name)}

-
-
-

身份证号

-

{maskIdNumber(profile.id_no)}

-
-
-
- ) : ( -
- -

您尚未完成实名认证

-

- 根据相关法律法规要求,使用HTTP代理服务需要先完成实名认证 -

- - - - -
- )} - - - - 重要提示 - - 为响应国家相关规定,使用HTTP代理需完成实名认证。 - 认证服务由支付宝提供,您的个人信息将受到严格保护,仅用于账户安全认证。 - - -
- ) -} - -// 修改手机号对话框 -function ChangePhoneDialog(props: { - open: boolean - onOpenChange: (open: boolean) => void - currentPhone?: string - refreshProfile: () => Promise -}) { - const {open, onOpenChange, currentPhone, refreshProfile} = props - - const phoneChangeSchema = z.object({ - newPhone: z.string().length(11, '请输入11位手机号码'), - verifyCode: z.string().min(4, '请输入验证码'), - }) - - type PhoneChangeSchema = z.infer - - const form = useForm({ - resolver: zodResolver(phoneChangeSchema), - defaultValues: { - newPhone: '', - verifyCode: '', - }, - }) - - const [isSubmitting, setIsSubmitting] = useState(false) - const [countdown, setCountdown] = useState(0) - - const onSubmit = async (data: PhoneChangeSchema) => { - setIsSubmitting(true) - try { - // 这里调用修改手机号的API - // 示例: await changePhone(data.newPhone, data.verifyCode) - - toast.success('手机号更新成功') - await refreshProfile() - onOpenChange(false) - } - catch (error) { - toast.error('更新失败', { - description: (error as Error).message || '请稍后重试', - }) - } - finally { - setIsSubmitting(false) - } - } - - const sendVerifyCode = () => { - const newPhone = form.getValues('newPhone') - if (!newPhone || newPhone.length !== 11) { - form.setError('newPhone', {message: '请输入有效的手机号码'}) - return - } - - // 这里调用发送验证码的API - // 示例: await sendSmsCode(newPhone) - toast.success('验证码已发送') - - // 开始倒计时 - setCountdown(60) - const timer = setInterval(() => { - setCountdown(prev => { - if (prev <= 1) { - clearInterval(timer) - return 0 - } - return prev - 1 - }) - }, 1000) - } - - return ( - - - - 修改手机号 - - -
- {currentPhone && ( -
- - -
- )} - - - {({id, field}) => ( - - )} - - - - {({id, field}) => ( -
- - -
- )} -
- - - - -
-
-
- ) -} - -// 修改密码对话框 -function ChangePasswordDialog(props: { - open: boolean - onOpenChange: (open: boolean) => void - refreshProfile: () => Promise -}) { - const {open, onOpenChange, refreshProfile} = props - - const passwordChangeSchema = z.object({ - oldPassword: z.string().min(6, '密码至少需要6个字符'), - newPassword: z.string().min(6, '密码至少需要6个字符'), - confirmPassword: z.string().min(6, '密码至少需要6个字符'), - }).refine(data => data.newPassword === data.confirmPassword, { - message: '两次输入的新密码不一致', - path: ['confirmPassword'], - }) - - type PasswordChangeSchema = z.infer - - const form = useForm({ - resolver: zodResolver(passwordChangeSchema), - defaultValues: { - oldPassword: '', - newPassword: '', - confirmPassword: '', - }, - }) - - const [isSubmitting, setIsSubmitting] = useState(false) - - const onSubmit = async (data: PasswordChangeSchema) => { - setIsSubmitting(true) - try { - // 这里调用修改密码的API - // 示例: await changePassword(data.oldPassword, data.newPassword) - - toast.success('密码修改成功') - onOpenChange(false) - } - catch (error) { - toast.error('修改失败', { - description: (error as Error).message || '请稍后重试', - }) - } - finally { - setIsSubmitting(false) - } - } - - return ( - - - - 修改密码 - - -
- - {({id, field}) => ( - - )} - - - - {({id, field}) => ( - - )} - - - - {({id, field}) => ( - - )} - - - - - -
-
-
- ) -} - -// 客服经理二维码组件 -function ServiceManagerQRCode(props: { - wechat?: string -}) { - const {wechat} = props + const admin = props.profile.admin_id const canvasRef = useRef(null) useEffect(() => { - if (wechat && canvasRef.current) { - qrcode.toCanvas(canvasRef.current, wechat, { + if (admin && canvasRef.current) { + qrcode.toCanvas(canvasRef.current, String(admin), { width: 180, margin: 0, }).catch(err => { console.error(err) }) } - }, [wechat]) + }, [admin]) return ( -
- + + + + 关于售后 + + + + +
+

+ 1.全国100万+动态IP代理资源免费测试,先测后买让您安心使用。 +

+ +

+ 2.注册即享新人福利,专业的客户经理,多维度为您提供在线代理相关答疑 +

+ +

+ 3.1V1专属售后答疑,技术团队7*24小时在线支持提供专属解决方案 +

+
+ +
+

您的专属客服经理

+
+ +
+

+ 扫描上方二维码添加客服经理微信
获取更多帮助与支持 +

+
+
+
+ ) +} + +function BasicForm(props: { + profile: User +}) { + + const schema = z.object({ + username: z.string(), + email: z.string().email('请输入正确的邮箱'), + contact_qq: z.string(), + contact_wechat: z.string(), + }) + type Schema = z.infer + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + username: props.profile.username || '', + email: props.profile.email || '', + contact_qq: props.profile.contact_qq || '', + contact_wechat: props.profile.contact_wechat || '', + }, + }) + const handler = form.handleSubmit(async value => { + try { + const resp = await update(value) + if (!resp.success) { + throw new Error(resp.message) + } + + await refreshProfile() + toast.success(`保存成功`) + } + catch (e) { + console.error(e) + toast.error(`保存失败`, { + description: e instanceof Error ? e.message : String(e), + }) + } + }) + + const refreshProfile = useProfileStore(store => store.refreshProfile) + + return ( +
+

基本信息

+
+ + name={`username`} + label={用户名} + className={`grid grid-cols-[48px_1fr] grid-rows-[auto_auto] gap-x-4`} + classNames={{ + message: `col-start-2`, + }} + > + {({field}) => ( + + )} + + + name={`email`} + label={邮箱} + className={`grid grid-cols-[48px_1fr] grid-rows-[auto_auto] gap-x-4`} + classNames={{ + message: `col-start-2`, + }} + > + {({field}) => ( + + )} + + + name={`contact_qq`} + label={QQ} + className={`grid grid-cols-[48px_1fr] grid-rows-[auto_auto] gap-x-4`} + classNames={{ + message: `col-start-2`, + }} + > + {({field}) => ( + + )} + + + name={`contact_wechat`} + label={微信} + className={`grid grid-cols-[48px_1fr] grid-rows-[auto_auto] gap-x-4`} + classNames={{ + message: `col-start-2`, + }} + > + {({field}) => ( + + )} + +
+ + +
+
) } -// 工具函数:遮盖手机号 -function maskPhone(phone?: string) { - if (!phone) return '' - return phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') -} +function PasswordForm(props: { + profile: User +}) { -// 工具函数:遮盖姓名 -function maskName(name?: string) { - if (!name) return '' - if (name.length <= 2) { - return name.charAt(0) + '*' + // ====================== + // open + // ====================== + + const [open, setOpen] = useState(false) + + // ====================== + // form + // ====================== + + const schema = z.object({ + phone: z.string().regex(/^1\d{10}$/, `请输入正确的手机号`), + captcha: z.string().nonempty('请输入验证码'), + code: z.string().regex(/^\d{6}$/, `请输入正确的验证码`), + password: z.string().min(6, `密码至少6位`), + confirm_password: z.string(), + }).refine(data => data.password === data.confirm_password, { + message: '两次输入的密码不一致', + path: ['confirm_password'], + }) + type Schema = z.infer + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + phone: '', + captcha: '', + code: '', + password: '', + confirm_password: '', + }, + }) + const handler = form.handleSubmit(async value => { + try { + const resp = await updatePassword({ + phone: value.phone, + code: value.code, + password: value.password, + }) + if (!resp.success) { + throw new Error(resp.message) + } + + toast.success(`保存成功`) + setOpen(false) + } + catch (e) { + console.error(e) + toast.error(`保存失败`, { + description: e instanceof Error ? e.message : String(e), + }) + } + }) + + // ====================== + // phone code + // ====================== + + const [captchaUrl, setCaptchaUrl] = useState(`/captcha?t=${new Date().getTime()}`) + const [captchaWait, setCaptchaWait] = useState(0) + const interval = useRef(null) + + const refreshCaptcha = () => { + setCaptchaUrl(`/captcha?t=${new Date().getTime()}`) } - const stars = '*'.repeat(name.length - 2) - return name.charAt(0) + stars + name.charAt(name.length - 1) -} -// 工具函数:遮盖身份证号 -function maskIdNumber(idNumber?: string) { - if (!idNumber) return '' - return idNumber.replace(/^(.{4})(.*)(.{4})$/, '$1**********$3') + const sendVerifier = async () => { + const result = await form.trigger(['phone', 'captcha']) + if (!result) { + return + } + + const {phone, captcha} = form.getValues() + const resp = await sendSMS({phone, captcha}) + if (!resp.success) { + toast.error(resp.message) + refreshCaptcha() + return + } + + setCaptchaWait(60) + interval.current = setInterval(() => { + setCaptchaWait(wait => { + if (wait <= 1) { + clearInterval(interval.current!) + return 0 + } + return wait - 1 + }) + }, 1000) + + toast.success(`验证码已发送,请注意查收`) + } + + // ====================== + // render + // ====================== + + return ( + + + + + + + 修改密码 + +
+ + {/* 手机号 */} + name={`phone`} label={`手机号`} className={`flex-auto`}> + {({field}) => ( + + )} + + + name={`captcha`} label={`验证码`}> + {({field}) => ( +
+ + +
+ )} + + + {/* 短信令牌 */} + name={`code`} label={`短信令牌`} className={`flex-auto`}> + {({field}) => ( +
+ + +
+ )} + + + {/* 新密码 */} + name={`password`} label={`新密码`} className={`flex-auto`}> + {({field}) => ( + + )} + + + {/* 确认密码 */} + name={`confirm_password`} label={`确认密码`} className={`flex-auto`}> + {({field}) => ( + + )} + + + + + + + +
+
+ ) } diff --git a/src/components/composites/purchase/_client/right.tsx b/src/components/composites/purchase/_client/right.tsx index 1504797..d63d784 100644 --- a/src/components/composites/purchase/_client/right.tsx +++ b/src/components/composites/purchase/_client/right.tsx @@ -9,7 +9,7 @@ import alipay from '@/components/composites/purchase/_assets/alipay.svg' import wechat from '@/components/composites/purchase/_assets/wechat.svg' import balance from '@/components/composites/purchase/_assets/balance.svg' import {useProfileStore} from '@/components/providers/StoreProvider' -import RechargeModal from '@/components/composites/purchase/_client/recharge' +import RechargeModal from '@/components/composites/recharge' import Pay from '@/components/composites/purchase/_client/pay' import {buttonVariants} from '@/components/ui/button' import Link from 'next/link' diff --git a/src/components/composites/purchase/_client/recharge.tsx b/src/components/composites/recharge/index.tsx similarity index 98% rename from src/components/composites/purchase/_client/recharge.tsx rename to src/components/composites/recharge/index.tsx index 4de604b..9e0b374 100644 --- a/src/components/composites/purchase/_client/recharge.tsx +++ b/src/components/composites/recharge/index.tsx @@ -17,11 +17,11 @@ import {zodResolver} from '@hookform/resolvers/zod' import {toast} from 'sonner' import wechat from '@/components/composites/purchase/_assets/wechat.svg' import alipay from '@/components/composites/purchase/_assets/alipay.svg' -import {useContext, useRef, useState} from 'react' +import {useRef, useState} from 'react' import {Loader} from 'lucide-react' import {RechargeByAlipay, RechargeByAlipayConfirm, RechargeByWechat, RechargeByWechatConfirm} from '@/actions/user' import * as qrcode from 'qrcode' -import {StoreContext, useProfileStore} from '@/components/providers/StoreProvider' +import {useProfileStore} from '@/components/providers/StoreProvider' const schema = zod.object({ method: zod.enum(['alipay', 'wechat']), diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index c20a3e4..0246d86 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -3,33 +3,33 @@ import {merge} from '@/lib/utils' import {cva} from 'class-variance-authority' const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + 'inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0', { variants: { variant: { default: - "bg-primary text-primary-foreground shadow hover:bg-primary/90", + 'bg-primary text-primary-foreground shadow hover:bg-primary/90', destructive: - "bg-fail text-fail-foreground shadow-sm hover:bg-destructive/90", + 'bg-fail text-fail-foreground shadow-sm hover:bg-destructive/90', outline: - "border border-input shadow-sm hover:bg-secondary hover:text-secondary-foreground bg-card", + 'border border-input shadow-sm hover:bg-secondary hover:text-secondary-foreground bg-card', secondary: - "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", - ghost: "hover:bg-secondary hover:text-secondary-foreground", - link: "text-primary underline-offset-4 hover:underline", + 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80', + ghost: 'hover:bg-secondary hover:text-secondary-foreground', + link: 'text-primary underline-offset-4 hover:underline', }, size: { - default: "h-9 px-4 py-2", - sm: "h-8 rounded-md px-3 text-xs", - lg: "h-10 rounded-md px-8", - icon: "h-9 w-9", + default: 'h-9 px-4 py-2', + sm: 'h-8 rounded-md px-3 text-xs', + lg: 'h-10 rounded-md px-8', + icon: 'h-9 w-9', }, }, defaultVariants: { - variant: "default", - size: "default", + variant: 'default', + size: 'default', }, - } + }, ) type ButtonProps = React.ComponentProps<'button'> & { @@ -40,6 +40,7 @@ function Button(rawProps: ButtonProps) { const {className, theme, ...props} = rawProps return (