From 8bd53d86625df5b1df872de3aeb70462fc96fbd6 Mon Sep 17 00:00:00 2001 From: querwurzel <> Date: Sat, 11 Nov 2023 15:05:28 +0100 Subject: [PATCH] add robots.txt, introduce first search draft --- .../binpastes/paste/api/PasteController.java | 4 +- frontend/index.html | 1 + frontend/public/robots.txt | 2 + frontend/src/App.tsx | 2 +- frontend/src/api/client.ts | 7 +- .../components/CreatePaste/CreatePaste.tsx | 4 +- .../CreatePaste/createPaste.module.css | 2 +- .../components/RecentPastes/RecentPastes.tsx | 2 + .../RecentPastes/recentPastes.module.css | 10 ++- .../components/SearchPastes/SearchPastes.tsx | 67 ++++++++++++++++--- .../SearchPastes/searchPastes.module.css | 61 +++++++++++++++++ frontend/src/components/Spinner/Spinner.tsx | 10 +++ frontend/src/components/Spinner/spinner.css | 56 ++++++++++++++++ frontend/src/pages/Search.tsx | 58 ++++++++++------ 14 files changed, 247 insertions(+), 39 deletions(-) create mode 100644 frontend/public/robots.txt create mode 100644 frontend/src/components/SearchPastes/searchPastes.module.css create mode 100644 frontend/src/components/Spinner/Spinner.tsx create mode 100644 frontend/src/components/Spinner/spinner.css diff --git a/backend/src/main/java/com/github/binpastes/paste/api/PasteController.java b/backend/src/main/java/com/github/binpastes/paste/api/PasteController.java index 62c30ef..b70aa56 100644 --- a/backend/src/main/java/com/github/binpastes/paste/api/PasteController.java +++ b/backend/src/main/java/com/github/binpastes/paste/api/PasteController.java @@ -29,8 +29,8 @@ import static com.github.binpastes.paste.api.model.ListView.ListItemView; -@RestController @Validated +@RestController @RequestMapping("/api/v1/paste") class PasteController { @@ -83,7 +83,7 @@ public Mono searchPastes( final String term, final ServerHttpResponse response ) { - response.getHeaders().add(HttpHeaders.CACHE_CONTROL, "max-age=300"); + response.getHeaders().add(HttpHeaders.CACHE_CONTROL, "max-age=60"); return pasteService .findByFullText(term) .map(paste -> SearchItemView.of(paste, term)) diff --git a/frontend/index.html b/frontend/index.html index 713ddcc..b69f8c0 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -8,6 +8,7 @@ + diff --git a/frontend/public/robots.txt b/frontend/public/robots.txt new file mode 100644 index 0000000..d346249 --- /dev/null +++ b/frontend/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: /api/ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 9d3fc62..624d242 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -23,8 +23,8 @@ const App: () => JSX.Element = () => {
- +
diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 94ba078..e0839d8 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -56,14 +56,15 @@ const findAll = (): Promise> => { const searchAll = (term: string): Promise> => { const params = new URLSearchParams([['term', term]]); - const url = new URL('/api/v1/paste/search?' + params.toString(), apiBaseUrl()); + const url = new URL('/api/v1/paste/search?' + encodeURI(params.toString()), apiBaseUrl()); return fetch(url) .then(value => value.json()) - .then(value => value.pastes); + .then(value => value.pastes) + .catch(_ => []) } -const deletePaste = (id: string) => { +const deletePaste = (id: string): Promise => { const url = new URL('/api/v1/paste/' + id, apiBaseUrl()); return fetch(url, { diff --git a/frontend/src/components/CreatePaste/CreatePaste.tsx b/frontend/src/components/CreatePaste/CreatePaste.tsx index a238e76..82af189 100644 --- a/frontend/src/components/CreatePaste/CreatePaste.tsx +++ b/frontend/src/components/CreatePaste/CreatePaste.tsx @@ -36,6 +36,7 @@ const CreatePaste: Component = ({onCreatePaste, initialPaste}) const [lastPasteUrl, setLastPasteUrl] = createSignal(); let creationForm: HTMLFormElement + let submitInput: HTMLInputElement const updateFormField = (fieldName: keyof FormModel) => (event: Event) => { const inputElement = event.currentTarget as HTMLInputElement; @@ -81,6 +82,7 @@ const CreatePaste: Component = ({onCreatePaste, initialPaste}) resetStore(); setLastPasteUrl(url); }) + .catch(e => submitInput.style.backgroundColor = 'red'); } return ( @@ -153,7 +155,7 @@ const CreatePaste: Component = ({onCreatePaste, initialPaste})

{lastPasteUrl()}

- + diff --git a/frontend/src/components/CreatePaste/createPaste.module.css b/frontend/src/components/CreatePaste/createPaste.module.css index a7019ed..5dfe5d6 100644 --- a/frontend/src/components/CreatePaste/createPaste.module.css +++ b/frontend/src/components/CreatePaste/createPaste.module.css @@ -65,7 +65,7 @@ .createForm input[type=submit], .createForm input[type=reset] { display: inline-block; - width: initial; + color: var(--color-text); margin: .5rem; } diff --git a/frontend/src/components/RecentPastes/RecentPastes.tsx b/frontend/src/components/RecentPastes/RecentPastes.tsx index b191b74..039227b 100644 --- a/frontend/src/components/RecentPastes/RecentPastes.tsx +++ b/frontend/src/components/RecentPastes/RecentPastes.tsx @@ -76,6 +76,8 @@ const RecentPastes: () => JSX.Element = () => { + Search all pastes +
    {item =>
  1. diff --git a/frontend/src/components/RecentPastes/recentPastes.module.css b/frontend/src/components/RecentPastes/recentPastes.module.css index 5a955e7..f085eb1 100644 --- a/frontend/src/components/RecentPastes/recentPastes.module.css +++ b/frontend/src/components/RecentPastes/recentPastes.module.css @@ -14,7 +14,6 @@ list-style: none; margin: 0; padding: 0; - } .recentPastes .item { @@ -50,3 +49,12 @@ border-bottom: 1px dotted #a5a5a5; } } + +.searchLink { + margin-left: 1rem; + margin-bottom: 1rem; +} + +.searchLinkActive { + display: none; +} \ No newline at end of file diff --git a/frontend/src/components/SearchPastes/SearchPastes.tsx b/frontend/src/components/SearchPastes/SearchPastes.tsx index 7bd6f0c..e4f0cc0 100644 --- a/frontend/src/components/SearchPastes/SearchPastes.tsx +++ b/frontend/src/components/SearchPastes/SearchPastes.tsx @@ -1,13 +1,25 @@ -import {createResource, createSignal, JSX} from 'solid-js'; +import {Component, createResource, createSignal, JSX, Show} from 'solid-js'; +import {A} from '@solidjs/router'; import ApiClient from '../../api/client'; -import {PasteListView} from '../../api/model/PasteListView'; +import {PasteSearchView} from '../../api/model/PasteSearchView'; +import {toDateTimeString} from '../../datetime/DateTimeUtil'; +import styles from "./searchPastes.module.css"; -const SearchPastes: () => JSX.Element = () => { +type SearchPastesProps = { + term: String + pastes: PasteSearchView + onSearchEnter: (term: String) => void +} + +const SearchPastes: Component = ({term, pastes, onSearchEnter}): JSX.Element => { const [search, setSearch] = createSignal(); const [results, { refetch }] = createResource(() => search(), () => searchTerm()); + let searchInput: HTMLInputElement; + + const searchTerm = (): Promise> => { if (search() && search().length >= 3) { return ApiClient.searchAll(search()); @@ -21,17 +33,56 @@ const SearchPastes: () => JSX.Element = () => { refetch(); } - const submitSearchForm = (e: Event) => { + + const submitOnClick = (e: Event) => { e.preventDefault(); - refetch(); + if (searchInput.value?.length >= 3) { + onSearchEnter(searchInput.value); + } + } + + const submitOnEnter = (e: Event) => { + e.preventDefault(); + + if (searchInput.value?.length >= 3) { + if (e instanceof KeyboardEvent && e.key === "Enter") { + onSearchEnter(searchInput.value); + } + } } return ( -
    + <> +
    +
    + + + +
    +
    + + + Nothing found

    }> + +
      + {item => +
    1. +

      {item.title || 'Untitled' }

      +

      + Created: | + Expires: | + Size: {item.sizeInBytes} bytes +

      +
      “{item.highlight} [..]”
      +
    2. + } +
      +
    -

    BinPastes

    +
    +
    -
    + ) } diff --git a/frontend/src/components/SearchPastes/searchPastes.module.css b/frontend/src/components/SearchPastes/searchPastes.module.css new file mode 100644 index 0000000..89d63c1 --- /dev/null +++ b/frontend/src/components/SearchPastes/searchPastes.module.css @@ -0,0 +1,61 @@ + +.searchForm { + border: none; + padding: 0; +} + +.searchForm fieldset { + border: 1px dotted #a5a5a5; + border-radius: 7px; + margin-bottom: 10px; + padding: 5px 10px; +} + +.searchForm input[type=search] { + display: inline-block; + width: 20rem; + margin: 0; +} + +.searchForm input[type=submit], +.searchForm input[type=reset] { + display: inline-block; + padding: 0.4rem 0.8rem; + color: var(--color-text); + margin: .5rem; + cursor: pointer; +} + + + + +.searchResults { + margin-top: 0; + padding-top: 0; + border-left: 1px dotted #a5a5a5; +} + +.searchResults ol { + list-style: none; + margin: 0; + padding: 0; + +} + +.searchResults .item { + list-style: none; + padding-left: 1rem; + border-top: 1px dotted #a5a5a5; +} + +.searchResults .item p { + display: inline-flex; + align-items: start; + gap: 3px; +} + +.searchResults .item p { + display: inline-flex; + align-items: start; + gap: 3px; +} \ No newline at end of file diff --git a/frontend/src/components/Spinner/Spinner.tsx b/frontend/src/components/Spinner/Spinner.tsx new file mode 100644 index 0000000..1b5c147 --- /dev/null +++ b/frontend/src/components/Spinner/Spinner.tsx @@ -0,0 +1,10 @@ +import {JSX} from 'solid-js'; +import './spinner.css'; + +const Spinner: () => JSX.Element = () => { + return ( +
    + ) +} + +export default Spinner; diff --git a/frontend/src/components/Spinner/spinner.css b/frontend/src/components/Spinner/spinner.css new file mode 100644 index 0000000..77f5799 --- /dev/null +++ b/frontend/src/components/Spinner/spinner.css @@ -0,0 +1,56 @@ +.lds-ellipsis { + display: inline-block; + position: relative; + width: 80px; + height: 80px; +} +.lds-ellipsis div { + background: #000; + position: absolute; + top: 33px; + width: 13px; + height: 13px; + border-radius: 50%; + animation-timing-function: cubic-bezier(0, 1, 1, 0); +} +.lds-ellipsis div:nth-child(1) { + left: 8px;color:black; + animation: lds-ellipsis1 0.6s infinite; +} +.lds-ellipsis div:nth-child(2) { + left: 8px; + animation: lds-ellipsis2 0.6s infinite; +} +.lds-ellipsis div:nth-child(3) { + left: 32px; + animation: lds-ellipsis2 0.6s infinite; +} +.lds-ellipsis div:nth-child(4) { + left: 56px; + animation: lds-ellipsis3 0.6s infinite; +} + +@keyframes lds-ellipsis1 { + 0% { + transform: scale(0); + } + 100% { + transform: scale(1); + } +} +@keyframes lds-ellipsis3 { + 0% { + transform: scale(1); + } + 100% { + transform: scale(0); + } +} +@keyframes lds-ellipsis2 { + 0% { + transform: translate(0, 0); + } + 100% { + transform: translate(24px, 0); + } +} diff --git a/frontend/src/pages/Search.tsx b/frontend/src/pages/Search.tsx index 74fcbd6..ebb08c1 100644 --- a/frontend/src/pages/Search.tsx +++ b/frontend/src/pages/Search.tsx @@ -1,35 +1,49 @@ -import {JSX, createResource} from 'solid-js'; -import {A, useNavigate, useLocation} from '@solidjs/router'; +import {JSX, createResource, Switch, Match} from 'solid-js'; +import {A, useLocation, useSearchParams} from '@solidjs/router'; import ApiClient from '../api/client'; import {toDateTimeString} from '../datetime/DateTimeUtil'; +import SearchPastes from '../components/SearchPastes/SearchPastes'; +import Spinner from '../components/Spinner/Spinner'; const Search: () => JSX.Element = () => { - const navigate = useNavigate(); - const location = useLocation(); - const [pastes, { mutate, refetch }] = createResource(() => location.query.q, (term) => ApiClient.searchAll(term)); + const [searchTerm, setSearchTerm] = useSearchParams(); + + const delay = (delayInMs) => { + return new Promise(resolve => setTimeout(resolve, delayInMs)); + }; + + const effectiveTerm = () => { + return (searchTerm.q && searchTerm.q.length >= 3) ? searchTerm.q : null; + } + + const [pastes] = createResource( + effectiveTerm, + (term) => delay(600).then(_ => ApiClient.searchAll(term)), + //(term) => ApiClient.searchAll(term) + {initialValue: []} + ); + + function onSearchEnter(term: String) { + setSearchTerm({q: term}) + } return ( <> -

    Search

    - Nothing found

    }> -
      - {item => -
    1. -

      {item.title || 'Untitled' }

      -

      - Created: | - Expires: | - Size: {item.sizeInBytes} bytes -

      -
      “{item.highlight} [..]”
      -
    2. - } -
      -
    -
    + + + + + + + + + + + + ) }