styled edit form

This commit is contained in:
oiov
2025-05-06 18:59:38 +08:00
parent c99f3c31a6
commit a538b1c5a8
9 changed files with 248 additions and 229 deletions
+19 -12
View File
@@ -16,7 +16,9 @@ import {
CardHeader,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Modal } from "@/components/ui/modal";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import {
Table,
TableBody,
@@ -82,7 +84,7 @@ export default function UsersList({ user }: UrlListProps) {
});
const { mutate } = useSWRConfig();
const { data, error, isLoading } = useSWR<{ total: number; list: User[] }>(
const { data, isLoading } = useSWR<{ total: number; list: User[] }>(
`/api/user/admin?page=${currentPage}&size=${pageSize}&email=${searchParams.email}&userName=${searchParams.userName}`,
fetcher,
{
@@ -122,16 +124,6 @@ export default function UsersList({ user }: UrlListProps) {
</div>
</CardHeader>
<CardContent>
{isShowForm && (
<UserForm
user={{ id: user.id, name: user.name || "" }}
isShowForm={isShowForm}
setShowForm={setShowForm}
type="edit"
initData={currentEditUser}
onRefresh={handleRefresh}
/>
)}
<div className="mb-2 flex-row items-center gap-2 space-y-2 sm:flex sm:space-y-0">
<div className="relative w-full">
<Input
@@ -257,7 +249,7 @@ export default function UsersList({ user }: UrlListProps) {
</Badge>
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
<StatusDot status={user.active} />
<Switch defaultChecked={user.active === 1} />
</TableCell>
<TableCell className="col-span-1 hidden justify-center sm:flex">
{timeAgo(user.createdAt || "")}
@@ -299,6 +291,21 @@ export default function UsersList({ user }: UrlListProps) {
</Table>
</CardContent>
</Card>
<Modal
className="md:max-w-2xl"
showModal={isShowForm}
setShowModal={setShowForm}
>
<UserForm
user={{ id: user.id, name: user.name || "" }}
isShowForm={isShowForm}
setShowForm={setShowForm}
type="edit"
initData={currentEditUser}
onRefresh={handleRefresh}
/>
</Modal>
</>
);
}
@@ -19,6 +19,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Modal } from "@/components/ui/modal";
import { Skeleton } from "@/components/ui/skeleton";
import { Switch } from "@/components/ui/switch";
import {
@@ -190,17 +191,6 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
</div>
</CardHeader>
<CardContent>
{isShowForm && (
<RecordForm
user={{ id: user.id, name: user.name || "" }}
isShowForm={isShowForm}
setShowForm={setShowForm}
type={formType}
initData={currentEditRecord}
action={action}
onRefresh={handleRefresh}
/>
)}
<Table>
<TableHeader className="bg-gray-100/50 dark:bg-primary-foreground">
<TableRow className="grid grid-cols-3 items-center sm:grid-cols-8">
@@ -351,6 +341,22 @@ export default function UserRecordsList({ user, action }: RecordListProps) {
</Table>
</CardContent>
</Card>
<Modal
className="md:max-w-2xl"
showModal={isShowForm}
setShowModal={setShowForm}
>
<RecordForm
user={{ id: user.id, name: user.name || "" }}
isShowForm={isShowForm}
setShowForm={setShowForm}
type={formType}
initData={currentEditRecord}
action={action}
onRefresh={handleRefresh}
/>
</Modal>
</>
);
}
+18 -27
View File
@@ -23,11 +23,6 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@/components/ui/hover-card";
import { Input } from "@/components/ui/input";
import { Modal } from "@/components/ui/modal";
import { Skeleton } from "@/components/ui/skeleton";
@@ -46,10 +41,8 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import StatusDot from "@/components/dashboard/status-dot";
import { FormType } from "@/components/forms/record-form";
import { UrlForm } from "@/components/forms/url-form";
import { BlurImg } from "@/components/shared/blur-image";
import { CopyButton } from "@/components/shared/copy-button";
import { EmptyPlaceholder } from "@/components/shared/empty-placeholder";
import { Icons } from "@/components/shared/icons";
@@ -105,9 +98,6 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
const [pageSize, setPageSize] = useState(10);
const [isShowStats, setShowStats] = useState(false);
const [isShowQrcode, setShowQrcode] = useState(false);
const [qrcodeInfo, setQrcodeInfo] = useState({
payload: "",
});
const [selectedUrl, setSelectedUrl] = useState<ShortUrlFormData | null>(null);
const [searchParams, setSearchParams] = useState({
slug: "",
@@ -191,17 +181,6 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
</div>
</CardHeader>
<CardContent>
{isShowForm && (
<UrlForm
user={{ id: user.id, name: user.name || "" }}
isShowForm={isShowForm}
setShowForm={setShowForm}
type={formType}
initData={currentEditUrl}
action={action}
onRefresh={handleRefresh}
/>
)}
<div className="mb-2 flex-row items-center gap-2 space-y-2 sm:flex sm:space-y-0">
<div className="relative w-full">
<Input
@@ -392,12 +371,6 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
setShowForm(false);
setFormType("edit");
setShowForm(!isShowForm);
if (!isShowForm) {
window.scrollTo({
top: 0,
behavior: "smooth",
});
}
}}
>
<p className="hidden sm:block">Edit</p>
@@ -464,6 +437,7 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
</CardContent>
</Card>
{/* QR code editor */}
<Modal
className="md:max-w-lg"
showModal={isShowQrcode}
@@ -476,6 +450,23 @@ export default function UserUrlsList({ user, action }: UrlListProps) {
/>
)}
</Modal>
{/* Url form */}
<Modal
className="md:max-w-2xl"
showModal={isShowForm}
setShowModal={setShowForm}
>
<UrlForm
user={{ id: user.id, name: user.name || "" }}
isShowForm={isShowForm}
setShowForm={setShowForm}
type={formType}
initData={currentEditUrl}
action={action}
onRefresh={handleRefresh}
/>
</Modal>
</>
);
}
+15 -5
View File
@@ -3,14 +3,24 @@ import React from "react";
interface SectionColumnsType {
title: string;
children: React.ReactNode;
required?: boolean;
className?: string;
}
export function FormSectionColumns({ title, children }: SectionColumnsType) {
export function FormSectionColumns({
title,
children,
required,
className,
}: SectionColumnsType) {
return (
<div className="grid grid-cols-1 items-center gap-x-12 gap-y-2 py-2">
<h2 className="col-span-4 text-base font-semibold leading-none">
{title}
</h2>
<div className="grid w-full grid-cols-1 items-center gap-x-12 gap-y-2 py-2">
<div className="col-span-4 flex items-start gap-0.5 text-sm leading-none">
<h2 className="font-semibold">{title}</h2>
{required && (
<span className="text-neutral-500 dark:text-neutral-300">*</span>
)}
</div>
<div className="col-span-6">{children}</div>
</div>
);
+42 -36
View File
@@ -23,6 +23,7 @@ import {
SelectTrigger,
SelectValue,
} from "../ui/select";
import { Switch } from "../ui/switch";
export type FormData = CreateDNSRecord;
@@ -155,13 +156,13 @@ export function RecordForm({
};
return (
<div className="mb-4 rounded-lg border border-dashed shadow-sm animate-in fade-in-50">
<div>
<div className="rounded-t-lg bg-muted px-4 py-2 text-lg font-semibold">
{type === "add" ? "Create" : "Edit"} record
</div>
<form className="p-4" onSubmit={onSubmit}>
<div className="items-center justify-start gap-4 md:flex">
<FormSectionColumns title="Type">
<FormSectionColumns title="Type" required>
<Select
onValueChange={(value: RecordType) => {
setValue("type", value);
@@ -183,12 +184,12 @@ export function RecordForm({
</Select>
<p className="p-1 text-[13px] text-muted-foreground">Required.</p>
</FormSectionColumns>
<FormSectionColumns title="Name">
<FormSectionColumns title="Name" required>
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="name">
Name (required)
</Label>
<div className="relative">
<div className="relative w-full">
<Input
id="name"
className="flex-1 shadow-inner"
@@ -215,6 +216,32 @@ export function RecordForm({
)}
</div>
</FormSectionColumns>
</div>
<div className="items-center justify-start gap-4 md:flex">
<FormSectionColumns title="TTL" required>
<Select
onValueChange={(value: string) => {
setValue("ttl", Number(value));
}}
name="ttl"
defaultValue={String(initData?.ttl || 1)}
>
<SelectTrigger className="w-full shadow-inner">
<SelectValue placeholder="Select a time" />
</SelectTrigger>
<SelectContent>
{TTL_ENUMS.map((ttl) => (
<SelectItem key={ttl.value} value={ttl.value}>
{ttl.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="p-1 text-[13px] text-muted-foreground">
Optional. Time To Live.
</p>
</FormSectionColumns>
<FormSectionColumns
title={
currentRecordType === "CNAME"
@@ -254,29 +281,6 @@ export function RecordForm({
</div>
<div className="items-center justify-start gap-4 md:flex">
<FormSectionColumns title="TTL">
<Select
onValueChange={(value: string) => {
setValue("ttl", Number(value));
}}
name="ttl"
defaultValue={String(initData?.ttl || 1)}
>
<SelectTrigger className="w-full shadow-inner">
<SelectValue placeholder="Select a time" />
</SelectTrigger>
<SelectContent>
{TTL_ENUMS.map((ttl) => (
<SelectItem key={ttl.value} value={ttl.value}>
{ttl.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="p-1 text-[13px] text-muted-foreground">
Optional. Time To Live.
</p>
</FormSectionColumns>
<FormSectionColumns title="Comment">
<div className="flex items-center gap-2">
<Label className="sr-only" htmlFor="comment">
@@ -293,15 +297,17 @@ export function RecordForm({
Enter your comment here (up to 100 characters)
</p>
</FormSectionColumns>
{/* <FormSectionColumns title="Proxy">
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="proxy">
Proxy
</Label>
<Switch id="proxied" {...register("proxied")} />
</div>
<p className="p-1 text-[13px] text-muted-foreground">Proxy status</p>
</FormSectionColumns> */}
<FormSectionColumns title="Proxy">
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="proxy">
Proxy
</Label>
<Switch id="proxied" {...register("proxied")} />
</div>
<p className="p-1 text-[13px] text-muted-foreground">
Proxy status
</p>
</FormSectionColumns>
</div>
{/* Action buttons */}
+7 -10
View File
@@ -154,13 +154,13 @@ export function UrlForm({
};
return (
<div className="mb-4 rounded-lg border border-dashed shadow-sm animate-in fade-in-50">
<div>
<div className="rounded-t-lg bg-muted px-4 py-2 text-lg font-semibold">
{type === "add" ? "Create" : "Edit"} short link
</div>
<form className="p-4" onSubmit={onSubmit}>
<div className="items-center justify-start gap-4 md:flex">
<FormSectionColumns title="Target URL">
<FormSectionColumns title="Target URL" required>
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="target">
Target
@@ -184,13 +184,13 @@ export function UrlForm({
)}
</div>
</FormSectionColumns>
<FormSectionColumns title="Short Link">
<FormSectionColumns title="Short Link" required>
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="url">
Url
</Label>
<div className="relative flex items-center">
<div className="relative flex w-full items-center">
<Select
onValueChange={(value: string) => {
setValue("prefix", value);
@@ -210,12 +210,9 @@ export function UrlForm({
))}
</SelectContent>
</Select>
{/* <span className="pointer-events-none absolute left-20 whitespace-nowrap text-sm text-gray-400">
/s/
</span> */}
<Input
id="url"
className="w-3/5 flex-1 rounded-none pl-[8px] shadow-inner"
className="w-full rounded-none pl-[8px] shadow-inner"
size={20}
{...register("url")}
disabled={type === "edit"}
@@ -276,7 +273,7 @@ export function UrlForm({
)}
</div>
</FormSectionColumns>
<FormSectionColumns title="Expiration">
<FormSectionColumns title="Expiration" required>
<Select
onValueChange={(value: string) => {
setValue("expiration", value);
@@ -285,7 +282,7 @@ export function UrlForm({
defaultValue={initData?.expiration || "-1"}
>
<SelectTrigger className="w-full shadow-inner">
<SelectValue placeholder="Select a time" />
<SelectValue placeholder="Select a time range" />
</SelectTrigger>
<SelectContent>
{EXPIRATION_ENUMS.map((e) => (
+127 -125
View File
@@ -111,142 +111,144 @@ export function UserForm({
};
return (
<form
className="mb-4 rounded-lg border border-dashed p-4 shadow-sm animate-in fade-in-50"
onSubmit={onSubmit}
>
<div className="items-center justify-start gap-4 md:flex">
<FormSectionColumns title="Email">
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="email">
Email
<div>
<div className="rounded-t-lg bg-muted px-4 py-2 text-lg font-semibold">
Edit User
</div>
<form className="max-w-2xl p-4" onSubmit={onSubmit}>
<div className="items-center justify-start gap-4 md:flex">
<FormSectionColumns title="Email" required>
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="email">
Email
</Label>
<Input
id="email"
className="flex-1 shadow-inner"
size={32}
disabled
{...register("email")}
/>
</div>
{errors?.email && (
<p className="p-1 text-[13px] text-red-600">
{errors.email.message}
</p>
)}
</FormSectionColumns>
<FormSectionColumns title="Name">
<Label className="sr-only" htmlFor="name">
Name
</Label>
<Input
id="email"
id="name"
className="flex-1 shadow-inner"
size={32}
disabled
{...register("email")}
size={20}
{...register("name")}
/>
</div>
{errors?.email && (
<p className="p-1 text-[13px] text-red-600">
{errors.email.message}
</p>
)}
</FormSectionColumns>
<FormSectionColumns title="Name">
<Label className="sr-only" htmlFor="name">
Name
</Label>
<Input
id="name"
className="flex-1 shadow-inner"
size={20}
{...register("name")}
/>
{errors?.name && (
<p className="p-1 text-[13px] text-red-600">
{errors.name.message}
</p>
)}
</FormSectionColumns>
<FormSectionColumns title="Active">
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="active">
Active
</Label>
<Switch
id="active"
{...register("active")}
defaultChecked={initData?.active === 1}
onCheckedChange={(value) => setValue("active", value ? 1 : 0)}
/>
</div>
</FormSectionColumns>
</div>
{errors?.name && (
<p className="p-1 text-[13px] text-red-600">
{errors.name.message}
</p>
)}
</FormSectionColumns>
</div>
<div className="items-center justify-start gap-4 md:flex">
<FormSectionColumns title="Role">
<Select
onValueChange={(value: string) => {
setValue("role", value as UserRole);
}}
name="role"
defaultValue={`${initData?.role}` || "USER"}
>
<SelectTrigger className="w-full shadow-inner">
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
{ROLE_ENUM.map((role) => (
<SelectItem key={role.value} value={role.value}>
{role.label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormSectionColumns>
<FormSectionColumns title="Plan">
<Select
onValueChange={(value: string) => {
setValue("team", value);
}}
name="team"
defaultValue={`${initData?.team}` || "free"}
>
<SelectTrigger className="w-full shadow-inner">
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
<SelectContent>
{["free", "premium", "business"].map((role) => (
<SelectItem key={role} value={role}>
{role}
</SelectItem>
))}
</SelectContent>
</Select>
</FormSectionColumns>
</div>
<div className="items-center justify-start gap-4 md:flex">
<FormSectionColumns title="Role">
<Select
onValueChange={(value: string) => {
setValue("role", value as UserRole);
}}
name="role"
defaultValue={`${initData?.role}` || "USER"}
>
<SelectTrigger className="w-full shadow-inner">
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
{ROLE_ENUM.map((role) => (
<SelectItem key={role.value} value={role.value}>
{role.label}
</SelectItem>
))}
</SelectContent>
</Select>
</FormSectionColumns>
<FormSectionColumns title="Plan">
<Select
onValueChange={(value: string) => {
setValue("team", value);
}}
name="team"
defaultValue={`${initData?.team}` || "free"}
>
<SelectTrigger className="w-full shadow-inner">
<SelectValue placeholder="Select a plan" />
</SelectTrigger>
<SelectContent>
{["free", "premium", "business"].map((role) => (
<SelectItem key={role} value={role}>
{role}
</SelectItem>
))}
</SelectContent>
</Select>
</FormSectionColumns>
<FormSectionColumns title="Active">
<div className="flex w-full items-center gap-2">
<Label className="sr-only" htmlFor="active">
Active
</Label>
<Switch
id="active"
{...register("active")}
defaultChecked={initData?.active === 1}
onCheckedChange={(value) => setValue("active", value ? 1 : 0)}
/>
</div>
</FormSectionColumns>
</div>
{/* Action buttons */}
<div className="mt-3 flex justify-end gap-3">
{type === "edit" && (
{/* Action buttons */}
<div className="mt-3 flex justify-end gap-3">
{type === "edit" && (
<Button
type="button"
variant="destructive"
className="mr-auto w-[80px] px-0"
onClick={() => handleDelete()}
disabled={isDeleting}
>
{isDeleting ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
<p>Delete</p>
)}
</Button>
)}
<Button
type="button"
variant="destructive"
className="mr-auto w-[80px] px-0"
onClick={() => handleDelete()}
disabled={isDeleting}
type="reset"
variant="outline"
className="w-[80px] px-0"
onClick={() => setShowForm(false)}
>
{isDeleting ? (
Cancle
</Button>
<Button
type="submit"
variant="blue"
disabled={isPending}
className="w-[80px] shrink-0 px-0"
>
{isPending ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
<p>Delete</p>
<p>{type === "edit" ? "Update" : "Save"}</p>
)}
</Button>
)}
<Button
type="reset"
variant="outline"
className="w-[80px] px-0"
onClick={() => setShowForm(false)}
>
Cancle
</Button>
<Button
type="submit"
variant="blue"
disabled={isPending}
className="w-[80px] shrink-0 px-0"
>
{isPending ? (
<Icons.spinner className="size-4 animate-spin" />
) : (
<p>{type === "edit" ? "Update" : "Save"}</p>
)}
</Button>
</div>
</form>
</div>
</form>
</div>
);
}
+2 -2
View File
@@ -38,8 +38,8 @@ export default async function HeroLanding() {
</h1>
<p className="max-w-2xl text-balance text-muted-foreground sm:text-lg">
Link shortening, domain hosting, email receivers or senders <br /> and
screenshot api, everything you need to build better.
Link shortening, domain hosting, email manager and <br />
open api, everything you need to build better.
</p>
<div className="flex items-center justify-center gap-4">
+1 -1
View File
File diff suppressed because one or more lines are too long