From 4b8dec77dffba85bf2bc42aa553d0e0deee0a2fe Mon Sep 17 00:00:00 2001 From: Pete F Date: Thu, 12 Sep 2024 09:04:01 +0100 Subject: [PATCH] Reimplement UI using EUI components --- newswires/app/db/FingerpostWireEntry.scala | 11 +- newswires/client/src/App.css | 44 ----- newswires/client/src/App.tsx | 196 +++++---------------- newswires/client/src/NavigationList.tsx | 146 +++++++++++++++ newswires/client/src/WiresCards.tsx | 170 ++++++++++++++++++ newswires/client/src/icons.ts | 37 ++++ newswires/client/src/index.css | 76 -------- newswires/client/src/main.tsx | 2 +- 8 files changed, 400 insertions(+), 282 deletions(-) delete mode 100644 newswires/client/src/App.css create mode 100644 newswires/client/src/NavigationList.tsx create mode 100644 newswires/client/src/WiresCards.tsx create mode 100644 newswires/client/src/icons.ts delete mode 100644 newswires/client/src/index.css diff --git a/newswires/app/db/FingerpostWireEntry.scala b/newswires/app/db/FingerpostWireEntry.scala index 0825b7fc..7667da72 100644 --- a/newswires/app/db/FingerpostWireEntry.scala +++ b/newswires/app/db/FingerpostWireEntry.scala @@ -74,13 +74,14 @@ object FingerpostWireEntry extends SQLSyntaxSupport[FingerpostWireEntry] { sqls"$query <% (${FingerpostWireEntry.syn.column("content")}->>$fieldName)" val headline = filterElement("headline") - val subhead = filterElement("subhead") - val byline = filterElement("byline") - val keywords = filterElement("keywords") - val bodyText = filterElement("body_text") +// val subhead = filterElement("subhead") +// val byline = filterElement("byline") +// val keywords = filterElement("keywords") +// val bodyText = filterElement("body_text") val filters = - sqls"$headline OR $subhead OR $byline OR $keywords OR $bodyText" + sqls"$headline" +// sqls"$headline OR $subhead OR $byline OR $keywords OR $bodyText" sql"""| SELECT ${FingerpostWireEntry.syn.result.*} | FROM ${FingerpostWireEntry as syn} diff --git a/newswires/client/src/App.css b/newswires/client/src/App.css deleted file mode 100644 index 10615607..00000000 --- a/newswires/client/src/App.css +++ /dev/null @@ -1,44 +0,0 @@ -/* -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} -*/ diff --git a/newswires/client/src/App.tsx b/newswires/client/src/App.tsx index 2ea41aad..94a0205e 100644 --- a/newswires/client/src/App.tsx +++ b/newswires/client/src/App.tsx @@ -1,9 +1,17 @@ -import { css } from '@emotion/react'; +import { + EuiFieldSearch, + EuiHeader, + EuiHeaderSectionItem, + EuiLoadingLogo, + EuiPageTemplate, + EuiProvider, + EuiTitle, +} from '@elastic/eui'; import { useEffect, useMemo, useState } from 'react'; -import sanitizeHtml from 'sanitize-html'; -import './App.css'; +import '@elastic/eui/dist/eui_theme_light.css'; +import { WireCardList } from './WiresCards'; -type WireData = { +export type WireData = { id: number; externalId: string; ingestedAt: string; @@ -53,14 +61,6 @@ export function App() { const [query, setQuery] = useState(''); - const [selected, setSelected] = useState(undefined); - - const safeBodyText = useMemo(() => { - return selected?.content.body_text - ? sanitizeHtml(selected.content.body_text) - : undefined; - }, [selected]); - const updateQuery = useMemo(() => debounce(setQuery, 750), []); useEffect(() => { @@ -81,149 +81,33 @@ export function App() { }, [query]); return ( -
-
-

- Newswires -

- - - updateQuery(e.target.value)} /> - -
-
- {'error' in pageState && ( -

Sorry, failed to load because of {pageState.error}

- )} - {'loading' in pageState &&

Loading, please wait...

} - {Array.isArray(pageState) && ( -
    - {pageState.map((item) => ( -
  • setSelected(item)} - > - {item.content.headline ?? ''} -
  • - ))} -
- )} -
- {selected && ( -
- -
- {selected.content.headline &&

{selected.content.headline}

} - {selected.content.subhead && - selected.content.subhead !== selected.content.headline && ( -

{selected.content.subhead}

- )} - {selected.content.byline && ( -

- By:

{selected.content.byline}
-

- )} - {selected.content.keywords && ( -

- - Keywords:{' '} - - {selected.content.keywords} -

- )} - {selected.content.usage && ( -

- - Usage restrictions: - {' '} - - {selected.content.usage} - -

- )} -
- {selected.content.location && ( -

- {selected.content.location} -

- )} - {safeBodyText && ( -
- )} -
-
- )} -
+ + + + + +

Newswires

