mirror of
https://github.com/Dokploy/templates
synced 2025-06-26 18:16:07 +00:00
Implement routing and search functionality enhancements
- Wrapped the main application in a BrowserRouter to enable routing. - Integrated react-router-dom for managing search parameters in the Search component. - Updated search functionality to initialize from URL parameters and reflect changes in the URL when the search query is modified. - Added logic to filter templates based on search query and selected tags, improving user experience.
This commit is contained in:
@@ -10,14 +10,13 @@
|
|||||||
"preview": "vite preview"
|
"preview": "vite preview"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"yaml":"2.7.1",
|
|
||||||
"@iarna/toml": "^2.2.5",
|
|
||||||
"@codemirror/autocomplete": "^6.18.6",
|
"@codemirror/autocomplete": "^6.18.6",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
"@codemirror/language": "^6.10.1",
|
"@codemirror/language": "^6.10.1",
|
||||||
"@codemirror/legacy-modes": "6.4.0",
|
"@codemirror/legacy-modes": "6.4.0",
|
||||||
"@codemirror/view": "6.29.0",
|
"@codemirror/view": "6.29.0",
|
||||||
|
"@iarna/toml": "^2.2.5",
|
||||||
"@radix-ui/react-dialog": "^1.1.6",
|
"@radix-ui/react-dialog": "^1.1.6",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
"@radix-ui/react-dropdown-menu": "^2.1.6",
|
||||||
"@radix-ui/react-label": "^2.1.2",
|
"@radix-ui/react-label": "^2.1.2",
|
||||||
@@ -35,11 +34,13 @@
|
|||||||
"next-themes": "^0.4.5",
|
"next-themes": "^0.4.5",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
|
"react-router-dom": "^7.4.1",
|
||||||
"sonner": "^2.0.1",
|
"sonner": "^2.0.1",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss": "^4.0.12",
|
"tailwindcss": "^4.0.12",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vite-plugin-static-copy": "2.3.0",
|
"vite-plugin-static-copy": "2.3.0",
|
||||||
|
"yaml": "2.7.1",
|
||||||
"zustand": "^5.0.3"
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
57
app/pnpm-lock.yaml
generated
57
app/pnpm-lock.yaml
generated
@@ -80,6 +80,9 @@ importers:
|
|||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.0.0(react@19.0.0)
|
version: 19.0.0(react@19.0.0)
|
||||||
|
react-router-dom:
|
||||||
|
specifier: ^7.4.1
|
||||||
|
version: 7.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
sonner:
|
sonner:
|
||||||
specifier: ^2.0.1
|
specifier: ^2.0.1
|
||||||
version: 2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
@@ -1121,6 +1124,9 @@ packages:
|
|||||||
'@types/babel__traverse@7.20.6':
|
'@types/babel__traverse@7.20.6':
|
||||||
resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
|
resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==}
|
||||||
|
|
||||||
|
'@types/cookie@0.6.0':
|
||||||
|
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
|
||||||
|
|
||||||
'@types/estree@1.0.6':
|
'@types/estree@1.0.6':
|
||||||
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==}
|
||||||
|
|
||||||
@@ -1220,6 +1226,10 @@ packages:
|
|||||||
convert-source-map@2.0.0:
|
convert-source-map@2.0.0:
|
||||||
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==}
|
||||||
|
|
||||||
|
cookie@1.0.2:
|
||||||
|
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
copy-to-clipboard@3.3.3:
|
copy-to-clipboard@3.3.3:
|
||||||
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
|
resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==}
|
||||||
|
|
||||||
@@ -1498,6 +1508,23 @@ packages:
|
|||||||
'@types/react':
|
'@types/react':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
react-router-dom@7.4.1:
|
||||||
|
resolution: {integrity: sha512-L3/4tig0Lvs6m6THK0HRV4eHUdpx0dlJasgCxXKnavwhh4tKYgpuZk75HRYNoRKDyDWi9QgzGXsQ1oQSBlWpAA==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=18'
|
||||||
|
react-dom: '>=18'
|
||||||
|
|
||||||
|
react-router@7.4.1:
|
||||||
|
resolution: {integrity: sha512-Vmizn9ZNzxfh3cumddqv3kLOKvc7AskUT0dC1prTabhiEi0U4A33LmkDOJ79tXaeSqCqMBXBU/ySX88W85+EUg==}
|
||||||
|
engines: {node: '>=20.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=18'
|
||||||
|
react-dom: '>=18'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
|
||||||
react-style-singleton@2.2.3:
|
react-style-singleton@2.2.3:
|
||||||
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
|
resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -1538,6 +1565,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
set-cookie-parser@2.7.1:
|
||||||
|
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
|
||||||
|
|
||||||
sonner@2.0.1:
|
sonner@2.0.1:
|
||||||
resolution: {integrity: sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==}
|
resolution: {integrity: sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -1576,6 +1606,9 @@ packages:
|
|||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
turbo-stream@2.4.0:
|
||||||
|
resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
|
||||||
|
|
||||||
typescript@5.7.3:
|
typescript@5.7.3:
|
||||||
resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
|
resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==}
|
||||||
engines: {node: '>=14.17'}
|
engines: {node: '>=14.17'}
|
||||||
@@ -2626,6 +2659,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@babel/types': 7.26.9
|
'@babel/types': 7.26.9
|
||||||
|
|
||||||
|
'@types/cookie@0.6.0': {}
|
||||||
|
|
||||||
'@types/estree@1.0.6': {}
|
'@types/estree@1.0.6': {}
|
||||||
|
|
||||||
'@types/node@20.17.24':
|
'@types/node@20.17.24':
|
||||||
@@ -2756,6 +2791,8 @@ snapshots:
|
|||||||
|
|
||||||
convert-source-map@2.0.0: {}
|
convert-source-map@2.0.0: {}
|
||||||
|
|
||||||
|
cookie@1.0.2: {}
|
||||||
|
|
||||||
copy-to-clipboard@3.3.3:
|
copy-to-clipboard@3.3.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
toggle-selection: 1.0.6
|
toggle-selection: 1.0.6
|
||||||
@@ -2998,6 +3035,22 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/react': 19.0.10
|
'@types/react': 19.0.10
|
||||||
|
|
||||||
|
react-router-dom@7.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
|
dependencies:
|
||||||
|
react: 19.0.0
|
||||||
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
react-router: 7.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
|
|
||||||
|
react-router@7.4.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
|
dependencies:
|
||||||
|
'@types/cookie': 0.6.0
|
||||||
|
cookie: 1.0.2
|
||||||
|
react: 19.0.0
|
||||||
|
set-cookie-parser: 2.7.1
|
||||||
|
turbo-stream: 2.4.0
|
||||||
|
optionalDependencies:
|
||||||
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
|
||||||
react-style-singleton@2.2.3(@types/react@19.0.10)(react@19.0.0):
|
react-style-singleton@2.2.3(@types/react@19.0.10)(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
get-nonce: 1.0.1
|
get-nonce: 1.0.1
|
||||||
@@ -3049,6 +3102,8 @@ snapshots:
|
|||||||
|
|
||||||
semver@6.3.1: {}
|
semver@6.3.1: {}
|
||||||
|
|
||||||
|
set-cookie-parser@2.7.1: {}
|
||||||
|
|
||||||
sonner@2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
sonner@2.0.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
@@ -3076,6 +3131,8 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
|
turbo-stream@2.4.0: {}
|
||||||
|
|
||||||
typescript@5.7.3: {}
|
typescript@5.7.3: {}
|
||||||
|
|
||||||
undici-types@6.19.8: {}
|
undici-types@6.19.8: {}
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ import Navigation from "./components/Navigation";
|
|||||||
import Search from "./components/Search";
|
import Search from "./components/Search";
|
||||||
import { useStore } from "@/store";
|
import { useStore } from "@/store";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
import { BrowserRouter } from "react-router-dom";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const view = useStore((state) => state.view);
|
const view = useStore((state) => state.view);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen">
|
<BrowserRouter>
|
||||||
<Navigation />
|
<div className="min-h-screen">
|
||||||
<Search />
|
<Navigation />
|
||||||
<TemplateGrid view={view} />
|
<Search />
|
||||||
</div>
|
<TemplateGrid view={view} />
|
||||||
|
</div>
|
||||||
|
</BrowserRouter>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ import { Check, ChevronsUpDown } from "lucide-react";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
|
||||||
import SelectedTags from "./SelectedTags";
|
import SelectedTags from "./SelectedTags";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
const Search = () => {
|
const Search = () => {
|
||||||
const { templates, searchQuery, setSearchQuery, setView, templatesCount } =
|
const { templates, searchQuery, setSearchQuery, setView, templatesCount, setFilteredTemplates, setTemplatesCount } =
|
||||||
useStore();
|
useStore();
|
||||||
const selectedTags = useStore((state) => state.selectedTags);
|
const selectedTags = useStore((state) => state.selectedTags);
|
||||||
const addSelectedTag = useStore((state) => state.addSelectedTag);
|
const addSelectedTag = useStore((state) => state.addSelectedTag);
|
||||||
@@ -26,6 +27,7 @@ const Search = () => {
|
|||||||
const [open, setOpen] = React.useState(false);
|
const [open, setOpen] = React.useState(false);
|
||||||
const [tagSearch, setTagSearch] = React.useState("");
|
const [tagSearch, setTagSearch] = React.useState("");
|
||||||
const view = useStore((state) => state.view);
|
const view = useStore((state) => state.view);
|
||||||
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
|
|
||||||
// Get all unique tags, safely handle empty templates
|
// Get all unique tags, safely handle empty templates
|
||||||
const uniqueTags = React.useMemo(() => {
|
const uniqueTags = React.useMemo(() => {
|
||||||
@@ -43,6 +45,53 @@ const Search = () => {
|
|||||||
);
|
);
|
||||||
}, [uniqueTags, tagSearch]);
|
}, [uniqueTags, tagSearch]);
|
||||||
|
|
||||||
|
// Initialize search query from URL params and apply filters
|
||||||
|
React.useEffect(() => {
|
||||||
|
const queryFromUrl = searchParams.get("q") || "";
|
||||||
|
if (queryFromUrl !== searchQuery) {
|
||||||
|
setSearchQuery(queryFromUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply filters whenever templates, search query or selected tags change
|
||||||
|
if (templates) {
|
||||||
|
const filtered = templates.filter((template) => {
|
||||||
|
// Filter by search query
|
||||||
|
const matchesSearch = template.name
|
||||||
|
.toLowerCase()
|
||||||
|
.includes(queryFromUrl.toLowerCase());
|
||||||
|
|
||||||
|
// Filter by selected tags
|
||||||
|
const matchesTags =
|
||||||
|
selectedTags.length === 0 ||
|
||||||
|
selectedTags.every((tag) => template.tags.includes(tag));
|
||||||
|
|
||||||
|
return matchesSearch && matchesTags;
|
||||||
|
});
|
||||||
|
|
||||||
|
setFilteredTemplates(filtered);
|
||||||
|
setTemplatesCount(filtered.length);
|
||||||
|
}
|
||||||
|
}, [searchParams, templates, selectedTags, setSearchQuery, setFilteredTemplates, setTemplatesCount]);
|
||||||
|
|
||||||
|
// Update URL params when search query changes
|
||||||
|
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
const newQuery = e.target.value;
|
||||||
|
setSearchQuery(newQuery);
|
||||||
|
if (newQuery) {
|
||||||
|
setSearchParams({ q: newQuery });
|
||||||
|
} else {
|
||||||
|
searchParams.delete("q");
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clear search and URL params
|
||||||
|
const handleClearSearch = () => {
|
||||||
|
setSearchQuery("");
|
||||||
|
searchParams.delete("q");
|
||||||
|
setSearchParams(searchParams);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" mx-auto p-4 lg:p-12 border-b w-full">
|
<div className=" mx-auto p-4 lg:p-12 border-b w-full">
|
||||||
{/* <h1 className="text-2xl md:text-3xl xl:text-4xl font-bold text-center mb-8">
|
{/* <h1 className="text-2xl md:text-3xl xl:text-4xl font-bold text-center mb-8">
|
||||||
@@ -63,11 +112,11 @@ const Search = () => {
|
|||||||
type="text"
|
type="text"
|
||||||
placeholder="Search templates..."
|
placeholder="Search templates..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
onChange={handleSearchChange}
|
||||||
className="w-full p-6"
|
className="w-full p-6"
|
||||||
/>
|
/>
|
||||||
{searchQuery.length > 0 ? (
|
{searchQuery.length > 0 ? (
|
||||||
<div className="cursor-pointer" onClick={() => setSearchQuery("")}>
|
<div className="cursor-pointer" onClick={handleClearSearch}>
|
||||||
<XIcon className="absolute end-3 translate-y-3.5 top-1/2 h-5 w-5 text-gray-400" />
|
<XIcon className="absolute end-3 translate-y-3.5 top-1/2 h-5 w-5 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
Reference in New Issue
Block a user