From f622b852313001b006e3fc485dd6d407a7346292 Mon Sep 17 00:00:00 2001 From: fab-scm Date: Thu, 4 May 2023 15:48:25 +0200 Subject: [PATCH 1/7] add popover component from shadcn --- app/components/ui/popover.tsx | 31 ++++++++ package-lock.json | 134 ++++++++++++++++++++++++++++++++++ package.json | 1 + 3 files changed, 166 insertions(+) create mode 100644 app/components/ui/popover.tsx diff --git a/app/components/ui/popover.tsx b/app/components/ui/popover.tsx new file mode 100644 index 000000000..ceee78f28 --- /dev/null +++ b/app/components/ui/popover.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as PopoverPrimitive from "@radix-ui/react-popover" + +import { cn } from "@/lib/utils" + +const Popover = PopoverPrimitive.Root + +const PopoverTrigger = PopoverPrimitive.Trigger + +const PopoverContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + + + +)) +PopoverContent.displayName = PopoverPrimitive.Content.displayName + +export { Popover, PopoverTrigger, PopoverContent } diff --git a/package-lock.json b/package-lock.json index 13634b5cf..fa1cff3bf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@prisma/client": "^4.9.0", "@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-navigation-menu": "^1.1.1", + "@radix-ui/react-popover": "^1.0.5", "@remix-run/express": "^1.12.0", "@remix-run/node": "^1.12.0", "@remix-run/react": "^1.12.0", @@ -2628,6 +2629,32 @@ "npm": ">=6.0.0" } }, + "node_modules/@floating-ui/core": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-0.7.3.tgz", + "integrity": "sha512-buc8BXHmG9l82+OQXOFU3Kr2XQx9ys01U/Q9HMIrZ300iLc8HLMgh7dcCqgYzAzf4BkoQvDcXf5Y+CuEZ5JBYg==" + }, + "node_modules/@floating-ui/dom": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-0.5.4.tgz", + "integrity": "sha512-419BMceRLq0RrmTSDxn8hf9R3VCJv2K9PUfugh5JyEFmdjzDo+e8U5EdR8nzKq8Yj1htzLm3b6eQEEam3/rrtg==", + "dependencies": { + "@floating-ui/core": "^0.7.3" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-0.7.2.tgz", + "integrity": "sha512-1T0sJcpHgX/u4I1OzIEhlcrvkUN8ln39nz7fMoE/2HDHrPiMFoOGR7++GYyfUmIQHkkrTinaeQsO3XWubjSvGg==", + "dependencies": { + "@floating-ui/dom": "^0.5.3", + "use-isomorphic-layout-effect": "^1.1.1" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/@gar/promisify": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", @@ -3038,6 +3065,19 @@ "@babel/runtime": "^7.13.10" } }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz", + "integrity": "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.2.tgz", @@ -3194,6 +3234,55 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-popover": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.5.tgz", + "integrity": "sha512-GRHZ8yD12MrN2NLobHPE8Rb5uHTxd9x372DE9PPNnBjpczAQHcZ5ne0KXG4xpf+RDdXSzdLv9ym6mYJCDTaUZg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.3", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.2", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-popper": "1.1.1", + "@radix-ui/react-portal": "1.0.2", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.1.tgz", + "integrity": "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.2", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-portal": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.2.tgz", @@ -3303,6 +3392,30 @@ "react": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.0.0.tgz", + "integrity": "sha512-TB7pID8NRMEHxb/qQJpvSt3hQU4sqNPM1VCTjTRjEOa7cEop/QMuq8S6fb/5Tsz64kqSvB9WnwsDHtjnrM9qew==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.0.0.tgz", + "integrity": "sha512-imZ3aYcoYCKhhgNpkNDh/aTiU05qw9hX+HHI1QDBTyIlcFjgeFlKKySNGMwTp7nYFLQg/j0VA2FmCY4WPDDHMg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-visually-hidden": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.2.tgz", @@ -3316,6 +3429,14 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/rect": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.0.tgz", + "integrity": "sha512-d0O68AYy/9oeEy1DdC07bz1/ZXX+DqCskRd3i4JzLSTXwefzaepQrKjXC7aNM8lTHjFLDO0pDgaEiQ7jEk+HVg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, "node_modules/@remix-run/dev": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@remix-run/dev/-/dev-1.14.1.tgz", @@ -19317,6 +19438,19 @@ "react": ">=16" } }, + "node_modules/use-isomorphic-layout-effect": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz", + "integrity": "sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/use-sidecar": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", diff --git a/package.json b/package.json index 3169c618c..043cf0b8a 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@prisma/client": "^4.9.0", "@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-navigation-menu": "^1.1.1", + "@radix-ui/react-popover": "^1.0.5", "@remix-run/express": "^1.12.0", "@remix-run/node": "^1.12.0", "@remix-run/react": "^1.12.0", From 4cea48fc3890190b6eae18e6a2c6713f971138b0 Mon Sep 17 00:00:00 2001 From: fab-scm Date: Fri, 12 May 2023 13:20:52 +0200 Subject: [PATCH 2/7] introduce new shadcn components --- app/components/ui/calendar.tsx | 25 +++-- app/components/ui/dialog.tsx | 128 ++++++++++++++++++++++ app/components/ui/tabs.tsx | 55 ++++++++++ app/components/ui/toast.tsx | 128 ++++++++++++++++++++++ app/components/ui/toaster.tsx | 35 ++++++ app/components/ui/use-toast.ts | 189 +++++++++++++++++++++++++++++++++ 6 files changed, 553 insertions(+), 7 deletions(-) create mode 100644 app/components/ui/dialog.tsx create mode 100644 app/components/ui/tabs.tsx create mode 100644 app/components/ui/toast.tsx create mode 100644 app/components/ui/toaster.tsx create mode 100644 app/components/ui/use-toast.ts diff --git a/app/components/ui/calendar.tsx b/app/components/ui/calendar.tsx index 53c5cab6a..e1bef38ed 100644 --- a/app/components/ui/calendar.tsx +++ b/app/components/ui/calendar.tsx @@ -13,29 +13,39 @@ function Calendar({ className, classNames, showOutsideDays = true, + // disabled={ after: new Date() }, ...props }: CalendarProps) { + return ( ( + +
+ {children} +
+
+) +DialogPortal.displayName = DialogPrimitive.Portal.displayName + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/app/components/ui/tabs.tsx b/app/components/ui/tabs.tsx new file mode 100644 index 000000000..b706ec977 --- /dev/null +++ b/app/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/app/components/ui/toast.tsx b/app/components/ui/toast.tsx new file mode 100644 index 000000000..e06db931f --- /dev/null +++ b/app/components/ui/toast.tsx @@ -0,0 +1,128 @@ +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import type { VariantProps } from "class-variance-authority" +import { cva } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full", + { + variants: { + variant: { + default: "bg-background border", + destructive: + "group destructive border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/app/components/ui/toaster.tsx b/app/components/ui/toaster.tsx new file mode 100644 index 000000000..e2233852a --- /dev/null +++ b/app/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" +import { useToast } from "@/components/ui/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/app/components/ui/use-toast.ts b/app/components/ui/use-toast.ts new file mode 100644 index 000000000..91630054f --- /dev/null +++ b/app/components/ui/use-toast.ts @@ -0,0 +1,189 @@ +// Inspired by react-hot-toast library +import * as React from "react" + +import type { ToastActionElement, ToastProps } from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_VALUE + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +interface Toast extends Omit {} + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } From 36ad8fff0d596e16f672b7caf6bcfd9eafb4e87a Mon Sep 17 00:00:00 2001 From: fab-scm Date: Fri, 12 May 2023 13:27:32 +0200 Subject: [PATCH 3/7] introduce time-filter component --- app/components/Map/Map.tsx | 3 +- app/components/header/navBar/NavBar.tsx | 109 ++++- .../header/navBar/time-filter/time-filter.tsx | 426 ++++++++++++++++++ app/components/search/Search.tsx | 1 + app/routes/explore.tsx | 2 + package-lock.json | 176 +++++--- package.json | 5 +- public/locales/de/navbar.json | 15 + public/locales/en/navbar.json | 15 + styles/app.css | 1 + 10 files changed, 667 insertions(+), 86 deletions(-) create mode 100644 app/components/header/navBar/time-filter/time-filter.tsx create mode 100644 public/locales/de/navbar.json create mode 100644 public/locales/en/navbar.json diff --git a/app/components/Map/Map.tsx b/app/components/Map/Map.tsx index 086e4c934..b9cdec49f 100644 --- a/app/components/Map/Map.tsx +++ b/app/components/Map/Map.tsx @@ -29,9 +29,10 @@ const Map = forwardRef( style={{ width: "100%", height: "100%", - position: "absolute", + position: "fixed", top: 0, left: 0, + zIndex: 2, }} touchZoomRotate={false} {...props} diff --git a/app/components/header/navBar/NavBar.tsx b/app/components/header/navBar/NavBar.tsx index e4cbeb2be..e35a6676e 100644 --- a/app/components/header/navBar/NavBar.tsx +++ b/app/components/header/navBar/NavBar.tsx @@ -1,18 +1,36 @@ import React, { useEffect, useRef } from "react"; import Search from "~/components/search/Search"; -import { SunIcon, CalendarDaysIcon } from "@heroicons/react/24/outline"; -import { Calendar } from "@/components/ui/calendar"; +import { SunIcon, CalendarIcon } from "@heroicons/react/24/outline"; +import { TimeFilter } from "~/components/header/navBar/time-filter/time-filter"; +import type { DateRange } from "react-day-picker"; +import getUserLocale from "get-user-locale"; +import { format } from "date-fns"; +import { useTranslation } from "react-i18next"; +import type { Device } from "@prisma/client"; interface NavBarProps { - devices: any; + devices: Device[]; } +type ValuePiece = Date | string | null; + +type Value = ValuePiece + + export default function NavBar(props: NavBarProps) { + let { t } = useTranslation("navbar"); + + const [timeState, setTimeState] = React.useState("live"); + const [isDialogOpen, setIsDialogOpen] = React.useState(false); const [isHovered, setIsHovered] = React.useState(false); const [showSearch, setShowSearch] = React.useState(false); - const [date, setDate] = React.useState(new Date()); const searchRef = useRef(null); + const [value, onChange] = React.useState(null); + const [dateRange, setDateRange] = React.useState(undefined); + const [singleDate, setSingleDate] = React.useState(undefined) + const userLocaleString = getUserLocale(); + /** * Focus the search input */ @@ -54,6 +72,12 @@ export default function NavBar(props: NavBarProps) { }; }); + useEffect(() => { + console.log("dateRange", dateRange); + console.log("time", value); + console.log("singleDate", singleDate); + }, [dateRange, value, singleDate]); + return (
{!isHovered && !showSearch ? ( @@ -65,12 +89,50 @@ export default function NavBar(props: NavBarProps) { >
-
Temperatur
+
+ {t("temperature_label")} +
- +
- 01.01.2022 - 05.01-2022 + {timeState === "live" ? ( + Live + ) : timeState === "pointintime" ? ( + singleDate ? ( + <> + {format( + singleDate, + userLocaleString === "de" ? "dd/MM/yyyy" : "MM/dd/yyyy" + )} + + ) : ( + t("date_picker_label") + ) + ) : timeState === "timeperiod" ? ( + dateRange?.from ? ( + dateRange.to ? ( + <> + {format( + dateRange.from, + userLocaleString === "de" ? "dd/MM/yyyy" : "MM/dd/yyyy" + )}{" "} + -{" "} + {format( + dateRange.to, + userLocaleString === "de" ? "dd/MM/yyyy" : "MM/dd/yyyy" + )} + + ) : ( + format( + dateRange.from, + userLocaleString === "de" ? "dd/MM/yyyy" : "MM/dd/yyyy" + ) + ) + ) : ( + t("date_range_picker_label") + ) + ) : null}
@@ -78,7 +140,9 @@ export default function NavBar(props: NavBarProps) {
{ - setIsHovered(false); + if (!isDialogOpen) { + setIsHovered(false); + } }} > -
-
- +
+
) : (
{ setIsHovered(false); }} diff --git a/app/components/header/navBar/time-filter/time-filter.tsx b/app/components/header/navBar/time-filter/time-filter.tsx new file mode 100644 index 000000000..3e3856c16 --- /dev/null +++ b/app/components/header/navBar/time-filter/time-filter.tsx @@ -0,0 +1,426 @@ +"use client"; + +import * as React from "react"; +// import { useSearchParams, useSubmit } from "@remix-run/react"; +import { format } from "date-fns"; +import { de, enGB } from "date-fns/locale"; +import { CalendarIcon } from "@heroicons/react/24/outline"; +import { Clock, CalendarSearch, CalendarClock } from "lucide-react"; +import type { DateRange } from "react-day-picker"; + +import { cn } from "@/lib/utils"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Form } from "@remix-run/react"; +import { useToast } from "@/components/ui/use-toast"; + +import { getUserLocale } from "get-user-locale"; +import { useTranslation } from "react-i18next"; + +interface TimeFilterProps { + className?: React.HTMLAttributes["className"]; + + dateRange: DateRange | undefined; + setDateRange: (date: DateRange | undefined) => void; + + singleDate: Date | undefined; + setSingleDate: (date: Date | undefined) => void; + + isDialogOpen: boolean; + setIsDialogOpen: (open: boolean) => void; + + setIsHovered: (hovered: boolean) => void; + + timeState: string; + setTimeState: (value: string) => void; + + onChange: (timerange: any) => void; + value: any; +} + +export function TimeFilter(props: TimeFilterProps) { + // const submit = useSubmit(); + // const [searchParams] = useSearchParams(); + const { toast } = useToast(); + + let { t } = useTranslation("navbar"); + const userLocaleString = getUserLocale(); + const userLocale = userLocaleString === "de" ? de : enGB; + + const today = new Date(); + + return ( +
+ + + + + props.setIsHovered(false)} + > + + + + + {t("live_label")} + + + + {t("pointintime_label")} + + + + {t("timeperiod_label")} + + + +
+ {t("live_description")} +
+
+
{ + props.setTimeState("live"); + props.setIsDialogOpen(false); + }} + > + + + + +
+
+
+ +
+ {t("pointintime_description")} +
+
+ {props.singleDate === undefined ? ( +
+ Please select a date +
+ ) : ( +
+
+ {props.singleDate?.getDate() < 10 + ? "0" + props.singleDate?.getDate() + : props.singleDate?.getDate()} +
+
+
+ {new Intl.DateTimeFormat( + userLocaleString === "de" ? "de" : "en-GB", + { month: "long" } + ).format(props.singleDate)}{" "} + {props.singleDate?.getFullYear()} +
+
+ {new Intl.DateTimeFormat( + userLocaleString === "de" ? "de" : "en-GB", + { weekday: "long" } + ).format(props.singleDate)} +
+
+
+ )} +
+
+ { + props.setSingleDate(value); + }} + locale={userLocale} + className="mx-auto" + disabled={{ after: today }} + toMonth={today} + /> +
+
+ + +
+
+
{ + if (props.singleDate === undefined) { + e.preventDefault(); + toast({ + description: "Please select a date", + }); + } else { + props.setTimeState("pointintime"); + props.setIsDialogOpen(false); + } + }} + > + + + + + + + +
+
+
+ +
+ {t("timeperiod_description")} +
+
+ {props.dateRange === undefined || + props.dateRange.from === undefined ? ( +
+ Please select a date range +
+ ) : ( +
+
+
+ {props.dateRange?.from?.getDate() < 10 + ? "0" + props.dateRange.from?.getDate() + : props.dateRange.from?.getDate()} +
+
+
+ {new Intl.DateTimeFormat( + userLocaleString === "de" ? "de" : "en-GB", + { month: "long" } + ).format(props.dateRange.from)}{" "} + {props.dateRange.from?.getFullYear()} +
+
+ {new Intl.DateTimeFormat( + userLocaleString === "de" ? "de" : "en-GB", + { weekday: "long" } + ).format(props.dateRange.from)} +
+
+
+ +
+
+ - +
+
+ + {props.dateRange.to !== undefined ? ( +
+
+ {props.dateRange.to?.getDate() < 10 + ? "0" + props.dateRange.to?.getDate() + : props.dateRange.to?.getDate()} +
+
+
+ {new Intl.DateTimeFormat( + userLocaleString === "de" ? "de" : "en-GB", + { month: "long" } + ).format(props.dateRange.to)}{" "} + {props.dateRange.to?.getFullYear()} +
+
+ {new Intl.DateTimeFormat( + userLocaleString === "de" ? "de" : "en-GB", + { weekday: "long" } + ).format(props.dateRange.to)} +
+
+
+ ) : ( +
+ )} +
+ )} +
+
+ +
+
+ + +
+
+
{ + if ( + props.dateRange?.from === undefined || + props.dateRange?.to === undefined + ) { + e.preventDefault(); + toast({ + description: "Please select a date range", + }); + } else { + props.setTimeState("timeperiod"); + props.setIsDialogOpen(false); + } + }} + > + + + + + + + + + + +
+
+ + + +
+
+ ); +} diff --git a/app/components/search/Search.tsx b/app/components/search/Search.tsx index badc4a720..b29283e36 100644 --- a/app/components/search/Search.tsx +++ b/app/components/search/Search.tsx @@ -171,6 +171,7 @@ export default function Search(props: SearchProps) {
+

diff --git a/app/routes/explore.tsx b/app/routes/explore.tsx index 77d8f2b10..5e29fb3bf 100644 --- a/app/routes/explore.tsx +++ b/app/routes/explore.tsx @@ -26,6 +26,7 @@ import { } from "~/components/Map/Layers"; import type { Device } from "@prisma/client"; import OverlaySearch from "~/components/search/OverlaySearch"; +import { Toaster } from "~/components/ui//toaster"; export async function loader({ request }: LoaderArgs) { const devices = await getDevices(); @@ -116,6 +117,7 @@ export default function Explore() { + { showSearch ? : null }

diff --git a/package-lock.json b/package-lock.json index fa1cff3bf..9a27edd69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,9 +10,11 @@ "@heroicons/react": "^2.0.15", "@mantine/hooks": "^6.0.8", "@prisma/client": "^4.9.0", - "@radix-ui/react-dialog": "^1.0.2", + "@radix-ui/react-dialog": "^1.0.3", "@radix-ui/react-navigation-menu": "^1.1.1", "@radix-ui/react-popover": "^1.0.5", + "@radix-ui/react-tabs": "^1.0.3", + "@radix-ui/react-toast": "^1.1.3", "@remix-run/express": "^1.12.0", "@remix-run/node": "^1.12.0", "@remix-run/react": "^1.12.0", @@ -26,6 +28,7 @@ "date-fns": "^2.29.3", "express": "^4.18.2", "express-prometheus-middleware": "^1.2.0", + "get-user-locale": "^2.2.1", "i18next": "^22.4.15", "i18next-browser-languagedetector": "^7.0.1", "i18next-fs-backend": "^2.1.1", @@ -3323,6 +3326,27 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.3.tgz", + "integrity": "sha512-stjCkIoMe6h+1fWtXlA6cRfikdBzCLp3SnVk7c48cv/uy3DTGoXhN76YaOYUJuy3aEDvDIKwKR5KSmvrtPvQPQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-collection": "1.0.2", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", @@ -3335,6 +3359,50 @@ "react": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.3.tgz", + "integrity": "sha512-4CkF/Rx1GcrusI/JZ1Rvyx4okGUs6wEenWA0RG/N+CwkRhTy7t54y7BLsWUXrAz/GRbBfHQg/Odfs/RoW0CiRA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-roving-focus": "1.0.3", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.3.tgz", + "integrity": "sha512-yHFgpxi9wjbfPvpSPdYAzivCqw48eA1ofT8m/WqYOVTxKPdmQMuVKRYPlMmj4C1d6tJdFj/LBa1J4iY3fL4OwQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-collection": "1.0.2", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.3", + "@radix-ui/react-portal": "1.0.2", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-controllable-state": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-visually-hidden": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", @@ -4627,6 +4695,19 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.14.194", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.194.tgz", + "integrity": "sha512-r22s9tAS7imvBt2lyHC9B8AGwWnXaYb1tY09oyLkXDs4vArpYJzw09nj8MLx5VfciBPGIb+ZwG0ssYnEPJxn/g==" + }, + "node_modules/@types/lodash.memoize": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/lodash.memoize/-/lodash.memoize-4.1.7.tgz", + "integrity": "sha512-lGN7WeO4vO6sICVpf041Q7BX/9k1Y24Zo3FY0aUezr1QlKznpjzsDk3T3wvH8ofYzoK0QupN9TWcFAFZlyPwQQ==", + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mapbox__point-geometry": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.2.tgz", @@ -5578,38 +5659,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-node": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", - "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, - "dependencies": { - "acorn": "^7.0.0", - "acorn-walk": "^7.0.0", - "xtend": "^4.0.2" - } - }, - "node_modules/acorn-node/node_modules/acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -7723,15 +7772,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/defined": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", - "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/degenerator": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-3.0.2.tgz", @@ -7813,23 +7853,6 @@ "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" }, - "node_modules/detective": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", - "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", - "dev": true, - "dependencies": { - "acorn-node": "^1.8.2", - "defined": "^1.0.0", - "minimist": "^1.2.6" - }, - "bin": { - "detective": "bin/detective.js" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -10777,6 +10800,18 @@ "node": ">= 4.0.0" } }, + "node_modules/get-user-locale": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/get-user-locale/-/get-user-locale-2.2.1.tgz", + "integrity": "sha512-3814zipTZ2MvczOcppEXB3jXu+0HWwj5WmPI6//SeCnUIUaRXu7W4S54eQZTEPadlMZefE+jAlPOn+zY3tD4Qw==", + "dependencies": { + "@types/lodash.memoize": "^4.1.7", + "lodash.memoize": "^4.1.1" + }, + "funding": { + "url": "https://github.com/wojtekmaj/get-user-locale?sponsor=1" + } + }, "node_modules/getos": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/getos/-/getos-3.2.1.tgz", @@ -12345,6 +12380,15 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jiti": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", + "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, "node_modules/joi": { "version": "17.8.3", "resolved": "https://registry.npmjs.org/joi/-/joi-17.8.3.tgz", @@ -12751,8 +12795,7 @@ "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==" }, "node_modules/lodash.merge": { "version": "4.6.2", @@ -18475,20 +18518,20 @@ } }, "node_modules/tailwindcss": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.7.tgz", - "integrity": "sha512-B6DLqJzc21x7wntlH/GsZwEXTBttVSl1FtCzC8WP4oBc/NKef7kaax5jeihkkCEWc831/5NDJ9gRNDK6NEioQQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.0.tgz", + "integrity": "sha512-hOXlFx+YcklJ8kXiCAfk/FMyr4Pm9ck477G0m/us2344Vuj355IpoEDB5UmGAsSpTBmr+4ZhjzW04JuFXkb/fw==", "dev": true, "dependencies": { "arg": "^5.0.2", "chokidar": "^3.5.3", "color-name": "^1.1.4", - "detective": "^5.2.1", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.2.12", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", + "jiti": "^1.17.2", "lilconfig": "^2.0.6", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -18502,7 +18545,8 @@ "postcss-selector-parser": "^6.0.11", "postcss-value-parser": "^4.2.0", "quick-lru": "^5.1.1", - "resolve": "^1.22.1" + "resolve": "^1.22.1", + "sucrase": "^3.29.0" }, "bin": { "tailwind": "lib/cli.js", diff --git a/package.json b/package.json index 043cf0b8a..c43e6f95a 100644 --- a/package.json +++ b/package.json @@ -39,9 +39,11 @@ "@heroicons/react": "^2.0.15", "@mantine/hooks": "^6.0.8", "@prisma/client": "^4.9.0", - "@radix-ui/react-dialog": "^1.0.2", + "@radix-ui/react-dialog": "^1.0.3", "@radix-ui/react-navigation-menu": "^1.1.1", "@radix-ui/react-popover": "^1.0.5", + "@radix-ui/react-tabs": "^1.0.3", + "@radix-ui/react-toast": "^1.1.3", "@remix-run/express": "^1.12.0", "@remix-run/node": "^1.12.0", "@remix-run/react": "^1.12.0", @@ -55,6 +57,7 @@ "date-fns": "^2.29.3", "express": "^4.18.2", "express-prometheus-middleware": "^1.2.0", + "get-user-locale": "^2.2.1", "i18next": "^22.4.15", "i18next-browser-languagedetector": "^7.0.1", "i18next-fs-backend": "^2.1.1", diff --git a/public/locales/de/navbar.json b/public/locales/de/navbar.json new file mode 100644 index 000000000..74d257321 --- /dev/null +++ b/public/locales/de/navbar.json @@ -0,0 +1,15 @@ +{ + "date_picker_label": "Wähle einen Zeitpunkt", + "date_range_picker_label": "Wähle einen Zeitraum", + "search_label": "Suche", + "temperature_label": "Temperatur", + "date_label": "Zeitraum", + "ctrl": "Strg", + "button": "Anzeigen", + "live_label": "Live", + "live_description": "Zeige live Daten auf der Karte an. Diese Daten werden jede 5 Minuten aktualisiert. Wenn Du auf eine Station klickst kannst du dir die letzten Messungen anzeigen.", + "pointintime_label": "Zeitpunkt", + "pointintime_description": "Zeige einen historischen Zeitpunkt auf der Karte an.", + "timeperiod_label": "Zeitraum", + "timeperiod_description": "Erforsche die Entwicklung der Phänomene in einem bestimmten Zeitfenster." +} \ No newline at end of file diff --git a/public/locales/en/navbar.json b/public/locales/en/navbar.json new file mode 100644 index 000000000..39014d903 --- /dev/null +++ b/public/locales/en/navbar.json @@ -0,0 +1,15 @@ +{ + "date_picker_label": "Select a date", + "date_range_picker_label": "Select a time period", + "search_label": "Search", + "temperature_label": "Temperature", + "date_label": "Time period", + "ctrl": "Ctrl", + "button": "Show", + "live_label": "Live", + "live_description": "Show live data on the map. This data is updated every 5 minutes. If you click on a station, you can view the last measurements.", + "pointintime_label": "Point in time", + "pointintime_description": "Show a historical point in time on the map.", + "timeperiod_label": "Time period", + "timeperiod_description": "Explore the development of the phenomena in a specific time window." +} \ No newline at end of file diff --git a/styles/app.css b/styles/app.css index 67393a4e5..6f141d732 100644 --- a/styles/app.css +++ b/styles/app.css @@ -11,6 +11,7 @@ --color-gray-400: #767474; --color-green-100: #69ac54; --color-green-300: #709f61; + --color-red-500: #f10000; --color-orange-500: #f97316; --color-headerBorder: #e3e3e3; From 2295838e5a956f67169493a67eb92b9e4edfd355 Mon Sep 17 00:00:00 2001 From: fab-scm <82449462+fab-scm@users.noreply.github.com> Date: Tue, 30 May 2023 20:20:47 +0200 Subject: [PATCH 4/7] Feat/dropdown menu (#33) * add shadcn components * add shadcn components input and label * rename getSession to getUserSession * change centration from margin to justify * small change to time-filter translation variable * introduce login check and dropdown menu * add login and register routes relative to /explore * add translations for all components * add better user feedback to log out action * again user feedback change * user feedback toast * colors/toasts/login_label * workaround for toasting and remove console.logs * eslint fix * solve toast problem (better workaround) --- app/components/header/Header.tsx | 2 +- app/components/header/menu/Menu.tsx | 275 +++++++++++++++--- app/components/header/navBar/NavBar.tsx | 14 +- .../header/navBar/time-filter/time-filter.tsx | 6 +- app/components/ui/avatar.tsx | 50 ++++ app/components/ui/dropdown-menu.tsx | 200 +++++++++++++ app/components/ui/input.tsx | 25 ++ app/components/ui/label.tsx | 26 ++ app/components/ui/use-toast.ts | 4 +- app/routes/explore.tsx | 6 +- app/routes/explore/login.tsx | 228 +++++++++++++++ app/routes/explore/register.tsx | 220 ++++++++++++++ app/session.server.ts | 11 +- package-lock.json | 81 ++++++ package.json | 3 + public/locales/de/login.json | 9 + public/locales/de/menu.json | 22 ++ public/locales/de/register.json | 9 + public/locales/en/login.json | 9 + public/locales/en/menu.json | 22 ++ public/locales/en/register.json | 9 + 21 files changed, 1164 insertions(+), 67 deletions(-) create mode 100644 app/components/ui/avatar.tsx create mode 100644 app/components/ui/dropdown-menu.tsx create mode 100644 app/components/ui/input.tsx create mode 100644 app/components/ui/label.tsx create mode 100644 app/routes/explore/login.tsx create mode 100644 app/routes/explore/register.tsx create mode 100644 public/locales/de/login.json create mode 100644 public/locales/de/menu.json create mode 100644 public/locales/de/register.json create mode 100644 public/locales/en/login.json create mode 100644 public/locales/en/menu.json create mode 100644 public/locales/en/register.json diff --git a/app/components/header/Header.tsx b/app/components/header/Header.tsx index a22a6e5e7..9c2768f72 100644 --- a/app/components/header/Header.tsx +++ b/app/components/header/Header.tsx @@ -8,7 +8,7 @@ interface HeaderProps { export default function Header(props: HeaderProps) { return ( -
+
diff --git a/app/components/header/menu/Menu.tsx b/app/components/header/menu/Menu.tsx index 827cf89a5..7ec3a39bd 100644 --- a/app/components/header/menu/Menu.tsx +++ b/app/components/header/menu/Menu.tsx @@ -1,58 +1,237 @@ -// import * as Dialog from '@radix-ui/react-dialog'; -import { Bars3Icon } from '@heroicons/react/24/outline' -import { Link, useLocation } from "@remix-run/react"; -import React from 'react'; +import { Form, Link, useNavigation, useSearchParams } from "@remix-run/react"; +// import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { useToast } from "@/components/ui/use-toast"; +import { useLoaderData } from "@remix-run/react"; +import type { loader } from "~/routes/explore"; +import { + Bars3Icon, + UserCircleIcon, + CpuChipIcon, + Cog6ToothIcon, + ArrowRightOnRectangleIcon, + ArrowLeftOnRectangleIcon, + PlusCircleIcon, + GlobeAltIcon, + PuzzlePieceIcon, + QuestionMarkCircleIcon, + EnvelopeIcon, + IdentificationIcon, + LockClosedIcon, + CurrencyEuroIcon, + UserGroupIcon, + UserIcon, + ArrowTopRightOnSquareIcon, +} from "@heroicons/react/24/outline"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { useEffect, useRef, useState } from "react"; +import { useTranslation } from "react-i18next"; +export function useFirstRender() { + const firstRender = useRef(true); + + useEffect(() => { + firstRender.current = false; + }, []); + + return firstRender.current; +} export default function Menu() { + const [searchParams] = useSearchParams(); + const data = useLoaderData(); + // const url = useLocation().pathname; + const [open, setOpen] = useState(false); + const { toast } = useToast(); + const navigation = useNavigation(); + const isLoggingOut = Boolean(navigation.state === "submitting"); + const [timeToToast, setTimeToToast] = useState(false); - const [isOpen, setIsOpen] = React.useState(false); - const toggleDrawer = () => setIsOpen(!isOpen); + const { t } = useTranslation("menu"); - const location = useLocation(); + const firstRender = useFirstRender(); + useEffect(() => { + if (!firstRender && !timeToToast) { + setTimeToToast(true); + } else if (!firstRender && timeToToast) { + if (data.user === null) { + toast({ + description: t("toast_logout_success"), + }); + } + if (data.user !== null) { + const creationDate = Date.parse(data.user.createdAt); + const now = Date.now(); + const diff = now - creationDate; + console.log(diff); + if (diff < 10000) { + toast({ + description: t("toast_user_creation_success"), + }); + setTimeout(() => { + toast({ + description: t("toast_login_success"), + }); + }, 100); + } else { + console.log(data.user); + toast({ + description: t("toast_login_success"), + }); + } + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data.user, toast, firstRender]); return ( - //
- // - // - // - // - // - // - // - // {/* Fill me with customized content */} - // - // Impressum - // - // - // - // - // - // - // - //
- -
- - - -
+ + +
+ +
+
+ + + {data.user === null ? ( +
+

{t("title")}

+

+ {t("subtitle")} +

+
+ ) : ( +
+

Max Mustermann

+

+ {data.user.email} +

+
+ )} +
+ + {data.user !== null ? ( + + + + {t("profile_label")} + + + + {t("settings_label")} + + + + {t("my_devices_label")} + + + + {t("add_device_label")} + + + + ) : null} + + + + + {t("tutorials_label")} + + + + + + + {t("api_docs_label")} + + + + + + + + + {t("faq_label")} + + + + {t("contact_label")} + + + + {t("imprint_label")} + + + + {t("data_protection_label")} + + + + + + + {t("donate_label")} + + + + {t("promotion_label")} + + + + + {data.user === null ? ( + setOpen(false)} + > + + + ) : ( +
{ + setOpen(false); + // toast({ + // description: "Logging out ...", + // }); + }} + > + +
+ )} +
+
+
); } diff --git a/app/components/header/navBar/NavBar.tsx b/app/components/header/navBar/NavBar.tsx index e35a6676e..de82141b9 100644 --- a/app/components/header/navBar/NavBar.tsx +++ b/app/components/header/navBar/NavBar.tsx @@ -72,14 +72,14 @@ export default function NavBar(props: NavBarProps) { }; }); - useEffect(() => { - console.log("dateRange", dateRange); - console.log("time", value); - console.log("singleDate", singleDate); - }, [dateRange, value, singleDate]); + // useEffect(() => { + // console.log("dateRange", dateRange); + // console.log("time", value); + // console.log("singleDate", singleDate); + // }, [dateRange, value, singleDate]); return ( -
+
{!isHovered && !showSearch ? (
{timeState === "live" ? ( - Live + {t("live_label")} ) : timeState === "pointintime" ? ( singleDate ? ( <> diff --git a/app/components/header/navBar/time-filter/time-filter.tsx b/app/components/header/navBar/time-filter/time-filter.tsx index 3e3856c16..fb181d3d1 100644 --- a/app/components/header/navBar/time-filter/time-filter.tsx +++ b/app/components/header/navBar/time-filter/time-filter.tsx @@ -45,7 +45,7 @@ export function TimeFilter(props: TimeFilterProps) { // const [searchParams] = useSearchParams(); const { toast } = useToast(); - let { t } = useTranslation("navbar"); + const { t } = useTranslation("navbar"); const userLocaleString = getUserLocale(); const userLocale = userLocaleString === "de" ? de : enGB; @@ -158,7 +158,7 @@ export function TimeFilter(props: TimeFilterProps) {
{props.singleDate === undefined ? (
- Please select a date + {t("date_picker_label")}
) : (
@@ -269,7 +269,7 @@ export function TimeFilter(props: TimeFilterProps) { {props.dateRange === undefined || props.dateRange.from === undefined ? (
- Please select a date range + {t("date_range_picker_label")}
) : (
diff --git a/app/components/ui/avatar.tsx b/app/components/ui/avatar.tsx new file mode 100644 index 000000000..51e507ba9 --- /dev/null +++ b/app/components/ui/avatar.tsx @@ -0,0 +1,50 @@ +"use client" + +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/app/components/ui/dropdown-menu.tsx b/app/components/ui/dropdown-menu.tsx new file mode 100644 index 000000000..93dbd1626 --- /dev/null +++ b/app/components/ui/dropdown-menu.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" +import { Check, ChevronRight, Circle } from "lucide-react" + +import { cn } from "@/lib/utils" + +const DropdownMenu = DropdownMenuPrimitive.Root + +const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger + +const DropdownMenuGroup = DropdownMenuPrimitive.Group + +const DropdownMenuPortal = DropdownMenuPrimitive.Portal + +const DropdownMenuSub = DropdownMenuPrimitive.Sub + +const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup + +const DropdownMenuSubTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, children, ...props }, ref) => ( + + {children} + + +)) +DropdownMenuSubTrigger.displayName = + DropdownMenuPrimitive.SubTrigger.displayName + +const DropdownMenuSubContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSubContent.displayName = + DropdownMenuPrimitive.SubContent.displayName + +const DropdownMenuContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + + + +)) +DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName + +const DropdownMenuItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName + +const DropdownMenuCheckboxItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, checked, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuCheckboxItem.displayName = + DropdownMenuPrimitive.CheckboxItem.displayName + +const DropdownMenuRadioItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + {children} + +)) +DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName + +const DropdownMenuLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & { + inset?: boolean + } +>(({ className, inset, ...props }, ref) => ( + +)) +DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName + +const DropdownMenuSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName + +const DropdownMenuShortcut = ({ + className, + ...props +}: React.HTMLAttributes) => { + return ( + + ) +} +DropdownMenuShortcut.displayName = "DropdownMenuShortcut" + +export { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuCheckboxItem, + DropdownMenuRadioItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuShortcut, + DropdownMenuGroup, + DropdownMenuPortal, + DropdownMenuSub, + DropdownMenuSubContent, + DropdownMenuSubTrigger, + DropdownMenuRadioGroup, +} diff --git a/app/components/ui/input.tsx b/app/components/ui/input.tsx new file mode 100644 index 000000000..929e05f50 --- /dev/null +++ b/app/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/app/components/ui/label.tsx b/app/components/ui/label.tsx new file mode 100644 index 000000000..b32881e64 --- /dev/null +++ b/app/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { VariantProps, cva } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/app/components/ui/use-toast.ts b/app/components/ui/use-toast.ts index 91630054f..b672dcf2c 100644 --- a/app/components/ui/use-toast.ts +++ b/app/components/ui/use-toast.ts @@ -3,8 +3,8 @@ import * as React from "react" import type { ToastActionElement, ToastProps } from "@/components/ui/toast" -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 +const TOAST_LIMIT = 2 +const TOAST_REMOVE_DELAY = 3000 type ToasterToast = ToastProps & { id: string diff --git a/app/routes/explore.tsx b/app/routes/explore.tsx index 5e29fb3bf..18407c495 100644 --- a/app/routes/explore.tsx +++ b/app/routes/explore.tsx @@ -27,10 +27,14 @@ import { import type { Device } from "@prisma/client"; import OverlaySearch from "~/components/search/OverlaySearch"; import { Toaster } from "~/components/ui//toaster"; +import { getUser } from "~/session.server"; export async function loader({ request }: LoaderArgs) { const devices = await getDevices(); - return json({ devices }); + + const user = await getUser(request); + + return json({ devices, user }); } export const links: LinksFunction = () => { diff --git a/app/routes/explore/login.tsx b/app/routes/explore/login.tsx new file mode 100644 index 000000000..bc06c1952 --- /dev/null +++ b/app/routes/explore/login.tsx @@ -0,0 +1,228 @@ +import type { ActionArgs, LoaderArgs, MetaFunction } from "@remix-run/node"; +import { json, redirect } from "@remix-run/node"; +import { + Form, + Link, + useActionData, + useNavigation, + useSearchParams, +} from "@remix-run/react"; +import * as React from "react"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +import { verifyLogin } from "~/models/user.server"; +import { createUserSession, getUserId } from "~/session.server"; +import { safeRedirect, validateEmail } from "~/utils"; +import { X } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +export async function loader({ request }: LoaderArgs) { + const userId = await getUserId(request); + if (userId) return redirect("/"); + return json({}); +} + +export async function action({ request }: ActionArgs) { + const formData = await request.formData(); + const email = formData.get("email"); + const password = formData.get("password"); + const redirectTo = safeRedirect(formData.get("redirectTo"), "/"); + const remember = formData.get("remember"); + + if (!validateEmail(email)) { + return json( + { errors: { email: "Email is invalid", password: null } }, + { status: 400 } + ); + } + + if (typeof password !== "string" || password.length === 0) { + return json( + { errors: { password: "Password is required", email: null } }, + { status: 400 } + ); + } + + if (password.length < 8) { + return json( + { errors: { password: "Password is too short", email: null } }, + { status: 400 } + ); + } + + const user = await verifyLogin(email, password); + + if (!user) { + return json( + { errors: { email: "Invalid email or password", password: null } }, + { status: 400 } + ); + } + + return createUserSession({ + request, + userId: user.id, + remember: remember === "on" ? true : false, + redirectTo, + }); +} + +export const meta: MetaFunction = () => { + return { + title: "Login", + }; +}; + +export default function LoginPage() { + const [searchParams] = useSearchParams(); + const redirectTo = searchParams.get("redirectTo") || "/explore"; + const actionData = useActionData(); + const emailRef = React.useRef(null); + const passwordRef = React.useRef(null); + + const { t } = useTranslation("login"); + const navigation = useNavigation(); + const isLoggingIn = Boolean(navigation.state === "submitting"); + + React.useEffect(() => { + if (actionData?.errors?.email) { + emailRef.current?.focus(); + } else if (actionData?.errors?.password) { + passwordRef.current?.focus(); + } + }, [actionData]); + + return ( +
+ +
+ +
+ {t("login_label")} + + + +
+
+
+ +
+ + {actionData?.errors?.email && ( +
+ {actionData.errors.email} +
+ )} +
+
+ +
+ +
+ + {actionData?.errors?.password && ( +
+ {actionData.errors.password} +
+ )} +
+
+ + + +
+
+ + +
+
+ {t("no_account_label")}{" "} + + {t("register_label")} + +
+
+
+
+
+
+ ); +} diff --git a/app/routes/explore/register.tsx b/app/routes/explore/register.tsx new file mode 100644 index 000000000..43ee03e8e --- /dev/null +++ b/app/routes/explore/register.tsx @@ -0,0 +1,220 @@ +import type { ActionArgs, LoaderArgs, MetaFunction } from "@remix-run/node"; +import { json, redirect } from "@remix-run/node"; +import { + Form, + Link, + useActionData, + useNavigation, + useSearchParams, +} from "@remix-run/react"; +import * as React from "react"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +import { createUserSession, getUserId } from "~/session.server"; +import { createUser, getUserByEmail } from "~/models/user.server"; +import { safeRedirect, validateEmail } from "~/utils"; + +import { X } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +export async function loader({ request }: LoaderArgs) { + const userId = await getUserId(request); + if (userId) return redirect("/"); + return json({}); +} + +export async function action({ request }: ActionArgs) { + const formData = await request.formData(); + const email = formData.get("email"); + const password = formData.get("password"); + const redirectTo = safeRedirect(formData.get("redirectTo"), "/explore"); + + if (!validateEmail(email)) { + return json( + { errors: { email: "Email is invalid", password: null } }, + { status: 400 } + ); + } + + if (typeof password !== "string" || password.length === 0) { + return json( + { errors: { password: "Password is required", email: null } }, + { status: 400 } + ); + } + + if (password.length < 8) { + return json( + { errors: { password: "Password is too short", email: null } }, + { status: 400 } + ); + } + + const existingUser = await getUserByEmail(email); + if (existingUser) { + return json( + { + errors: { + email: "A user already exists with this email", + password: null, + }, + }, + { status: 400 } + ); + } + + const user = await createUser(email, password); + + return createUserSession({ + request, + userId: user.id, + remember: false, + redirectTo, + }); +} + +export const meta: MetaFunction = () => { + return { + title: "Sign Up", + }; +}; + +export default function RegisterDialog() { + const { t } = useTranslation("register"); + const [searchParams] = useSearchParams(); + const redirectTo = searchParams.get("redirectTo") ?? undefined; + const actionData = useActionData(); + const emailRef = React.useRef(null); + const passwordRef = React.useRef(null); + + const navigation = useNavigation(); + const isCreating = Boolean(navigation.state === "submitting"); + + React.useEffect(() => { + if (actionData?.errors?.email) { + emailRef.current?.focus(); + } else if (actionData?.errors?.password) { + passwordRef.current?.focus(); + } + }, [actionData]); + + return ( +
+ +
+ +
+ {t("register_label")} + + + +
+
+
+ +
+ + {actionData?.errors?.email && ( +
+ {actionData.errors.email} +
+ )} +
+
+ +
+ +
+ + {actionData?.errors?.password && ( +
+ {actionData.errors.password} +
+ )} +
+
+ + + +
+
+ {t("already_account_label")}{" "} + + {t("login_label")} + +
+
+
+
+
+
+ ); +} diff --git a/app/session.server.ts b/app/session.server.ts index 31a861e4b..629a65518 100644 --- a/app/session.server.ts +++ b/app/session.server.ts @@ -19,7 +19,7 @@ export const sessionStorage = createCookieSessionStorage({ const USER_SESSION_KEY = "userId"; -export async function getSession(request: Request) { +export async function getUserSession(request: Request) { const cookie = request.headers.get("Cookie"); return sessionStorage.getSession(cookie); } @@ -27,7 +27,7 @@ export async function getSession(request: Request) { export async function getUserId( request: Request ): Promise { - const session = await getSession(request); + const session = await getUserSession(request); const userId = session.get(USER_SESSION_KEY); return userId; } @@ -74,7 +74,7 @@ export async function createUserSession({ remember: boolean; redirectTo: string; }) { - const session = await getSession(request); + const session = await getUserSession(request); session.set(USER_SESSION_KEY, userId); return redirect(redirectTo, { headers: { @@ -88,8 +88,9 @@ export async function createUserSession({ } export async function logout(request: Request) { - const session = await getSession(request); - return redirect("/", { + const session = await getUserSession(request); + console.log("session", session); + return redirect("/explore", { headers: { "Set-Cookie": await sessionStorage.destroySession(session), }, diff --git a/package-lock.json b/package-lock.json index 9a27edd69..71ff4f955 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,10 @@ "@heroicons/react": "^2.0.15", "@mantine/hooks": "^6.0.8", "@prisma/client": "^4.9.0", + "@radix-ui/react-avatar": "^1.0.2", "@radix-ui/react-dialog": "^1.0.3", + "@radix-ui/react-dropdown-menu": "^2.0.4", + "@radix-ui/react-label": "^2.0.1", "@radix-ui/react-navigation-menu": "^1.1.1", "@radix-ui/react-popover": "^1.0.5", "@radix-ui/react-tabs": "^1.0.3", @@ -3081,6 +3084,22 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.0.2.tgz", + "integrity": "sha512-XRL8z2l9V7hRLCPjHWg/34RBPZUGpmOjmsRSNvIh2DI28GyIWDChbcsDUVc63MzOItk6Q83Ob2KK8k2FUlXlGA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.0.2.tgz", @@ -3173,6 +3192,25 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.0.4.tgz", + "integrity": "sha512-y6AT9+MydyXcByivdK1+QpjWoKaC7MLjkS/cH1Q3keEyMvDkiY85m8o2Bi6+Z1PPUlCsMULopxagQOSfN0wahg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-menu": "2.0.4", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.0.tgz", @@ -3211,6 +3249,49 @@ "react": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.1.tgz", + "integrity": "sha512-qcfbS3B8hTYmEO44RNcXB6pegkxRsJIbdxTMu0PEX0Luv5O2DvTIwwVYxQfUwLpM88EL84QRPLOLgwUSApMsLQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.0.4.tgz", + "integrity": "sha512-mzKR47tZ1t193trEqlQoJvzY4u9vYfVH16ryBrVrCAGZzkgyWnMQYEZdUkM7y8ak9mrkKtJiqB47TlEnubeOFQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-collection": "1.0.2", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-direction": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.3", + "@radix-ui/react-focus-guards": "1.0.0", + "@radix-ui/react-focus-scope": "1.0.2", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-popper": "1.1.1", + "@radix-ui/react-portal": "1.0.2", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-roving-focus": "1.0.3", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "aria-hidden": "^1.1.1", + "react-remove-scroll": "2.5.5" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-navigation-menu": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.1.2.tgz", diff --git a/package.json b/package.json index c43e6f95a..de45dcdf7 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,10 @@ "@heroicons/react": "^2.0.15", "@mantine/hooks": "^6.0.8", "@prisma/client": "^4.9.0", + "@radix-ui/react-avatar": "^1.0.2", "@radix-ui/react-dialog": "^1.0.3", + "@radix-ui/react-dropdown-menu": "^2.0.4", + "@radix-ui/react-label": "^2.0.1", "@radix-ui/react-navigation-menu": "^1.1.1", "@radix-ui/react-popover": "^1.0.5", "@radix-ui/react-tabs": "^1.0.3", diff --git a/public/locales/de/login.json b/public/locales/de/login.json new file mode 100644 index 000000000..f22c19a36 --- /dev/null +++ b/public/locales/de/login.json @@ -0,0 +1,9 @@ +{ + "login_label": "Einloggen", + "email_label": "E-Mail", + "password_label": "Passwort", + "transition_label": "Wird eingeloggt...", + "remember_label": "Eingeloggt bleiben", + "no_account_label": "Noch kein Konto?", + "register_label": "Registrieren" +} diff --git a/public/locales/de/menu.json b/public/locales/de/menu.json new file mode 100644 index 000000000..b5dede065 --- /dev/null +++ b/public/locales/de/menu.json @@ -0,0 +1,22 @@ +{ + "title": "Wilkommen", + "subtitle": "Bitte loggen Sie sich ein, um mehr Inhalte zu sehen.", + "profile_label": "Profil", + "settings_label": "Einstellungen", + "my_devices_label": "Meine Geräte", + "add_device_label": "Gerät hinzufügen", + "tutorials_label": "Tutorials", + "api_docs_label": "API Dokumentation", + "faq_label": "FAQ", + "contact_label": "Kontakt", + "imprint_label": "Impressum", + "data_protection_label": "Datenschutz", + "donate_label": "Spenden", + "promotion_label": "Förderung", + "login_label": "Einloggen", + "logout_label": "Ausloggen", + + "toast_login_success": "Erfolgreich eingeloggt", + "toast_logout_success": "Erfolgreich ausgeloggt", + "toast_user_creation_success": "Benutzer erfolgreich erstellt" +} \ No newline at end of file diff --git a/public/locales/de/register.json b/public/locales/de/register.json new file mode 100644 index 000000000..5f087ae93 --- /dev/null +++ b/public/locales/de/register.json @@ -0,0 +1,9 @@ +{ + "register_label": "Registrieren", + "email_label": "E-Mail", + "password_label": "Passwort", + "account_label": "Konto erstellen", + "transition_label": "Erstelle Konto...", + "already_account_label": "Bereits ein Konto?", + "login_label": "Einloggen" +} \ No newline at end of file diff --git a/public/locales/en/login.json b/public/locales/en/login.json new file mode 100644 index 000000000..ed697335d --- /dev/null +++ b/public/locales/en/login.json @@ -0,0 +1,9 @@ +{ + "login_label": "Log in", + "email_label": "Email", + "password_label": "Password", + "transition_label": "Logging in...", + "remember_label": "Remember me", + "no_account_label": "Don't have an account?", + "register_label": "Sign up" +} \ No newline at end of file diff --git a/public/locales/en/menu.json b/public/locales/en/menu.json new file mode 100644 index 000000000..a5f746022 --- /dev/null +++ b/public/locales/en/menu.json @@ -0,0 +1,22 @@ +{ + "title": "Welcome", + "subtitle": "Please sign in to see more content", + "profile_label": "Profile", + "settings_label": "Settings", + "my_devices_label": "My Devices", + "add_device_label": "Add Device", + "tutorials_label": "Tutorials", + "api_docs_label": "API Docs", + "faq_label": "FAQ", + "contact_label": "Contact", + "imprint_label": "Imprint", + "data_protection_label": "Data Protection", + "donate_label": "Donate", + "promotion_label": "Promotion", + "login_label": "Log in", + "logout_label": "Log out", + + "toast_login_success": "Successfully logged in", + "toast_logout_success": "Successfully logged out", + "toast_user_creation_success": "Successfully created account" +} \ No newline at end of file diff --git a/public/locales/en/register.json b/public/locales/en/register.json new file mode 100644 index 000000000..3864f0cb0 --- /dev/null +++ b/public/locales/en/register.json @@ -0,0 +1,9 @@ +{ + "register_label": "Sign up", + "email_label": "Email", + "password_label": "Password", + "account_label": "Create account", + "transition_label": "Creating account...", + "already_account_label": "Already have an account?", + "login_label": "Log in" +} \ No newline at end of file From 1347e272acc48c0a8d2d490f3ed5ccac002e0793 Mon Sep 17 00:00:00 2001 From: fab-scm Date: Tue, 30 May 2023 23:36:33 +0200 Subject: [PATCH 5/7] retain searchParams on login/logout/search --- app/components/header/menu/Menu.tsx | 5 ++--- app/components/search/SearchList.tsx | 23 +++++++++++------------ app/components/search/SearchListItem.tsx | 19 +++++++++---------- app/routes/explore/login.tsx | 2 +- app/routes/explore/register.tsx | 2 +- app/routes/logout.tsx | 7 +++++-- app/session.server.ts | 15 ++++++++++----- 7 files changed, 39 insertions(+), 34 deletions(-) diff --git a/app/components/header/menu/Menu.tsx b/app/components/header/menu/Menu.tsx index 7ec3a39bd..bda72542e 100644 --- a/app/components/header/menu/Menu.tsx +++ b/app/components/header/menu/Menu.tsx @@ -46,8 +46,8 @@ export function useFirstRender() { export default function Menu() { const [searchParams] = useSearchParams(); + const redirectTo = (searchParams.size > 0 ? "/explore?" + searchParams.toString() : "/explore") const data = useLoaderData(); - // const url = useLocation().pathname; const [open, setOpen] = useState(false); const { toast } = useToast(); const navigation = useNavigation(); @@ -71,7 +71,6 @@ export default function Menu() { const creationDate = Date.parse(data.user.createdAt); const now = Date.now(); const diff = now - creationDate; - console.log(diff); if (diff < 10000) { toast({ description: t("toast_user_creation_success"), @@ -82,7 +81,6 @@ export default function Menu() { }); }, 100); } else { - console.log(data.user); toast({ description: t("toast_login_success"), }); @@ -220,6 +218,7 @@ export default function Menu() { // }); }} > +