+
+
+ + updateQuery(e.target.value)} /> + +
+ + {'error' in pageState && ( + +

Sorry, failed to load because of {pageState.error}

+
+ )} + {'loading' in pageState && ( + } + title={

Loading Wires

} + /> + )} + {Array.isArray(pageState) && } +
+
+
); } diff --git a/newswires/client/src/NavigationList.tsx b/newswires/client/src/NavigationList.tsx new file mode 100644 index 00000000..21d4ee20 --- /dev/null +++ b/newswires/client/src/NavigationList.tsx @@ -0,0 +1,146 @@ +import type { Query } from '@elastic/eui'; +import { + EuiCallOut, + EuiErrorBoundary, + EuiHealth, + EuiSearchBar, +} from '@elastic/eui'; +import { useState } from 'react'; + +const tags = [ + { + name: 'tag1', + color: 'primary', + }, + { + name: 'tag2', + color: 'secondary', + }, + { + name: 'tag3', + color: 'accent', + }, + { + name: 'tag4', + color: 'warning', + }, +]; + +function getLocation(query: Query): string { + const params = new URLSearchParams(); + params.set('q', query.text); + console.log(params.toString()); + return `?${params.toString()}`; +} + +export const NavigationList = () => { + const maybeQueryString = + new URLSearchParams(window.location.search).get('q') ?? ''; + const initialQuery = EuiSearchBar.Query.parse(maybeQueryString); + + const [query, setQuery] = useState(initialQuery); // todo -- extract initial query from URL + const [error, setError] = useState(null); + + return ( + + { + if (error) { + setError(error); + } else { + setError(null); + setQuery(query); + history.pushState(undefined, '', getLocation(query)); + } + }} + filters={[ + { + type: 'field_value_selection', + field: 'tag', + name: 'or', + multiSelect: 'or', + options: tags.map((tag) => ({ + value: tag.name, + view: {tag.name}, + })), + }, + { + type: 'field_value_selection', + field: 'tag', + name: 'and', + multiSelect: 'and', + options: tags.map((tag) => ({ + value: tag.name, + view: {tag.name}, + })), + }, + ]} + /> + {error && ( + + )} + + ); +}; + +// const [navIsOpen, setNavIsOpen] = useState( +// (JSON.parse( +// String(localStorage.getItem('euiCollapsibleNavExample--isDocked')), +// ) || false) as boolean, +// ); +// const [navIsDocked, setNavIsDocked] = useState( +// (JSON.parse( +// String(localStorage.getItem('euiCollapsibleNavExample--isDocked')), +// ) || true) as boolean, +// ); + +// +// setNavIsOpen((isOpen) => !isOpen)} +// /> +// +// } +// onClose={() => setNavIsOpen(false)} +// > +//
+// +// +// +//

Newswires

+//
+//
+// +// { +// setNavIsDocked(!navIsDocked); +// localStorage.setItem( +// 'euiCollapsibleNavExample--isDocked', +// JSON.stringify(!navIsDocked), +// ); +// }} +// /> +// +//
+// +//

+// The docked status is being stored in{' '} +// localStorage. +//

+//
+// +//
+//
diff --git a/newswires/client/src/WiresCards.tsx b/newswires/client/src/WiresCards.tsx new file mode 100644 index 00000000..98f65e44 --- /dev/null +++ b/newswires/client/src/WiresCards.tsx @@ -0,0 +1,170 @@ +import { + EuiBadge, + EuiButton, + EuiCard, + EuiDescriptionList, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, + useGeneratedHtmlId, +} from '@elastic/eui'; +import { css } from '@emotion/react'; +import { useMemo, useState } from 'react'; +import sanitizeHtml from 'sanitize-html'; +import type { WireData } from './App'; + +export const WireCardList = ({ wires }: { wires: WireData[] }) => { + const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [selectedWireId, setSelectedWireId] = useState( + undefined, + ); + const selectedWire = useMemo( + () => wires.find((wire) => wire.id == selectedWireId), + [wires, selectedWireId], + ); + const safeBodyText = useMemo(() => { + return selectedWire?.content.body_text + ? sanitizeHtml(selectedWire.content.body_text) + : undefined; + }, [selectedWire]); + const pushedFlyoutTitleId = useGeneratedHtmlId({ + prefix: 'pushedFlyoutTitle', + }); + + let flyout; + + const toggleVisibilityFor = (id: number) => { + if (id == selectedWireId) { + setIsFlyoutVisible(!isFlyoutVisible); + } else { + setIsFlyoutVisible(true); + } + setSelectedWireId(id); + }; + + if (isFlyoutVisible && !!selectedWire) { + const { byline, keywords, usage } = selectedWire.content; + const listItems = [ + { + title: 'Byline', + description: byline ?? 'Not found', + }, + { + title: 'Keywords', + description: ( + + {keywords?.split('+').map((keyword) => ( + + {keyword} + + ))} + + ), + }, + { + title: 'Usage restrictions', + description: usage ?? 'Not found', + }, + { + title: 'Body text', + description: safeBodyText ? ( +
+ ) : ( + 'Not found' + ), + }, + ]; + + flyout = ( + setIsFlyoutVisible(false)} + aria-labelledby={pushedFlyoutTitleId} + > + + +

{selectedWire.content.headline}

+
+ + +

+ {selectedWire.content.subhead} +

+ + + + +
+ + setIsFlyoutVisible(false)}>Close + +
+ ); + } + + return ( +
+ + {wires.map((wire) => ( + toggleVisibilityFor(wire.id)} + selected={selectedWireId == wire.id} + /> + ))} + + {flyout} +
+ ); +}; + +const WirePanel = ({ + wire, + onClick, + selected, +}: { + wire: WireData; + onClick: () => void; + selected?: boolean; +}) => { + return ( + + +

{wire.content.headline ?? ''}

+ + } + icon={ + + PA + + } + layout="horizontal" + display={selected ? 'primary' : 'plain'} + onClick={onClick} + > + {wire.content.subhead ?? ''} +
+
+ ); +}; diff --git a/newswires/client/src/icons.ts b/newswires/client/src/icons.ts new file mode 100644 index 00000000..bf53de57 --- /dev/null +++ b/newswires/client/src/icons.ts @@ -0,0 +1,37 @@ +import { icon as arrowDown } from '@elastic/eui/es/components/icon/assets/arrow_down'; +import { icon as arrowLeft } from '@elastic/eui/es/components/icon/assets/arrow_left'; +import { icon as arrowRight } from '@elastic/eui/es/components/icon/assets/arrow_right'; +import { icon as arrowEnd } from '@elastic/eui/es/components/icon/assets/arrowEnd'; +import { icon as arrowStart } from '@elastic/eui/es/components/icon/assets/arrowStart'; +import { icon as check } from '@elastic/eui/es/components/icon/assets/check'; +import { icon as clock } from '@elastic/eui/es/components/icon/assets/clock'; +import { icon as cross } from '@elastic/eui/es/components/icon/assets/cross'; +import { icon as dot } from '@elastic/eui/es/components/icon/assets/dot'; +import { icon as doubleArrowLeft } from '@elastic/eui/es/components/icon/assets/doubleArrowLeft'; +import { icon as empty } from '@elastic/eui/es/components/icon/assets/empty'; +import { icon as faceSad } from '@elastic/eui/es/components/icon/assets/face_sad'; +import { icon as menu } from '@elastic/eui/es/components/icon/assets/menu'; +import { icon as returnKey } from '@elastic/eui/es/components/icon/assets/return_key'; +import { icon as search } from '@elastic/eui/es/components/icon/assets/search'; +import { icon as warning } from '@elastic/eui/es/components/icon/assets/warning'; +import { appendIconComponentCache } from '@elastic/eui/es/components/icon/icon'; + +// One or more icons are passed in as an object of iconKey (string): IconComponent +appendIconComponentCache({ + clock, + arrowDown, + arrowLeft, + arrowRight, + arrowEnd, + arrowStart, + dot, + cross, + menu, + doubleArrowLeft, + search, + faceSad, + warning, + empty, + returnKey, + check, +}); diff --git a/newswires/client/src/index.css b/newswires/client/src/index.css deleted file mode 100644 index 77c5e67c..00000000 --- a/newswires/client/src/index.css +++ /dev/null @@ -1,76 +0,0 @@ -body { - margin: 0; - display: flex; - place-items: center; - width: calc(100vw - 16px); - height: calc(100vh - 16px); - margin: 8px; -} - -#root { - width: 100%; - height: 100%; -} - -/* -:root { - font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif; - line-height: 1.5; - font-weight: 400; - - color-scheme: light dark; - color: rgba(255, 255, 255, 0.87); - background-color: #242424; - - font-synthesis: none; - text-rendering: optimizeLegibility; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -a { - font-weight: 500; - color: #646cff; - text-decoration: inherit; -} -a:hover { - color: #535bf2; -} - -h1 { - font-size: 3.2em; - line-height: 1.1; -} - -button { - border-radius: 8px; - border: 1px solid transparent; - padding: 0.6em 1.2em; - font-size: 1em; - font-weight: 500; - font-family: inherit; - background-color: #1a1a1a; - cursor: pointer; - transition: border-color 0.25s; -} -button:hover { - border-color: #646cff; -} -button:focus, -button:focus-visible { - outline: 4px auto -webkit-focus-ring-color; -} - -@media (prefers-color-scheme: light) { - :root { - color: #213547; - background-color: #ffffff; - } - a:hover { - color: #747bff; - } - button { - background-color: #f9f9f9; - } -} -*/ diff --git a/newswires/client/src/main.tsx b/newswires/client/src/main.tsx index f8907fe8..85f30585 100644 --- a/newswires/client/src/main.tsx +++ b/newswires/client/src/main.tsx @@ -1,7 +1,7 @@ import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { App } from './App.tsx'; -import './index.css'; +import './icons'; createRoot(document.getElementById('root')!).render(