diff --git a/app/package.json b/app/package.json index 19dc0c5..49503ad 100644 --- a/app/package.json +++ b/app/package.json @@ -10,14 +10,13 @@ "preview": "vite preview" }, "dependencies": { - "yaml":"2.7.1", - "@iarna/toml": "^2.2.5", "@codemirror/autocomplete": "^6.18.6", "@codemirror/lang-json": "^6.0.1", "@codemirror/lang-yaml": "^6.1.1", "@codemirror/language": "^6.10.1", "@codemirror/legacy-modes": "6.4.0", "@codemirror/view": "6.29.0", + "@iarna/toml": "^2.2.5", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.2", @@ -35,11 +34,13 @@ "next-themes": "^0.4.5", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-router-dom": "^7.4.1", "sonner": "^2.0.1", "tailwind-merge": "^3.0.2", "tailwindcss": "^4.0.12", "tailwindcss-animate": "^1.0.7", "vite-plugin-static-copy": "2.3.0", + "yaml": "2.7.1", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/app/pnpm-lock.yaml b/app/pnpm-lock.yaml index b73fc7d..9fec5d4 100644 --- a/app/pnpm-lock.yaml +++ b/app/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: react-dom: specifier: ^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: specifier: ^2.0.1 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': resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + '@types/estree@1.0.6': resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} @@ -1220,6 +1226,10 @@ packages: convert-source-map@2.0.0: 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: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} @@ -1498,6 +1508,23 @@ packages: '@types/react': 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: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -1538,6 +1565,9 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + sonner@2.0.1: resolution: {integrity: sha512-FRBphaehZ5tLdLcQ8g2WOIRE+Y7BCfWi5Zyd8bCvBjiW8TxxAyoWZIxS661Yz6TGPqFQ4VLzOF89WEYhfynSFQ==} peerDependencies: @@ -1576,6 +1606,9 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + turbo-stream@2.4.0: + resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + typescript@5.7.3: resolution: {integrity: sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==} engines: {node: '>=14.17'} @@ -2626,6 +2659,8 @@ snapshots: dependencies: '@babel/types': 7.26.9 + '@types/cookie@0.6.0': {} + '@types/estree@1.0.6': {} '@types/node@20.17.24': @@ -2756,6 +2791,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.0.2: {} + copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 @@ -2998,6 +3035,22 @@ snapshots: optionalDependencies: '@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): dependencies: get-nonce: 1.0.1 @@ -3049,6 +3102,8 @@ snapshots: 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): dependencies: react: 19.0.0 @@ -3076,6 +3131,8 @@ snapshots: tslib@2.8.1: {} + turbo-stream@2.4.0: {} + typescript@5.7.3: {} undici-types@6.19.8: {} diff --git a/app/src/App.tsx b/app/src/App.tsx index f6c69ec..9864c07 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -3,16 +3,19 @@ import Navigation from "./components/Navigation"; import Search from "./components/Search"; import { useStore } from "@/store"; import "./App.css"; +import { BrowserRouter } from "react-router-dom"; function App() { const view = useStore((state) => state.view); return ( -
- - - -
+ +
+ + + +
+
); } diff --git a/app/src/components/Search.tsx b/app/src/components/Search.tsx index a114c19..a810923 100644 --- a/app/src/components/Search.tsx +++ b/app/src/components/Search.tsx @@ -16,9 +16,10 @@ import { Check, ChevronsUpDown } from "lucide-react"; import React from "react"; import { Tabs, TabsList, TabsTrigger } from "./ui/tabs"; import SelectedTags from "./SelectedTags"; +import { useSearchParams } from "react-router-dom"; const Search = () => { - const { templates, searchQuery, setSearchQuery, setView, templatesCount } = + const { templates, searchQuery, setSearchQuery, setView, templatesCount, setFilteredTemplates, setTemplatesCount } = useStore(); const selectedTags = useStore((state) => state.selectedTags); const addSelectedTag = useStore((state) => state.addSelectedTag); @@ -26,6 +27,7 @@ const Search = () => { const [open, setOpen] = React.useState(false); const [tagSearch, setTagSearch] = React.useState(""); const view = useStore((state) => state.view); + const [searchParams, setSearchParams] = useSearchParams(); // Get all unique tags, safely handle empty templates const uniqueTags = React.useMemo(() => { @@ -43,6 +45,53 @@ const Search = () => { ); }, [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) => { + 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 (
{/*

@@ -63,11 +112,11 @@ const Search = () => { type="text" placeholder="Search templates..." value={searchQuery} - onChange={(e) => setSearchQuery(e.target.value)} + onChange={handleSearchChange} className="w-full p-6" /> {searchQuery.length > 0 ? ( -
setSearchQuery("")}> +
) : (