Components
Loading preview...
Enhanced shadcn/ui dialog
npx shadcn@latest add https://21st.dev/r/originui/dialog"use client";
import { useCharacterLimit } from "@/components/hooks/use-character-limit";
import { useImageUpload } from "@/components/hooks/use-image-upload";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Check, ImagePlus, X } from "lucide-react";
import { useId, useState } from "react";
function Component() {
const id = useId();
const maxLength = 180;
const {
value,
characterCount,
handleChange,
maxLength: limit,
} = useCharacterLimit({
maxLength,
initialValue:
"Hey, I am Margaret, a web developer who loves turning ideas into amazing websites!",
});
return (
<Dialog>
<DialogTrigger asChild>
<Button variant="outline">Edit profile</Button>
</DialogTrigger>
<DialogContent className="flex flex-col gap-0 overflow-y-visible p-0 sm:max-w-lg [&>button:last-child]:top-3.5">
<DialogHeader className="contents space-y-0 text-left">
<DialogTitle className="border-b border-border px-6 py-4 text-base">
Edit profile
</DialogTitle>
</DialogHeader>
<DialogDescription className="sr-only">
Make changes to your profile here. You can change your photo and set a username.
</DialogDescription>
<div className="overflow-y-auto">
<ProfileBg defaultImage="https://originui.com/profile-bg.jpg" />
<Avatar defaultImage="https://originui.com/avatar-72-01.jpg" />
<div className="px-6 pb-6 pt-4">
<form className="space-y-4">
<div className="flex flex-col gap-4 sm:flex-row">
<div className="flex-1 space-y-2">
<Label htmlFor={`${id}-first-name`}>First name</Label>
<Input
id={`${id}-first-name`}
placeholder="Matt"
defaultValue="Margaret"
type="text"
required
/>
</div>
<div className="flex-1 space-y-2">
<Label htmlFor={`${id}-last-name`}>Last name</Label>
<Input
id={`${id}-last-name`}
placeholder="Welsh"
defaultValue="Villard"
type="text"
required
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor={`${id}-username`}>Username</Label>
<div className="relative">
<Input
id={`${id}-username`}
className="peer pe-9"
placeholder="Username"
defaultValue="margaret-villard-69"
type="text"
required
/>
<div className="pointer-events-none absolute inset-y-0 end-0 flex items-center justify-center pe-3 text-muted-foreground/80 peer-disabled:opacity-50">
<Check
size={16}
strokeWidth={2}
className="text-emerald-500"
aria-hidden="true"
/>
</div>
</div>
</div>
<div className="space-y-2">
<Label htmlFor={`${id}-website`}>Website</Label>
<div className="flex rounded-lg shadow-sm shadow-black/5">
<span className="-z-10 inline-flex items-center rounded-s-lg border border-input bg-background px-3 text-sm text-muted-foreground">
https://
</span>
<Input
id={`${id}-website`}
className="-ms-px rounded-s-none shadow-none"
placeholder="yourwebsite.com"
defaultValue="www.margaret.com"
type="text"
/>
</div>
</div>
<div className="space-y-2">
<Label htmlFor={`${id}-bio`}>Biography</Label>
<Textarea
id={`${id}-bio`}
placeholder="Write a few sentences about yourself"
defaultValue={value}
maxLength={maxLength}
onChange={handleChange}
aria-describedby={`${id}-description`}
/>
<p
id={`${id}-description`}
className="mt-2 text-right text-xs text-muted-foreground"
role="status"
aria-live="polite"
>
<span className="tabular-nums">{limit - characterCount}</span> characters left
</p>
</div>
</form>
</div>
</div>
<DialogFooter className="border-t border-border px-6 py-4">
<DialogClose asChild>
<Button type="button" variant="outline">
Cancel
</Button>
</DialogClose>
<DialogClose asChild>
<Button type="button">Save changes</Button>
</DialogClose>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function ProfileBg({ defaultImage }: { defaultImage?: string }) {
const [hideDefault, setHideDefault] = useState(false);
const { previewUrl, fileInputRef, handleThumbnailClick, handleFileChange, handleRemove } =
useImageUpload();
const currentImage = previewUrl || (!hideDefault ? defaultImage : null);
const handleImageRemove = () => {
handleRemove();
setHideDefault(true);
};
return (
<div className="h-32">
<div className="relative flex h-full w-full items-center justify-center overflow-hidden bg-muted">
{currentImage && (
<img
className="h-full w-full object-cover"
src={currentImage}
alt={previewUrl ? "Preview of uploaded image" : "Default profile background"}
width={512}
height={96}
/>
)}
<div className="absolute inset-0 flex items-center justify-center gap-2">
<button
type="button"
className="z-50 flex size-10 cursor-pointer items-center justify-center rounded-full bg-black/60 text-white outline-offset-2 transition-colors hover:bg-black/80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70"
onClick={handleThumbnailClick}
aria-label={currentImage ? "Change image" : "Upload image"}
>
<ImagePlus size={16} strokeWidth={2} aria-hidden="true" />
</button>
{currentImage && (
<button
type="button"
className="z-50 flex size-10 cursor-pointer items-center justify-center rounded-full bg-black/60 text-white outline-offset-2 transition-colors hover:bg-black/80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70"
onClick={handleImageRemove}
aria-label="Remove image"
>
<X size={16} strokeWidth={2} aria-hidden="true" />
</button>
)}
</div>
</div>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
accept="image/*"
aria-label="Upload image file"
/>
</div>
);
}
function Avatar({ defaultImage }: { defaultImage?: string }) {
const { previewUrl, fileInputRef, handleThumbnailClick, handleFileChange } = useImageUpload();
const currentImage = previewUrl || defaultImage;
return (
<div className="-mt-10 px-6">
<div className="relative flex size-20 items-center justify-center overflow-hidden rounded-full border-4 border-background bg-muted shadow-sm shadow-black/10">
{currentImage && (
<img
src={currentImage}
className="h-full w-full object-cover"
width={80}
height={80}
alt="Profile image"
/>
)}
<button
type="button"
className="absolute flex size-8 cursor-pointer items-center justify-center rounded-full bg-black/60 text-white outline-offset-2 transition-colors hover:bg-black/80 focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring/70"
onClick={handleThumbnailClick}
aria-label="Change profile picture"
>
<ImagePlus size={16} strokeWidth={2} aria-hidden="true" />
</button>
<input
type="file"
ref={fileInputRef}
onChange={handleFileChange}
className="hidden"
accept="image/*"
aria-label="Upload profile picture"
/>
</div>
</div>
);
}
export { Component };