style: enhance template selection UI and add view modes toggle (#1094)

* feat: enhance template selection UI and add view modes toggle

* fix: show template tags only in detailed view mode

* refactor: set detailed

---------

Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
This commit is contained in:
Vishal kadam 2025-01-13 02:04:28 +05:30 committed by GitHub
parent c0a2d2c399
commit c9308aebc2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -35,6 +35,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Select,
SelectContent,
@ -52,7 +53,6 @@ import {
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
import { ScrollArea } from "@radix-ui/react-scroll-area";
import {
BookText,
CheckIcon,
@ -61,12 +61,15 @@ import {
Github,
Globe,
HelpCircle,
LayoutGrid,
List,
PuzzleIcon,
SearchIcon,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
interface Props {
projectId: string;
}
@ -74,8 +77,9 @@ interface Props {
export const AddTemplate = ({ projectId }: Props) => {
const [query, setQuery] = useState("");
const [open, setOpen] = useState(false);
const { data } = api.compose.templates.useQuery();
const [viewMode, setViewMode] = useState<"detailed" | "icon">("detailed");
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const { data } = api.compose.templates.useQuery();
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: tags, isLoading: isLoadingTags } =
api.compose.getTags.useQuery();
@ -108,29 +112,28 @@ export const AddTemplate = ({ projectId }: Props) => {
<span>Template</span>
</DropdownMenuItem>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-7xl p-0">
<div className="sticky top-0 z-10 flex flex-col gap-4 bg-background p-6 border-b">
<DialogHeader>
<DialogContent className="max-h-screen sm:max-w-[90vw] p-0">
<DialogHeader className="sticky top-0 z-10 bg-background p-6 border-b">
<div className="flex flex-col space-y-4">
<div className="flex items-center justify-between">
<div>
<DialogTitle>Create from Template</DialogTitle>
<DialogDescription>
Create an open source application from a template
</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="flex flex-col md:flex-row gap-2">
</div>
<div className="flex items-center gap-4">
<Input
placeholder="Search Template"
onChange={(e) => setQuery(e.target.value)}
className="w-full"
className="w-[200px]"
value={query}
/>
<Popover modal={true}>
<PopoverTrigger asChild>
<Button
variant="outline"
className={cn(
"md:max-w-[15rem] w-full justify-between !bg-input",
)}
className={cn("w-[200px] justify-between !bg-input")}
>
{isLoadingTags
? "Loading...."
@ -143,17 +146,19 @@ export const AddTemplate = ({ projectId }: Props) => {
</PopoverTrigger>
<PopoverContent className="p-0" align="start">
<Command>
<CommandInput placeholder="Search tag..." className="h-9" />
<CommandInput
placeholder="Search tag..."
className="h-9"
/>
{isLoadingTags && (
<span className="py-6 text-center text-sm">
Loading Tags....
</span>
)}
<CommandEmpty>No tags found.</CommandEmpty>
<ScrollArea className="h-96 overflow-y-auto">
<ScrollArea className="h-96">
<CommandGroup>
{tags?.map((tag) => {
return (
{tags?.map((tag) => (
<CommandItem
value={tag}
key={tag}
@ -177,16 +182,50 @@ export const AddTemplate = ({ projectId }: Props) => {
)}
/>
</CommandItem>
);
})}
))}
</CommandGroup>
</ScrollArea>
</Command>
</PopoverContent>
</Popover>
<Button
size="icon"
onClick={() =>
setViewMode(viewMode === "detailed" ? "icon" : "detailed")
}
className="h-9 w-9"
>
{viewMode === "detailed" ? (
<LayoutGrid className="size-4" />
) : (
<List className="size-4" />
)}
</Button>
</div>
</div>
<div className="p-6 w-full">
{selectedTags.length > 0 && (
<div className="flex flex-wrap justify-end gap-2">
{selectedTags.map((tag) => (
<Badge
key={tag}
variant="secondary"
className="cursor-pointer"
onClick={() =>
setSelectedTags(selectedTags.filter((t) => t !== tag))
}
>
{tag} ×
</Badge>
))}
</div>
)}
</div>
</DialogHeader>
<ScrollArea className="h-[calc(98vh-8rem)]">
<div className="p-6">
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
{templates.length === 0 ? (
<div className="flex justify-center items-center w-full gap-2 min-h-[50vh]">
<SearchIcon className="text-muted-foreground size-6" />
@ -195,86 +234,118 @@ export const AddTemplate = ({ projectId }: Props) => {
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-4 w-full gap-4">
{templates?.map((template, index) => (
<div key={`template-${index}`}>
<div
key={template.id}
className="flex flex-col gap-4 border p-6 rounded-lg h-full"
className={cn(
"grid gap-6",
viewMode === "detailed"
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-6"
: "grid-cols-2 sm:grid-cols-4 lg:grid-cols-8",
)}
>
{templates?.map((template, index) => (
<div
key={`template-${index}`}
className={cn(
"flex flex-col border rounded-lg overflow-hidden relative",
viewMode === "icon" && "h-[200px]",
viewMode === "detailed" && "h-[400px]",
)}
>
<Badge className="absolute top-2 right-2" variant="blue">
{template.version}
</Badge>
{/* Template Header */}
<div
className={cn(
"flex-none p-6 pb-3 flex flex-col items-center gap-4 bg-muted/30",
viewMode === "detailed" && "border-b",
)}
>
<div className="flex flex-col gap-4">
<div className="flex flex-col items-center gap-2">
<img
src={`/templates/${template.logo}`}
className="size-28 object-contain"
alt=""
className={cn(
"object-contain",
viewMode === "detailed" ? "size-24" : "size-16",
)}
alt={template.name}
/>
</div>
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2 justify-center items-center">
<div className="flex flex-col gap-2 items-center justify-center">
<div className="flex flex-row gap-2 flex-wrap">
<span className="text-sm font-medium">
<div className="flex flex-col items-center gap-2">
<span className="text-sm font-medium line-clamp-1">
{template.name}
</span>
<Badge>{template.version}</Badge>
{viewMode === "detailed" &&
template.tags.length > 0 && (
<div className="flex flex-wrap justify-center gap-1.5">
{template.tags.map((tag) => (
<Badge
key={tag}
variant="green"
className="text-[10px] px-2 py-0"
>
{tag}
</Badge>
))}
</div>
)}
</div>
</div>
<div className="flex flex-row gap-0">
{/* Template Content */}
{viewMode === "detailed" && (
<ScrollArea className="flex-1 p-6">
<div className="text-sm text-muted-foreground">
{template.description}
</div>
</ScrollArea>
)}
{/* Create Button */}
<div
className={cn(
"flex-none px-6 pb-6 pt-3 mt-auto",
viewMode === "detailed"
? "flex items-center justify-between bg-muted/30 border-t"
: "flex justify-center",
)}
>
{viewMode === "detailed" && (
<div className="flex gap-2">
<Link
href={template.links.github}
target="_blank"
className={
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none"
}
className="text-muted-foreground hover:text-foreground transition-colors"
>
<Github className="size-4 text-muted-foreground" />
<Github className="size-5" />
</Link>
{template.links.website && (
<Link
href={template.links.website}
target="_blank"
className={
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none"
}
className="text-muted-foreground hover:text-foreground transition-colors"
>
<Globe className="size-4 text-muted-foreground" />
<Globe className="size-5" />
</Link>
)}
{template.links.docs && (
<Link
href={template.links.docs}
target="_blank"
className={
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none"
}
className="text-muted-foreground hover:text-foreground transition-colors"
>
<BookText className="size-4 text-muted-foreground" />
<BookText className="size-5" />
</Link>
)}
<Link
href={`https://github.com/Dokploy/dokploy/tree/canary/apps/dokploy/templates/${template.id}`}
target="_blank"
className={
"text-sm text-muted-foreground p-3 rounded-full hover:bg-border items-center flex transition-colors focus-visible:ring-ring focus-visible:ring-2 focus-visible:outline-none"
}
>
<Code className="size-4 text-muted-foreground" />
</Link>
</div>
<div className="flex flex-row gap-2 flex-wrap justify-center">
{template.tags.map((tag) => (
<Badge variant="secondary" key={tag}>
{tag}
</Badge>
))}
</div>
</div>
)}
<AlertDialog>
<AlertDialogTrigger asChild>
<Button onSelect={(e) => e.preventDefault()}>
<Button
size="sm"
className={cn(
"w-auto",
viewMode === "detailed" && "w-auto",
)}
>
Create
</Button>
</AlertDialogTrigger>
@ -304,9 +375,9 @@ export const AddTemplate = ({ projectId }: Props) => {
side="top"
>
<span>
If ot server is selected, the
application will be deployed on the
server where the user is logged in.
If ot server is selected, the application
will be deployed on the server where the
user is logged in.
</span>
</TooltipContent>
</Tooltip>
@ -369,18 +440,12 @@ export const AddTemplate = ({ projectId }: Props) => {
</AlertDialogContent>
</AlertDialog>
</div>
<p className="text-sm text-muted-foreground line-clamp-3">
{template.description}
</p>
</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
</ScrollArea>
</DialogContent>
</Dialog>
);