diff --git a/app/package.json b/app/package.json index a1e76f2..a0f490f 100644 --- a/app/package.json +++ b/app/package.json @@ -16,7 +16,6 @@ "@codemirror/language": "^6.10.1", "@codemirror/legacy-modes": "6.4.0", "@codemirror/view": "6.29.0", - "zustand":"5.0.3", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index 1d51e6d..3669f77 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -93,7 +93,7 @@ importers: specifier: 2.3.0 version: 2.3.0(vite@6.2.1(@types/node@20.17.24)(jiti@2.4.2)(lightningcss@1.29.2)) zustand: - specifier: 5.0.3 + specifier: ^5.0.3 version: 5.0.3(@types/react@19.0.10)(react@19.0.0) devDependencies: '@types/node': diff --git a/app/src/App.tsx b/app/src/App.tsx index 9f7195f..f6c69ec 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,7 +1,7 @@ import TemplateGrid from "./components/TemplateGrid"; import Navigation from "./components/Navigation"; import Search from "./components/Search"; -import { useStore } from "./store"; +import { useStore } from "@/store"; import "./App.css"; function App() { diff --git a/app/src/components/Navigation.tsx b/app/src/components/Navigation.tsx index 26c9bc4..c1f041c 100644 --- a/app/src/components/Navigation.tsx +++ b/app/src/components/Navigation.tsx @@ -1,5 +1,5 @@ import { ModeToggle } from "@/mode-toggle"; -import { StarIcon } from "lucide-react"; +import { Plus, StarIcon } from "lucide-react"; import { Button } from "./ui/button"; import { useEffect, useState } from "react"; import DokployLogo from "./ui/dokploy-logo"; @@ -29,6 +29,18 @@ const Navigation = () => {

Dokploy Templates

+ + + +
+ + )} + + + + Docker Compose + + + Configuration + + + + {templateFiles?.dockerCompose && ( +
+ +
+ + +
+
+ )} +
+ + + {templateFiles?.config && ( +
+ +
+ + +
+
+ )} +
+
+ + {!templateFiles?.dockerCompose && !templateFiles?.config && ( +
+

+ No configuration files available for this template. +

+
+ )} + + )} + + + + + ); +}; + +export default TemplateDialog; diff --git a/app/src/components/TemplateGrid.tsx b/app/src/components/TemplateGrid.tsx index 9f56eaa..34c3eeb 100644 --- a/app/src/components/TemplateGrid.tsx +++ b/app/src/components/TemplateGrid.tsx @@ -1,5 +1,4 @@ import React, { useEffect, useState } from "react"; -import { Input } from "./ui/input"; import { Card, CardHeader, @@ -7,23 +6,10 @@ import { CardContent, CardFooter, } from "./ui/card"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, -} from "./ui/dialog"; -import { Button } from "./ui/button"; -import { toast } from "sonner"; -import copy from "copy-to-clipboard"; -import { CodeEditor } from "./ui/code-editor"; -import { useStore } from "../store"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs"; -import { Label } from "./ui/label"; -import { Clipboard } from "lucide-react"; +import { useStore } from "@/store"; import { Skeleton } from "./ui/skeleton"; import { cn } from "@/lib/utils"; +import TemplateDialog from "./TemplateDialog"; interface Template { id: string; @@ -49,7 +35,13 @@ interface TemplateGridProps { } const TemplateGrid: React.FC = ({ view }) => { - const { templates, setTemplates } = useStore(); + const { + templates, + setTemplates, + setTemplatesCount, + filteredTemplates, + setFilteredTemplates, + } = useStore(); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const searchQuery = useStore((state) => state.searchQuery); @@ -73,6 +65,8 @@ const TemplateGrid: React.FC = ({ view }) => { } const data = await response.json(); setTemplates(data); + setFilteredTemplates(data); + setTemplatesCount(data.length); setLoading(false); } catch (err) { setError(err instanceof Error ? err.message : "An error occurred"); @@ -81,7 +75,7 @@ const TemplateGrid: React.FC = ({ view }) => { }; fetchTemplates(); - }, [setTemplates]); + }, [setTemplates, setFilteredTemplates]); const fetchTemplateFiles = async (templateId: string) => { setModalLoading(true); @@ -111,32 +105,25 @@ const TemplateGrid: React.FC = ({ view }) => { fetchTemplateFiles(template.id); }; - const filteredTemplates = templates.filter((template) => { - // Filter by search query - const matchesSearch = template.name - .toLowerCase() - .includes(searchQuery.toLowerCase()); + useEffect(() => { + const filtered = templates.filter((template) => { + // Filter by search query + const matchesSearch = template.name + .toLowerCase() + .includes(searchQuery.toLowerCase()); - // Filter by selected tags - const matchesTags = - selectedTags.length === 0 || - selectedTags.every((tag) => template.tags.includes(tag)); + // Filter by selected tags + const matchesTags = + selectedTags.length === 0 || + selectedTags.every((tag) => template.tags.includes(tag)); - return matchesSearch && matchesTags; - }); + return matchesSearch && matchesTags; + }); - console.log(filteredTemplates); - - const getBase64Config = () => { - if (!templateFiles?.dockerCompose && !templateFiles?.config) return ""; - - const configObj = { - compose: templateFiles.dockerCompose || "", - config: templateFiles.config || "", - }; - - return btoa(JSON.stringify(configObj, null, 2)); - }; + console.log("ffiltered tem", filtered.length); + setTemplatesCount(filtered.length); + setFilteredTemplates(filtered); + }, [searchQuery, selectedTags]); if (loading) { return ( @@ -192,21 +179,28 @@ const TemplateGrid: React.FC = ({ view }) => { " cursor-pointer hover:shadow-lg transition-all duration-200 h-full max-h-[300px]", { "flex-col": view === "grid", - "flex-row": view === "rows", + "flex-row gap-0": view === "rows", } )} > - - - {template.name} - {template.name} - + + {template.name} + {template.name}

