From f38cd3020025308eb7d8fd7c91ec57d71fbad793 Mon Sep 17 00:00:00 2001 From: Haydn Ewers <27211+haydn@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:57:48 +1000 Subject: [PATCH] improve mobile support --- README.md | 2 +- src/app/SourceForm.tsx | 234 ------------------ src/app/page.module.css | 17 ++ src/app/page.tsx | 20 +- .../SourceConfigurationFieldSet.tsx | 103 ++++++++ src/components/SourceForm.tsx | 75 ++++++ src/{app => components}/SourceHtmlSamples.tsx | 0 src/{app => components}/SourceItems.tsx | 0 src/{app => components}/SourceOverview.tsx | 14 +- src/components/SourcePreview.tsx | 68 +++++ src/{app => components}/SourceStats.tsx | 8 + src/dialogs/AddSourceDialog.module.css | 16 ++ src/dialogs/AddSourceDialog.tsx | 74 ++++-- src/dialogs/EditSourceDialog.module.css | 16 ++ src/dialogs/EditSourceDialog.tsx | 85 +++++-- 15 files changed, 434 insertions(+), 298 deletions(-) delete mode 100644 src/app/SourceForm.tsx create mode 100644 src/app/page.module.css create mode 100644 src/components/SourceConfigurationFieldSet.tsx create mode 100644 src/components/SourceForm.tsx rename src/{app => components}/SourceHtmlSamples.tsx (100%) rename src/{app => components}/SourceItems.tsx (100%) rename src/{app => components}/SourceOverview.tsx (81%) create mode 100644 src/components/SourcePreview.tsx rename src/{app => components}/SourceStats.tsx (81%) create mode 100644 src/dialogs/AddSourceDialog.module.css create mode 100644 src/dialogs/EditSourceDialog.module.css diff --git a/README.md b/README.md index 6eaf499..71c1992 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Harvester -Aggregate data scrapped from web pages. +Aggregate data scrapped from the web. ## API diff --git a/src/app/SourceForm.tsx b/src/app/SourceForm.tsx deleted file mode 100644 index 151640b..0000000 --- a/src/app/SourceForm.tsx +++ /dev/null @@ -1,234 +0,0 @@ -"use client"; - -import type { FormAction } from "@colonydb/anthill"; -import { Block } from "@colonydb/anthill/Block"; -import { Button } from "@colonydb/anthill/Button"; -import { Card } from "@colonydb/anthill/Card"; -import { CardContent } from "@colonydb/anthill/CardContent"; -import { DialogHeader } from "@colonydb/anthill/DialogHeader"; -import { Form } from "@colonydb/anthill/Form"; -import { FormFooter } from "@colonydb/anthill/FormFooter"; -import { Heading } from "@colonydb/anthill/Heading"; -import { Inline } from "@colonydb/anthill/Inline"; -import { RegularField } from "@colonydb/anthill/RegularField"; -import { Section } from "@colonydb/anthill/Section"; -import { Specimen } from "@colonydb/anthill/Specimen"; -import { Stack } from "@colonydb/anthill/Stack"; -import { StringInput } from "@colonydb/anthill/StringInput"; -import { TabBlock } from "@colonydb/anthill/TabBlock"; -import { useForm } from "@colonydb/anthill/useForm"; -import { useCallback } from "react"; -import * as v from "valibot"; -import type { SourceConfig } from "@/index"; -import { sourceConfigSchema } from "@/schemas/sourceConfigSchema"; -import SourceHtmlSamples from "./SourceHtmlSamples"; -import SourceItems from "./SourceItems"; -import SourceOverview from "./SourceOverview"; -import SourceStats from "./SourceStats"; - -type Props = { - id: string; - initialData: SourceConfig; - onCancel: () => void; - onSubmit: (list: SourceConfig) => void; - onSuccess: () => void; - title: string; -}; - -export const SourceForm = ({ id, initialData, onCancel, onSubmit, onSuccess, title }: Props) => { - const action = useCallback>( - async ({ data }) => { - onSubmit(data); - return { - data, - ok: true, - }; - }, - [onSubmit], - ); - return ( -
- - {title} - - } - footer={ - Cancel} - /> - } - > - -
- - -
-
-
-
- ); -}; - -const Configuration = () => ( -
Configuration}> - - - - - - - - - - CSS selector identifying the items on the page to be listed. - - } - label="Item Selector" - name="itemSelector" - required - > - - - - CSS selector applied to each item to identify its title. If not provided, the - text of the entire item will be used. - - } - label="Title Selector" - name="titleSelector" - > - - - - CSS selector applied to each item to identify its link. If not provided, the - items itself will be used. - - } - label="Link Selector" - name="linkSelector" - > - - - - ), - }, - { - key: "filters", - label: "Filters", - content: ( - - - A comma-separated list of phrases to filter the items by. If an item does not - contain any of the phrases, it will be excluded from the list. - - } - label="Include" - name="include" - > - - - - A comma-separated list of phrases to filter the items by. If an item contains - any of the phrases, it will be excluded from the list. - - } - label="Exclude" - name="exclude" - > - - - - ), - }, - ]} - seamless - /> - -
-); - -const Preview = () => { - const { data } = useForm(sourceConfigSchema); - return ( -
Preview}> - - - - - - - ) : ( - - Not configured - - ), - }, - { - key: "items", - label: "Items", - content: v.is(sourceConfigSchema, data) ? ( - - - - ) : ( - - Not configured - - ), - }, - { - key: "matched", - label: "Sample HTML", - content: v.is(sourceConfigSchema, data) ? ( - - ) : ( - - Not configured - - ), - }, - ]} - seamless - /> -
- ); -}; diff --git a/src/app/page.module.css b/src/app/page.module.css new file mode 100644 index 0000000..9a4ce8b --- /dev/null +++ b/src/app/page.module.css @@ -0,0 +1,17 @@ +.container { + container-type: inline-size; + padding: 0 1rlh 1rlh; + + .content { + column-rule: var(--context-border); + display: grid; + gap: 2rlh; + grid-template-columns: minmax(min-content, 10rlh) minmax(min-content, 1fr); + } +} + +@container (width < 600px) { + .container .content { + grid-template-columns: auto; + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 7a9efc6..4c54813 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -7,13 +7,14 @@ import { MultiColumnStack } from "@colonydb/anthill/MultiColumnStack"; import { Section } from "@colonydb/anthill/Section"; import { Stack } from "@colonydb/anthill/Stack"; import useSWR from "swr/immutable"; +import SourceItems from "@/components/SourceItems"; +import SourceOverview from "@/components/SourceOverview"; import { AddSourceDialog } from "@/dialogs/AddSourceDialog"; import { EditSourceDialog } from "@/dialogs/EditSourceDialog"; import { RemoveSourceDialog } from "@/dialogs/RemoveSourceDialog"; import { listFetcher } from "@/fetchers/listFetcher"; import type { SourceConfig } from "@/index"; -import SourceItems from "./SourceItems"; -import SourceOverview from "./SourceOverview"; +import styles from "./page.module.css"; const HomePage = () => { const { @@ -23,20 +24,13 @@ const HomePage = () => { } = useSWR, Error>("lists", listFetcher); return ( -
+
{isLoading ? ( "Loading…" ) : error ? ( error.message ) : ( -
+
}> @@ -74,7 +68,9 @@ const HomePage = () => {
- +
Items}> + +
)} diff --git a/src/components/SourceConfigurationFieldSet.tsx b/src/components/SourceConfigurationFieldSet.tsx new file mode 100644 index 0000000..5c430d5 --- /dev/null +++ b/src/components/SourceConfigurationFieldSet.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { Block } from "@colonydb/anthill/Block"; +import { Heading } from "@colonydb/anthill/Heading"; +import { RegularField } from "@colonydb/anthill/RegularField"; +import { Section } from "@colonydb/anthill/Section"; +import { Stack } from "@colonydb/anthill/Stack"; +import { StringInput } from "@colonydb/anthill/StringInput"; +import { TabBlock } from "@colonydb/anthill/TabBlock"; + +export const SourceConfigurationFieldSet = () => ( +
Configuration}> + + + + + + + + + + CSS selector identifying the items on the page to be listed. + + } + label="Item Selector" + name="itemSelector" + required + > + + + + CSS selector applied to each item to identify its title. If not provided, the + text of the entire item will be used. + + } + label="Title Selector" + name="titleSelector" + > + + + + CSS selector applied to each item to identify its link. If not provided, the + items itself will be used. + + } + label="Link Selector" + name="linkSelector" + > + + + + ), + }, + { + key: "filters", + label: "Filters", + content: ( + + + A comma-separated list of phrases to filter the items by. If an item does not + contain any of the phrases, it will be excluded from the list. + + } + label="Include" + name="include" + > + + + + A comma-separated list of phrases to filter the items by. If an item contains + any of the phrases, it will be excluded from the list. + + } + label="Exclude" + name="exclude" + > + + + + ), + }, + ]} + seamless + /> + +
+); diff --git a/src/components/SourceForm.tsx b/src/components/SourceForm.tsx new file mode 100644 index 0000000..d31bc32 --- /dev/null +++ b/src/components/SourceForm.tsx @@ -0,0 +1,75 @@ +"use client"; + +import type { FormAction } from "@colonydb/anthill"; +import { Button } from "@colonydb/anthill/Button"; +import { Card } from "@colonydb/anthill/Card"; +import { CardContent } from "@colonydb/anthill/CardContent"; +import { DialogHeader } from "@colonydb/anthill/DialogHeader"; +import { Form } from "@colonydb/anthill/Form"; +import { FormFooter } from "@colonydb/anthill/FormFooter"; +import { Heading } from "@colonydb/anthill/Heading"; +import { useCallback } from "react"; +import type { SourceConfig } from "@/index"; +import { sourceConfigSchema } from "@/schemas/sourceConfigSchema"; +import { SourceConfigurationFieldSet } from "./SourceConfigurationFieldSet"; +import { SourcePreview } from "./SourcePreview"; + +type Props = { + id: string; + initialData: SourceConfig; + onCancel: () => void; + onSubmit: (list: SourceConfig) => void; + onSuccess: () => void; + title: string; +}; + +export const SourceForm = ({ id, initialData, onCancel, onSubmit, onSuccess, title }: Props) => { + const action = useCallback>( + async ({ data }) => { + onSubmit(data); + return { + data, + ok: true, + }; + }, + [onSubmit], + ); + return ( +
+ + {title} + + } + footer={ + Cancel} + /> + } + > + +
+ + +
+
+
+
+ ); +}; diff --git a/src/app/SourceHtmlSamples.tsx b/src/components/SourceHtmlSamples.tsx similarity index 100% rename from src/app/SourceHtmlSamples.tsx rename to src/components/SourceHtmlSamples.tsx diff --git a/src/app/SourceItems.tsx b/src/components/SourceItems.tsx similarity index 100% rename from src/app/SourceItems.tsx rename to src/components/SourceItems.tsx diff --git a/src/app/SourceOverview.tsx b/src/components/SourceOverview.tsx similarity index 81% rename from src/app/SourceOverview.tsx rename to src/components/SourceOverview.tsx index fbb6e92..3b6aac8 100644 --- a/src/app/SourceOverview.tsx +++ b/src/components/SourceOverview.tsx @@ -21,19 +21,7 @@ const SourceOverview = ({ actions, autoRefresh = false, config, debug = false }: const { data, error, isLoading } = useSource(config, debug, autoRefresh); return ( -
- {Temporal.Instant.from(data?.fetchedAt).toLocaleString(undefined, { - dateStyle: "short", - timeStyle: "long", - })} - - ) : null - } - > +
{data?.items.some( ({ firstSeen }) => Temporal.Now.instant() diff --git a/src/components/SourcePreview.tsx b/src/components/SourcePreview.tsx new file mode 100644 index 0000000..2248bd8 --- /dev/null +++ b/src/components/SourcePreview.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { Heading } from "@colonydb/anthill/Heading"; +import { Inline } from "@colonydb/anthill/Inline"; +import { Section } from "@colonydb/anthill/Section"; +import { Specimen } from "@colonydb/anthill/Specimen"; +import { Stack } from "@colonydb/anthill/Stack"; +import { TabBlock } from "@colonydb/anthill/TabBlock"; +import { useForm } from "@colonydb/anthill/useForm"; +import * as v from "valibot"; +import { sourceConfigSchema } from "@/schemas/sourceConfigSchema"; +import SourceHtmlSamples from "./SourceHtmlSamples"; +import SourceItems from "./SourceItems"; +import SourceOverview from "./SourceOverview"; +import SourceStats from "./SourceStats"; + +export const SourcePreview = () => { + const { data } = useForm(sourceConfigSchema); + return ( +
Preview}> + + + + + + + ) : ( + + Not configured + + ), + }, + { + key: "items", + label: "Items", + content: v.is(sourceConfigSchema, data) ? ( + + + + ) : ( + + Not configured + + ), + }, + { + key: "matched", + label: "Sample HTML", + content: v.is(sourceConfigSchema, data) ? ( + + ) : ( + + Not configured + + ), + }, + ]} + seamless + /> +
+ ); +}; diff --git a/src/app/SourceStats.tsx b/src/components/SourceStats.tsx similarity index 81% rename from src/app/SourceStats.tsx rename to src/components/SourceStats.tsx index 95bdf20..f36312d 100644 --- a/src/app/SourceStats.tsx +++ b/src/components/SourceStats.tsx @@ -4,6 +4,7 @@ import { Badge } from "@colonydb/anthill/Badge"; import { Block } from "@colonydb/anthill/Block"; import { Icon } from "@colonydb/anthill/Icon"; import { Inline } from "@colonydb/anthill/Inline"; +import { Temporal } from "temporal-polyfill"; import { useSource } from "@/hooks/useSource"; import type { SourceConfig } from "@/index"; @@ -25,6 +26,13 @@ const SourceStats = ({ config }: Props) => { ) : data?.debug !== undefined ? ( <> + + Source fetched:{" "} + {Temporal.Instant.from(data.fetchedAt).toLocaleString(undefined, { + dateStyle: "short", + timeStyle: "long", + })} + Items matched: {data.debug.itemsMatched} Items omitted by "Include" filter: {data.debug.itemsRemovedByIncludeFilter} Items omitted by "Exclude" filter: {data.debug.itemsRemovedByExcludeFilter} diff --git a/src/dialogs/AddSourceDialog.module.css b/src/dialogs/AddSourceDialog.module.css new file mode 100644 index 0000000..0ac7aae --- /dev/null +++ b/src/dialogs/AddSourceDialog.module.css @@ -0,0 +1,16 @@ +.container { + container-type: inline-size; + + .content { + column-rule: var(--context-border); + display: grid; + gap: calc(var(--context-spacing) * 2); + grid-template-columns: var(--width-small) 1fr; + } +} + +@container (width < 960px) { + .container .content { + grid-template-columns: auto; + } +} diff --git a/src/dialogs/AddSourceDialog.tsx b/src/dialogs/AddSourceDialog.tsx index cc31247..3664429 100644 --- a/src/dialogs/AddSourceDialog.tsx +++ b/src/dialogs/AddSourceDialog.tsx @@ -1,21 +1,51 @@ "use client"; +import type { FormAction } from "@colonydb/anthill"; +import { Button } from "@colonydb/anthill/Button"; +import { Card } from "@colonydb/anthill/Card"; +import { CardContent } from "@colonydb/anthill/CardContent"; import { Dialog } from "@colonydb/anthill/Dialog"; +import { DialogHeader } from "@colonydb/anthill/DialogHeader"; +import { Form } from "@colonydb/anthill/Form"; +import { FormFooter } from "@colonydb/anthill/FormFooter"; +import { Heading } from "@colonydb/anthill/Heading"; import { Icon } from "@colonydb/anthill/Icon"; +import { useCallback } from "react"; import useSWRMutation from "swr/mutation"; import { v4 as uuid } from "uuid"; -import { SourceForm } from "@/app/SourceForm"; +import { SourceConfigurationFieldSet } from "@/components/SourceConfigurationFieldSet"; +import { SourcePreview } from "@/components/SourcePreview"; import { addSource } from "@/mutations/addSource"; +import { sourceConfigSchema } from "@/schemas/sourceConfigSchema"; +import styles from "./AddSourceDialog.module.css"; export const AddSourceDialog = () => { const { trigger } = useSWRMutation("lists", addSource); + + const action = useCallback>( + async ({ data }) => { + trigger(data); + return { + data, + ok: true, + }; + }, + [trigger], + ); + return ( } render={(closeDialog) => ( - { + setTimeout(() => { + closeDialog(); + }, 1000); + }} + id="addSource" initialData={{ exclude: "", id: uuid(), @@ -26,19 +56,31 @@ export const AddSourceDialog = () => { titleSelector: "", url: "", }} - onCancel={() => { - closeDialog(); - }} - onSubmit={async (source) => { - await trigger(source); - }} - onSuccess={() => { - setTimeout(() => { - closeDialog(); - }, 1000); - }} - title="Add Source" - /> + schema={sourceConfigSchema} + > + + Add Source + + } + footer={ + Cancel} + /> + } + > + +
+
+ + +
+
+
+
+ )} hue="gray" width="large" diff --git a/src/dialogs/EditSourceDialog.module.css b/src/dialogs/EditSourceDialog.module.css new file mode 100644 index 0000000..0ac7aae --- /dev/null +++ b/src/dialogs/EditSourceDialog.module.css @@ -0,0 +1,16 @@ +.container { + container-type: inline-size; + + .content { + column-rule: var(--context-border); + display: grid; + gap: calc(var(--context-spacing) * 2); + grid-template-columns: var(--width-small) 1fr; + } +} + +@container (width < 960px) { + .container .content { + grid-template-columns: auto; + } +} diff --git a/src/dialogs/EditSourceDialog.tsx b/src/dialogs/EditSourceDialog.tsx index 2284cbd..4f02401 100644 --- a/src/dialogs/EditSourceDialog.tsx +++ b/src/dialogs/EditSourceDialog.tsx @@ -1,12 +1,23 @@ "use client"; +import type { FormAction } from "@colonydb/anthill"; +import { Button } from "@colonydb/anthill/Button"; +import { Card } from "@colonydb/anthill/Card"; +import { CardContent } from "@colonydb/anthill/CardContent"; import { Dialog } from "@colonydb/anthill/Dialog"; +import { DialogHeader } from "@colonydb/anthill/DialogHeader"; +import { Form } from "@colonydb/anthill/Form"; +import { FormFooter } from "@colonydb/anthill/FormFooter"; +import { Heading } from "@colonydb/anthill/Heading"; import { Icon } from "@colonydb/anthill/Icon"; import { useCallback } from "react"; import useSWRMutation from "swr/mutation"; -import { SourceForm } from "@/app/SourceForm"; +import { SourceConfigurationFieldSet } from "@/components/SourceConfigurationFieldSet"; +import { SourcePreview } from "@/components/SourcePreview"; import type { SourceConfig } from "@/index"; import { editSource } from "@/mutations/editSource"; +import { sourceConfigSchema } from "@/schemas/sourceConfigSchema"; +import styles from "./EditSourceDialog.module.css"; type Props = { source: SourceConfig; @@ -15,30 +26,60 @@ type Props = { export const EditSourceDialog = ({ source }: Props) => { const { trigger } = useSWRMutation("lists", editSource); - const render = useCallback( - (closeDialog: () => void) => ( - { - closeDialog(); - }} - onSubmit={async (source) => { - await trigger(source); - }} - onSuccess={() => { - setTimeout(() => { - closeDialog(); - }, 1000); - }} - title="Edit Source" - /> - ), - [source, trigger], + const action = useCallback>( + async ({ data }) => { + trigger(data); + return { + data, + ok: true, + }; + }, + [trigger], ); return ( - } padded render={render} width="large"> + } + padded + render={(closeDialog) => ( +
{ + setTimeout(() => { + closeDialog(); + }, 1000); + }} + id={`editSource:${source.id}`} + initialData={source} + schema={sourceConfigSchema} + > + + Edit Source + + } + footer={ + Cancel} + /> + } + > + +
+
+ + +
+
+
+
+
+ )} + width="large" + > Edit
); -- 2.51.2