首页页面
This commit is contained in:
63
src/components/authGuard.tsx
Normal file
63
src/components/authGuard.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { useEffect, useState } from "react"
|
||||
import { Navigate, useLocation } from "react-router-dom"
|
||||
import { getProfile, refreshAuth } from "@/api/auth"
|
||||
|
||||
export default function AuthGuard({ children }: { children: React.ReactNode }) {
|
||||
const location = useLocation()
|
||||
const [isValid, setIsValid] = useState<boolean | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
const accessToken = localStorage.getItem("auth_token")
|
||||
const refreshToken = localStorage.getItem("auth_refresh")
|
||||
|
||||
// 没 Token,判定无效
|
||||
if (!accessToken && !refreshToken) {
|
||||
setIsValid(false)
|
||||
return
|
||||
}
|
||||
|
||||
// 尝试用现有 AccessToken 获取用户信息
|
||||
const profileResp = await getProfile()
|
||||
if (profileResp.success) {
|
||||
setIsValid(true)
|
||||
return
|
||||
}
|
||||
|
||||
// AccessToken 无效 尝试用 RefreshToken 刷新
|
||||
if (refreshToken) {
|
||||
const newTokens = await refreshAuth()
|
||||
if (newTokens) {
|
||||
setIsValid(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新失败或无 RefreshToken,为未登录
|
||||
localStorage.removeItem("auth_token")
|
||||
localStorage.removeItem("auth_refresh")
|
||||
localStorage.removeItem("auth_expires_in")
|
||||
setIsValid(false)
|
||||
}
|
||||
|
||||
checkAuth()
|
||||
}, [])
|
||||
|
||||
if (isValid === null)
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen">
|
||||
验证身份中...
|
||||
</div>
|
||||
)
|
||||
|
||||
// if (!isValid) {
|
||||
// return (
|
||||
// <Navigate
|
||||
// to={`/login?redirect=${encodeURIComponent(location.pathname)}`}
|
||||
// replace
|
||||
// />
|
||||
// )
|
||||
// }
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
149
src/components/composites/dialogs/editInfoDialog.tsx
Normal file
149
src/components/composites/dialogs/editInfoDialog.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import { Eye, EyeOff } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Label } from "@/components/ui/label"
|
||||
|
||||
interface EditInfoDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
initialData?: {
|
||||
username: string
|
||||
taobao: string
|
||||
password: string
|
||||
email: string
|
||||
wechat: string
|
||||
qq: string
|
||||
}
|
||||
onSave?: (data: any) => void
|
||||
}
|
||||
|
||||
export function EditInfoDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
initialData = {
|
||||
username: "191****1234",
|
||||
taobao: "191****1234",
|
||||
password: "********",
|
||||
email: "123456123@qq.com",
|
||||
wechat: "",
|
||||
qq: "",
|
||||
},
|
||||
onSave,
|
||||
}: EditInfoDialogProps) {
|
||||
const [formData, setFormData] = useState(initialData)
|
||||
const [showPassword, setShowPassword] = useState(false)
|
||||
|
||||
const handleChange = (field: string, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
onSave?.(formData)
|
||||
onOpenChange(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[480px] p-6">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-semibold text-center">
|
||||
编辑信息
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4 mt-2">
|
||||
{/* 用户名 */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">用户名</Label>
|
||||
<Input
|
||||
value={formData.username}
|
||||
onChange={e => handleChange("username", e.target.value)}
|
||||
className="h-11 bg-gray-50 border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 淘宝会员名 */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">淘宝会员名</Label>
|
||||
<Input
|
||||
value={formData.taobao}
|
||||
onChange={e => handleChange("taobao", e.target.value)}
|
||||
className="h-11 bg-gray-50 border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 密码 */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">密码</Label>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={formData.password}
|
||||
onChange={e => handleChange("password", e.target.value)}
|
||||
className="h-11 bg-gray-50 border-gray-200 pr-10"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||
>
|
||||
{showPassword ? (
|
||||
<EyeOff className="h-4 w-4" />
|
||||
) : (
|
||||
<Eye className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 邮箱 */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">邮箱</Label>
|
||||
<Input
|
||||
value={formData.email}
|
||||
onChange={e => handleChange("email", e.target.value)}
|
||||
className="h-11 bg-gray-50 border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 微信 */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">微信</Label>
|
||||
<Input
|
||||
value={formData.wechat}
|
||||
onChange={e => handleChange("wechat", e.target.value)}
|
||||
placeholder="请输入"
|
||||
className="h-11 bg-gray-50 border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* QQ */}
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-sm text-gray-600">QQ</Label>
|
||||
<Input
|
||||
value={formData.qq}
|
||||
onChange={e => handleChange("qq", e.target.value)}
|
||||
placeholder="请输入"
|
||||
className="h-11 bg-gray-50 border-gray-200"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 保存按钮 */}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
className="w-full h-11 mt-2 bg-blue-600 hover:bg-blue-700 text-white font-medium"
|
||||
>
|
||||
保存
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
123
src/components/data-table.tsx
Normal file
123
src/components/data-table.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import {
|
||||
type ColumnDef,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table"
|
||||
import { Loader } from "lucide-react"
|
||||
import { Pagination, type PaginationProps } from "@/components/ui/pagination"
|
||||
import {
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
Table as TableRoot,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export type DataTableProps<T> = {
|
||||
data: T[]
|
||||
status: "load" | "done" | "fail"
|
||||
columns: ColumnDef<T>[]
|
||||
pagination?: PaginationProps
|
||||
classNames?: {
|
||||
headRow?: string
|
||||
dataRow?: string
|
||||
}
|
||||
}
|
||||
|
||||
export default function DataTable<T extends Record<string, unknown>>(
|
||||
props: DataTableProps<T>,
|
||||
) {
|
||||
const table = useReactTable({
|
||||
data: props.data,
|
||||
columns: props.columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
manualPagination: true,
|
||||
rowCount: props.pagination?.total ?? props.data.length,
|
||||
state: {
|
||||
pagination: {
|
||||
pageIndex: props.pagination?.page ?? 0,
|
||||
pageSize: props.pagination?.size ?? props.data.length,
|
||||
},
|
||||
columnFilters: [],
|
||||
},
|
||||
autoResetPageIndex: false,
|
||||
})
|
||||
console.log("data length:", props.data.length)
|
||||
console.log("pageSize:", props.pagination?.size ?? props.data.length)
|
||||
console.log("rows:", table.getRowModel().rows.length)
|
||||
return (
|
||||
<>
|
||||
{/* 数据表 */}
|
||||
<div className="rounded-md relative bg-card">
|
||||
<TableRoot>
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map(group => (
|
||||
<TableRow key={group.id}>
|
||||
{group.headers.map(header => (
|
||||
<TableHead key={header.id} colSpan={header.colSpan}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext(),
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{props.status === "fail" ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={props.columns.length}
|
||||
className="text-center text-fail"
|
||||
>
|
||||
加载失败
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : !props.data?.length ? (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={props.columns.length}
|
||||
className="text-center"
|
||||
>
|
||||
暂无数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
table.getRowModel().rows.map(row => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className={cn("h-14", props.classNames?.dataRow)}
|
||||
>
|
||||
{row.getVisibleCells().map(cell => (
|
||||
<TableCell key={cell.id}>
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext(),
|
||||
)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</TableRoot>
|
||||
{props.status === "load" && (
|
||||
<div className="absolute inset-0 bg-white/10 backdrop-blur-xs flex items-center justify-center gap-2 transition">
|
||||
<Loader className="animate-spin" />
|
||||
<span>加载中</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 分页器 */}
|
||||
{props.pagination && <Pagination {...props.pagination} />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import * as React from "react"
|
||||
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils/index"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
import { Dialog as DialogPrimitive } from "radix-ui"
|
||||
import type * as React from "react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { cn } from "@/lib/utils/index"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
@@ -38,7 +37,7 @@ function DialogOverlay({
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -60,7 +59,7 @@ function DialogContent({
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -72,8 +71,7 @@ function DialogContent({
|
||||
className="absolute top-2 right-2"
|
||||
size="icon-sm"
|
||||
>
|
||||
<XIcon
|
||||
/>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
</DialogPrimitive.Close>
|
||||
@@ -106,7 +104,7 @@ function DialogFooter({
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -129,7 +127,7 @@ function DialogTitle({
|
||||
data-slot="dialog-title"
|
||||
className={cn(
|
||||
"font-heading text-base leading-none font-medium",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
@@ -145,7 +143,7 @@ function DialogDescription({
|
||||
data-slot="dialog-description"
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||
className
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
22
src/components/ui/label.tsx
Normal file
22
src/components/ui/label.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as React from "react"
|
||||
import { Label as LabelPrimitive } from "radix-ui"
|
||||
|
||||
import { cn } from "@/lib/utils/index"
|
||||
|
||||
function Label({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
|
||||
return (
|
||||
<LabelPrimitive.Root
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Label }
|
||||
304
src/components/ui/pagination.tsx
Normal file
304
src/components/ui/pagination.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
MoreHorizontalIcon,
|
||||
} from "lucide-react"
|
||||
import type * as React from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "./select"
|
||||
|
||||
export interface PaginationProps {
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
sizeOptions?: number[]
|
||||
onPageChange?: (page: number) => void
|
||||
onSizeChange?: (size: number) => void
|
||||
className?: string
|
||||
}
|
||||
|
||||
function Pagination({
|
||||
page,
|
||||
size,
|
||||
total,
|
||||
sizeOptions = [10, 20, 50, 100],
|
||||
onPageChange,
|
||||
onSizeChange,
|
||||
className,
|
||||
}: PaginationProps) {
|
||||
const [currentPage, setCurrentPage] = useState(page)
|
||||
const totalPages = Math.ceil(total / size)
|
||||
|
||||
// 同步外部 page 变化
|
||||
useEffect(() => {
|
||||
setCurrentPage(page)
|
||||
}, [page])
|
||||
|
||||
// 分页器逻辑
|
||||
const generatePaginationItems = () => {
|
||||
// 最多显示7个页码,其余用省略号
|
||||
const SIBLINGS = 1 // 当前页左右各显示的页码数
|
||||
const DOTS = -1 // 省略号标记
|
||||
|
||||
if (totalPages <= 7) {
|
||||
// 总页数少于7,全部显示
|
||||
return Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||
}
|
||||
|
||||
// 是否需要显示左边的省略号
|
||||
const showLeftDots = currentPage > 2 + SIBLINGS
|
||||
|
||||
// 是否需要显示右边的省略号
|
||||
const showRightDots = currentPage < totalPages - (2 + SIBLINGS)
|
||||
|
||||
if (showLeftDots && showRightDots) {
|
||||
// 两边都有省略号
|
||||
const leftSiblingIndex = Math.max(currentPage - SIBLINGS, 1)
|
||||
const rightSiblingIndex = Math.min(currentPage + SIBLINGS, totalPages)
|
||||
|
||||
return [
|
||||
1,
|
||||
DOTS,
|
||||
...Array.from(
|
||||
{ length: rightSiblingIndex - leftSiblingIndex + 1 },
|
||||
(_, i) => leftSiblingIndex + i,
|
||||
),
|
||||
DOTS,
|
||||
totalPages,
|
||||
]
|
||||
}
|
||||
|
||||
if (!showLeftDots && showRightDots) {
|
||||
// 只有右边有省略号
|
||||
return [
|
||||
...Array.from({ length: 3 + SIBLINGS * 2 }, (_, i) => i + 1),
|
||||
DOTS,
|
||||
totalPages,
|
||||
]
|
||||
}
|
||||
|
||||
if (showLeftDots && !showRightDots) {
|
||||
// 只有左边有省略号
|
||||
return [
|
||||
1,
|
||||
DOTS,
|
||||
...Array.from(
|
||||
{ length: 3 + SIBLINGS * 2 },
|
||||
(_, i) => totalPages - (3 + SIBLINGS * 2) + i + 1,
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
const handlePageChange = (newPage: number) => {
|
||||
if (newPage < 1 || newPage > totalPages || newPage === currentPage) {
|
||||
return
|
||||
}
|
||||
setCurrentPage(newPage)
|
||||
onPageChange?.(newPage)
|
||||
}
|
||||
|
||||
const handlePageSizeChange = (newSize: string) => {
|
||||
const parsedSize = parseInt(newSize, 10)
|
||||
if (onSizeChange) {
|
||||
onSizeChange(parsedSize)
|
||||
}
|
||||
}
|
||||
|
||||
const paginationItems = generatePaginationItems()
|
||||
|
||||
return (
|
||||
<div className={`flex flex-wrap items-center gap-4 ${className || ""}`}>
|
||||
<div className="flex-none flex items-center gap-2 text-sm text-muted-foreground">
|
||||
共 {total} 条记录,每页
|
||||
<Select value={size.toString()} onValueChange={handlePageSizeChange}>
|
||||
<SelectTrigger className="h-8 w-20">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{sizeOptions.map(option => (
|
||||
<SelectItem key={option} value={option.toString()}>
|
||||
{option}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
条
|
||||
</div>
|
||||
|
||||
<PaginationLayout>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<PaginationPrevious
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
className={
|
||||
currentPage === 1 ? "opacity-50 pointer-events-none" : ""
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{paginationItems.map((pageNum, index) => {
|
||||
if (pageNum === -1) {
|
||||
return (
|
||||
<PaginationItem key={`dots-${index}`}>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<PaginationItem key={pageNum}>
|
||||
<PaginationLink
|
||||
isActive={pageNum === currentPage}
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
>
|
||||
{pageNum}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
)
|
||||
})}
|
||||
|
||||
<PaginationItem>
|
||||
<PaginationNext
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
className={
|
||||
currentPage === totalPages
|
||||
? "opacity-50 pointer-events-none"
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</PaginationLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationLayout({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"nav">) {
|
||||
return (
|
||||
<nav
|
||||
// role="navigation"
|
||||
aria-label="pagination"
|
||||
data-slot="pagination"
|
||||
className={cn("flex-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationContent({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"ul">) {
|
||||
return (
|
||||
<ul
|
||||
data-slot="pagination-content"
|
||||
className={cn("flex flex-row items-center gap-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationItem({ ...props }: React.ComponentProps<"li">) {
|
||||
return <li data-slot="pagination-item" {...props} />
|
||||
}
|
||||
|
||||
type PaginationLinkProps = {
|
||||
isActive?: boolean
|
||||
} & React.ComponentProps<"a">
|
||||
|
||||
function PaginationLink({
|
||||
className,
|
||||
isActive,
|
||||
...props
|
||||
}: PaginationLinkProps) {
|
||||
return (
|
||||
<a
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
data-slot="pagination-link"
|
||||
data-active={isActive}
|
||||
className={cn(
|
||||
"inline-flex items-center justify-center text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 h-9 w-9 rounded-md border border-input hover:bg-secondary hover:text-secondary-foreground",
|
||||
`bg-card`,
|
||||
isActive &&
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationPrevious({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to previous page"
|
||||
className={cn("gap-1 px-2.5 sm:pl-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronLeftIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationNext({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof PaginationLink>) {
|
||||
return (
|
||||
<PaginationLink
|
||||
aria-label="Go to next page"
|
||||
className={cn("gap-1 px-2.5 sm:pr-2.5", className)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronRightIcon />
|
||||
</PaginationLink>
|
||||
)
|
||||
}
|
||||
|
||||
function PaginationEllipsis({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
aria-hidden
|
||||
data-slot="pagination-ellipsis"
|
||||
className={cn("flex size-9 items-center justify-center", className)}
|
||||
{...props}
|
||||
>
|
||||
<MoreHorizontalIcon className="size-4" />
|
||||
<span className="sr-only">More pages</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationEllipsis,
|
||||
PaginationItem,
|
||||
PaginationLayout,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
}
|
||||
192
src/components/ui/select.tsx
Normal file
192
src/components/ui/select.tsx
Normal file
@@ -0,0 +1,192 @@
|
||||
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
|
||||
import { Select as SelectPrimitive } from "radix-ui"
|
||||
import type * as React from "react"
|
||||
import { cn } from "@/lib/utils/index"
|
||||
|
||||
function Select({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
|
||||
return <SelectPrimitive.Root data-slot="select" {...props} />
|
||||
}
|
||||
|
||||
function SelectGroup({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
|
||||
return (
|
||||
<SelectPrimitive.Group
|
||||
data-slot="select-group"
|
||||
className={cn("scroll-my-1 p-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectValue({
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
|
||||
return <SelectPrimitive.Value data-slot="select-value" {...props} />
|
||||
}
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
|
||||
size?: "sm" | "default"
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon asChild>
|
||||
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
|
||||
</SelectPrimitive.Icon>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
position = "item-aligned",
|
||||
align = "center",
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
<SelectPrimitive.Content
|
||||
data-slot="select-content"
|
||||
data-align-trigger={position === "item-aligned"}
|
||||
className={cn(
|
||||
"relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
position === "popper" &&
|
||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||
className,
|
||||
)}
|
||||
position={position}
|
||||
align={align}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectPrimitive.Viewport
|
||||
data-position={position}
|
||||
className={cn(
|
||||
"data-[position=popper]:h-(--radix-select-trigger-height) data-[position=popper]:w-full data-[position=popper]:min-w-(--radix-select-trigger-width)",
|
||||
position === "popper" && "",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</SelectPrimitive.Viewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectPrimitive.Content>
|
||||
</SelectPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
|
||||
return (
|
||||
<SelectPrimitive.Label
|
||||
data-slot="select-label"
|
||||
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center">
|
||||
<SelectPrimitive.ItemIndicator>
|
||||
<CheckIcon className="pointer-events-none" />
|
||||
</SelectPrimitive.ItemIndicator>
|
||||
</span>
|
||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||
</SelectPrimitive.Item>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollUpButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpButton>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectScrollDownButton({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
|
||||
return (
|
||||
<SelectPrimitive.ScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"z-10 flex cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownButton>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectLabel,
|
||||
SelectScrollDownButton,
|
||||
SelectScrollUpButton,
|
||||
SelectSeparator,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
}
|
||||
114
src/components/ui/table.tsx
Normal file
114
src/components/ui/table.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils/index"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as React from "react"
|
||||
import { Tooltip as TooltipPrimitive } from "radix-ui"
|
||||
"use client"
|
||||
|
||||
import { cn } from "@/lib/utils/index"
|
||||
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
||||
import type * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function TooltipProvider({
|
||||
delayDuration = 0,
|
||||
@@ -19,7 +21,11 @@ function TooltipProvider({
|
||||
function Tooltip({
|
||||
...props
|
||||
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
|
||||
return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
return (
|
||||
<TooltipProvider>
|
||||
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
|
||||
</TooltipProvider>
|
||||
)
|
||||
}
|
||||
|
||||
function TooltipTrigger({
|
||||
@@ -40,13 +46,16 @@ function TooltipContent({
|
||||
data-slot="tooltip-content"
|
||||
sideOffset={sideOffset}
|
||||
className={cn(
|
||||
"z-50 inline-flex w-fit max-w-xs origin-(--radix-tooltip-content-transform-origin) items-center gap-1.5 rounded-md bg-foreground px-3 py-1.5 text-xs text-background has-data-[slot=kbd]:pr-1.5 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 **:data-[slot=kbd]:relative **:data-[slot=kbd]:isolate **:data-[slot=kbd]:z-50 **:data-[slot=kbd]:rounded-sm data-[state=delayed-open]:animate-in data-[state=delayed-open]:fade-in-0 data-[state=delayed-open]:zoom-in-95 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
className
|
||||
"bg-card border animate-in fade-in-0 zoom-in-95 ",
|
||||
"data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95",
|
||||
"data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||
"z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-sm text-balance",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
|
||||
{/* <TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" /> */}
|
||||
</TooltipPrimitive.Content>
|
||||
</TooltipPrimitive.Portal>
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ export type WrapProps = {
|
||||
|
||||
export default function Wrap(props: WrapProps) {
|
||||
return (
|
||||
<div className={`max-w-[1232px] w-full px-4 mx-auto ${props.className}`}>
|
||||
<div className={`max-w-308 w-full px-4 mx-auto ${props.className}`}>
|
||||
{props.children}
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user