{template.description}

@@ -255,197 +249,12 @@ const TemplateGrid: React.FC = ({ view }) => { - setSelectedTemplate(null)} - > - - -
- {selectedTemplate?.logo && ( - {selectedTemplate.name} - )} -
- - {selectedTemplate?.name} - -
- - {selectedTemplate?.version} - -
- {selectedTemplate?.links.github && ( - - GitHub - - )} - {selectedTemplate?.links.docs && ( - - Docs - - )} - - - Edit Template - -
-
-
-
-
- -
- - {selectedTemplate?.description} - -
- {selectedTemplate?.tags.map((tag) => ( - - {tag} - - ))} -
- - {modalLoading ? ( -
-
-

Loading template files...

-
- ) : ( -
- {(templateFiles?.dockerCompose || templateFiles?.config) && ( -
- -
- - -
-
- )} - - - - Docker Compose - - Configuration - - - {templateFiles?.dockerCompose && ( -
- - - - -
- )} -
- - - {templateFiles?.config && ( -
- - - - - -
- )} -
-
- - {!templateFiles?.dockerCompose && !templateFiles?.config && ( -
-

- No configuration files available for this template. -

-
- )} -
- )} -
-
-
+ !open && setSelectedTemplate(null)} + /> ); }; diff --git a/app/src/components/ui/dialog.tsx b/app/src/components/ui/dialog.tsx index 835b2ef..ef5000e 100644 --- a/app/src/components/ui/dialog.tsx +++ b/app/src/components/ui/dialog.tsx @@ -61,10 +61,6 @@ function DialogContent({ {...props} > {children} - - - Close - ); @@ -74,9 +70,16 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { return (
+ > + {props.children} + + + + Close + +
); } diff --git a/app/src/store.ts b/app/src/store.ts deleted file mode 100644 index 2220c82..0000000 --- a/app/src/store.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { create } from "zustand"; -import { persist } from "zustand/middleware"; - -interface Template { - id: string; - name: string; - description: string; - version: string; - logo?: string; - links: { - github?: string; - website?: string; - docs?: string; - }; - tags: string[]; -} - -interface TemplateStore { - templates: Template[]; - setTemplates: (templates: Template[]) => void; - searchQuery: string; - setSearchQuery: (searchQuery: string) => void; - selectedTags: string[]; - addSelectedTag: (tag: string) => void; - removeSelectedTag: (tag: string) => void; - view: "grid" | "rows"; - setView: (view: "grid" | "rows") => void; -} - -export const useStore = create()( - persist( - (set) => ({ - templates: [], - setTemplates: (templates) => set({ templates }), - searchQuery: "", - setSearchQuery: (searchQuery) => set({ searchQuery }), - selectedTags: [], - addSelectedTag: (tag) => - set((state) => ({ - selectedTags: state.selectedTags.includes(tag) - ? state.selectedTags - : [...state.selectedTags, tag], - })), - removeSelectedTag: (tag) => - set((state) => ({ - selectedTags: state.selectedTags.filter((t) => t !== tag), - })), - view: "grid", - setView: (view) => set({ view }), - }), - { - name: "template-store", - partialize: (state) => ({ view: state.view }), // Only persist the view preference - } - ) -); diff --git a/app/src/store/index.ts b/app/src/store/index.ts index fb7c819..9f670f4 100644 --- a/app/src/store/index.ts +++ b/app/src/store/index.ts @@ -1,4 +1,5 @@ import { create } from "zustand"; +import { persist } from "zustand/middleware"; interface Template { id: string; @@ -14,16 +15,56 @@ interface Template { tags: string[]; } -interface Store { +interface TemplateStore { templates: Template[]; setTemplates: (templates: Template[]) => void; + templatesCount: number; + filteredTemplates: Template[]; + setFilteredTemplates: (filteredTemplates: Template[]) => void; + setTemplatesCount: (templatesCount: number) => void; + searchQuery: string; + setSearchQuery: (searchQuery: string) => void; + selectedTags: string[]; + addSelectedTag: (tag: string) => void; + removeSelectedTag: (tag: string) => void; + view: "grid" | "rows"; + setView: (view: "grid" | "rows") => void; + githubStars: number; setGithubStars: (count: number) => void; } -export const useStore = create((set) => ({ - templates: [], - setTemplates: (templates) => set({ templates }), - githubStars: 0, - setGithubStars: (count) => set({ githubStars: count }), -})); +export const useStore = create()( + persist( + (set) => ({ + templates: [], + setTemplates: (templates) => set({ templates }), + templatesCount: 0, + setTemplatesCount: (templatesCount) => set({ templatesCount }), + filteredTemplates: [], + setFilteredTemplates: (filteredTemplates) => + set({ filteredTemplates }), + searchQuery: "", + setSearchQuery: (searchQuery) => set({ searchQuery }), + selectedTags: [], + addSelectedTag: (tag) => + set((state) => ({ + selectedTags: state.selectedTags.includes(tag) + ? state.selectedTags + : [...state.selectedTags, tag], + })), + removeSelectedTag: (tag) => + set((state) => ({ + selectedTags: state.selectedTags.filter((t) => t !== tag), + })), + view: "grid", + setView: (view) => set({ view }), + githubStars: 0, + setGithubStars: (count) => set({ githubStars: count }), + }), + { + name: "template-store", + partialize: (state) => ({ view: state.view }), // Only persist the view preference + } + ) +);