url -> long_url
This commit is contained in:
parent
b15cac7ed0
commit
dbf0b5d670
21 changed files with 237 additions and 175 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -35,7 +35,7 @@
|
|||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/prop-types": "^15.7.11",
|
||||
"@types/react": "^18",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"eslint": "^8",
|
||||
|
@ -44,7 +44,7 @@
|
|||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "^5"
|
||||
"typescript": "^5.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/prop-types": "^15.7.11",
|
||||
"@types/react": "^18",
|
||||
"@types/react": "^18.2.55",
|
||||
"@types/react-dom": "^18",
|
||||
"autoprefixer": "^10.4.17",
|
||||
"eslint": "^8",
|
||||
|
|
12
src/app/[slug]/db.tsx
Normal file
12
src/app/[slug]/db.tsx
Normal file
|
@ -0,0 +1,12 @@
|
|||
"use server";
|
||||
import { initConnection } from "@/components/db-utils";
|
||||
|
||||
export async function querydb(slug: string) {
|
||||
let db = await initConnection();
|
||||
let long_url = await db.query(`
|
||||
select * from url:[$id];
|
||||
`, { id: slug });
|
||||
|
||||
console.log(long_url, slug)
|
||||
return long_url;
|
||||
}
|
10
src/app/[slug]/page.tsx
Normal file
10
src/app/[slug]/page.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { redirect } from "next/navigation";
|
||||
import { querydb } from "./db";
|
||||
|
||||
// export default function Page({ params }: { params: { slug: string } }) {
|
||||
// redirect(`https://${params.slug}`)
|
||||
// }
|
||||
|
||||
export default function Page({ params }: { params: { slug: string } }) {
|
||||
redirect(`https://${querydb(params.slug)}`)
|
||||
}
|
113
src/app/create/create.tsx
Normal file
113
src/app/create/create.tsx
Normal file
|
@ -0,0 +1,113 @@
|
|||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CopyIcon } from "@radix-ui/react-icons";
|
||||
import { useFormState, useFormStatus } from "react-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { querydb } from "./db";
|
||||
import { formSchema } from "./schema";
|
||||
|
||||
const initialState = {
|
||||
url: null,
|
||||
}
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
return (
|
||||
<Button type="submit">Submit</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CreateCard() {
|
||||
// @ts-ignore
|
||||
const [state, formAction] = useFormState(querydb, initialState);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
url: "",
|
||||
},
|
||||
})
|
||||
|
||||
const handleCopyUrl = () => {
|
||||
if (state && state.url) {
|
||||
const currentSiteName = window.location.hostname;
|
||||
const url = `https://${currentSiteName}/${state.url.toString()}`;
|
||||
navigator.clipboard.writeText(url)
|
||||
.catch(err => {
|
||||
console.error('Failed to copy URL to clipboard:', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
// <CardGrid>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create a Shortened URL</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
{/* @ts-ignore */}
|
||||
<form action={form.handleSubmit(formAction)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Enter a url</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="nexveridian.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton />
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
{state && state.url && (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button onClick={handleCopyUrl}>
|
||||
<CopyIcon className="mr-2 h-4 w-4" /> Copy Url
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<p className="text-lg">
|
||||
Url added to clipboard
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
// </CardGrid>
|
||||
);
|
||||
}
|
|
@ -3,7 +3,7 @@ import { initConnection } from "@/components/db-utils";
|
|||
import { formSchema } from "./schema";
|
||||
|
||||
export async function querydb(prevState: any, formData: FormData) {
|
||||
const values = formSchema.safeParse({ url: formData.get("url") })
|
||||
const values = formSchema.safeParse(formData)
|
||||
if (!values.success) {
|
||||
return { error: values.error };
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export async function querydb(prevState: any, formData: FormData) {
|
|||
let db = await initConnection();
|
||||
let url = await db.query(`
|
||||
create url:[rand::string(8)] CONTENT {
|
||||
long_url: string::replace($long_url, "http://", "https://"),
|
||||
long_url: string::replace(string::replace($long_url, "https://", ""), "http://", ""),
|
||||
clicks: 0,
|
||||
date_added: time::now(),
|
||||
date_accessed: <future> { time::now() }
|
||||
|
|
|
@ -1,109 +0,0 @@
|
|||
"use client";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { CopyIcon } from "@radix-ui/react-icons";
|
||||
import { useFormState, useFormStatus } from "react-dom";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { querydb } from "./db";
|
||||
import { formSchema } from "./schema";
|
||||
|
||||
const initialState = {
|
||||
url: null,
|
||||
}
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus();
|
||||
return (
|
||||
<Button type="submit">Submit</Button>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [state, formAction] = useFormState(querydb, initialState);
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: {
|
||||
url: "",
|
||||
},
|
||||
})
|
||||
|
||||
const handleCopyUrl = () => {
|
||||
if (state && state.url) {
|
||||
navigator.clipboard.writeText(state.url)
|
||||
.catch(err => {
|
||||
console.error('Failed to copy URL to clipboard:', err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="hidden items-start justify-center gap-6 rounded-lg p-8 md:grid lg:grid-cols-3 xl:grid-cols-3">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Create a Shortened URL</CardTitle>
|
||||
</CardHeader>
|
||||
|
||||
<CardContent>
|
||||
<Form {...form}>
|
||||
<form action={form.handleSubmit(querydb)} className="space-y-8">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Enter a url</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="nexveridian.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<SubmitButton />
|
||||
</form>
|
||||
</Form>
|
||||
</CardContent>
|
||||
|
||||
<CardFooter>
|
||||
{state && state.url && (
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<Button onClick={handleCopyUrl}>
|
||||
<CopyIcon className="mr-2 h-4 w-4" /> Copy Url
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent>
|
||||
<p className="text-lg">
|
||||
Url added to clipboard
|
||||
</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div >
|
||||
);
|
||||
}
|
23
src/app/global-error.tsx
Normal file
23
src/app/global-error.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
"use client";
|
||||
import CardGrid from "@/components/card-grid";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
}) {
|
||||
return (
|
||||
<CardGrid>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-2xl text-red-400">Error</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</CardGrid>
|
||||
);
|
||||
}
|
23
src/app/not-found.tsx
Normal file
23
src/app/not-found.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
"use client";
|
||||
import CardGrid from "@/components/card-grid";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
|
||||
export default function GlobalError({
|
||||
error,
|
||||
}: {
|
||||
error: Error & { digest?: string }
|
||||
}) {
|
||||
return (
|
||||
<CardGrid>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-center text-2xl text-red-400">404 - Not Found</CardTitle>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</CardGrid>
|
||||
);
|
||||
}
|
|
@ -1,21 +1,24 @@
|
|||
"use client";
|
||||
import CardGrid from "@/components/card-grid";
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardTitle
|
||||
} from "@/components/ui/card";
|
||||
import Link from "next/link";
|
||||
import CreateCard from "./create/create";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="hidden items-start justify-center gap-6 rounded-lg p-8 md:grid lg:grid-cols-2 xl:grid-cols-3">
|
||||
<Card>
|
||||
<CardGrid>
|
||||
{/* <Card>
|
||||
<Link href="/create">
|
||||
<CardHeader>
|
||||
<CardTitle>Create a Shortened URL</CardTitle>
|
||||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
</Card> */}
|
||||
<CreateCard />
|
||||
|
||||
<Card>
|
||||
<Link href="/stats">
|
||||
|
@ -24,6 +27,6 @@ export default function Home() {
|
|||
</CardHeader>
|
||||
</Link>
|
||||
</Card>
|
||||
</div >
|
||||
</CardGrid>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,9 +8,9 @@ export default function Nav() {
|
|||
<Link href="/">Next Url Shortener</Link>
|
||||
</div>
|
||||
|
||||
<div className="transition-colors text-foreground/50 hover:text-foreground/100">
|
||||
{/* <div className="transition-colors text-foreground/50 hover:text-foreground/100">
|
||||
<Link href="/create">Create</Link>
|
||||
</div>
|
||||
</div> */}
|
||||
|
||||
<div className="transition-colors text-foreground/50 hover:text-foreground/100">
|
||||
<Link href="/stats">Stats</Link>
|
||||
|
|
18
src/components/card-grid.tsx
Normal file
18
src/components/card-grid.tsx
Normal file
|
@ -0,0 +1,18 @@
|
|||
export default function CardGrid({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`hidden items-start justify-center gap-6 rounded-lg p-8 md:grid lg:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4" ${className}`}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
@ -36,7 +36,7 @@ const buttonVariants = cva(
|
|||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
VariantProps<typeof buttonVariants> {
|
||||
asChild?: boolean
|
||||
}
|
||||
|
||||
|
|
|
@ -73,4 +73,4 @@ const CardFooter = React.forwardRef<
|
|||
))
|
||||
CardFooter.displayName = "CardFooter"
|
||||
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||
export { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import * as React from "react"
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronRightIcon,
|
||||
DotFilledIcon,
|
||||
} from "@radix-ui/react-icons"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
@ -185,19 +185,8 @@ const DropdownMenuShortcut = ({
|
|||
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
|
||||
|
||||
export {
|
||||
DropdownMenu,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuPortal,
|
||||
DropdownMenuSub,
|
||||
DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator,
|
||||
DropdownMenuShortcut, DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuSubTrigger, DropdownMenuTrigger
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import * as React from "react"
|
||||
import {
|
||||
Controller,
|
||||
ControllerProps,
|
||||
|
@ -10,8 +10,8 @@ import {
|
|||
useFormContext,
|
||||
} from "react-hook-form"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Label } from "@/components/ui/label"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Form = FormProvider
|
||||
|
||||
|
@ -165,12 +165,7 @@ const FormMessage = React.forwardRef<
|
|||
FormMessage.displayName = "FormMessage"
|
||||
|
||||
export {
|
||||
useFormField,
|
||||
Form,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormMessage,
|
||||
FormField,
|
||||
Form, FormControl,
|
||||
FormDescription, FormField, FormItem,
|
||||
FormLabel, FormMessage, useFormField
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ import * as React from "react"
|
|||
import { cn } from "@/lib/utils"
|
||||
|
||||
export interface InputProps
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> {}
|
||||
extends React.InputHTMLAttributes<HTMLInputElement> { }
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||
({ className, type, ...props }, ref) => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as React from "react"
|
||||
import * as LabelPrimitive from "@radix-ui/react-label"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
@ -11,7 +11,7 @@ const labelVariants = cva(
|
|||
const Label = React.forwardRef<
|
||||
React.ElementRef<typeof LabelPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
|
||||
VariantProps<typeof labelVariants>
|
||||
VariantProps<typeof labelVariants>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<LabelPrimitive.Root
|
||||
ref={ref}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react"
|
||||
import * as PopoverPrimitive from "@radix-ui/react-popover"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
@ -28,4 +28,4 @@ const PopoverContent = React.forwardRef<
|
|||
))
|
||||
PopoverContent.displayName = PopoverPrimitive.Content.displayName
|
||||
|
||||
export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
|
||||
export { Popover, PopoverAnchor, PopoverContent, PopoverTrigger }
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import * as React from "react"
|
||||
import {
|
||||
CaretSortIcon,
|
||||
CheckIcon,
|
||||
|
@ -6,6 +5,7 @@ import {
|
|||
ChevronUpIcon,
|
||||
} from "@radix-ui/react-icons"
|
||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
@ -80,7 +80,7 @@ const SelectContent = React.forwardRef<
|
|||
className={cn(
|
||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-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",
|
||||
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",
|
||||
"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}
|
||||
|
@ -91,7 +91,7 @@ const SelectContent = React.forwardRef<
|
|||
className={cn(
|
||||
"p-1",
|
||||
position === "popper" &&
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
@ -149,14 +149,5 @@ const SelectSeparator = React.forwardRef<
|
|||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||
|
||||
export {
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectValue,
|
||||
SelectTrigger,
|
||||
SelectContent,
|
||||
SelectLabel,
|
||||
SelectItem,
|
||||
SelectSeparator,
|
||||
SelectScrollUpButton,
|
||||
SelectScrollDownButton,
|
||||
Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue
|
||||
}
|
||||
|
|
|
@ -109,12 +109,6 @@ const TableCaption = React.forwardRef<
|
|||
TableCaption.displayName = "TableCaption"
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
Table, TableBody, TableCaption, TableCell, TableFooter,
|
||||
TableHead, TableHeader, TableRow
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue