diff options
author | polwex <polwex@sortug.com> | 2025-05-29 16:25:31 +0700 |
---|---|---|
committer | polwex <polwex@sortug.com> | 2025-05-29 16:25:31 +0700 |
commit | a03c92dc82ad527d7da6bbaa3c43000e2e5f0e69 (patch) | |
tree | 9a47cae250d043d31f751c1383bdcbe09d4bc9d8 /src/components | |
parent | 7de09570c0d7907424c30f492207e80ff69e4061 (diff) |
better better
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/logintest/Form.tsx | 53 | ||||
-rw-r--r-- | src/components/logintest/ServerForm.tsx | 67 | ||||
-rw-r--r-- | src/components/ui/badge.tsx | 36 | ||||
-rw-r--r-- | src/components/ui/tabs.tsx | 64 |
4 files changed, 220 insertions, 0 deletions
diff --git a/src/components/logintest/Form.tsx b/src/components/logintest/Form.tsx new file mode 100644 index 0000000..a593acb --- /dev/null +++ b/src/components/logintest/Form.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { useFormStatus } from "react-dom"; + +const SubmitButton = () => { + const { pending } = useFormStatus(); + return ( + <> + <button + disabled={pending} + type="submit" + className="hover:bg-slate-50 w-fit rounded-lg bg-white p-2" + > + {pending ? "Pending..." : "Submit"} + </button> + </> + ); +}; + +export const Form = ({ + message, + greet, +}: { + message: Promise<string>; + greet: (formData: FormData) => Promise<void>; +}) => ( + <div style={{ border: "3px blue dashed", margin: "1em", padding: "1em" }}> + <p>{message}</p> + <form action={greet}> + <div className="flex flex-col gap-1 text-left"> + <div> + Name:{" "} + <input + name="name" + required + className="invalid:border-red-500 rounded-sm border px-2 py-1" + /> + </div> + <div> + Email:{" "} + <input + type="email" + name="email" + required + className="invalid:border-red-500 rounded-sm border px-2 py-1" + /> + </div> + <SubmitButton /> + </div> + </form> + <h3>This is a client component.</h3> + </div> +); diff --git a/src/components/logintest/ServerForm.tsx b/src/components/logintest/ServerForm.tsx new file mode 100644 index 0000000..8e629b8 --- /dev/null +++ b/src/components/logintest/ServerForm.tsx @@ -0,0 +1,67 @@ +async function submitUserProfile(formData: FormData) { + "use server"; + const name = formData.get("name"); + const age = formData.get("age"); + const favoriteColor = formData.get("favoriteColor"); + const hobby = formData.get("hobby"); + const isSubscribed = formData.get("newsletter") === "on"; + + console.log({ + name, + age, + favoriteColor, + hobby, + isSubscribed, + }); +} + +export const ServerForm = () => { + return ( + <form action={submitUserProfile} className="space-y-4"> + <div style={{ display: "flex", gap: 4, marginBottom: 4 }}> + <label htmlFor="name">Full Name</label> + <input type="text" name="name" id="name" required /> + </div> + + <div style={{ display: "flex", gap: 4, marginBottom: 4 }}> + <label htmlFor="age">Age</label> + <input type="number" name="age" id="age" min="13" max="120" /> + </div> + + <div style={{ display: "flex", gap: 4, marginBottom: 4 }}> + <label htmlFor="favoriteColor">Favorite Color</label> + <select name="favoriteColor" id="favoriteColor"> + <option value="red">Red</option> + <option value="blue">Blue</option> + <option value="green">Green</option> + <option value="purple">Purple</option> + <option value="yellow">Yellow</option> + </select> + </div> + + <div style={{ display: "flex", gap: 4, marginBottom: 4 }}> + <label htmlFor="hobby">Favorite Hobby</label> + <input + type="text" + name="hobby" + id="hobby" + placeholder="e.g. Reading, Gaming, Cooking" + /> + </div> + + <div style={{ display: "flex", gap: 4, marginBottom: 4 }}> + <label> + <input type="checkbox" name="newsletter" /> + Subscribe to newsletter + </label> + </div> + + <button + type="submit" + className="hover:bg-slate-50 w-fit rounded-lg bg-white p-2" + > + Save Profile + </button> + </form> + ); +}; diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx new file mode 100644 index 0000000..12daad7 --- /dev/null +++ b/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes<HTMLDivElement>, + VariantProps<typeof badgeVariants> {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( + <div className={cn(badgeVariants({ variant }), className)} {...props} /> + ) +} + +export { Badge, badgeVariants }
\ No newline at end of file diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..3d6f3ac --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,64 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Root>) { + return ( + <TabsPrimitive.Root + data-slot="tabs" + className={cn("flex flex-col gap-2", className)} + {...props} + /> + ) +} + +function TabsList({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.List>) { + return ( + <TabsPrimitive.List + data-slot="tabs-list" + className={cn( + "bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]", + className + )} + {...props} + /> + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Trigger>) { + return ( + <TabsPrimitive.Trigger + data-slot="tabs-trigger" + className={cn( + "data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + className + )} + {...props} + /> + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps<typeof TabsPrimitive.Content>) { + return ( + <TabsPrimitive.Content + data-slot="tabs-content" + className={cn("flex-1 outline-none", className)} + {...props} + /> + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent } |