From df0de6522df2116c0806ce6405697b3f5e46e02f Mon Sep 17 00:00:00 2001 From: loloToster Date: Wed, 13 Dec 2023 21:48:50 +0100 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9A=A7=20started=20working=20on=20sea?= =?UTF-8?q?rch=20bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app.ts | 9 ++- .../src/components/ActionBtn/ActionBtn.scss | 1 + client/src/components/Header/Header.scss | 46 ++++++++++++-- client/src/components/Header/Header.tsx | 61 ++++++++++++++++++- client/src/index.tsx | 28 +-------- client/src/routes.tsx | 37 +++++++++++ 6 files changed, 148 insertions(+), 34 deletions(-) create mode 100644 client/src/routes.tsx diff --git a/app.ts b/app.ts index 5857732..bbc9135 100644 --- a/app.ts +++ b/app.ts @@ -221,10 +221,15 @@ apiRouter.delete("/item/:id", async (req, res) => { }) apiRouter.get("/items", async (req, res) => { - const trashed = req.query.trashed === "true" ? 1 : 0 + const { trashed, q } = req.query + + const textSearch = typeof q === "string" ? + ` AND title LIKE '%' || ? || '%'` : + "" db.all( - `SELECT * FROM items WHERE trashed = ${trashed} ORDER BY created_at DESC`, + `SELECT * FROM items WHERE trashed = ${trashed === "true" ? 1 : 0}${textSearch} ORDER BY created_at DESC`, + [q], (err, rows) => { if (err) return res.status(500).send() res.send(rows) diff --git a/client/src/components/ActionBtn/ActionBtn.scss b/client/src/components/ActionBtn/ActionBtn.scss index 7da4e2b..4cd26f3 100644 --- a/client/src/components/ActionBtn/ActionBtn.scss +++ b/client/src/components/ActionBtn/ActionBtn.scss @@ -10,6 +10,7 @@ cursor: pointer; color: white; text-decoration: none; + text-align: center; &:hover, &:focus { diff --git a/client/src/components/Header/Header.scss b/client/src/components/Header/Header.scss index e1575a3..101ecd9 100644 --- a/client/src/components/Header/Header.scss +++ b/client/src/components/Header/Header.scss @@ -11,6 +11,10 @@ background-color: #{c.$bg}ee; z-index: 1; + @media (max-width: 580px) { + flex-wrap: wrap; + } + @media (max-width: 800px) { padding: 20px 10px; } @@ -19,7 +23,36 @@ font-size: 2rem; color: c.$main; text-decoration: none; - margin-right: auto; + margin-right: 16px; + } + + &__search { + display: flex; + background-color: c.$grey; + margin: 0 auto; + padding: 6px; + gap: 6px; + border-radius: 4px; + + @media (max-width: 580px) { + order: 1; + width: 100%; + margin-top: 10px; + } + + svg { + fill: c.$light-grey; + vertical-align: middle; + } + + input { + width: 18vw; + font-size: 1rem; + + &::placeholder { + color: c.$light-grey; + } + } } --fab-size: min(15vw, 15vh); @@ -47,7 +80,7 @@ font-weight: bold; color: c.$light-grey; text-decoration: none; - margin-right: 16px; + margin: 0 16px; svg { fill: currentColor; @@ -65,10 +98,13 @@ &__secondary { margin-right: 0; + margin-left: auto; + } + } - span { - display: none; - } + @media (max-width: 900px) { + &__secondary span { + display: none; } } } diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index 386c344..480734c 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -1,14 +1,73 @@ -import { Link, useLocation } from "react-router-dom" +import { useEffect, useRef, useState } from "react" +import { Link, useLocation, useNavigate, useSearchParams } from "react-router-dom" + import "./Header.scss" +import routes from "src/routes" + +const QUERY_KEY = "q" +const searchableRoutes = routes.filter(r => r.searchable && r.path).map(r => r.path) as string[] + function Header() { const location = useLocation() + const navigate = useNavigate() + + const [searchParams, setSearchParams] = useSearchParams(); + const [searchQuery, setSearchQuery] = useState(searchParams.get(QUERY_KEY) || "") + + const searchInput = useRef(null) + + useEffect(() => { + const param = searchParams.get(QUERY_KEY) + + if (param !== searchQuery) + setSearchQuery(param || "") + }, [searchParams]) + + const handleSearchEnter = () => { + searchInput.current?.focus() + } + + const handleSearchClear = () => { + setSearchQuery("") + searchInput.current?.focus() + } + + const handleSearch = (e: React.KeyboardEvent) => { + if (e.key !== "Enter") return + + if (!searchableRoutes.includes(location.pathname)) + navigate(searchableRoutes[0]) + + setSearchParams({ [QUERY_KEY]: searchQuery }) + } return (
Cloudia +
+ {searchQuery ? ( + + ) : ( + + )} + setSearchQuery(e.target.value)} + onKeyPress={handleSearch} + placeholder="Search Cloudia" /> +
Trash diff --git a/client/src/index.tsx b/client/src/index.tsx index a7a81af..9cef129 100644 --- a/client/src/index.tsx +++ b/client/src/index.tsx @@ -8,35 +8,11 @@ import App from "./App" import reportWebVitals from "./reportWebVitals" import "./sass/style.scss" -import ItemListPage from "./pages/ItemListPage/ItemListPage" -import AddFilePage from "./pages/AddFilesPage/AddFilesPage" -import TrashItemListPage from "./pages/TrashItemListPage/TrashItemListPage" -import FileDetailsPage from "./pages/FileDetailsPage/FileDetailsPage" +import routes from "./routes" const router = createBrowserRouter([{ element: , - children: [ - { - path: "/", - element: - }, - { - path: "/add", - element: - }, - { - path: "/trash", - element: - }, - { - path: "/file/:id", - element: - }, - { - path: "/*", - element: 404 - } - ] + children: routes }]) ReactDOM.createRoot( diff --git a/client/src/routes.tsx b/client/src/routes.tsx new file mode 100644 index 0000000..2679cc1 --- /dev/null +++ b/client/src/routes.tsx @@ -0,0 +1,37 @@ +import { RouteObject } from "react-router-dom" + +import ItemListPage from "./pages/ItemListPage/ItemListPage" +import AddFilePage from "./pages/AddFilesPage/AddFilesPage" +import TrashItemListPage from "./pages/TrashItemListPage/TrashItemListPage" +import FileDetailsPage from "./pages/FileDetailsPage/FileDetailsPage" + +export type RouteWithMeta = RouteObject & { + searchable?: boolean +} + +const routes: RouteWithMeta[] = [ + { + path: "/", + element: , + searchable: true + }, + { + path: "/trash", + element: , + searchable: true + }, + { + path: "/add", + element: + }, + { + path: "/file/:id", + element: + }, + { + path: "/*", + element: 404 + } +] + +export default routes From f1e592e4f623d2d3ccdac87c26772e649bf248a2 Mon Sep 17 00:00:00 2001 From: loloToster Date: Thu, 8 Aug 2024 14:24:42 +0200 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9C=A8=20created=20basic=20client-side?= =?UTF-8?q?=20search?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 8 ++- client/src/components/Header/Header.scss | 20 ++++-- client/src/components/Header/Header.tsx | 78 +++++++++------------ client/src/components/ItemList/ItemList.tsx | 45 ++++++++---- client/src/contexts/searchContext.tsx | 35 +++++++++ 5 files changed, 121 insertions(+), 65 deletions(-) create mode 100644 client/src/contexts/searchContext.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index b04aa3f..258f845 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,14 +1,18 @@ import { Outlet, ScrollRestoration } from "react-router-dom" import { ItemsCacheContextProvider } from "./contexts/itemsCacheContext" +import { SearchContextProvider } from "./contexts/searchContext" + import Header from "./components/Header/Header" function App() { return (
-
- + +
+ +
diff --git a/client/src/components/Header/Header.scss b/client/src/components/Header/Header.scss index 101ecd9..0155d30 100644 --- a/client/src/components/Header/Header.scss +++ b/client/src/components/Header/Header.scss @@ -34,10 +34,8 @@ gap: 6px; border-radius: 4px; - @media (max-width: 580px) { - order: 1; - width: 100%; - margin-top: 10px; + &:has(input:focus) { + outline: 2px solid c.$main; } svg { @@ -53,6 +51,20 @@ color: c.$light-grey; } } + + @media (max-width: 580px) { + order: 1; + width: 100%; + margin-top: 10px; + + input { + width: 100%; + } + } + } + + &__spacer { + flex-grow: 1; } --fab-size: min(15vw, 15vh); diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index 480734c..c189089 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -1,29 +1,18 @@ -import { useEffect, useRef, useState } from "react" -import { Link, useLocation, useNavigate, useSearchParams } from "react-router-dom" - -import "./Header.scss" +import { useEffect, useRef } from "react" +import { Link, useLocation } from "react-router-dom" import routes from "src/routes" +import { useSearch } from "src/contexts/searchContext" -const QUERY_KEY = "q" -const searchableRoutes = routes.filter(r => r.searchable && r.path).map(r => r.path) as string[] +import "./Header.scss" function Header() { const location = useLocation() - const navigate = useNavigate() - const [searchParams, setSearchParams] = useSearchParams(); - const [searchQuery, setSearchQuery] = useState(searchParams.get(QUERY_KEY) || "") + const { searchQuery, setSearchQuery } = useSearch() const searchInput = useRef(null) - useEffect(() => { - const param = searchParams.get(QUERY_KEY) - - if (param !== searchQuery) - setSearchQuery(param || "") - }, [searchParams]) - const handleSearchEnter = () => { searchInput.current?.focus() } @@ -33,41 +22,40 @@ function Header() { searchInput.current?.focus() } - const handleSearch = (e: React.KeyboardEvent) => { - if (e.key !== "Enter") return - - if (!searchableRoutes.includes(location.pathname)) - navigate(searchableRoutes[0]) - - setSearchParams({ [QUERY_KEY]: searchQuery }) - } + // clear search on page change + useEffect(() => { + setSearchQuery("") + }, [location.pathname, setSearchQuery]) return (
Cloudia -
- {searchQuery ? ( - - ) : ( - - )} - setSearchQuery(e.target.value)} - onKeyPress={handleSearch} - placeholder="Search Cloudia" /> -
+ {routes.find(r => r.path === location.pathname)?.searchable ? ( +
+ {searchQuery ? ( + + ) : ( + + )} + setSearchQuery(e.target.value)} + placeholder="Search Cloudia" /> +
+ ) : ( +
+ )} Trash diff --git a/client/src/components/ItemList/ItemList.tsx b/client/src/components/ItemList/ItemList.tsx index 91974a6..4fe8025 100644 --- a/client/src/components/ItemList/ItemList.tsx +++ b/client/src/components/ItemList/ItemList.tsx @@ -1,6 +1,8 @@ import { useEffect, useRef, useState } from "react" import { ClientItem } from "@backend-types/types" +import { useSearch } from "src/contexts/searchContext" + import "./ItemList.scss" import QuickActions from "../QuickActions/QuickActions" @@ -166,6 +168,8 @@ function ItemList(props: Props) { handleItemRemoval(id, "delete") } + const { searchQuery } = useSearch() + return (
{loading && [...Array(5)].map((_, i) => ( @@ -175,20 +179,33 @@ function ItemList(props: Props) { {uploads.map((up, i) => ( ))} - {!loading && items.map(item => { - const itemProps = { - key: item.id, - onRestore: handleRestore, - onDelete: item.trashed ? handleDelete : handleTrash, - onSelect: handleSelect, - onRangeSelect: handleRangeSelect - } - - if (item.type === "text") - return - else - return - })} + {!loading && items.filter( + i => { + if (!searchQuery.length) return true + + const sq = searchQuery.toLowerCase() + + if (i.title.toLowerCase().includes(sq)) + return true + + if (i.type === "text" && i.text.toLowerCase().includes(sq)) + return true + + return false + }).map(item => { + const itemProps = { + key: item.id, + onRestore: handleRestore, + onDelete: item.trashed ? handleDelete : handleTrash, + onSelect: handleSelect, + onRangeSelect: handleRangeSelect + } + + if (item.type === "text") + return + else + return + })}
) } diff --git a/client/src/contexts/searchContext.tsx b/client/src/contexts/searchContext.tsx new file mode 100644 index 0000000..5989969 --- /dev/null +++ b/client/src/contexts/searchContext.tsx @@ -0,0 +1,35 @@ +import { + createContext, + Dispatch, + SetStateAction, + useContext, + useState +} from "react" + +export interface SearchContextI { + searchQuery: string, + setSearchQuery: Dispatch> +} + +export const SearchContext = createContext({ + searchQuery: "", + setSearchQuery: () => null +}) + +export const SearchContextProvider = (props: { + children: React.ReactNode +}) => { + const [searchQuery, setSearchQuery] = useState("") + + return ( + + {props.children} + + ) +} + +export const useSearch = () => { + return useContext(SearchContext) +}