From b69146ede00ff0061569d8ff65f4643867546fa5 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 25 Apr 2023 15:25:11 +0200 Subject: [PATCH 001/299] add route for campaigns in sidebar --- .../explore/sidebar/campaigns/index.tsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 app/routes/explore/sidebar/campaigns/index.tsx diff --git a/app/routes/explore/sidebar/campaigns/index.tsx b/app/routes/explore/sidebar/campaigns/index.tsx new file mode 100644 index 00000000..caae91b5 --- /dev/null +++ b/app/routes/explore/sidebar/campaigns/index.tsx @@ -0,0 +1,27 @@ +import { LoaderArgs, json } from "@remix-run/node"; +import { useLoaderData } from "@remix-run/react"; + +export async function loader({ params }: LoaderArgs) { + console.log(process.env.OSEM_API_URL); + // request to API with deviceID + const response = await fetch(process.env.OSEM_API_URL + "/users/campaigns/"); + const data = await response.json(); + if (data.code === "UnprocessableEntity") { + throw new Response("Campaigns not found", { status: 502 }); + } + return json(data); +} + +export default function Campaigns() { + const data = useLoaderData(); + console.log(data); + return ( +
+ {data.data.stream.map((item: any) => ( +
+

{item.title}

+
+ ))} +
+ ); +} From 5ba9460b08e73bfd7ed0ad08f8f3e97b73f74ba1 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 25 Apr 2023 15:28:10 +0200 Subject: [PATCH 002/299] use flex container for links and add campaign link in sidebar --- app/routes/explore/sidebar.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/app/routes/explore/sidebar.tsx b/app/routes/explore/sidebar.tsx index 7f93d95b..d90ace64 100644 --- a/app/routes/explore/sidebar.tsx +++ b/app/routes/explore/sidebar.tsx @@ -22,12 +22,15 @@ export default function Sidebar() { + + ); +} From 4dad5dda6f870249b434d87d2b2260d56083da41 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 29 Apr 2023 08:51:42 +0200 Subject: [PATCH 008/299] move files, fetch phenomena --- app/routes/campaigns/create.tsx | 306 ++++++++++++++++++ app/routes/campaigns/index.tsx | 205 ++++++++++++ .../explore/sidebar/campaigns/create.tsx | 83 ----- .../explore/sidebar/campaigns/index.tsx | 57 ---- 4 files changed, 511 insertions(+), 140 deletions(-) create mode 100644 app/routes/campaigns/create.tsx create mode 100644 app/routes/campaigns/index.tsx delete mode 100644 app/routes/explore/sidebar/campaigns/create.tsx delete mode 100644 app/routes/explore/sidebar/campaigns/index.tsx diff --git a/app/routes/campaigns/create.tsx b/app/routes/campaigns/create.tsx new file mode 100644 index 00000000..43a79fcf --- /dev/null +++ b/app/routes/campaigns/create.tsx @@ -0,0 +1,306 @@ +import { Form, Link, useLoaderData } from "@remix-run/react"; +import type { ActionArgs, LoaderArgs } from "@remix-run/node"; +import { DropdownMenuCheckboxItemProps } from "@radix-ui/react-dropdown-menu"; +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useState } from "react"; +import { Button } from "~/components/ui/button"; +import { ChevronDown } from "lucide-react"; + +type Checked = DropdownMenuCheckboxItemProps["checked"]; + +export async function action({ request }: ActionArgs) { + const formData = await request.formData(); + console.log(formData); + const title = formData.get("title"); + const description = formData.get("description"); + const phenomena = formData.get("phenomena"); + console.log(phenomena); + return; +} + +export async function loader({ params }: LoaderArgs) { + // request to fetch all phenomena + const response = await fetch("https://api.sensors.wiki/phenomena/all"); + const data = await response.json(); + if (data.code === "UnprocessableEntity") { + throw new Response("Phenomena not found", { status: 502 }); + } + const phenomena = data.map( + (d: { label: { item: { text: any }[] } }) => d.label.item[0].text + ); + return phenomena; +} + +export default function CreateCampaign() { + const phenomena = useLoaderData(); + const [phenomenaState, setPhenomenaState] = useState( + Object.fromEntries(phenomena.map((p: any) => [p, false])) + ); + + console.log(phenomenaState); + + return ( +
+
+
+
+ +
+ + {/* {actionData?.errors?.title && ( +
+ {actionData.errors.email} +
+ )} */} +
+
+ +
+ +
+ + {/* {actionData?.errors?.description && ( +
+ {actionData.errors.description} +
+ )} */} +
+
+
+ +
+ + {/* {actionData?.errors?.polygonDraw && ( +
+ {actionData.errors.polygonDraw} +
+ )} */} +
+
+
+ +
+ + {/* {actionData?.errors?.keywords && ( +
+ {actionData.errors.keywords} +
+ )} */} +
+
+
+ +
+ + {/* {actionData?.errors?.priority && ( +
+ {actionData.errors.priority} +
+ )} */} +
+
+
+ +
+ + {/* {actionData?.errors?.location && ( +
+ {actionData.errors.location} +
+ )} */} +
+
+
+ +
+ + {/* {actionData?.errors?.startDate && ( +
+ {actionData.errors.startDate} +
+ )} */} +
+
+
+ +
+ + {/* {actionData?.errors?.endDate && ( +
+ {actionData.errors.endDate} +
+ )} */} +
+
+
+ + + + + + {phenomena.map((p: any) => { + return ( + + setPhenomenaState({ + ...phenomenaState, + [p]: !phenomenaState[p], + }) + } + > + {p} + + ); + })} + + +
+ + {/* */} + +
+ + Kampagnen Übersicht + +
+
+
+
+ ); +} diff --git a/app/routes/campaigns/index.tsx b/app/routes/campaigns/index.tsx new file mode 100644 index 00000000..80803214 --- /dev/null +++ b/app/routes/campaigns/index.tsx @@ -0,0 +1,205 @@ +import { LoaderArgs, json } from "@remix-run/node"; +import { Link, useLoaderData } from "@remix-run/react"; +import clsx from "clsx"; +import { useState } from "react"; +import { Switch } from "~/components/ui/switch"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuRadioGroup, + DropdownMenuRadioItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Button } from "~/components/ui/button"; +import { ChevronDown } from "lucide-react"; +import Header from "./header"; +import { Map } from "~/components/Map"; + +export async function loader({ params }: LoaderArgs) { + console.log(process.env.OSEM_API_URL); + // request to API with deviceID + const response = await fetch(process.env.OSEM_API_URL + "/users/campaigns/"); + const data = await response.json(); + if (data.code === "UnprocessableEntity") { + throw new Response("Campaigns not found", { status: 502 }); + } + return json(data); +} + +export default function Campaigns() { + const data = useLoaderData(); + const [showMap, setShowMap] = useState(false); + const [searchTerm, setSearchTerm] = useState(""); + const [sortBy, setSortBy] = useState(""); + const [urgency, setUrgency] = useState(""); + + const resetFilters = () => { + setUrgency(""); + setSortBy(""); + }; + + const filteredCampaigns = data.data.stream.filter((campaign: any) => { + const titleMatches = campaign.title + .toLowerCase() + .includes(searchTerm.toLowerCase()); + const priorityMatches = + !urgency || campaign.priority.toLowerCase() === urgency.toLowerCase(); + return titleMatches && priorityMatches; + }); + + const sortedCampaigns = filteredCampaigns.sort((a: any, b: any) => { + const urgencyOrder = { urgent: 0, high: 1, medium: 2, low: 3 }; // Define urgency priority order + return ( + urgencyOrder[a.priority.toLowerCase() as keyof typeof urgencyOrder] - + urgencyOrder[b.priority.toLowerCase() as keyof typeof urgencyOrder] + ); + }); + + const displayedCampaigns = + sortBy === "dringlichkeit" ? sortedCampaigns : filteredCampaigns; + + return ( +
+
+ {/* + + */} + setSearchTerm(event.target.value)} + /> +
+ + + + + + + + Urgent + + High + + + + + + + + + + + + Dringlichkeit + + + Erstellung + + + + + +
+ Karte anzeigen + setShowMap(!showMap)} + /> +
+
+ {data.data.stream.length === 0 ? ( +
+ Zurzeit gibt es noch keine Kampagnen. Klicke hier um eine Kampagne zu + erstellen +
+ ) : ( + //
+
+
+
+ {displayedCampaigns.map((item: any) => ( + + +
+
+ {item.priority} +
+
+

{item.title}

+

{item.description}

+ + Learn More + + {item.description} +
+
+ ))} +
+
+ {showMap && ( +
+ +
+ )} +
+ )} +
+ ); +} diff --git a/app/routes/explore/sidebar/campaigns/create.tsx b/app/routes/explore/sidebar/campaigns/create.tsx deleted file mode 100644 index 487d44e4..00000000 --- a/app/routes/explore/sidebar/campaigns/create.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Form, Link } from "@remix-run/react"; - -export default function CreateCampaign() { - return ( -
-
-
-
- -
- - {/* {actionData?.errors?.author && ( -
- {actionData.errors.email} -
- )} */} -
-
- -
- -
- - {/* {actionData?.errors?.description && ( -
- {actionData.errors.description} -
- )} */} -
-
- - {/* */} - -
- - Kampagnen Übersicht - -
-
-
-
- ); -} diff --git a/app/routes/explore/sidebar/campaigns/index.tsx b/app/routes/explore/sidebar/campaigns/index.tsx deleted file mode 100644 index cebedaff..00000000 --- a/app/routes/explore/sidebar/campaigns/index.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import { LoaderArgs, json } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; -import clsx from "clsx"; - -export async function loader({ params }: LoaderArgs) { - console.log(process.env.OSEM_API_URL); - // request to API with deviceID - const response = await fetch(process.env.OSEM_API_URL + "/users/campaigns/"); - const data = await response.json(); - if (data.code === "UnprocessableEntity") { - throw new Response("Campaigns not found", { status: 502 }); - } - return json(data); -} - -export default function Campaigns() { - const data = useLoaderData(); - console.log(data); - return ( -
- {data.data.stream.length === 0 ? ( -
- Zurzeit gibt es noch keine Kampagnen. Klicke hier um eine Kampagne zu - erstellen -
- ) : ( - data.data.stream.map((item: any) => ( -
-
-
- {item.priority} -
-
-

{item.title}

-

{item.description}

- - Learn More - -
- )) - )} -
- ); -} From 12a83fdad4b60e409e05c5160e54edfd5391e752 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 29 Apr 2023 08:52:12 +0200 Subject: [PATCH 009/299] add shadcn switch --- app/components/ui/switch.tsx | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 app/components/ui/switch.tsx diff --git a/app/components/ui/switch.tsx b/app/components/ui/switch.tsx new file mode 100644 index 00000000..835c3965 --- /dev/null +++ b/app/components/ui/switch.tsx @@ -0,0 +1,27 @@ +import * as React from "react"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; + +import { cn } from "@/lib/utils"; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; From 64a13d3fa15dcf485647104241fff54d96fd7e98 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 29 Apr 2023 10:17:53 +0200 Subject: [PATCH 010/299] add campaign link to header --- app/components/landing/header.tsx | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/app/components/landing/header.tsx b/app/components/landing/header.tsx index 26a326de..b0c0d91d 100644 --- a/app/components/landing/header.tsx +++ b/app/components/landing/header.tsx @@ -23,6 +23,10 @@ const links = [ name: "Partners", link: "#partners", }, + { + name: "Campaigns", + link: "/campaigns", + }, ]; export default function Header() { @@ -34,12 +38,12 @@ export default function Header() { }; return ( -
- - - + - - - - + + + + + + )}
{/* */} From 2217988dfd05ddf3fa5f601bb097e37f57269ba1 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 2 Jul 2023 13:18:23 +0200 Subject: [PATCH 088/299] display comments --- app/models/campaign.server.ts | 1 + app/routes/campaigns/$slug.tsx | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/models/campaign.server.ts b/app/models/campaign.server.ts index d5bf72df..568f59f6 100644 --- a/app/models/campaign.server.ts +++ b/app/models/campaign.server.ts @@ -5,6 +5,7 @@ import { prisma } from "~/db.server"; export function getCampaign({ slug }: Pick) { return prisma.campaign.findFirst({ where: { slug }, + include: { comments: true }, }); } diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index 58f5a7d2..e7e102b6 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -178,10 +178,11 @@ export default function CampaignId() {

{data.title}

-

- Beschreibung -

+

Beschreibung

{data.description}

+

Fragen und Kommentare

+ +

{data.comments.map((c) => c.content)}

{/*
*/} {() => ( From eda115c5b3b383314d964ec564e7a1961d678111 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 2 Jul 2023 13:23:47 +0200 Subject: [PATCH 089/299] move download geojson function to lib --- app/lib/download-geojson.ts | 18 ++++++++++++++++++ app/routes/campaigns/$slug.tsx | 27 +++++---------------------- 2 files changed, 23 insertions(+), 22 deletions(-) create mode 100644 app/lib/download-geojson.ts diff --git a/app/lib/download-geojson.ts b/app/lib/download-geojson.ts new file mode 100644 index 00000000..3f2c0ee3 --- /dev/null +++ b/app/lib/download-geojson.ts @@ -0,0 +1,18 @@ +import { valid } from "geojson-validation"; + +export function downloadGeojSON(data: any) { + //@ts-ignore + const geojson = JSON.parse(JSON.stringify(data)); + if (valid(geojson)) { + const geojsonString = JSON.stringify(geojson); + const blob = new Blob([geojsonString], { type: "application/json" }); + const url = URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.download = "geojson_data.json"; + link.click(); + + URL.revokeObjectURL(url); + } +} diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index e7e102b6..ebd179ea 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -33,6 +33,7 @@ import { MarkdownEditor } from "~/markdown.client"; import { createComment } from "~/models/comment.server"; import maplibregl from "maplibre-gl/dist/maplibre-gl.css"; import { Switch } from "~/components/ui/switch"; +import { downloadGeojSON } from "~/lib/download-geojson"; export const links: LinksFunction = () => { return [ @@ -130,25 +131,6 @@ export default function CampaignId() { // }); // }, []); - function downloadGeojSON() { - //@ts-ignore - const geojson = JSON.parse(JSON.stringify(data.feature[0])); - console.log(geojson); - console.log(valid(geojson)); - if (valid(geojson)) { - const geojsonString = JSON.stringify(geojson); - const blob = new Blob([geojsonString], { type: "application/json" }); - const url = URL.createObjectURL(blob); - - const link = document.createElement("a"); - link.href = url; - link.download = "geojson_data.json"; - link.click(); - - URL.revokeObjectURL(url); - } - } - return (
@@ -174,14 +156,12 @@ export default function CampaignId() { > {data.priority} -

{data.title}

Beschreibung

{data.description}

Fragen und Kommentare

-

{data.comments.map((c) => c.content)}

{/* */} @@ -214,7 +194,10 @@ export default function CampaignId() {
)} - + {/* @ts-ignore */} + From 87bff17c036b1c0013e323a9342eb8ecdff25a6a Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 2 Jul 2023 14:44:57 +0200 Subject: [PATCH 090/299] tabview --- app/routes/campaigns/$slug.tsx | 228 ++++++++++++++++++++++----------- 1 file changed, 150 insertions(+), 78 deletions(-) diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index ebd179ea..eb3f78cf 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -34,6 +34,7 @@ import { createComment } from "~/models/comment.server"; import maplibregl from "maplibre-gl/dist/maplibre-gl.css"; import { Switch } from "~/components/ui/switch"; import { downloadGeojSON } from "~/lib/download-geojson"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; export const links: LinksFunction = () => { return [ @@ -121,7 +122,10 @@ const layer: LayerProps = { export default function CampaignId() { const data = useLoaderData(); const [comment, setComment] = useState(""); - const [showMap, setShowMap] = useState(false); + const [showMap, setShowMap] = useState(true); + const [tabView, setTabView] = useState<"overview" | "calendar" | "comments">( + "overview" + ); const textAreaRef = useRef(); const { toast } = useToast(); const participate = () => {}; @@ -133,7 +137,73 @@ export default function CampaignId() { return (
-
+
+ + + + + + + Teilnehmen + +

+ Indem Sie auf Teilnehmen klicken stimmen Sie zu, dass Sie der + Kampagnenleiter unter der von Ihnen angegebenen Email- Adresse + kontaktieren darf! +

+

+ Bitte gib ausserdem an, ob du bereits über die benötigte + Hardware verfügst. +

+
+
+ +
+
+ + +
+
+ + +
+
+ + + + +
+
+ + + + + + + Teilen + + + + + + + {/* @ts-ignore */} + Karte anzeigen setShowMap(!showMap)} />
-
+
+

+ {data.title} +

+ + {data.priority} + +
+
+ + + + {/* + {t("live_label")} */} + + + + + + + + + + +

Beschreibung

+

{data.description}

+
+ + +

Fragen und Kommentare

+

{data.comments.map((c) => c.content)}

+ {/*
*/} + + {() => ( +
+ +
+ + Bild hinzufügen + + + Markdown unterstützt + +
+ + + + +
+ )} +
+
+
-

{data.title} -

-

Beschreibung

-

{data.description}

-

Fragen und Kommentare

+ */} + {/*

Fragen und Kommentare

{data.comments.map((c) => c.content)}

- {/*
*/} {() => (
@@ -193,75 +333,7 @@ export default function CampaignId() {
)} -
- {/* @ts-ignore */} - - - - - - - - Teilen - - - - - - - - - - - - - Teilnehmen - -

- Indem Sie auf Teilnehmen klicken stimmen Sie zu, dass Sie - der Kampagnenleiter unter der von Ihnen angegebenen Email- - Adresse kontaktieren darf! -

-

- Bitte gib ausserdem an, ob du bereits über die benötigte - Hardware verfügst. -

-
-
-
-
-
- - -
-
- - -
-
- - - -
-
-
+ */}
{showMap && ( From 085ed5a11f3e4894d19f49326adedae468bef582 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 2 Jul 2023 14:59:14 +0200 Subject: [PATCH 091/299] display comments in md --- app/routes/campaigns/$slug.tsx | 6 +++++- package.json | 1 + yarn.lock | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index eb3f78cf..50654985 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -35,6 +35,7 @@ import maplibregl from "maplibre-gl/dist/maplibre-gl.css"; import { Switch } from "~/components/ui/switch"; import { downloadGeojSON } from "~/lib/download-geojson"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import Markdown from "markdown-to-jsx"; export const links: LinksFunction = () => { return [ @@ -251,7 +252,10 @@ export default function CampaignId() {

Fragen und Kommentare

-

{data.comments.map((c) => c.content)}

+ {data.comments.map((c, i) => { + return {c.content}; + })} + {/*

{data.comments.map((c) => c.content)}

*/} {/*
*/} {() => ( diff --git a/package.json b/package.json index 6cc9a2de..555c95ee 100644 --- a/package.json +++ b/package.json @@ -95,6 +95,7 @@ "mapbox-gl-draw-rectangle-mode": "^1.0.4", "maplibre-gl": "^2.4.0", "maplibre-gl-draw-circle": "^0.1.1", + "markdown-to-jsx": "^7.2.1", "morgan": "^1.10.0", "numeral": "^2.0.6", "postcss-import": "^15.1.0", diff --git a/yarn.lock b/yarn.lock index 03b3955a..4d7db740 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9279,6 +9279,11 @@ markdown-table@^3.0.0: resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== +markdown-to-jsx@^7.2.1: + version "7.2.1" + resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.2.1.tgz#87061fd3176ad926ef3d99493e5c57f6335e0c51" + integrity sha512-9HrdzBAo0+sFz9ZYAGT5fB8ilzTW+q6lPocRxrIesMO+aB40V9MgFfbfMXxlGjf22OpRy+IXlvVaQenicdpgbg== + mdast-util-definitions@^5.0.0: version "5.1.2" resolved "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz" From bf516d4d70f866c3ae7323fa9975eb0e63b58b33 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 3 Jul 2023 11:46:13 +0200 Subject: [PATCH 092/299] delete comments --- app/models/comment.server.ts | 30 +++----------- app/routes/campaigns/$slug.tsx | 73 ++++++++++++++++++++++++++-------- 2 files changed, 62 insertions(+), 41 deletions(-) diff --git a/app/models/comment.server.ts b/app/models/comment.server.ts index 795153f1..c37dddcb 100644 --- a/app/models/comment.server.ts +++ b/app/models/comment.server.ts @@ -1,30 +1,6 @@ import type { User, Comment } from "@prisma/client"; import { prisma } from "~/db.server"; -// export function createComment( -// content: string, -// campaignSlug: string, -// ownerId: User["id"] -// ) { -// return prisma.comment.create({ -// data: { -// content, -// createdAt: new Date(), -// updatedAt: new Date(), -// owner: { -// connect: { -// id: ownerId, -// }, -// }, -// campaign: { -// connect: { -// slug: campaignSlug, -// }, -// }, -// }, -// }); -// } - export function createComment({ content, campaignSlug, @@ -50,3 +26,9 @@ export function createComment({ }, }); } + +export function deleteComment({ id }: Pick) { + return prisma.comment.deleteMany({ + where: { id }, + }); +} diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index 50654985..bb2695f1 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -30,7 +30,7 @@ import { valid } from "geojson-validation"; import ShareLink from "~/components/bottom-bar/share-link"; import { ClientOnly } from "remix-utils"; import { MarkdownEditor } from "~/markdown.client"; -import { createComment } from "~/models/comment.server"; +import { createComment, deleteComment } from "~/models/comment.server"; import maplibregl from "maplibre-gl/dist/maplibre-gl.css"; import { Switch } from "~/components/ui/switch"; import { downloadGeojSON } from "~/lib/download-geojson"; @@ -52,6 +52,9 @@ export async function action(args: ActionArgs) { if (_action === "PUBLISH") { return publishAction(args); } + if (_action === "DELETE") { + return deleteCommentAction(args); + } // if (_action === "UPDATE") { // return updateAction(args); // } @@ -84,6 +87,24 @@ async function publishAction({ request, params }: ActionArgs) { } } +async function deleteCommentAction({ request }: ActionArgs) { + const formData = await request.formData(); + const commentId = formData.get("deleteComment"); + if (typeof commentId !== "string" || commentId.length === 0) { + return json( + { errors: { commentId: "commentId is required", body: null } }, + { status: 400 } + ); + } + try { + const commentToDelete = await deleteComment({ id: commentId }); + return json({ ok: true }); + } catch (error) { + console.error(`form not submitted ${error}`); + return json({ error }); + } +} + // export async function action({ request }: ActionArgs) { // // const ownerId = await requireUserId(request); // const formData = await request.formData(); @@ -100,14 +121,14 @@ export const meta: MetaFunction = ({ params }) => ({ viewport: "width=device-width,initial-scale=1", }); -export async function loader({ params }: LoaderArgs) { - // request to API with deviceID +export async function loader({ request, params }: LoaderArgs) { + const userId = await requireUserId(request); const campaign = await getCampaign({ slug: params.slug ?? "" }); if (!campaign) { throw new Response("Campaign not found", { status: 502 }); } - return json(campaign); + return json({ campaign, userId }); } const layer: LayerProps = { @@ -122,6 +143,8 @@ const layer: LayerProps = { export default function CampaignId() { const data = useLoaderData(); + const campaign = data.campaign; + const userId = data.userId; const [comment, setComment] = useState(""); const [showMap, setShowMap] = useState(true); const [tabView, setTabView] = useState<"overview" | "calendar" | "comments">( @@ -214,20 +237,20 @@ export default function CampaignId() {

- {data.title} + {campaign.title}

- {data.priority} + {campaign.priority}
@@ -247,15 +270,31 @@ export default function CampaignId() {

Beschreibung

-

{data.description}

+

{campaign.description}

Fragen und Kommentare

- {data.comments.map((c, i) => { - return {c.content}; + {campaign.comments.map((c, i) => { + return ( +
+ {userId === campaign.ownerId && ( + + + + + )} + {c.content}; +
+ ); })} - {/*

{data.comments.map((c) => c.content)}

*/} {/*
*/} {() => ( @@ -345,9 +384,9 @@ export default function CampaignId() { From d9ba6b715768d2f5f8d095cd04d1689753d872fd Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 3 Jul 2023 13:19:50 +0200 Subject: [PATCH 093/299] fix download button --- app/routes/campaigns/$slug.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index bb2695f1..b3aa07e4 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -225,7 +225,7 @@ export default function CampaignId() {
{/* @ts-ignore */} - Karte anzeigen From b847247c77e8e47ddb4947dace522a27780805c8 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 3 Jul 2023 14:16:46 +0200 Subject: [PATCH 094/299] edit comment --- app/models/comment.server.ts | 14 +- app/routes/campaigns/$slug.tsx | 226 +++++++++++++++++++-------------- 2 files changed, 144 insertions(+), 96 deletions(-) diff --git a/app/models/comment.server.ts b/app/models/comment.server.ts index c37dddcb..c1c773a9 100644 --- a/app/models/comment.server.ts +++ b/app/models/comment.server.ts @@ -1,4 +1,4 @@ -import type { User, Comment } from "@prisma/client"; +import type { User, Comment, Prisma } from "@prisma/client"; import { prisma } from "~/db.server"; export function createComment({ @@ -32,3 +32,15 @@ export function deleteComment({ id }: Pick) { where: { id }, }); } + +export async function updateComment(commentId: string, content: string) { + return prisma.comment.update({ + where: { + id: commentId, + }, + data: { + content: content, + updatedAt: new Date(), + }, + }); +} diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index b3aa07e4..6e3e41bb 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -30,7 +30,11 @@ import { valid } from "geojson-validation"; import ShareLink from "~/components/bottom-bar/share-link"; import { ClientOnly } from "remix-utils"; import { MarkdownEditor } from "~/markdown.client"; -import { createComment, deleteComment } from "~/models/comment.server"; +import { + createComment, + deleteComment, + updateComment, +} from "~/models/comment.server"; import maplibregl from "maplibre-gl/dist/maplibre-gl.css"; import { Switch } from "~/components/ui/switch"; import { downloadGeojSON } from "~/lib/download-geojson"; @@ -55,12 +59,40 @@ export async function action(args: ActionArgs) { if (_action === "DELETE") { return deleteCommentAction(args); } + if (_action === "EDIT") { + return updateAction(args); + } // if (_action === "UPDATE") { // return updateAction(args); // } throw new Error("Unknown action"); } +async function updateAction({ request }: ActionArgs) { + const formData = await request.formData(); + const content = formData.get("editComment"); + if (typeof content !== "string" || content.length === 0) { + return json( + { errors: { content: "content is required", body: null } }, + { status: 400 } + ); + } + const commentId = formData.get("commentId"); + if (typeof commentId !== "string" || commentId.length === 0) { + return json( + { errors: { commentId: "commentId is required", body: null } }, + { status: 400 } + ); + } + try { + const comment = await updateComment(commentId, content); + return json({ ok: true }); + } catch (error) { + console.error(`form not submitted ${error}`); + return json({ error }); + } +} + async function publishAction({ request, params }: ActionArgs) { const ownerId = await requireUserId(request); const formData = await request.formData(); @@ -146,6 +178,9 @@ export default function CampaignId() { const campaign = data.campaign; const userId = data.userId; const [comment, setComment] = useState(""); + const [editComment, setEditComment] = useState(""); + const [editCommentId, setEditCommentId] = useState(""); + const [showMap, setShowMap] = useState(true); const [tabView, setTabView] = useState<"overview" | "calendar" | "comments">( "overview" @@ -224,7 +259,6 @@ export default function CampaignId() { - {/* @ts-ignore */} @@ -275,117 +309,119 @@ export default function CampaignId() {

Fragen und Kommentare

- {campaign.comments.map((c, i) => { + {campaign.comments.map((c: any, i: number) => { return (
{userId === campaign.ownerId && ( - - - + + - + {editCommentId === c.id && ( + + {() => ( +
+ +
+ + Bild hinzufügen + + + Markdown unterstützt + +
+
+ + + +
+
+ )} +
+ )} + )} {c.content};
); })} - {/*
*/} - - {() => ( -
- -
- - Bild hinzufügen - - - Markdown unterstützt - + {!editComment && ( + + {() => ( +
+ +
+ + Bild hinzufügen + + + Markdown unterstützt + +
+ + + +
-
- - -
-
- )} - + )} + + )} -
- {/* - {data.priority} - -

- {data.title} -

*/} - {/*

Fragen und Kommentare

-

{data.comments.map((c) => c.content)}

- - {() => ( -
- -
- - Bild hinzufügen - - - Markdown unterstützt - -
-
- - -
-
- )} -
*/} -
+
{showMap && ( Date: Mon, 3 Jul 2023 17:30:43 +0200 Subject: [PATCH 095/299] create events --- app/models/campaign.server.ts | 2 +- app/models/campaignevents.server.ts | 64 +++++++++++++++++ app/routes/campaigns/$slug.tsx | 71 ++++++++++++++++++- .../20230703145146_events/migration.sql | 28 ++++++++ .../migrations/20230703150234_/migration.sql | 8 +++ prisma/schema.prisma | 23 ++++-- 6 files changed, 190 insertions(+), 6 deletions(-) create mode 100644 app/models/campaignevents.server.ts create mode 100644 prisma/migrations/20230703145146_events/migration.sql create mode 100644 prisma/migrations/20230703150234_/migration.sql diff --git a/app/models/campaign.server.ts b/app/models/campaign.server.ts index 568f59f6..77d4bc2c 100644 --- a/app/models/campaign.server.ts +++ b/app/models/campaign.server.ts @@ -5,7 +5,7 @@ import { prisma } from "~/db.server"; export function getCampaign({ slug }: Pick) { return prisma.campaign.findFirst({ where: { slug }, - include: { comments: true }, + include: { comments: true, events: true }, }); } diff --git a/app/models/campaignevents.server.ts b/app/models/campaignevents.server.ts new file mode 100644 index 00000000..410a6f89 --- /dev/null +++ b/app/models/campaignevents.server.ts @@ -0,0 +1,64 @@ +import type { User, CampaignEvent } from "@prisma/client"; +import { prisma } from "~/db.server"; + +export function createEvent({ + title, + description, + campaignSlug, + startDate, + endDate, + ownerId, +}: Pick< + CampaignEvent, + "title" | "description" | "campaignSlug" | "startDate" | "endDate" +> & { + ownerId: User["id"]; +}) { + return prisma.campaignEvent.create({ + data: { + title, + description, + startDate, + endDate, + createdAt: new Date(), + updatedAt: new Date(), + owner: { + connect: { + id: ownerId, + }, + }, + campaign: { + connect: { + slug: campaignSlug, + }, + }, + }, + }); +} + +export function deleteEvent({ id }: Pick) { + return prisma.campaignEvent.deleteMany({ + where: { id }, + }); +} + +export async function updateEvent( + eventId: string, + title?: string, + description?: string, + startDate?: Date, + endDate?: Date +) { + return prisma.campaignEvent.update({ + where: { + id: eventId, + }, + data: { + title, + description, + startDate, + endDate, + updatedAt: new Date(), + }, + }); +} diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index 6e3e41bb..f50ca5e8 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -40,6 +40,7 @@ import { Switch } from "~/components/ui/switch"; import { downloadGeojSON } from "~/lib/download-geojson"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import Markdown from "markdown-to-jsx"; +import { createEvent } from "~/models/campaignevents.server"; export const links: LinksFunction = () => { return [ @@ -62,6 +63,9 @@ export async function action(args: ActionArgs) { if (_action === "EDIT") { return updateAction(args); } + if (_action === "CREATE_EVENT") { + return createCampaignEvent(args); + } // if (_action === "UPDATE") { // return updateAction(args); // } @@ -119,6 +123,49 @@ async function publishAction({ request, params }: ActionArgs) { } } +async function createCampaignEvent({ request, params }: ActionArgs) { + const ownerId = await requireUserId(request); + const formData = await request.formData(); + const title = formData.get("title"); + const description = formData.get("description"); + const startDate = new Date(); + const endDate = new Date(); + + if (typeof title !== "string" || title.length === 0) { + return json( + { errors: { title: "title is required", body: null } }, + { status: 400 } + ); + } + if (typeof description !== "string" || description.length === 0) { + return json( + { errors: { description: "description is required", body: null } }, + { status: 400 } + ); + } + const campaignSlug = params.slug; + if (typeof campaignSlug !== "string" || campaignSlug.length === 0) { + return json( + { errors: { campaignSlug: "campaignSlug is required", body: null } }, + { status: 400 } + ); + } + try { + const event = await createEvent({ + title, + description, + startDate, + endDate, + campaignSlug, + ownerId, + }); + return json({ ok: true }); + } catch (error) { + console.error(`form not submitted ${error}`); + return json({ error }); + } +} + async function deleteCommentAction({ request }: ActionArgs) { const formData = await request.formData(); const commentId = formData.get("deleteComment"); @@ -259,6 +306,7 @@ export default function CampaignId() { + {/* @ts-ignore */} @@ -306,7 +354,26 @@ export default function CampaignId() {

Beschreibung

{campaign.description}

- + + {campaign.events.length === 0 ? ( +
+ {" "} +

+ Noch keine Events für diese Kampagne. Erstelle ein Event:{" "} +

+
+ + + + + + +
+
+ ) : null} +

Fragen und Kommentare

{campaign.comments.map((c: any, i: number) => { @@ -421,7 +488,9 @@ export default function CampaignId() { Date: Tue, 4 Jul 2023 07:46:41 +0200 Subject: [PATCH 096/299] delete events --- ...ts.server.ts => campaign-events.server.ts} | 0 app/routes/campaigns/$slug.tsx | 52 +++++++++++++++++-- 2 files changed, 49 insertions(+), 3 deletions(-) rename app/models/{campaignevents.server.ts => campaign-events.server.ts} (100%) diff --git a/app/models/campaignevents.server.ts b/app/models/campaign-events.server.ts similarity index 100% rename from app/models/campaignevents.server.ts rename to app/models/campaign-events.server.ts diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index f50ca5e8..cf7a2176 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -40,7 +40,7 @@ import { Switch } from "~/components/ui/switch"; import { downloadGeojSON } from "~/lib/download-geojson"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import Markdown from "markdown-to-jsx"; -import { createEvent } from "~/models/campaignevents.server"; +import { createEvent, deleteEvent } from "~/models/campaign-events.server"; export const links: LinksFunction = () => { return [ @@ -66,6 +66,9 @@ export async function action(args: ActionArgs) { if (_action === "CREATE_EVENT") { return createCampaignEvent(args); } + if (_action === "DELETE_EVENT") { + return deleteCampaignEvent(args); + } // if (_action === "UPDATE") { // return updateAction(args); // } @@ -184,6 +187,24 @@ async function deleteCommentAction({ request }: ActionArgs) { } } +async function deleteCampaignEvent({ request }: ActionArgs) { + const formData = await request.formData(); + const eventId = formData.get("eventId"); + if (typeof eventId !== "string" || eventId.length === 0) { + return json( + { errors: { eventId: "eventId is required", body: null } }, + { status: 400 } + ); + } + try { + const eventToDelete = await deleteEvent({ id: eventId }); + return json({ ok: true }); + } catch (error) { + console.error(`form not submitted ${error}`); + return json({ error }); + } +} + // export async function action({ request }: ActionArgs) { // // const ownerId = await requireUserId(request); // const formData = await request.formData(); @@ -355,7 +376,7 @@ export default function CampaignId() {

{campaign.description}

- {campaign.events.length === 0 ? ( + {campaign.events.length === 0 && (
{" "}

@@ -372,7 +393,32 @@ export default function CampaignId() {

- ) : null} + )} + {campaign.events.map((e, i) => ( +
+

{e.title}

+

{e.description}

+

{e.startDate}

+

{e.endDate}

+ {userId === e.ownerId && ( +
+
+ + +
+ +
+ )} +
+ ))}

Fragen und Kommentare

From d77cdf4fc716f349b9e791a4165bbf7827ee1f92 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 4 Jul 2023 07:54:22 +0200 Subject: [PATCH 097/299] update events --- app/routes/campaigns/$slug.tsx | 68 +++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 2 deletions(-) diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index cf7a2176..47b65790 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -40,7 +40,11 @@ import { Switch } from "~/components/ui/switch"; import { downloadGeojSON } from "~/lib/download-geojson"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import Markdown from "markdown-to-jsx"; -import { createEvent, deleteEvent } from "~/models/campaign-events.server"; +import { + createEvent, + deleteEvent, + updateEvent, +} from "~/models/campaign-events.server"; export const links: LinksFunction = () => { return [ @@ -69,6 +73,9 @@ export async function action(args: ActionArgs) { if (_action === "DELETE_EVENT") { return deleteCampaignEvent(args); } + if (_action === "UPDATE_EVENT") { + return updateCampaignEvent(args); + } // if (_action === "UPDATE") { // return updateAction(args); // } @@ -205,6 +212,47 @@ async function deleteCampaignEvent({ request }: ActionArgs) { } } +async function updateCampaignEvent({ request }: ActionArgs) { + const formData = await request.formData(); + const eventId = formData.get("eventId"); + if (typeof eventId !== "string" || eventId.length === 0) { + return json( + { errors: { eventId: "eventId is required", body: null } }, + { status: 400 } + ); + } + + const title = formData.get("title"); + if (typeof title !== "string" || title.length === 0) { + return json( + { errors: { title: "title is required", body: null } }, + { status: 400 } + ); + } + const description = formData.get("description"); + if (typeof description !== "string" || description.length === 0) { + return json( + { errors: { description: "description is required", body: null } }, + { status: 400 } + ); + } + const startDate = new Date(); + const endDate = new Date(); + try { + const event = await updateEvent( + eventId, + title, + description, + startDate, + endDate + ); + return json({ ok: true }); + } catch (error) { + console.error(`form not submitted ${error}`); + return json({ error }); + } +} + // export async function action({ request }: ActionArgs) { // // const ownerId = await requireUserId(request); // const formData = await request.formData(); @@ -414,7 +462,23 @@ export default function CampaignId() { DELETE - +
+ + + + + + + +
)}
From 20a3cfa18df367573243eb18215aa622485840c5 Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 4 Jul 2023 08:23:01 +0200 Subject: [PATCH 098/299] move actions --- app/lib/actions.ts | 183 ++++++++++++++++++++++++++ app/routes/campaigns/$slug.tsx | 233 +++++---------------------------- 2 files changed, 214 insertions(+), 202 deletions(-) create mode 100644 app/lib/actions.ts diff --git a/app/lib/actions.ts b/app/lib/actions.ts new file mode 100644 index 00000000..656b9ebb --- /dev/null +++ b/app/lib/actions.ts @@ -0,0 +1,183 @@ +import { ActionArgs, json } from "@remix-run/server-runtime"; +import { + createComment, + deleteComment, + updateComment, +} from "~/models/comment.server"; +import { + createEvent, + deleteEvent, + updateEvent, +} from "~/models/campaign-events.server"; +import { requireUserId } from "~/session.server"; + +export async function updateCommentAction({ request }: ActionArgs) { + const formData = await request.formData(); + const content = formData.get("editComment"); + if (typeof content !== "string" || content.length === 0) { + return json( + { errors: { content: "content is required", body: null } }, + { status: 400 } + ); + } + const commentId = formData.get("commentId"); + if (typeof commentId !== "string" || commentId.length === 0) { + return json( + { errors: { commentId: "commentId is required", body: null } }, + { status: 400 } + ); + } + try { + const comment = await updateComment(commentId, content); + return json({ ok: true }); + } catch (error) { + console.error(`form not submitted ${error}`); + return json({ error }); + } +} + +export async function publishCommentAction({ request, params }: ActionArgs) { + const ownerId = await requireUserId(request); + const formData = await request.formData(); + const content = formData.get("comment"); + if (typeof content !== "string" || content.length === 0) { + return json( + { errors: { content: "content is required", body: null } }, + { status: 400 } + ); + } + const campaignSlug = params.slug; + if (typeof campaignSlug !== "string" || campaignSlug.length === 0) { + return json( + { errors: { campaignSlug: "campaignSlug is required", body: null } }, + { status: 400 } + ); + } + try { + const comment = await createComment({ content, campaignSlug, ownerId }); + return json({ ok: true }); + } catch (error) { + console.error(`form not submitted ${error}`); + return json({ error }); + } +} + +export async function createCampaignEvent({ request, params }: ActionArgs) { + const ownerId = await requireUserId(request); + const formData = await request.formData(); + const title = formData.get("title"); + const description = formData.get("description"); + const startDate = new Date(); + const endDate = new Date(); + + if (typeof title !== "string" || title.length === 0) { + return json( + { errors: { title: "title is required", body: null } }, + { status: 400 } + ); + } + if (typeof description !== "string" || description.length === 0) { + return json( + { errors: { description: "description is required", body: null } }, + { status: 400 } + ); + } + const campaignSlug = params.slug; + if (typeof campaignSlug !== "string" || campaignSlug.length === 0) { + return json( + { errors: { campaignSlug: "campaignSlug is required", body: null } }, + { status: 400 } + ); + } + try { + const event = await createEvent({ + title, + description, + startDate, + endDate, + campaignSlug, + ownerId, + }); + return json({ ok: true }); + } catch (error) { + console.error(`form not submitted ${error}`); + return json({ error }); + } +} + +export async function deleteCommentAction({ request }: ActionArgs) { + const formData = await request.formData(); + const commentId = formData.get("deleteComment"); + if (typeof commentId !== "string" || commentId.length === 0) { + return json( + { errors: { commentId: "commentId is required", body: null } }, + { status: 400 } + ); + } + try { + const commentToDelete = await deleteComment({ id: commentId }); + return json({ ok: true }); + } catch (error) { + console.error(`form not submitted ${error}`); + return json({ error }); + } +} + +export async function deleteCampaignEvent({ request }: ActionArgs) { + const formData = await request.formData(); + const eventId = formData.get("eventId"); + if (typeof eventId !== "string" || eventId.length === 0) { + return json( + { errors: { eventId: "eventId is required", body: null } }, + { status: 400 } + ); + } + try { + const eventToDelete = await deleteEvent({ id: eventId }); + return json({ ok: true }); + } catch (error) { + console.error(`form not submitted ${error}`); + return json({ error }); + } +} + +export async function updateCampaignEvent({ request }: ActionArgs) { + const formData = await request.formData(); + const eventId = formData.get("eventId"); + if (typeof eventId !== "string" || eventId.length === 0) { + return json( + { errors: { eventId: "eventId is required", body: null } }, + { status: 400 } + ); + } + + const title = formData.get("title"); + if (typeof title !== "string" || title.length === 0) { + return json( + { errors: { title: "title is required", body: null } }, + { status: 400 } + ); + } + const description = formData.get("description"); + if (typeof description !== "string" || description.length === 0) { + return json( + { errors: { description: "description is required", body: null } }, + { status: 400 } + ); + } + const startDate = new Date(); + const endDate = new Date(); + try { + const event = await updateEvent( + eventId, + title, + description, + startDate, + endDate + ); + return json({ ok: true }); + } catch (error) { + console.error(`form not submitted ${error}`); + return json({ error }); + } +} diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index 47b65790..e49b2277 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -30,21 +30,20 @@ import { valid } from "geojson-validation"; import ShareLink from "~/components/bottom-bar/share-link"; import { ClientOnly } from "remix-utils"; import { MarkdownEditor } from "~/markdown.client"; -import { - createComment, - deleteComment, - updateComment, -} from "~/models/comment.server"; + import maplibregl from "maplibre-gl/dist/maplibre-gl.css"; import { Switch } from "~/components/ui/switch"; import { downloadGeojSON } from "~/lib/download-geojson"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import Markdown from "markdown-to-jsx"; import { - createEvent, - deleteEvent, - updateEvent, -} from "~/models/campaign-events.server"; + publishCommentAction, + createCampaignEvent, + deleteCampaignEvent, + deleteCommentAction, + updateCampaignEvent, + updateCommentAction, +} from "~/lib/actions"; export const links: LinksFunction = () => { return [ @@ -58,198 +57,24 @@ export const links: LinksFunction = () => { export async function action(args: ActionArgs) { const formData = await args.request.clone().formData(); const _action = formData.get("_action"); - if (_action === "PUBLISH") { - return publishAction(args); - } - if (_action === "DELETE") { - return deleteCommentAction(args); - } - if (_action === "EDIT") { - return updateAction(args); - } - if (_action === "CREATE_EVENT") { - return createCampaignEvent(args); - } - if (_action === "DELETE_EVENT") { - return deleteCampaignEvent(args); - } - if (_action === "UPDATE_EVENT") { - return updateCampaignEvent(args); - } - // if (_action === "UPDATE") { - // return updateAction(args); - // } - throw new Error("Unknown action"); -} -async function updateAction({ request }: ActionArgs) { - const formData = await request.formData(); - const content = formData.get("editComment"); - if (typeof content !== "string" || content.length === 0) { - return json( - { errors: { content: "content is required", body: null } }, - { status: 400 } - ); - } - const commentId = formData.get("commentId"); - if (typeof commentId !== "string" || commentId.length === 0) { - return json( - { errors: { commentId: "commentId is required", body: null } }, - { status: 400 } - ); - } - try { - const comment = await updateComment(commentId, content); - return json({ ok: true }); - } catch (error) { - console.error(`form not submitted ${error}`); - return json({ error }); - } -} - -async function publishAction({ request, params }: ActionArgs) { - const ownerId = await requireUserId(request); - const formData = await request.formData(); - const content = formData.get("comment"); - if (typeof content !== "string" || content.length === 0) { - return json( - { errors: { content: "content is required", body: null } }, - { status: 400 } - ); - } - const campaignSlug = params.slug; - if (typeof campaignSlug !== "string" || campaignSlug.length === 0) { - return json( - { errors: { campaignSlug: "campaignSlug is required", body: null } }, - { status: 400 } - ); - } - try { - const comment = await createComment({ content, campaignSlug, ownerId }); - return json({ ok: true }); - } catch (error) { - console.error(`form not submitted ${error}`); - return json({ error }); - } -} - -async function createCampaignEvent({ request, params }: ActionArgs) { - const ownerId = await requireUserId(request); - const formData = await request.formData(); - const title = formData.get("title"); - const description = formData.get("description"); - const startDate = new Date(); - const endDate = new Date(); - - if (typeof title !== "string" || title.length === 0) { - return json( - { errors: { title: "title is required", body: null } }, - { status: 400 } - ); - } - if (typeof description !== "string" || description.length === 0) { - return json( - { errors: { description: "description is required", body: null } }, - { status: 400 } - ); - } - const campaignSlug = params.slug; - if (typeof campaignSlug !== "string" || campaignSlug.length === 0) { - return json( - { errors: { campaignSlug: "campaignSlug is required", body: null } }, - { status: 400 } - ); - } - try { - const event = await createEvent({ - title, - description, - startDate, - endDate, - campaignSlug, - ownerId, - }); - return json({ ok: true }); - } catch (error) { - console.error(`form not submitted ${error}`); - return json({ error }); - } -} - -async function deleteCommentAction({ request }: ActionArgs) { - const formData = await request.formData(); - const commentId = formData.get("deleteComment"); - if (typeof commentId !== "string" || commentId.length === 0) { - return json( - { errors: { commentId: "commentId is required", body: null } }, - { status: 400 } - ); - } - try { - const commentToDelete = await deleteComment({ id: commentId }); - return json({ ok: true }); - } catch (error) { - console.error(`form not submitted ${error}`); - return json({ error }); - } -} - -async function deleteCampaignEvent({ request }: ActionArgs) { - const formData = await request.formData(); - const eventId = formData.get("eventId"); - if (typeof eventId !== "string" || eventId.length === 0) { - return json( - { errors: { eventId: "eventId is required", body: null } }, - { status: 400 } - ); - } - try { - const eventToDelete = await deleteEvent({ id: eventId }); - return json({ ok: true }); - } catch (error) { - console.error(`form not submitted ${error}`); - return json({ error }); - } -} - -async function updateCampaignEvent({ request }: ActionArgs) { - const formData = await request.formData(); - const eventId = formData.get("eventId"); - if (typeof eventId !== "string" || eventId.length === 0) { - return json( - { errors: { eventId: "eventId is required", body: null } }, - { status: 400 } - ); - } - - const title = formData.get("title"); - if (typeof title !== "string" || title.length === 0) { - return json( - { errors: { title: "title is required", body: null } }, - { status: 400 } - ); - } - const description = formData.get("description"); - if (typeof description !== "string" || description.length === 0) { - return json( - { errors: { description: "description is required", body: null } }, - { status: 400 } - ); - } - const startDate = new Date(); - const endDate = new Date(); - try { - const event = await updateEvent( - eventId, - title, - description, - startDate, - endDate - ); - return json({ ok: true }); - } catch (error) { - console.error(`form not submitted ${error}`); - return json({ error }); + switch (_action) { + case "PUBLISH": + return publishCommentAction(args); + case "DELETE": + return deleteCommentAction(args); + case "EDIT": + return updateCommentAction(args); + case "CREATE_EVENT": + return createCampaignEvent(args); + case "DELETE_EVENT": + return deleteCampaignEvent(args); + case "UPDATE_EVENT": + return updateCampaignEvent(args); + default: + // Handle the case when _action doesn't match any of the above cases + // For example, you can throw an error or return a default action + throw new Error(`Unknown action: ${_action}`); } } @@ -433,8 +258,12 @@ export default function CampaignId() {
- - + + - Karte anzeigen - setShowMap(!showMap)} - /> +
+ Karte anzeigen + setShowMap(!showMap)} + /> +

diff --git a/app/routes/create/form.tsx b/app/routes/create/form.tsx index 658f78a8..3581ec7d 100644 --- a/app/routes/create/form.tsx +++ b/app/routes/create/form.tsx @@ -35,6 +35,8 @@ import { SelectValue, } from "@/components/ui/select"; import { InfoCard } from "~/utils/info-card"; +import { ClientOnly } from "remix-utils"; +import { MarkdownEditor } from "~/markdown.client"; // import h337, { Heatmap } from "heatmap.js"; interface PhenomenaState { @@ -179,8 +181,10 @@ export default function CreateCampaign() { const navigate = useNavigate(); const phenomena = useLoaderData(); const { features } = useContext(FeatureContext); + const textAreaRef = useRef(); + const [description, setDescription] = useState(""); const titleRef = useRef(null); - const descriptionRef = useRef(null); + const descriptionRef = useRef(null); const priorityRef = useRef(null); const requiredParticipantsRef = useRef(null); const requiredSensorsRef = useRef(null); @@ -268,6 +272,7 @@ export default function CreateCampaign() { | RefObject | RefObject | RefObject + | RefObject ) => { if (ref.current) { window.scrollTo({ @@ -433,8 +438,33 @@ export default function CreateCampaign() { ), })} -
+
+ + {() => ( + <> + +
+ + Bild hinzufügen + + + Markdown unterstützt + +
+ + )} +
+ {/* - - - - + +
+ +
+ +
+
+
+ +
+ + + {() => ( + <> + +
+ + Bild hinzufügen + + + Markdown unterstützt + +
+ + )} +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
)} From f55d1562caa8b2debbbe8167f7b8aed1862eb0cc Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 5 Jul 2023 14:16:13 +0200 Subject: [PATCH 102/299] style edit events --- app/routes/campaigns/$slug.tsx | 190 ++++++++++++++++++++++++++------- 1 file changed, 152 insertions(+), 38 deletions(-) diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index 8801cd84..c2f1b94a 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -18,6 +18,12 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "@/components/ui/accordion"; import { useToast } from "~/components/ui/use-toast"; import { useEffect, useRef, useState } from "react"; import { Map } from "~/components/Map"; @@ -30,7 +36,14 @@ import { valid } from "geojson-validation"; import ShareLink from "~/components/bottom-bar/share-link"; import { ClientOnly } from "remix-utils"; import { MarkdownEditor } from "~/markdown.client"; - +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; import maplibregl from "maplibre-gl/dist/maplibre-gl.css"; import { Switch } from "~/components/ui/switch"; import { downloadGeojSON } from "~/lib/download-geojson"; @@ -44,6 +57,7 @@ import { updateCampaignEvent, updateCommentAction, } from "~/lib/actions"; +import { TrashIcon, EditIcon } from "lucide-react"; export const links: LinksFunction = () => { return [ @@ -118,6 +132,15 @@ export default function CampaignId() { const data = useLoaderData(); const campaign = data.campaign; const userId = data.userId; + const [eventEditMode, setEventEditMode] = useState(false); + const [editEventTitle, setEditEventTitle] = useState(""); + const [editEventDescription, setEditEventDescription] = useState< + string | undefined + >(""); + const [editEventStartDate, setEditEventStartDate] = useState< + Date | undefined + >(); + const [editEventEndDate, setEditEventEndDate] = useState(); const [comment, setComment] = useState(""); const [editComment, setEditComment] = useState(""); const [editCommentId, setEditCommentId] = useState(""); @@ -358,45 +381,136 @@ export default function CampaignId() {
)} {campaign.events.map((e, i) => ( -
-

{e.title}

-

{e.description}

-

{e.startDate}

-

{e.endDate}

- {userId === e.ownerId && ( -
-
- - -
-
- - - - - + + + +
+
+ + + + Beschreibung: + {eventEditMode ? ( + + {() => ( + <> + +
+ + Bild hinzufügen + + + Markdown unterstützt + +
+ + )} +
+ ) : ( + {e.description} + )} + Beginn: + {eventEditMode ? ( + setEditEventStartDate} + /> + ) : ( +

{e.startDate}

+ )} + Abschluss: +

{e.endDate}

+
+ {/* {userId === e.ownerId && ( */} + +
+ + + + + - -
-
- )} -

+ + + + {/* )} */} + ))} From d5af872623cac282f26a2d87763d59240b30ba8c Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 5 Jul 2023 15:56:39 +0200 Subject: [PATCH 103/299] style comments --- app/models/campaign.server.ts | 9 +- app/routes/campaigns/$slug.tsx | 367 ++++++++++++++++++--------------- 2 files changed, 214 insertions(+), 162 deletions(-) diff --git a/app/models/campaign.server.ts b/app/models/campaign.server.ts index 77d4bc2c..ac7d4103 100644 --- a/app/models/campaign.server.ts +++ b/app/models/campaign.server.ts @@ -5,7 +5,14 @@ import { prisma } from "~/db.server"; export function getCampaign({ slug }: Pick) { return prisma.campaign.findFirst({ where: { slug }, - include: { comments: true, events: true }, + include: { + comments: { + include: { + owner: true, + }, + }, + events: true, + }, }); } diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index c2f1b94a..45513083 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -18,12 +18,7 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from "@/components/ui/accordion"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { useToast } from "~/components/ui/use-toast"; import { useEffect, useRef, useState } from "react"; import { Map } from "~/components/Map"; @@ -58,6 +53,7 @@ import { updateCommentAction, } from "~/lib/actions"; import { TrashIcon, EditIcon } from "lucide-react"; +import { useNavigate } from "@remix-run/react"; export const links: LinksFunction = () => { return [ @@ -130,8 +126,10 @@ const layer: LayerProps = { export default function CampaignId() { const data = useLoaderData(); + const navigate = useNavigate(); const campaign = data.campaign; const userId = data.userId; + const [commentEditMode, setCommentEditMode] = useState(false); const [eventEditMode, setEventEditMode] = useState(false); const [editEventTitle, setEditEventTitle] = useState(""); const [editEventDescription, setEditEventDescription] = useState< @@ -263,19 +261,22 @@ export default function CampaignId() {
- - - {/* +
+ + + {/* {t("live_label")} */} - - - - - - - - - + + + + + + + + + +
+ {/*
*/}

Beschreibung

{campaign.description} @@ -381,29 +382,94 @@ export default function CampaignId() {
)} {campaign.events.map((e, i) => ( - - - -
- {eventEditMode ? ( - setEditEventTitle(e.target.value)} - placeholder="Enter new title" - /> - ) : ( -

{e.title}

+
+ + + +
+ {eventEditMode ? ( + setEditEventTitle(e.target.value)} + placeholder="Enter new title" + /> + ) : ( +

{e.title}

+ )} +
+ {userId === e.ownerId && ( +
+ +
+ + +
+
)} -
-
- -
+ + + + Beschreibung: + {eventEditMode ? ( + + {() => ( + <> + +
+ + Bild hinzufügen + + + Markdown unterstützt + +
+ + )} +
+ ) : ( + {e.description} + )} + Beginn: + {eventEditMode ? ( + setEditEventStartDate} + /> + ) : ( +

{e.startDate}

+ )} + Abschluss: +

{e.endDate}

+
+ {userId === e.ownerId && eventEditMode && ( + + + + + + + -
- - - - Beschreibung: - {eventEditMode ? ( - - {() => ( - <> - -
- - Bild hinzufügen - - - Markdown unterstützt - -
- - )} -
- ) : ( - {e.description} - )} - Beginn: - {eventEditMode ? ( - setEditEventStartDate} - /> - ) : ( -

{e.startDate}

+ )} - Abschluss: -

{e.endDate}

-
- {/* {userId === e.ownerId && ( */} - -
- - - - - - - -
-
- {/* )} */} - + +
))}

Fragen und Kommentare

{campaign.comments.map((c: any, i: number) => { return ( -
- {userId === campaign.ownerId && ( - <> -
- - -
- - {editCommentId === c.id && ( +
+ + + + {userId === c.ownerId && ( +
+ +
+ + +
+
+ )} +
+
+ +
+ + + CN + + {c.owner.name} +
+ {commentEditMode ? ( {() => (
@@ -573,6 +607,7 @@ export default function CampaignId() { name="_action" value="EDIT" type="submit" + className="float-right" > Veröffentlichen @@ -580,10 +615,11 @@ export default function CampaignId() {
)}
+ ) : ( + {c.content} )} - - )} - {c.content}; +
+
); })} @@ -611,7 +647,16 @@ export default function CampaignId() { name="comment" id="comment" > - @@ -620,8 +665,8 @@ export default function CampaignId() { )} + {/*
*/}
-
{showMap && ( From 25f82f49a39804c84efa9af18d266aaa0eda9938 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 5 Jul 2023 16:31:50 +0200 Subject: [PATCH 104/299] refactor events tab --- .../campaignId/event-tab/create-form.tsx | 108 ++++++++ .../campaignId/event-tab/event-cards.tsx | 181 ++++++++++++ app/routes/campaigns/$slug.tsx | 258 ++---------------- 3 files changed, 310 insertions(+), 237 deletions(-) create mode 100644 app/components/campaigns/campaignId/event-tab/create-form.tsx create mode 100644 app/components/campaigns/campaignId/event-tab/event-cards.tsx diff --git a/app/components/campaigns/campaignId/event-tab/create-form.tsx b/app/components/campaigns/campaignId/event-tab/create-form.tsx new file mode 100644 index 00000000..1e3a0764 --- /dev/null +++ b/app/components/campaigns/campaignId/event-tab/create-form.tsx @@ -0,0 +1,108 @@ +import { Form } from "@remix-run/react"; +import { ClientOnly } from "remix-utils"; +import { MarkdownEditor } from "~/markdown.client"; + +type EventFormProps = { + eventDescription: string; + setEventDescription: any; + eventTextAreaRef: any; +}; + +export default function EventForm({ + eventDescription, + setEventDescription, + eventTextAreaRef, +}: EventFormProps) { + return ( +
+ {" "} +

Noch keine Events für diese Kampagne. Erstelle ein Event:

+
+
+ +
+ +
+
+
+ +
+ + + {() => ( + <> + +
+ + Bild hinzufügen + + + Markdown unterstützt + +
+ + )} +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
+
+ ); +} diff --git a/app/components/campaigns/campaignId/event-tab/event-cards.tsx b/app/components/campaigns/campaignId/event-tab/event-cards.tsx new file mode 100644 index 00000000..2df46653 --- /dev/null +++ b/app/components/campaigns/campaignId/event-tab/event-cards.tsx @@ -0,0 +1,181 @@ +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Button } from "~/components/ui/button"; +import { EditIcon, TrashIcon } from "lucide-react"; +import { Form } from "@remix-run/react"; +import { ClientOnly } from "remix-utils"; +import { MarkdownEditor } from "~/markdown.client"; +import Markdown from "markdown-to-jsx"; + +type EventCardsProps = { + events: any[]; + eventEditMode: boolean; + setEventEditMode: any; + setEditEventTitle: any; + userId: string; + eventTextAreaRef: any; + editEventDescription: string; + setEditEventDescription: any; + editEventTitle: string; + setEditEventStartDate: any; +}; + +export default function EventCards({ + events, + eventEditMode, + editEventTitle, + setEditEventStartDate, + editEventDescription, + setEditEventDescription, + eventTextAreaRef, + setEventEditMode, + setEditEventTitle, + userId, +}: EventCardsProps) { + return ( +
+ {events.map((e: any, i: number) => ( +
+ + + +
+ {eventEditMode ? ( + setEditEventTitle(e.target.value)} + placeholder="Enter new title" + /> + ) : ( +

{e.title}

+ )} +
+ {userId === e.ownerId && ( +
+ +
+ + +
+
+ )} +
+
+ + Beschreibung: + {eventEditMode ? ( + + {() => ( + <> + +
+ + Bild hinzufügen + + + Markdown unterstützt + +
+ + )} +
+ ) : ( + {e.description} + )} + Beginn: + {eventEditMode ? ( + setEditEventStartDate} + /> + ) : ( +

{e.startDate}

+ )} + Abschluss: +

{e.endDate}

+
+ {userId === e.ownerId && eventEditMode && ( + +
+ + + + + + + +
+
+ )} +
+
+ ))} +
+ ); +} diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index 45513083..7f0b4012 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -54,6 +54,8 @@ import { } from "~/lib/actions"; import { TrashIcon, EditIcon } from "lucide-react"; import { useNavigate } from "@remix-run/react"; +import EventForm from "~/components/campaigns/campaignId/event-tab/create-form"; +import EventCards from "~/components/campaigns/campaignId/event-tab/event-cards"; export const links: LinksFunction = () => { return [ @@ -282,244 +284,26 @@ export default function CampaignId() { {campaign.description} - {campaign.events.length === 0 && ( -
- {" "} -

- Noch keine Events für diese Kampagne. Erstelle ein Event:{" "} -

-
-
- -
- -
-
-
- -
- - - {() => ( - <> - -
- - Bild hinzufügen - - - Markdown unterstützt - -
- - )} -
-
-
-
- -
- -
-
-
- -
- -
-
-
- -
-
-
+ {campaign.events.length === 0 ? ( + + ) : ( + )} - {campaign.events.map((e, i) => ( -
- - - -
- {eventEditMode ? ( - setEditEventTitle(e.target.value)} - placeholder="Enter new title" - /> - ) : ( -

{e.title}

- )} -
- {userId === e.ownerId && ( -
- -
- - -
-
- )} -
-
- - Beschreibung: - {eventEditMode ? ( - - {() => ( - <> - -
- - Bild hinzufügen - - - Markdown unterstützt - -
- - )} -
- ) : ( - {e.description} - )} - Beginn: - {eventEditMode ? ( - setEditEventStartDate} - /> - ) : ( -

{e.startDate}

- )} - Abschluss: -

{e.endDate}

-
- {userId === e.ownerId && eventEditMode && ( - -
- - - - - - - -
-
- )} -
-
- ))}

Fragen und Kommentare

From 4033f6baf5f1343607b28c58f2594d058bbfd877 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 5 Jul 2023 16:51:33 +0200 Subject: [PATCH 105/299] refactor comments tab --- .../campaignId/comment-tab/comment-cards.tsx | 139 +++++++++++++++ .../campaignId/comment-tab/comment-input.tsx | 61 +++++++ app/routes/campaigns/$slug.tsx | 168 ++---------------- 3 files changed, 219 insertions(+), 149 deletions(-) create mode 100644 app/components/campaigns/campaignId/comment-tab/comment-cards.tsx create mode 100644 app/components/campaigns/campaignId/comment-tab/comment-input.tsx diff --git a/app/components/campaigns/campaignId/comment-tab/comment-cards.tsx b/app/components/campaigns/campaignId/comment-tab/comment-cards.tsx new file mode 100644 index 00000000..1a9c0790 --- /dev/null +++ b/app/components/campaigns/campaignId/comment-tab/comment-cards.tsx @@ -0,0 +1,139 @@ +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { Form } from "@remix-run/react"; +import { Button } from "~/components/ui/button"; +import { TrashIcon, EditIcon } from "lucide-react"; +import { ClientOnly } from "remix-utils"; +import { MarkdownEditor } from "~/markdown.client"; +import Markdown from "markdown-to-jsx"; + +type CommentCardsProps = { + comments: any[]; + userId: string; + setCommentEditMode: any; + setEditCommentId: any; + setEditComment: any; + commentEditMode: boolean; + textAreaRef: any; + editComment: string; +}; + +export default function CommentCards({ + comments, + userId, + setCommentEditMode, + setEditComment, + setEditCommentId, + textAreaRef, + commentEditMode, + editComment, +}: CommentCardsProps) { + return ( +
+ {comments.map((c: any, i: number) => { + return ( +
+ + + + {userId === c.ownerId && ( +
+ +
+ + +
+
+ )} +
+
+ +
+ + + CN + + {c.owner.name} +
+ {commentEditMode ? ( + + {() => ( +
+ +
+ + Bild hinzufügen + + + Markdown unterstützt + +
+
+ + + +
+
+ )} +
+ ) : ( + {c.content} + )} +
+
+
+ ); + })} +
+ ); +} diff --git a/app/components/campaigns/campaignId/comment-tab/comment-input.tsx b/app/components/campaigns/campaignId/comment-tab/comment-input.tsx new file mode 100644 index 00000000..02ce5c42 --- /dev/null +++ b/app/components/campaigns/campaignId/comment-tab/comment-input.tsx @@ -0,0 +1,61 @@ +import { Form } from "@remix-run/react"; +import { ClientOnly } from "remix-utils"; +import { Button } from "~/components/ui/button"; +import { MarkdownEditor } from "~/markdown.client"; +import { useNavigate } from "@remix-run/react"; + +type CommentInputProps = { + textAreaRef: any; + comment: string | undefined; + setComment: (comment: string | undefined) => void; + setCommentEditMode: (editMode: boolean) => void; +}; + +export default function CommentInput({ + textAreaRef, + comment, + setComment, + setCommentEditMode, +}: CommentInputProps) { + const navigate = useNavigate(); + + return ( + + {() => ( +
+ +
+ Bild hinzufügen + + Markdown unterstützt + +
+
+ + +
+
+ )} +
+ ); +} diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index 7f0b4012..d34a914a 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -18,7 +18,6 @@ import { DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; -import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { useToast } from "~/components/ui/use-toast"; import { useEffect, useRef, useState } from "react"; import { Map } from "~/components/Map"; @@ -31,14 +30,7 @@ import { valid } from "geojson-validation"; import ShareLink from "~/components/bottom-bar/share-link"; import { ClientOnly } from "remix-utils"; import { MarkdownEditor } from "~/markdown.client"; -import { - Card, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@/components/ui/card"; + import maplibregl from "maplibre-gl/dist/maplibre-gl.css"; import { Switch } from "~/components/ui/switch"; import { downloadGeojSON } from "~/lib/download-geojson"; @@ -52,10 +44,11 @@ import { updateCampaignEvent, updateCommentAction, } from "~/lib/actions"; -import { TrashIcon, EditIcon } from "lucide-react"; import { useNavigate } from "@remix-run/react"; import EventForm from "~/components/campaigns/campaignId/event-tab/create-form"; import EventCards from "~/components/campaigns/campaignId/event-tab/event-cards"; +import CommentInput from "~/components/campaigns/campaignId/comment-tab/comment-input"; +import CommentCards from "~/components/campaigns/campaignId/comment-tab/comment-cards"; export const links: LinksFunction = () => { return [ @@ -307,146 +300,23 @@ export default function CampaignId() {

Fragen und Kommentare

- {campaign.comments.map((c: any, i: number) => { - return ( -
- - - - {userId === c.ownerId && ( -
- -
- - -
-
- )} -
-
- -
- - - CN - - {c.owner.name} -
- {commentEditMode ? ( - - {() => ( -
- -
- - Bild hinzufügen - - - Markdown unterstützt - -
-
- - - -
-
- )} -
- ) : ( - {c.content} - )} -
-
-
- ); - })} + {!editComment && ( - - {() => ( -
- -
- - Bild hinzufügen - - - Markdown unterstützt - -
-
- - -
-
- )} -
+ )}
{/*
*/} From d4db9e0b55b61f1d7e8cfccb4630dcf1a7b9f731 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 5 Jul 2023 16:53:40 +0200 Subject: [PATCH 106/299] fix imports --- app/routes/campaigns/$slug.tsx | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index d34a914a..5c882ab5 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -8,7 +8,7 @@ import { json } from "@remix-run/node"; import { Form, useCatch, useLoaderData } from "@remix-run/react"; import { Button } from "~/components/ui/button"; import { getCampaign } from "~/models/campaign.server"; -import { getUserId, requireUserId } from "~/session.server"; +import { requireUserId } from "~/session.server"; import { Dialog, DialogContent, @@ -19,17 +19,13 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { useToast } from "~/components/ui/use-toast"; -import { useEffect, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { Map } from "~/components/Map"; -import { LayerProps, MapProvider, Source, Layer } from "react-map-gl"; -import { campaignSchema } from "~/lib/validations/campaign"; -import { Feature } from "geojson"; +import type { LayerProps } from "react-map-gl"; +import { MapProvider, Source, Layer } from "react-map-gl"; import { ClockIcon } from "lucide-react"; import clsx from "clsx"; -import { valid } from "geojson-validation"; import ShareLink from "~/components/bottom-bar/share-link"; -import { ClientOnly } from "remix-utils"; -import { MarkdownEditor } from "~/markdown.client"; import maplibregl from "maplibre-gl/dist/maplibre-gl.css"; import { Switch } from "~/components/ui/switch"; @@ -44,7 +40,6 @@ import { updateCampaignEvent, updateCommentAction, } from "~/lib/actions"; -import { useNavigate } from "@remix-run/react"; import EventForm from "~/components/campaigns/campaignId/event-tab/create-form"; import EventCards from "~/components/campaigns/campaignId/event-tab/event-cards"; import CommentInput from "~/components/campaigns/campaignId/comment-tab/comment-input"; @@ -121,7 +116,6 @@ const layer: LayerProps = { export default function CampaignId() { const data = useLoaderData(); - const navigate = useNavigate(); const campaign = data.campaign; const userId = data.userId; const [commentEditMode, setCommentEditMode] = useState(false); From 796be8d891e9095de9205dcd3befdd84b0c1754b Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 8 Jul 2023 09:09:12 +0200 Subject: [PATCH 107/299] still show map when scrolling --- app/routes/campaigns/overview.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/app/routes/campaigns/overview.tsx b/app/routes/campaigns/overview.tsx index b7b83c22..6613c9fb 100644 --- a/app/routes/campaigns/overview.tsx +++ b/app/routes/campaigns/overview.tsx @@ -589,6 +589,7 @@ export default function Campaigns() {
)} +
{data.length === 0 ? (
Zurzeit gibt es noch keine Kampagnen. Klicke{" "} @@ -706,12 +707,12 @@ export default function Campaigns() { onZoom={(e) => setZoom(Math.floor(e.viewState.zoom))} ref={mapRef} style={{ - height: "60vh", + height: "calc(100vh - 190px)", width: "40vw", - position: "fixed", + position: "sticky", bottom: "10px", // marginTop: "2rem", - right: "10px", + top: 0, }} > {clusters.map((cluster) => { From 292df7452e6ff601b9191d97cebf0e6ee139c5b3 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 8 Jul 2023 09:09:59 +0200 Subject: [PATCH 108/299] translations --- app/routes/campaigns/$slug.tsx | 9 ++++----- app/routes/create/form.tsx | 28 ++++++++++++++++------------ public/locales/de/campaign-form.json | 12 ++++++++++++ public/locales/en/campaign-form.json | 12 ++++++++++++ 4 files changed, 44 insertions(+), 17 deletions(-) create mode 100644 public/locales/de/campaign-form.json create mode 100644 public/locales/en/campaign-form.json diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index 5c882ab5..6423b573 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -231,7 +231,7 @@ export default function CampaignId() {
-

+

{campaign.title}

{/*
*/} -

Beschreibung

+

Beschreibung

{campaign.description}
@@ -293,7 +293,6 @@ export default function CampaignId() { )} -

Fragen und Kommentare

- {!editComment && ( + {/* {!editComment && ( - )} + )} */}
{/*
*/} diff --git a/app/routes/create/form.tsx b/app/routes/create/form.tsx index a822a195..6f2f6c8e 100644 --- a/app/routes/create/form.tsx +++ b/app/routes/create/form.tsx @@ -24,6 +24,7 @@ import { campaignSchema } from "~/lib/validations/campaign"; import { Switch } from "@/components/ui/switch"; import { useToast } from "~/components/ui/use-toast"; import { useNavigate } from "@remix-run/react"; +import { useTranslation } from "react-i18next"; import { reverseGeocode } from "~/components/Map/geocoder-control"; import { Select, @@ -195,6 +196,8 @@ export default function CreateCampaign() { const exposureRef = useRef(null); const hardwareAvailableRef = useRef(null); + const { t } = useTranslation("campaign-form"); + // const [container, setContainer] = useState( // undefined // ); @@ -395,7 +398,7 @@ export default function CreateCampaign() {
@@ -431,7 +434,7 @@ export default function CreateCampaign() {
-
+
- Abschluss + {t("endDate")}
- Einsatzgebiet + {t("exposure")}
- Nein + No - Ja + Yes
{/* */} diff --git a/public/locales/de/campaign-form.json b/public/locales/de/campaign-form.json new file mode 100644 index 00000000..bce1a7b2 --- /dev/null +++ b/public/locales/de/campaign-form.json @@ -0,0 +1,12 @@ +{ + "title": "Titel", + "description": "Beschreibung", + "priority": "Priorität", + "participants": "Mindestanzahl Teilnehmer", + "sensors": "Gewünschte Anzahl Sensoren", + "startDate": "Beginn", + "endDate": "Abschluss", + "phenomena": "Phänomene", + "exposure": "Einsatzgebiet", + "hardware_available": "Hardware verfügbar" +} diff --git a/public/locales/en/campaign-form.json b/public/locales/en/campaign-form.json new file mode 100644 index 00000000..64561973 --- /dev/null +++ b/public/locales/en/campaign-form.json @@ -0,0 +1,12 @@ +{ + "title": "Title", + "description": "Description", + "priority": "Priority", + "participants": "Required number of participants", + "sensors": "Required number of sensors", + "startDate": "Start Date", + "endDate": "End Date", + "phenomena": "Phenomena", + "exposure": "Exposure", + "hardware_available": "Hardware available" +} From 2c03fc5cd9f9a27848cb58fbdbb11f27c9244f11 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 8 Jul 2023 09:54:57 +0200 Subject: [PATCH 109/299] add participants to campaign --- app/lib/actions.ts | 28 +++++++++++++++++++ app/models/campaign.server.ts | 27 ++++++++++++++---- app/routes/campaigns/$slug.tsx | 15 ++++++++-- .../20230708072353_participants/migration.sql | 26 +++++++++++++++++ prisma/schema.prisma | 17 +++++------ 5 files changed, 98 insertions(+), 15 deletions(-) create mode 100644 prisma/migrations/20230708072353_participants/migration.sql diff --git a/app/lib/actions.ts b/app/lib/actions.ts index 656b9ebb..26e9af77 100644 --- a/app/lib/actions.ts +++ b/app/lib/actions.ts @@ -10,6 +10,34 @@ import { updateEvent, } from "~/models/campaign-events.server"; import { requireUserId } from "~/session.server"; +import { updateCampaign } from "~/models/campaign.server"; + +export async function participate({ request }: ActionArgs) { + const ownerId = await requireUserId(request); + const formData = await request.formData(); + const campaignId = formData.get("campaignId"); + if (typeof campaignId !== "string" || campaignId.length === 0) { + return json( + { errors: { campaignId: "campaignId is required", body: null } }, + { status: 400 } + ); + } + // const email = formData.get("email"); + // const hardware = formData.get("hardware"); + // if (typeof email !== "string" || email.length === 0) { + // return json( + // { errors: { email: "email is required", body: null } }, + // { status: 400 } + // ); + // } + try { + const updated = await updateCampaign(campaignId, ownerId); + return json({ ok: true }); + } catch (error) { + console.error(`form not submitted ${error}`); + return json({ error }); + } +} export async function updateCommentAction({ request }: ActionArgs) { const formData = await request.formData(); diff --git a/app/models/campaign.server.ts b/app/models/campaign.server.ts index ac7d4103..090986a4 100644 --- a/app/models/campaign.server.ts +++ b/app/models/campaign.server.ts @@ -80,7 +80,7 @@ export async function createCampaign({ feature: feature === null ? {} : feature, description, priority, - participantCount: 0, + // participantCount: 0, country, requiredSensors, requiredParticipants, @@ -101,13 +101,30 @@ export async function createCampaign({ }); } +// export async function updateCampaign( +// id: string, +// options: Prisma.CampaignUpdateInput +// ) { +// return prisma.campaign.update({ +// where: { id }, +// data: options, +// }); +// } + export async function updateCampaign( - id: string, - options: Prisma.CampaignUpdateInput + campaignId: string, + participantId: string ) { return prisma.campaign.update({ - where: { id }, - data: options, + where: { + id: campaignId, + }, + data: { + participants: { + connect: { id: participantId }, + }, + updatedAt: new Date(), + }, }); } diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index 6423b573..bdd15b1f 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -26,7 +26,7 @@ import { MapProvider, Source, Layer } from "react-map-gl"; import { ClockIcon } from "lucide-react"; import clsx from "clsx"; import ShareLink from "~/components/bottom-bar/share-link"; - +import { updateCampaign } from "~/models/campaign.server"; import maplibregl from "maplibre-gl/dist/maplibre-gl.css"; import { Switch } from "~/components/ui/switch"; import { downloadGeojSON } from "~/lib/download-geojson"; @@ -39,6 +39,7 @@ import { deleteCommentAction, updateCampaignEvent, updateCommentAction, + participate, } from "~/lib/actions"; import EventForm from "~/components/campaigns/campaignId/event-tab/create-form"; import EventCards from "~/components/campaigns/campaignId/event-tab/event-cards"; @@ -71,6 +72,8 @@ export async function action(args: ActionArgs) { return deleteCampaignEvent(args); case "UPDATE_EVENT": return updateCampaignEvent(args); + case "PARTICIPATE": + return participate(args); default: // Handle the case when _action doesn't match any of the above cases // For example, you can throw an error or return a default action @@ -172,6 +175,12 @@ export default function CampaignId() {
+
- +
diff --git a/prisma/migrations/20230708072353_participants/migration.sql b/prisma/migrations/20230708072353_participants/migration.sql new file mode 100644 index 00000000..c7eff8cd --- /dev/null +++ b/prisma/migrations/20230708072353_participants/migration.sql @@ -0,0 +1,26 @@ +/* + Warnings: + + - You are about to drop the column `participantCount` on the `Campaign` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Campaign" DROP COLUMN "participantCount"; + +-- CreateTable +CREATE TABLE "_CampaignParticipant" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_CampaignParticipant_AB_unique" ON "_CampaignParticipant"("A", "B"); + +-- CreateIndex +CREATE INDEX "_CampaignParticipant_B_index" ON "_CampaignParticipant"("B"); + +-- AddForeignKey +ALTER TABLE "_CampaignParticipant" ADD CONSTRAINT "_CampaignParticipant_A_fkey" FOREIGN KEY ("A") REFERENCES "Campaign"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_CampaignParticipant" ADD CONSTRAINT "_CampaignParticipant_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 27d05036..b3444917 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -23,12 +23,13 @@ model User { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt - password Password? - notes Note[] - devices Device[] - campaigns Campaign[] - comments Comment[] - events CampaignEvent[] + password Password? + notes Note[] + devices Device[] + ownedCampaigns Campaign[] @relation("OwnedCampaigns") + participatingCampaigns Campaign[] @relation("CampaignParticipant") + comments Comment[] + events CampaignEvent[] profile Profile? } @@ -129,12 +130,12 @@ model Campaign { title String slug String @unique feature Json - owner User @relation(fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade) + owner User @relation("OwnedCampaigns", fields: [ownerId], references: [id], onDelete: Cascade, onUpdate: Cascade) ownerId String description String priority Priority country String? - participantCount Int + participants User[] @relation("CampaignParticipant") requiredParticipants Int? requiredSensors Int? createdAt DateTime From dd997395e23f17e3b5109c2f0bd8c406105cff22 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 8 Jul 2023 11:44:27 +0200 Subject: [PATCH 110/299] mention participants --- .../campaignId/comment-tab/comment-input.tsx | 26 +++++++++++++++- app/models/campaign.server.ts | 1 + app/routes/campaigns/$slug.tsx | 31 ++++++++++++++++--- package.json | 1 + yarn.lock | 5 +++ 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/app/components/campaigns/campaignId/comment-tab/comment-input.tsx b/app/components/campaigns/campaignId/comment-tab/comment-input.tsx index 02ce5c42..eb17e2e8 100644 --- a/app/components/campaigns/campaignId/comment-tab/comment-input.tsx +++ b/app/components/campaigns/campaignId/comment-tab/comment-input.tsx @@ -3,11 +3,19 @@ import { ClientOnly } from "remix-utils"; import { Button } from "~/components/ui/button"; import { MarkdownEditor } from "~/markdown.client"; import { useNavigate } from "@remix-run/react"; +// import Tribute from "tributejs"; +// import tributeStyles from "tributejs/tribute.css"; +// import type { LinksFunction } from "@remix-run/node"; +// import { useEffect } from "react"; + +// export const links: LinksFunction = () => { +// return [{ rel: "stylesheet", href: tributeStyles }]; +// }; type CommentInputProps = { textAreaRef: any; comment: string | undefined; - setComment: (comment: string | undefined) => void; + setComment: any; setCommentEditMode: (editMode: boolean) => void; }; @@ -19,6 +27,21 @@ export default function CommentInput({ }: CommentInputProps) { const navigate = useNavigate(); + // useEffect(() => { + // if (textAreaRef.current) { + // var tribute = new Tribute({ + // trigger: "@", + // values: [ + // { key: "Phil Heartman", value: "pheartman" }, + // { key: "Gordon Ramsey", value: "gramsey" }, + // ], + // itemClass: "bg-blue-700 text-black", + // }); + // tribute.attach(textAreaRef.current.textarea); + // } + // // eslint-disable-next-line react-hooks/exhaustive-deps + // }, [textAreaRef.current]); + return ( {() => ( @@ -38,6 +61,7 @@ export default function CommentInput({ diff --git a/app/models/campaign.server.ts b/app/models/campaign.server.ts index 090986a4..a579d830 100644 --- a/app/models/campaign.server.ts +++ b/app/models/campaign.server.ts @@ -12,6 +12,7 @@ export function getCampaign({ slug }: Pick) { }, }, events: true, + participants: true, }, }); } diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index bdd15b1f..7e536ae4 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -19,7 +19,7 @@ import { DialogTrigger, } from "@/components/ui/dialog"; import { useToast } from "~/components/ui/use-toast"; -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { Map } from "~/components/Map"; import type { LayerProps } from "react-map-gl"; import { MapProvider, Source, Layer } from "react-map-gl"; @@ -45,6 +45,8 @@ import EventForm from "~/components/campaigns/campaignId/event-tab/create-form"; import EventCards from "~/components/campaigns/campaignId/event-tab/event-cards"; import CommentInput from "~/components/campaigns/campaignId/comment-tab/comment-input"; import CommentCards from "~/components/campaigns/campaignId/comment-tab/comment-cards"; +import Tribute from "tributejs"; +import tributeStyles from "tributejs/tribute.css"; export const links: LinksFunction = () => { return [ @@ -52,6 +54,10 @@ export const links: LinksFunction = () => { rel: "stylesheet", href: maplibregl, }, + { + rel: "stylesheet", + href: tributeStyles, + }, ]; }; @@ -120,6 +126,9 @@ const layer: LayerProps = { export default function CampaignId() { const data = useLoaderData(); const campaign = data.campaign; + const participants = campaign.participants.map(function (participant) { + return { key: participant.name, value: participant.name }; + }); const userId = data.userId; const [commentEditMode, setCommentEditMode] = useState(false); const [eventEditMode, setEventEditMode] = useState(false); @@ -142,10 +151,22 @@ export default function CampaignId() { const [tabView, setTabView] = useState<"overview" | "calendar" | "comments">( "overview" ); - const textAreaRef = useRef(); + const textAreaRef = useRef(null); const eventTextAreaRef = useRef(); const { toast } = useToast(); - const participate = () => {}; + + useEffect(() => { + if (textAreaRef.current) { + var tribute = new Tribute({ + trigger: "@", + values: participants, + itemClass: "bg-blue-700 text-black", + }); + //@ts-ignore + tribute.attach(textAreaRef.current.textarea); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [textAreaRef.current]); // useEffect(() => { // toast({ // title: "HELLO", @@ -314,14 +335,14 @@ export default function CampaignId() { textAreaRef={textAreaRef} userId={userId} /> - {/* {!editComment && ( + {!editComment && ( - )} */} + )} {/*
*/} diff --git a/package.json b/package.json index 8dfbb030..4a8db056 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "tailwind-merge": "^1.13.2", "tailwindcss-animate": "^1.0.6", "tiny-invariant": "^1.3.1", + "tributejs": "^5.1.3", "use-supercluster": "^0.4.0", "zod": "^3.21.4" }, diff --git a/yarn.lock b/yarn.lock index 2563f9f8..d070b338 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13357,6 +13357,11 @@ traverse@~0.6.6: resolved "https://registry.npmjs.org/traverse/-/traverse-0.6.7.tgz" integrity sha512-/y956gpUo9ZNCb99YjxG7OaslxZWHfCHAUUfshwqOXmxUIvqLjVO581BT+gM59+QV9tFe6/CGG53tsA1Y7RSdg== +tributejs@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/tributejs/-/tributejs-5.1.3.tgz#980600fc72865be5868893078b4bfde721129eae" + integrity sha512-B5CXihaVzXw+1UHhNFyAwUTMDk1EfoLP5Tj1VhD9yybZ1I8DZJEv8tZ1l0RJo0t0tk9ZhR8eG5tEsaCvRigmdQ== + trim-lines@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" From 50dd82a134bc69d30d587208664e6f5ff18512be Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 10 Jul 2023 11:50:07 +0200 Subject: [PATCH 111/299] style slug page and overview-table --- .../overview-tab/overview-table.tsx | 67 ++++++ app/components/ui/table.tsx | 114 +++++++++ app/routes/campaigns/$slug.tsx | 225 +++++++++--------- package.json | 1 - 4 files changed, 295 insertions(+), 112 deletions(-) create mode 100644 app/components/campaigns/campaignId/overview-tab/overview-table.tsx create mode 100644 app/components/ui/table.tsx diff --git a/app/components/campaigns/campaignId/overview-tab/overview-table.tsx b/app/components/campaigns/campaignId/overview-tab/overview-table.tsx new file mode 100644 index 00000000..242a2b1a --- /dev/null +++ b/app/components/campaigns/campaignId/overview-tab/overview-table.tsx @@ -0,0 +1,67 @@ +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import type { Campaign } from "@prisma/client"; +import Markdown from "markdown-to-jsx"; + +type OverviewTableProps = { + campaign: Campaign; +}; + +export default function OverviewTable({ campaign }: OverviewTableProps) { + return ( + + Campaign Overview + + + Attribut + Wert + + + + + Beschreibung + + {campaign.description} + + + + Priorität + {campaign.priority} + + + Teilnehmer + + {campaign.participants.length} / {campaign.requiredParticipants} + + + + Erstellt am + {JSON.stringify(campaign.createdAt)} + + + Bearbeitet am + {JSON.stringify(campaign.updatedAt)} + + + Phänomene + {campaign.phenomena} + + + Exposure + {campaign.exposure} + + + Hardware verfügbar + {campaign.hardware_available ? "Ja" : "Nein"} + + +
+ ); +} diff --git a/app/components/ui/table.tsx b/app/components/ui/table.tsx new file mode 100644 index 00000000..4246a579 --- /dev/null +++ b/app/components/ui/table.tsx @@ -0,0 +1,114 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/app/routes/campaigns/$slug.tsx b/app/routes/campaigns/$slug.tsx index 7e536ae4..09bcb929 100644 --- a/app/routes/campaigns/$slug.tsx +++ b/app/routes/campaigns/$slug.tsx @@ -41,12 +41,14 @@ import { updateCommentAction, participate, } from "~/lib/actions"; +import OverviewTable from "~/components/campaigns/campaignId/overview-tab/overview-table"; import EventForm from "~/components/campaigns/campaignId/event-tab/create-form"; import EventCards from "~/components/campaigns/campaignId/event-tab/event-cards"; import CommentInput from "~/components/campaigns/campaignId/comment-tab/comment-input"; import CommentCards from "~/components/campaigns/campaignId/comment-tab/comment-cards"; import Tribute from "tributejs"; import tributeStyles from "tributejs/tribute.css"; +import { Campaign } from "@prisma/client"; export const links: LinksFunction = () => { return [ @@ -175,118 +177,121 @@ export default function CampaignId() { return (
-
- - - - - - - Teilnehmen - -

- Indem Sie auf Teilnehmen klicken stimmen Sie zu, dass Sie der - Kampagnenleiter unter der von Ihnen angegebenen Email- Adresse - kontaktieren darf! -

-

- Bitte gib ausserdem an, ob du bereits über die benötigte - Hardware verfügst. -

-
-
-
- -
-
- - -
-
- - +
+
+
+

+ {campaign.title} +

+ + {campaign.priority} + +
+
+ + + + + + + Teilnehmen + +

+ Indem Sie auf Teilnehmen klicken stimmen Sie zu, dass Sie + der Kampagnenleiter unter der von Ihnen angegebenen Email- + Adresse kontaktieren darf! +

+

+ Bitte gib ausserdem an, ob du bereits über die benötigte + Hardware verfügst. +

+
+
+ + +
+
+ + +
+
+ + +
-
- - - - - -
- - - - - - - Teilen - - - - - - - -
- Karte anzeigen - setShowMap(!showMap)} - /> + + + + + + + + + + + + + Teilen + + + + + + + +
+ Karte anzeigen + setShowMap(!showMap)} + /> +
-
-

- {campaign.title} -

- - {campaign.priority} - -
+
+
- + - {/* - {t("live_label")} */} @@ -299,8 +304,7 @@ export default function CampaignId() {
{/*
*/} -

Beschreibung

- {campaign.description} +
{campaign.events.length === 0 ? ( @@ -360,10 +364,9 @@ export default function CampaignId() { style={{ height: "60vh", width: "40vw", - position: "fixed", - bottom: "10px", - // marginTop: "2rem", - right: "10px", + position: "sticky", + top: 0, + marginLeft: "auto", }} > Date: Mon, 10 Jul 2023 12:13:55 +0200 Subject: [PATCH 112/299] move forms to dialog --- .../campaignId/event-tab/create-form.tsx | 202 ++++++++++-------- .../campaignId/event-tab/event-cards.tsx | 60 ++++-- 2 files changed, 157 insertions(+), 105 deletions(-) diff --git a/app/components/campaigns/campaignId/event-tab/create-form.tsx b/app/components/campaigns/campaignId/event-tab/create-form.tsx index 1e3a0764..88378073 100644 --- a/app/components/campaigns/campaignId/event-tab/create-form.tsx +++ b/app/components/campaigns/campaignId/event-tab/create-form.tsx @@ -1,6 +1,15 @@ import { Form } from "@remix-run/react"; import { ClientOnly } from "remix-utils"; import { MarkdownEditor } from "~/markdown.client"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; type EventFormProps = { eventDescription: string; @@ -14,95 +23,114 @@ export default function EventForm({ eventTextAreaRef, }: EventFormProps) { return ( -
- {" "} -

Noch keine Events für diese Kampagne. Erstelle ein Event:

-
-
- -
- -
-
-
- -
- - - {() => ( - <> - +

Noch keine Events für diese Kampagne.

+ + +

Erstelle hier ein Event

+
+ + + Erstelle ein Event + + Erstelle ein Event für diese Kampagne + + +
+ +
+ +
+ -
- - Bild hinzufügen - - - Markdown unterstützt - -
- - )} - -
-
-
- -
- -
-
-
- -
- +
+
+
+ +
+ + + {() => ( + <> + +
+ + Bild hinzufügen + + + Markdown unterstützt + +
+ + )} +
+
+
+
+ +
+ +
+
+
+ +
+ +
+
+
+ +
+
-
-
- -
- + +
); } diff --git a/app/components/campaigns/campaignId/event-tab/event-cards.tsx b/app/components/campaigns/campaignId/event-tab/event-cards.tsx index 2df46653..1e94d672 100644 --- a/app/components/campaigns/campaignId/event-tab/event-cards.tsx +++ b/app/components/campaigns/campaignId/event-tab/event-cards.tsx @@ -6,6 +6,15 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; import { Button } from "~/components/ui/button"; import { EditIcon, TrashIcon } from "lucide-react"; import { Form } from "@remix-run/react"; @@ -42,7 +51,7 @@ export default function EventCards({
{events.map((e: any, i: number) => (
- +
@@ -65,23 +74,38 @@ export default function EventCards({ > -
- - -
+ + + + + + + + Sind Sie sicher dass Sie dieses Event löschen + möchten? + + +
+ + +
+
+
)}
From edb4787d1cb245e16b9f3b23c23e884e09f389f8 Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 13 Jul 2023 16:43:06 +0200 Subject: [PATCH 113/299] merge dev --- .github/dependabot.yaml | 15 ++ .github/workflows/deploy.yml | 94 +++------ app/components/Map/Map.tsx | 6 +- app/components/Map/cluster/index.ts | 84 -------- .../map/layers/cluster/box-marker.tsx | 78 ++++++++ .../map/layers/cluster/cluster-layer.tsx | 152 +++++++++++++++ .../cluster/donut-chart-cluster.tsx | 0 .../map/{layers.ts => layers/index.ts} | 0 .../map/layers/mobile/color-palette.ts | 11 ++ .../map/layers/mobile/mobile-box-layer.tsx | 114 +++++++++++ .../map/layers/mobile/mobile-box-view.tsx | 106 +++++++++++ app/db.server.ts | 5 +- app/lib/validations/campaign-event.ts | 31 +++ app/models/device.server.ts | 7 +- app/models/phenomena.server.ts | 5 + app/routes/create/form.tsx | 11 +- app/routes/explore.tsx | 179 ++---------------- app/routes/explore/$deviceId.tsx | 87 +++++---- fly.toml | 55 ------ package-lock.json | 32 ++++ package.json | 5 + server.ts | 9 +- yarn.lock | 12 ++ 23 files changed, 676 insertions(+), 422 deletions(-) create mode 100644 .github/dependabot.yaml delete mode 100644 app/components/Map/cluster/index.ts create mode 100644 app/components/map/layers/cluster/box-marker.tsx create mode 100644 app/components/map/layers/cluster/cluster-layer.tsx rename app/components/map/{ => layers}/cluster/donut-chart-cluster.tsx (100%) rename app/components/map/{layers.ts => layers/index.ts} (100%) create mode 100644 app/components/map/layers/mobile/color-palette.ts create mode 100644 app/components/map/layers/mobile/mobile-box-layer.tsx create mode 100644 app/components/map/layers/mobile/mobile-box-view.tsx create mode 100644 app/lib/validations/campaign-event.ts create mode 100644 app/models/phenomena.server.ts delete mode 100644 fly.toml diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml new file mode 100644 index 00000000..8150d9fd --- /dev/null +++ b/.github/dependabot.yaml @@ -0,0 +1,15 @@ +version: 2 + +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index effd793e..15096b7b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -9,6 +9,7 @@ on: permissions: actions: write contents: read + packages: write jobs: lint: @@ -140,13 +141,6 @@ jobs: - name: ⬇️ Checkout repo uses: actions/checkout@v3 - - name: 👀 Read app name - uses: SebRollen/toml-action@v1.0.2 - id: app_name - with: - file: "fly.toml" - field: "app" - - name: 🐳 Set up Docker Buildx uses: docker/setup-buildx-action@v2 with: @@ -161,67 +155,37 @@ jobs: restore-keys: | ${{ runner.os }}-buildx- - # - name: 🔑 Fly Registry Auth - # uses: docker/login-action@v2 - # with: - # registry: registry.fly.io - # username: x - # password: ${{ secrets.FLY_API_TOKEN }} - - # - name: 🐳 Docker build - # uses: docker/build-push-action@v4 - # with: - # context: . - # push: true - # tags: registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.ref_name }}-${{ github.sha }} - # build-args: | - # COMMIT_SHA=${{ github.sha }} - # cache-from: type=local,src=/tmp/.buildx-cache - # cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: ghcr.io/opensensemap/frontend + + - name: 🔑 GitHub Registry Auth + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: 🐳 Docker build + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + COMMIT_SHA=${{ github.sha }} + cache-from: type=local,src=/tmp/.buildx-cache + cache-to: type=local,mode=max,dest=/tmp/.buildx-cache-new # This ugly bit is necessary if you don't want your cache to grow forever # till it hits GitHub's limit of 5GB. # Temp fix # https://github.com/docker/build-push-action/issues/252 # https://github.com/moby/buildkit/issues/1896 - # - name: 🚚 Move cache - # run: | - # rm -rf /tmp/.buildx-cache - # mv /tmp/.buildx-cache-new /tmp/.buildx-cache - - # deploy: - # name: 🚀 Deploy - # runs-on: ubuntu-latest - # needs: [lint, typecheck, vitest, cypress, build] - # # only build/deploy main branch on pushes - # if: ${{ (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev') && github.event_name == 'push' }} - - # steps: - # - name: 🛑 Cancel Previous Runs - # uses: styfle/cancel-workflow-action@0.11.0 - - # - name: ⬇️ Checkout repo - # uses: actions/checkout@v3 - - # - name: 👀 Read app name - # uses: SebRollen/toml-action@v1.0.2 - # id: app_name - # with: - # file: "fly.toml" - # field: "app" - - # - name: 🚀 Deploy Staging - # if: ${{ github.ref == 'refs/heads/dev' }} - # uses: superfly/flyctl-actions@1.3 - # with: - # args: "deploy --app ${{ steps.app_name.outputs.value }}-staging --image registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.ref_name }}-${{ github.sha }}" - # env: - # FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} - - # - name: 🚀 Deploy Production - # if: ${{ github.ref == 'refs/heads/main' }} - # uses: superfly/flyctl-actions@1.3 - # with: - # args: "deploy --image registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.ref_name }}-${{ github.sha }}" - # env: - # FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }} + - name: 🚚 Move cache + run: | + rm -rf /tmp/.buildx-cache + mv /tmp/.buildx-cache-new /tmp/.buildx-cache diff --git a/app/components/Map/Map.tsx b/app/components/Map/Map.tsx index 1d153c5f..3ee872a7 100644 --- a/app/components/Map/Map.tsx +++ b/app/components/Map/Map.tsx @@ -13,9 +13,9 @@ const Map = forwardRef( id="osem" dragRotate={false} initialViewState={{ - longitude: 7.5, - latitude: 51.5, - zoom: 7, + longitude: 10, + latitude: 25, + zoom: 2, }} mapStyle="mapbox://styles/mapbox/streets-v12" mapboxAccessToken={ENV.MAPBOX_ACCESS_TOKEN} diff --git a/app/components/Map/cluster/index.ts b/app/components/Map/cluster/index.ts deleted file mode 100644 index e4f9dd28..00000000 --- a/app/components/Map/cluster/index.ts +++ /dev/null @@ -1,84 +0,0 @@ -import type { Device } from "@prisma/client"; -import type { GeoJsonProperties } from "geojson"; - -// colors to use for the categories -const colors = ["#4EAF47", "#666", "#666"]; - -export type ClusterPropertiesType = GeoJsonProperties & - Device & { - active: number; - inactive: number; - old: number; - cluster: boolean; - cluster_id: number; - point_count: number; - point_count_abbreviated: number | string; - }; - -// code for creating an SVG donut chart from feature properties -const createDonutChart = (props: any) => { - const offsets = []; - const counts = [props.active, props.inactive, props.old]; - let total = 0; - for (const count of counts) { - offsets.push(total); - total += count; - } - const fontSize = - total >= 1000 ? 14 : total >= 100 ? 10 : total >= 10 ? 5 : 14; - const r = total >= 1000 ? 36 : total >= 100 ? 20 : total >= 10 ? 10 : 18; - const r0 = Math.round(r * 0.6); - const w = r * 2; - - let html = `
-`; - - for (let i = 0; i < counts.length; i++) { - html += donutSegment( - offsets[i] / total, - (offsets[i] + counts[i]) / total, - r, - r0, - colors[i] - ); - } - html += ` - -${total.toLocaleString()} - - -
`; - - const el = document.createElement("div"); - el.innerHTML = html; - - return el.firstChild; -}; - -function donutSegment( - start: number, - end: number, - r: number, - r0: number, - color: string -) { - if (end - start === 1) end -= 0.00001; - const a0 = 2 * Math.PI * (start - 0.25); - const a1 = 2 * Math.PI * (end - 0.25); - const x0 = Math.cos(a0), - y0 = Math.sin(a0); - const x1 = Math.cos(a1), - y1 = Math.sin(a1); - const largeArc = end - start > 0.5 ? 1 : 0; - - // draw an SVG path - return ``; -} - -export default createDonutChart; diff --git a/app/components/map/layers/cluster/box-marker.tsx b/app/components/map/layers/cluster/box-marker.tsx new file mode 100644 index 00000000..e5fee6e0 --- /dev/null +++ b/app/components/map/layers/cluster/box-marker.tsx @@ -0,0 +1,78 @@ +import type { Device } from "@prisma/client"; +import { Exposure } from "@prisma/client"; +import { useNavigate } from "@remix-run/react"; +import { AnimatePresence, motion } from "framer-motion"; +import { Box, Rocket } from "lucide-react"; +import type { MarkerProps } from "react-map-gl"; +import { Marker, useMap } from "react-map-gl"; +import { cn } from "~/lib/utils"; + +interface BoxMarkerProps extends MarkerProps { + device: Device; +} + +const getStatusColor = (device: Device) => { + if (device.status === "ACTIVE") { + if(device.exposure === Exposure.MOBILE) { + return "bg-blue-100"; + } + return "bg-green-100"; + } else if (device.status === "INACTIVE") { + return "bg-gray-100"; + } else { + return "bg-gray-100 opacity-50"; + } +} + +export default function BoxMarker({ device, ...props }: BoxMarkerProps) { + const navigate = useNavigate(); + const { osem } = useMap(); + + const isFullZoom = osem && osem?.getZoom() >= 14; + + return ( + + + navigate(`${device.id}`)} + > + + {device.exposure === Exposure.MOBILE ? ( + + ) : ( + + )} + {isFullZoom && device.status === "ACTIVE" ? ( +
+ ) : null} + + {isFullZoom ? ( + + {device.name} + + ) : null} + + + + ); +} diff --git a/app/components/map/layers/cluster/cluster-layer.tsx b/app/components/map/layers/cluster/cluster-layer.tsx new file mode 100644 index 00000000..0064b9dd --- /dev/null +++ b/app/components/map/layers/cluster/cluster-layer.tsx @@ -0,0 +1,152 @@ +import type { Device } from "@prisma/client"; +import type { + GeoJsonProperties, + BBox, + FeatureCollection, + Point, +} from "geojson"; +import { useMemo, useCallback, useState, useEffect } from "react"; +import { Marker, useMap } from "react-map-gl"; +import type { PointFeature } from "supercluster"; +import useSupercluster from "use-supercluster"; +import type { DeviceClusterProperties } from "~/routes/explore"; +import DonutChartCluster from "./donut-chart-cluster"; +import BoxMarker from "./box-marker"; +import debounce from "lodash.debounce"; + +const DEBOUNCE_VALUE = 50; + +// supercluster options +const options = { + radius: 50, + maxZoom: 14, + map: (props: any) => ({ categories: { [props.status]: 1 } }), + reduce: (accumulated: any, props: any) => { + const categories: any = {}; + // clone the categories object from the accumulator + for (const key in accumulated.categories) { + categories[key] = accumulated.categories[key]; + } + // add props' category data to the clone + for (const key in props.categories) { + if (key in accumulated.categories) { + categories[key] = accumulated.categories[key] + props.categories[key]; + } else { + categories[key] = props.categories[key]; + } + } + // assign the clone to the accumulator + accumulated.categories = categories; + }, +}; + +export default function ClusterLayer({ + devices, +}: { + devices: FeatureCollection; +}) { + const { osem: mapRef } = useMap(); + + // the viewport bounds and zoom level + const [bounds, setBounds] = useState( + mapRef?.getMap().getBounds().toArray().flat() as BBox + ); + const [zoom, setZoom] = useState(mapRef?.getZoom() || 0); + + // get clusters + const points: PointFeature[] = useMemo(() => { + return devices.features.map((device) => ({ + type: "Feature", + properties: { + cluster: false, + ...device.properties, + }, + geometry: device.geometry, + })); + }, [devices.features]); + + // get bounds and zoom level from the map + // debounce the change handler to prevent too many updates + const debouncedChangeHandler = debounce(() => { + if (!mapRef) return; + setBounds(mapRef.getMap().getBounds().toArray().flat() as BBox); + setZoom(mapRef.getZoom()); + }, DEBOUNCE_VALUE); + + // register the debounced change handler to map events + useEffect(() => { + if (!mapRef) return; + + mapRef?.getMap().on("load", debouncedChangeHandler); + mapRef?.getMap().on("zoom", debouncedChangeHandler); + mapRef?.getMap().on("move", debouncedChangeHandler); + mapRef?.getMap().on("resize", debouncedChangeHandler); + }, [debouncedChangeHandler, mapRef]); + + const { clusters, supercluster } = useSupercluster({ + points, + bounds, + zoom, + options, + }); + + const clusterOnClick = useCallback( + (cluster: DeviceClusterProperties) => { + // supercluster from hook can be null or undefined + if (!supercluster) return; + + const [longitude, latitude] = cluster.geometry.coordinates; + + const expansionZoom = Math.min( + supercluster.getClusterExpansionZoom(cluster.id as number), + 20 + ); + + mapRef?.getMap().flyTo({ + center: [longitude, latitude], + animate: true, + speed: 1.6, + zoom: expansionZoom, + essential: true, + }); + }, + [mapRef, supercluster] + ); + + const clusterMarker = useMemo(() => { + return clusters.map((cluster) => { + // every cluster point has coordinates + const [longitude, latitude] = cluster.geometry.coordinates; + // the point may be either a cluster or a crime point + const { cluster: isCluster } = cluster.properties; + + // we have a cluster to render + if (isCluster) { + return ( + + + + ); + } + + // we have a single device to render + return ( + + ); + }); + }, [clusterOnClick, clusters]); + + return <>{clusterMarker}; +} diff --git a/app/components/map/cluster/donut-chart-cluster.tsx b/app/components/map/layers/cluster/donut-chart-cluster.tsx similarity index 100% rename from app/components/map/cluster/donut-chart-cluster.tsx rename to app/components/map/layers/cluster/donut-chart-cluster.tsx diff --git a/app/components/map/layers.ts b/app/components/map/layers/index.ts similarity index 100% rename from app/components/map/layers.ts rename to app/components/map/layers/index.ts diff --git a/app/components/map/layers/mobile/color-palette.ts b/app/components/map/layers/mobile/color-palette.ts new file mode 100644 index 00000000..c61eea8e --- /dev/null +++ b/app/components/map/layers/mobile/color-palette.ts @@ -0,0 +1,11 @@ +import chroma from "chroma-js"; + +export const LOW_COLOR = "#375F73"; +export const HIGH_COLOR = "#B5F584"; + +export const createPalette = ( + min: number, + max: number, + minColor = LOW_COLOR, + maxColor = HIGH_COLOR +) => chroma.scale([minColor, maxColor]).domain([min, max]); diff --git a/app/components/map/layers/mobile/mobile-box-layer.tsx b/app/components/map/layers/mobile/mobile-box-layer.tsx new file mode 100644 index 00000000..2b286ce9 --- /dev/null +++ b/app/components/map/layers/mobile/mobile-box-layer.tsx @@ -0,0 +1,114 @@ +import type { Sensor } from "@prisma/client"; +import { + featureCollection, + lineString, + multiLineString, + point, +} from "@turf/helpers"; +import type { MultiLineString, Point } from "geojson"; +import { useEffect, useState } from "react"; +import { Layer, Source, useMap } from "react-map-gl"; +import bbox from "@turf/bbox"; +import { HIGH_COLOR, LOW_COLOR, createPalette } from "./color-palette"; + +const FIT_PADDING = 50; +const BOTTOM_BAR_HEIGHT = 400; + +export default function MobileBoxLayer({ + sensor, + minColor = LOW_COLOR, + maxColor = HIGH_COLOR, +}: { + sensor: Sensor; + minColor?: + | mapboxgl.CirclePaint["circle-color"] + | mapboxgl.LinePaint["line-color"]; + maxColor?: + | mapboxgl.CirclePaint["circle-color"] + | mapboxgl.LinePaint["line-color"]; +}) { + const [sourceData, setSourceData] = useState(); + + const { osem } = useMap(); + + useEffect(() => { + const sensorData = sensor.data! as unknown as { + value: String; + location?: number[]; + createdAt: Date; + }[]; + + // create color palette from min and max values + const minValue = Math.min(...sensorData.map((d) => Number(d.value))); + const maxValue = Math.max(...sensorData.map((d) => Number(d.value))); + const palette = createPalette( + minValue, + maxValue, + minColor as string, + maxColor as string + ); + + // generate points from the sensor data + // apply color from palette + const points = sensorData.map((measurement) => + point(measurement.location!, { + value: Number(measurement.value), + createdAt: new Date(measurement.createdAt), + color: palette(Number(measurement.value)).hex(), + }) + ); + + if (points.length === 0) return; + + // generate a line from the points + const line = lineString(points.map((point) => point.geometry.coordinates)); + const lines = multiLineString([line.geometry.coordinates]); + + setSourceData( + featureCollection([...points, lines]) + ); + }, [maxColor, minColor, sensor.data]); + + // fit the map to the bounds of the data + useEffect(() => { + if (!osem || !sourceData) return; + const [x1, y1, x2, y2] = bbox(sourceData); + osem?.fitBounds([x1, y1, x2, y2], { + padding: { + top: FIT_PADDING, + bottom: BOTTOM_BAR_HEIGHT + FIT_PADDING, + left: FIT_PADDING, + right: FIT_PADDING, + }, + }); + }, [osem, sourceData]); + + if (!sourceData) return null; + + return ( + + + + + ); +} diff --git a/app/components/map/layers/mobile/mobile-box-view.tsx b/app/components/map/layers/mobile/mobile-box-view.tsx new file mode 100644 index 00000000..bf5ad6b3 --- /dev/null +++ b/app/components/map/layers/mobile/mobile-box-view.tsx @@ -0,0 +1,106 @@ +import type { Sensor } from "@prisma/client"; +import MobileBoxLayer from "./mobile-box-layer"; +import { HIGH_COLOR, LOW_COLOR } from "./color-palette"; +import { useEffect, useRef, useState } from "react"; + +export default function MobileBoxView({ sensors }: { sensors: Sensor[] }) { + return ( +
+ {sensors.map((sensor) => ( + + ))} +
+ ); +} + +function SensorView({ sensor }: { sensor: Sensor }) { + const [minColor, setMinColor] = useState(LOW_COLOR); + const [maxColor, setMaxColor] = useState(HIGH_COLOR); + + return ( + <> + { + setMinColor(min); + setMaxColor(max); + }} + /> + + + ); +} + +function Legend({ + sensor, + onColorChange, +}: { + sensor: Sensor; + onColorChange?: (min: string, max: string) => void; +}) { + const minColorInputRef = useRef(null); + const maxColorInputRef = useRef(null); + + const [minColor, setMinColor] = useState(LOW_COLOR); + const [maxColor, setMaxColor] = useState(HIGH_COLOR); + + useEffect(() => { + onColorChange && onColorChange(minColor, maxColor); + }, [minColor, maxColor, onColorChange]); + + const sensorData = sensor.data! as unknown as { + value: String; + location?: number[]; + createdAt: Date; + }[]; + + const minValue = Math.min(...sensorData.map((d) => Number(d.value))); + const maxValue = Math.max(...sensorData.map((d) => Number(d.value))); + + return ( +
+ {sensor.title} +
+
minColorInputRef.current?.click()} + > + {minValue} + {sensor.unit} + setMinColor(e.target.value)} + /> +
+ maxColorInputRef.current?.click()} + > + {maxValue} + {sensor.unit} + setMaxColor(e.target.value)} + /> + +
+
+ ); +} diff --git a/app/db.server.ts b/app/db.server.ts index ef39a49b..3db12058 100644 --- a/app/db.server.ts +++ b/app/db.server.ts @@ -28,10 +28,9 @@ function getClient() { const isLocalHost = databaseUrl.hostname === "localhost"; - const PRIMARY_REGION = isLocalHost ? null : process.env.PRIMARY_REGION; - const FLY_REGION = isLocalHost ? null : process.env.FLY_REGION; + const PRIMARY_REGION = isLocalHost ? null : false; - const isReadReplicaRegion = !PRIMARY_REGION || PRIMARY_REGION === FLY_REGION; + const isReadReplicaRegion = !PRIMARY_REGION; if (!isLocalHost) { databaseUrl.host = `${databaseUrl.host}`; diff --git a/app/lib/validations/campaign-event.ts b/app/lib/validations/campaign-event.ts new file mode 100644 index 00000000..77be1132 --- /dev/null +++ b/app/lib/validations/campaign-event.ts @@ -0,0 +1,31 @@ +import * as z from "zod"; + +function checkValidDates(startDate: Date, endDate: Date | undefined) { + if (startDate && endDate) { + return startDate <= endDate; + } + return true; +} + +export const campaignEventSchema = z + .object({ + title: z + .string() + .min(3, "Der Titel muss mindestens 5 Zeichen lang sein!") + .max(52), + description: z + .string() + .min(5, "Die Beschreibung muss mindestens 5 Zeichen lang sein!"), + createdAt: z.date(), + updatedAt: z.date(), + startDate: z + .date() + .refine((value) => value !== undefined && value !== null, { + message: "Dies ist ein Pflichtfeld!", + }), + endDate: z.date().optional(), + }) + .refine( + (data) => checkValidDates(data.startDate, data.endDate), + "Der Beginn muss früher sein als der Abschluss der Kampagne!" + ); diff --git a/app/models/device.server.ts b/app/models/device.server.ts index 60063250..14fb9fc8 100644 --- a/app/models/device.server.ts +++ b/app/models/device.server.ts @@ -100,6 +100,9 @@ export async function getMeasurements( "&to-date=" + endDate.toISOString() //new Date().toISOString() ); - const jsonData = await response.json(); - return jsonData; + return (await response.json()) as { + value: String; + location?: number[]; + createdAt: Date; + }[]; } diff --git a/app/models/phenomena.server.ts b/app/models/phenomena.server.ts new file mode 100644 index 00000000..ce868bef --- /dev/null +++ b/app/models/phenomena.server.ts @@ -0,0 +1,5 @@ +export async function getPhenomena() { + const response = await fetch("https://api.sensors.wiki/phenomena/"); + const jsonData = await response.json(); + return jsonData; +} diff --git a/app/routes/create/form.tsx b/app/routes/create/form.tsx index 6f2f6c8e..298e720b 100644 --- a/app/routes/create/form.tsx +++ b/app/routes/create/form.tsx @@ -38,6 +38,7 @@ import { import { InfoCard } from "~/utils/info-card"; import { ClientOnly } from "remix-utils"; import { MarkdownEditor } from "~/markdown.client"; +import { getPhenomena } from "~/models/phenomena.server"; // import h337, { Heatmap } from "heatmap.js"; @@ -164,14 +165,11 @@ export async function action({ request }: ActionArgs) { export async function loader({ params }: LoaderArgs) { // request to fetch all phenomena - const response = await fetch( - "https://api.sensors.wiki/phenomena/all?language=de" - ); - const data = await response.json(); - if (data.code === "UnprocessableEntity") { + const response = await getPhenomena(); + if (response.code === "UnprocessableEntity") { throw new Response("Phenomena not found", { status: 502 }); } - const phenomena = data.map( + const phenomena = response.map( (d: { label: { item: { text: any }[] } }) => d.label.item[0].text ); return phenomena; @@ -183,6 +181,7 @@ export default function CreateCampaign() { const navigate = useNavigate(); const phenomena = useLoaderData(); const { features } = useContext(FeatureContext); + console.log(features); const textAreaRef = useRef(); const [description, setDescription] = useState(""); const titleRef = useRef(null); diff --git a/app/routes/explore.tsx b/app/routes/explore.tsx index 16c89499..ff979a1c 100644 --- a/app/routes/explore.tsx +++ b/app/routes/explore.tsx @@ -1,28 +1,20 @@ -import { Outlet, useNavigate } from "@remix-run/react"; +import { Outlet, useLoaderData } from "@remix-run/react"; import Map from "~/components/map"; import mapboxglcss from "mapbox-gl/dist/mapbox-gl.css"; import Header from "~/components/header"; - import type { LoaderArgs, LinksFunction } from "@remix-run/node"; -import { json } from "@remix-run/node"; -import { useLoaderData } from "@remix-run/react"; import { getDevices } from "~/models/device.server"; import type { MapRef } from "react-map-gl"; - -import { MapProvider, Marker } from "react-map-gl"; -import { useState, useRef, useMemo, useCallback } from "react"; +import { MapProvider } from "react-map-gl"; +import { useState, useRef } from "react"; import { useHotkeys } from "@mantine/hooks"; import OverlaySearch from "~/components/search/overlay-search"; import { Toaster } from "~/components/ui/toaster"; import { getUser } from "~/session.server"; -import useSupercluster from "use-supercluster"; -import DonutChartCluster from "~/components/map/cluster/donut-chart-cluster"; -import type { BBox, GeoJsonProperties } from "geojson"; import type Supercluster from "supercluster"; -import type { PointFeature } from "supercluster"; -import { Exposure, type Device } from "@prisma/client"; -import { Box, Rocket } from "lucide-react"; import { getProfileByUserId } from "~/models/profile.server"; +import ClusterLayer from "~/components/map/layers/cluster/cluster-layer"; +import { typedjson } from "remix-typedjson"; export type DeviceClusterProperties = | Supercluster.PointFeature @@ -34,39 +26,15 @@ export type DeviceClusterProperties = } >; -// supercluster options -const options = { - radius: 50, - maxZoom: 14, - map: (props: any) => ({ categories: { [props.status]: 1 } }), - reduce: (accumulated: any, props: any) => { - const categories: any = {}; - // clone the categories object from the accumulator - for (const key in accumulated.categories) { - categories[key] = accumulated.categories[key]; - } - // add props' category data to the clone - for (const key in props.categories) { - if (key in accumulated.categories) { - categories[key] = accumulated.categories[key] + props.categories[key]; - } else { - categories[key] = props.categories[key]; - } - } - // assign the clone to the accumulator - accumulated.categories = categories; - }, -}; - export async function loader({ request }: LoaderArgs) { const devices = await getDevices(); const user = await getUser(request); if (user) { const profile = await getProfileByUserId(user.id); - return json({ devices, user, profile }); + return typedjson({ devices, user, profile }); } - return json({ devices, user, profile: null }); + return typedjson({ devices, user, profile: null }); } export const links: LinksFunction = () => { @@ -85,15 +53,6 @@ export default function Explore() { const mapRef = useRef(null); const searchRef = useRef(null); - // get map bounds - const [viewState, setViewState] = useState({ - longitude: 52, - latitude: 7, - zoom: 2, - }); - - const navigate = useNavigate(); - const [showSearch, setShowSearch] = useState(false); /** @@ -118,122 +77,24 @@ export default function Explore() { ], ]); - // get clusters - const points: PointFeature[] = useMemo(() => { - return data.devices.features.map((device) => ({ - type: "Feature", - properties: { - cluster: false, - ...device.properties, - }, - geometry: device.geometry, - })); - }, [data.devices.features]); - - const bounds = mapRef.current - ? (mapRef.current.getMap().getBounds().toArray().flat() as BBox) - : ([-92, -72, 193, 76] as BBox); - - const { clusters, supercluster } = useSupercluster({ - points, - bounds, - zoom: viewState.zoom, - options, - }); - - const clusterOnClick = useCallback( - (cluster: DeviceClusterProperties) => { - // supercluster from hook can be null or undefined - if (!supercluster) return; - - const [longitude, latitude] = cluster.geometry.coordinates; - - const expansionZoom = Math.min( - supercluster.getClusterExpansionZoom(cluster.id as number), - 20 - ); - - mapRef.current?.getMap().flyTo({ - center: [longitude, latitude], - animate: true, - speed: 1.6, - zoom: expansionZoom, - essential: true, - }); - }, - [supercluster] - ); - - const clusterMarker = useMemo(() => { - return clusters.map((cluster) => { - // every cluster point has coordinates - const [longitude, latitude] = cluster.geometry.coordinates; - // the point may be either a cluster or a crime point - const { cluster: isCluster } = cluster.properties; - - // we have a cluster to render - if (isCluster) { - return ( - - - - ); - } - - // we have a single device to render - return ( - -
navigate(`${(cluster.properties as Device).id}`)} - > - - {(cluster.properties as Device).exposure === Exposure.MOBILE ? ( - - ) : ( - - )} - -
-
- ); - }); - }, [clusterOnClick, clusters, navigate]); - return (
- setViewState(evt.viewState)} - > - {clusterMarker} + + + + {showSearch ? ( + + ) : null} +
+ +
- - {showSearch ? ( - - ) : null} -
- -
); diff --git a/app/routes/explore/$deviceId.tsx b/app/routes/explore/$deviceId.tsx index 5ab53bb8..13c4de66 100644 --- a/app/routes/explore/$deviceId.tsx +++ b/app/routes/explore/$deviceId.tsx @@ -1,53 +1,57 @@ // Importing dependencies -import type { Sensor } from "@prisma/client"; +import { Exposure, type Sensor } from "@prisma/client"; import type { LoaderArgs } from "@remix-run/node"; import { useCatch, useLoaderData } from "@remix-run/react"; import { typedjson } from "remix-typedjson"; import BottomBar from "~/components/bottom-bar/bottom-bar"; +import MobileBoxView from "~/components/map/layers/mobile/mobile-box-view"; import { getDevice, getMeasurements } from "~/models/device.server"; export async function loader({ params, request }: LoaderArgs) { // Extracting the selected sensors from the URL query parameters using the stringToArray function const url = new URL(request.url); - if (params.deviceId) { - const device = await getDevice({ id: params.deviceId }); + if (!params.deviceId) { + throw new Response("Device not found", { status: 502 }); + } - // Find all sensors from the device response that have the same id as one of the sensor array value - const sensorIds = url.searchParams.getAll("sensor"); - var sensorsToQuery = device?.sensors.filter((sensor: Sensor) => - sensorIds.includes(sensor.id) - ); + const device = await getDevice({ id: params.deviceId }); - const selectedSensors: Sensor[] = []; - if (sensorsToQuery && sensorsToQuery.length > 0) { - await Promise.all( - sensorsToQuery.map(async (sensor: Sensor) => { - const sensorData = await getMeasurements( - params.deviceId, - sensor.id, - new Date(new Date().getTime() - 24 * 60 * 60 * 1000), //24 hours ago - new Date() - ); - const sensorToAdd = { - ...sensor, - data: sensorData, - }; - selectedSensors.push(sensorToAdd); - }) - ); - } + // Find all sensors from the device response that have the same id as one of the sensor array value + const sensorIds = url.searchParams.getAll("sensor"); + var sensorsToQuery = device?.sensors.filter((sensor: Sensor) => + sensorIds.includes(sensor.id) + ); - // Combine the device data with the selected sensors and return the result as JSON + add env variable - const data = { + if (!sensorsToQuery) { + return typedjson({ device: device, - selectedSensors: selectedSensors, + selectedSensors: [], OSEM_API_URL: process.env.OSEM_API_URL, - }; - return typedjson(data); - } else { - throw new Response("Device not found", { status: 502 }); + }); } + + const selectedSensors: Sensor[] = await Promise.all( + sensorsToQuery.map(async (sensor: Sensor) => { + const sensorData = await getMeasurements( + params.deviceId, + sensor.id, + new Date(new Date().getTime() - 24 * 60 * 60 * 1000), // 24 hours + new Date() + ); + return { + ...sensor, + data: sensorData as any, + }; + }) + ); + // Combine the device data with the selected sensors and return the result as JSON + add env variable + const data = { + device: device, + selectedSensors: selectedSensors, + OSEM_API_URL: process.env.OSEM_API_URL, + }; + return typedjson(data); } // Defining the component that will render the page @@ -55,10 +59,19 @@ export default function DeviceId() { // Retrieving the data returned by the loader using the useLoaderData hook const data = useLoaderData(); - // Rendering the BottomBar component with the device data - return data?.device && data.selectedSensors ? ( - - ) : null; + if (!data?.device && !data.selectedSensors) { + return null; + } + + return ( + <> + {/* If the box is mobile, iterate over selected sensors and show trajectory */} + {data.device.exposure === Exposure.MOBILE ? ( + + ) : null} + + + ); } // Defining a CatchBoundary component to handle errors thrown by the loader diff --git a/fly.toml b/fly.toml deleted file mode 100644 index c65016db..00000000 --- a/fly.toml +++ /dev/null @@ -1,55 +0,0 @@ -app = "frontend-d00c" -kill_signal = "SIGINT" -kill_timeout = 5 -processes = [ ] - -[env] -PORT = "8080" -METRICS_PORT = "8081" - -[metrics] -port = 8_081 -path = "/metrics" - -[deploy] -release_command = "npx prisma migrate deploy" - -[experimental] -allowed_public_ports = [ ] -auto_rollback = true - -[[services]] -internal_port = 8_080 -processes = [ "app" ] -protocol = "tcp" -script_checks = [ ] - - [services.concurrency] - hard_limit = 25 - soft_limit = 20 - type = "connections" - - [[services.ports]] - handlers = [ "http" ] - port = 80 - force_https = true - - [[services.ports]] - handlers = [ "tls", "http" ] - port = 443 - - [[services.tcp_checks]] - grace_period = "1s" - interval = "15s" - restart_limit = 0 - timeout = "2s" - - [[services.http_checks]] - interval = "10s" - grace_period = "5s" - method = "get" - path = "/healthcheck" - protocol = "http" - timeout = "2s" - tls_skip_verify = false - headers = { } diff --git a/package-lock.json b/package-lock.json index d197686e..2f8cab32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@remix-run/react": "^1.12.0", "@remix-run/server-runtime": "^1.12.0", "@turf/center": "^6.5.0", + "@turf/bbox": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/line-distance": "^4.7.3", "@types/mapbox-gl": "^2.7.11", @@ -41,6 +42,7 @@ "chart.js": "^4.3.0", "chartjs-adapter-date-fns": "^3.0.0", "circle-to-polygon": "^2.2.0", + "chroma-js": "^2.4.2", "class-variance-authority": "^0.5.3", "clsx": "^1.2.1", "compression": "^1.7.4", @@ -56,6 +58,7 @@ "i18next-fs-backend": "^2.1.1", "i18next-http-backend": "^2.2.0", "isbot": "^3.6.5", + "lodash.debounce": "^4.0.8", "lucide-react": "^0.179.0", "mapbox-gl": "^2.14.1", "morgan": "^1.10.0", @@ -96,6 +99,7 @@ "@testing-library/user-event": "^14.4.3", "@types/bcryptjs": "^2.4.2", "@types/circle-to-polygon": "^2.2.0", + "@types/chroma-js": "^2.4.0", "@types/compression": "^1.7.2", "@types/eslint": "^8.4.10", "@types/express": "^4.17.15", @@ -103,6 +107,7 @@ "@types/file-saver": "^2.0.5", "@types/geojson": "^7946.0.10", "@types/mapbox__mapbox-gl-draw": "^1.3.3", + "@types/lodash.debounce": "^4.0.7", "@types/morgan": "^1.9.4", "@types/node": "^18.11.18", "@types/numeral": "^2.0.2", @@ -7812,6 +7817,17 @@ "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-4.7.4.tgz", "integrity": "sha512-cvwz4EI9BjrgRHxmJ3Z3HKxq6k/fj/V95kwNZ8uWNLncCvrb7x1jUBDkQUo1tShnYdZ8eBgA+a2bvJmAM+MJ0A==" }, + "node_modules/@turf/meta": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@turf/meta/-/meta-6.5.0.tgz", + "integrity": "sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA==", + "dependencies": { + "@turf/helpers": "^6.5.0" + }, + "funding": { + "url": "https://opencollective.com/turf" + } + }, "node_modules/@types/acorn": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz", @@ -7879,6 +7895,12 @@ "@types/geojson": "*" } }, + "node_modules/@types/chroma-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@types/chroma-js/-/chroma-js-2.4.0.tgz", + "integrity": "sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw==", + "dev": true + }, "node_modules/@types/compression": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.7.2.tgz", @@ -8111,6 +8133,16 @@ "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz", "integrity": "sha512-D0lgCq+3VWV85ey1MZVkE8ZveyuvW5VAfuahVTQRpXFQTxw03SuIf1/K4UQ87MMIXVKzpFjXFiFMZzLj2kU+iA==" }, + + "node_modules/@types/lodash.debounce": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz", + "integrity": "sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/lodash.memoize": { "version": "4.1.7", "resolved": "https://registry.npmjs.org/@types/lodash.memoize/-/lodash.memoize-4.1.7.tgz", diff --git a/package.json b/package.json index 67d2fcf0..e01cbaea 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@remix-run/react": "^1.12.0", "@remix-run/server-runtime": "^1.12.0", "@turf/center": "^6.5.0", + "@turf/bbox": "^6.5.0", "@turf/helpers": "^6.5.0", "@turf/line-distance": "^4.7.3", "@types/heatmap.js": "^2.0.37", @@ -73,6 +74,7 @@ "chart.js": "^4.3.0", "chartjs-adapter-date-fns": "^3.0.0", "circle-to-polygon": "^2.2.0", + "chroma-js": "^2.4.2", "class-variance-authority": "^0.5.3", "clsx": "^1.2.1", "compression": "^1.7.4", @@ -91,6 +93,7 @@ "i18next-fs-backend": "^2.1.1", "i18next-http-backend": "^2.2.0", "isbot": "^3.6.5", + "lodash.debounce": "^4.0.8", "lucide-react": "^0.179.0", "mapbox-gl": "^2.14.1", "mapbox-gl-draw-rectangle-mode": "^1.0.4", @@ -139,6 +142,7 @@ "@testing-library/user-event": "^14.4.3", "@types/bcryptjs": "^2.4.2", "@types/circle-to-polygon": "^2.2.0", + "@types/chroma-js": "^2.4.0", "@types/compression": "^1.7.2", "@types/eslint": "^8.4.10", "@types/express": "^4.17.15", @@ -146,6 +150,7 @@ "@types/file-saver": "^2.0.5", "@types/geojson": "^7946.0.10", "@types/mapbox__mapbox-gl-draw": "^1.3.3", + "@types/lodash.debounce": "^4.0.7", "@types/morgan": "^1.9.4", "@types/node": "^18.11.18", "@types/numeral": "^2.0.2", diff --git a/server.ts b/server.ts index a806b03c..d3e20a4f 100644 --- a/server.ts +++ b/server.ts @@ -17,7 +17,6 @@ app.use( app.use((req, res, next) => { // helpful headers: - res.set("x-fly-region", process.env.FLY_REGION ?? "unknown"); res.set("Strict-Transport-Security", `max-age=${60 * 60 * 24 * 365 * 100}`); // /clean-urls/ -> /clean-urls @@ -36,24 +35,18 @@ app.use((req, res, next) => { // learn more: https://fly.io/docs/getting-started/multi-region-databases/#replay-the-request app.all("*", function getReplayResponse(req, res, next) { const { method, path: pathname } = req; - const { PRIMARY_REGION, FLY_REGION } = process.env; const isMethodReplayable = !["GET", "OPTIONS", "HEAD"].includes(method); - const isReadOnlyRegion = - FLY_REGION && PRIMARY_REGION && FLY_REGION !== PRIMARY_REGION; - const shouldReplay = isMethodReplayable && isReadOnlyRegion; + const shouldReplay = isMethodReplayable; if (!shouldReplay) return next(); const logInfo = { pathname, method, - PRIMARY_REGION, - FLY_REGION, }; console.info(`Replaying:`, logInfo); - res.set("fly-replay", `region=${PRIMARY_REGION}`); return res.sendStatus(409); }); diff --git a/yarn.lock b/yarn.lock index 20011d2c..b3c9d53f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3668,6 +3668,11 @@ resolved "https://registry.npmjs.org/@types/chai/-/chai-4.3.4.tgz" integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== +"@types/chroma-js@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@types/chroma-js/-/chroma-js-2.4.0.tgz#476a16ae848c77478079d6749236fdb98837b92c" + integrity sha512-JklMxityrwjBTjGY2anH8JaTx3yjRU3/sEHSblLH1ba5lqcSh1LnImXJZO5peJfXyqKYWjHTGy4s5Wz++hARrw== + "@types/circle-to-polygon@^2.2.0": version "2.2.0" resolved "https://registry.npmjs.org/@types/circle-to-polygon/-/circle-to-polygon-2.2.0.tgz" @@ -3847,6 +3852,13 @@ dependencies: "@types/geojson" "*" +"@types/lodash.debounce@^4.0.7": + version "4.0.7" + resolved "https://registry.yarnpkg.com/@types/lodash.debounce/-/lodash.debounce-4.0.7.tgz#0285879defb7cdb156ae633cecd62d5680eded9f" + integrity sha512-X1T4wMZ+gT000M2/91SYj0d/7JfeNZ9PeeOldSNoE/lunLeQXKvkmIumI29IaKMotU/ln/McOIvgzZcQ/3TrSA== + dependencies: + "@types/lodash" "*" + "@types/lodash.memoize@^4.1.7": version "4.1.7" resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.7.tgz#aff94ab32813c557cbc1104e127030e3d60a3b27" From 7232dd78a05b74e474d6c23de639b7d16f9ba70a Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 13 Jul 2023 20:56:20 +0200 Subject: [PATCH 114/299] merge dev --- .env.example | 17 +- app/components/device-card.tsx | 35 ++ app/components/error-boundary.tsx | 44 ++ app/components/header/menu/index.tsx | 7 +- app/components/label-button.tsx | 5 + app/components/nav-bar.tsx | 115 ++++ app/components/sidebar-nav.tsx | 43 ++ app/components/ui/alert.tsx | 59 ++ app/components/ui/button.tsx | 19 +- app/components/ui/checkbox.tsx | 30 + app/components/ui/dialog.tsx | 46 +- app/components/ui/form.tsx | 176 ++++++ app/components/ui/sheet.tsx | 144 +++++ app/models/profile.server.ts | 22 +- app/models/user.server.ts | 29 +- app/routes/explore/profile/$profile.tsx | 268 --------- .../profile/$userId.changeVisibility.tsx | 22 - app/routes/explore/profile/$userId.info.tsx | 19 - app/routes/explore/profile/me.tsx | 13 - app/routes/explore/register.tsx | 8 +- app/routes/join.tsx | 21 +- app/routes/login.tsx | 18 +- app/routes/profile.tsx | 19 + app/routes/profile/$username.tsx | 257 +++++++++ app/routes/profile/me.tsx | 19 + app/routes/resources/file.$fileId.tsx | 18 + app/routes/resources/user-avatar.tsx | 41 ++ app/routes/settings.tsx | 61 +++ app/routes/settings/account.tsx | 17 + app/routes/settings/notifications.tsx | 17 + app/routes/settings/profile.tsx | 187 +++++++ app/routes/settings/profile/photo.tsx | 198 +++++++ app/session.server.ts | 8 +- app/utils/forms.tsx | 78 +++ app/utils/misc.ts | 28 + app/utils/user-validation.ts | 25 + app/utils/zod-extensions.ts | 11 + package-lock.json | 513 +++++++++++++----- package.json | 12 +- .../migration.sql | 2 + .../migration.sql | 2 + .../migration.sql | 42 ++ .../migration.sql | 20 + prisma/schema.prisma | 47 +- public/img/user.png | Bin 0 -> 3012 bytes server.ts | 1 + 46 files changed, 2234 insertions(+), 549 deletions(-) create mode 100644 app/components/device-card.tsx create mode 100644 app/components/error-boundary.tsx create mode 100644 app/components/label-button.tsx create mode 100644 app/components/nav-bar.tsx create mode 100644 app/components/sidebar-nav.tsx create mode 100644 app/components/ui/alert.tsx create mode 100644 app/components/ui/checkbox.tsx create mode 100644 app/components/ui/form.tsx create mode 100644 app/components/ui/sheet.tsx delete mode 100644 app/routes/explore/profile/$profile.tsx delete mode 100644 app/routes/explore/profile/$userId.changeVisibility.tsx delete mode 100644 app/routes/explore/profile/$userId.info.tsx delete mode 100644 app/routes/explore/profile/me.tsx create mode 100644 app/routes/profile.tsx create mode 100644 app/routes/profile/$username.tsx create mode 100644 app/routes/profile/me.tsx create mode 100644 app/routes/resources/file.$fileId.tsx create mode 100644 app/routes/resources/user-avatar.tsx create mode 100644 app/routes/settings.tsx create mode 100644 app/routes/settings/account.tsx create mode 100644 app/routes/settings/notifications.tsx create mode 100644 app/routes/settings/profile.tsx create mode 100644 app/routes/settings/profile/photo.tsx create mode 100644 app/utils/forms.tsx create mode 100644 app/utils/misc.ts create mode 100644 app/utils/user-validation.ts create mode 100644 app/utils/zod-extensions.ts create mode 100644 prisma/migrations/20230627070530_add_device_description/migration.sql create mode 100644 prisma/migrations/20230627090256_add_device_visibility/migration.sql create mode 100644 prisma/migrations/20230627114606_add_profile_image/migration.sql create mode 100644 prisma/migrations/20230628130803_rename_name_username/migration.sql create mode 100644 public/img/user.png diff --git a/.env.example b/.env.example index f2997962..40175384 100644 --- a/.env.example +++ b/.env.example @@ -1,9 +1,22 @@ DATABASE_URL="postgresql://postgres:postgres@localhost:5432/opensensemap" SHADOW_DATABASE_URL="postgresql://postgres:postgres@localhost:5432/postgres" + SESSION_SECRET="super-duper-s3cret" -MAPTILER_KEY="PUT_YOUR_KEY_HERE" -OSEM_API_URL="http://localhost:8000" + MAPBOX_ACCESS_TOKEN="" MAPBOX_GEOCODING_API="https://api.mapbox.com/geocoding/v5/mapbox.places/" + OSEM_API_URL="https://api.opensensemap.org/" DIRECTUS_URL="https://coelho.opensensemap.org" + +MYBADGES_API_URL = "https://api.v2.mybadges.org/" +MYBADGES_URL = "https://mybadges.org/" +MYBADGES_SERVERADMIN_USERNAME = "" +MYBADGES_SERVERADMIN_PASSWORD = "" +MYBADGES_ISSUERID_OSEM = "" +MYBADGES_CLIENT_ID = "" +MYBADGES_CLIENT_SECRET = "" + +NOVU_API_URL = "" +NOVU_WEBSOCKET_URL = "" +NOVU_APPLICATION_IDENTIFIER = "" diff --git a/app/components/device-card.tsx b/app/components/device-card.tsx new file mode 100644 index 00000000..d39bb66a --- /dev/null +++ b/app/components/device-card.tsx @@ -0,0 +1,35 @@ +import { Circle } from "lucide-react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "./ui/card"; +import type { Device } from "@prisma/client"; + +interface DeviceCardProps { + device: Device; +} + +export default function DeviceCard({ device }: DeviceCardProps) { + return ( + + +
+ {device.name} + {device.description} +
+
+ +
+
+ + {device.model} +
+
Updated {device.updatedAt.toString()}
+
+
+
+ ); +} diff --git a/app/components/error-boundary.tsx b/app/components/error-boundary.tsx new file mode 100644 index 00000000..21d4a5eb --- /dev/null +++ b/app/components/error-boundary.tsx @@ -0,0 +1,44 @@ +import { + isRouteErrorResponse, + useParams, + useRouteError, +} from "@remix-run/react"; +import { type ErrorResponse } from "@remix-run/router"; +import { getErrorMessage } from "~/utils/misc"; + +type StatusHandler = (info: { + error: ErrorResponse; + params: Record; +}) => JSX.Element | null; + +export function GeneralErrorBoundary({ + defaultStatusHandler = ({ error }) => ( +

+ {error.status} {error.data} +

+ ), + statusHandlers, + unexpectedErrorHandler = (error) =>

{getErrorMessage(error)}

, +}: { + defaultStatusHandler?: StatusHandler; + statusHandlers?: Record; + unexpectedErrorHandler?: (error: unknown) => JSX.Element | null; +}) { + const error = useRouteError(); + const params = useParams(); + + if (typeof document !== "undefined") { + console.error(error); + } + + return ( +
+ {isRouteErrorResponse(error) + ? (statusHandlers?.[error.status] ?? defaultStatusHandler)({ + error, + params, + }) + : unexpectedErrorHandler(error)} +
+ ); +} diff --git a/app/components/header/menu/index.tsx b/app/components/header/menu/index.tsx index f5f6550d..c8bfaf34 100644 --- a/app/components/header/menu/index.tsx +++ b/app/components/header/menu/index.tsx @@ -145,16 +145,17 @@ export default function Menu() { )} {data.profile && ( - + + Profile )} - {t("settings_label")} + {t("settings_label")} - {t("my_devices_label")} + {t("my_devices_label")} diff --git a/app/components/label-button.tsx b/app/components/label-button.tsx new file mode 100644 index 00000000..4a77d3ad --- /dev/null +++ b/app/components/label-button.tsx @@ -0,0 +1,5 @@ +export function LabelButton({ + ...props +}: Omit, "className">) { + return