diff --git a/app/components/header/index.tsx b/app/components/header/index.tsx index dbc52aa5a..23897149b 100644 --- a/app/components/header/index.tsx +++ b/app/components/header/index.tsx @@ -8,7 +8,7 @@ interface HeaderProps { export default function Header(props: HeaderProps) { return ( -
+
diff --git a/app/components/header/menu/index.tsx b/app/components/header/menu/index.tsx index 827cf89a5..a722739b7 100644 --- a/app/components/header/menu/index.tsx +++ b/app/components/header/menu/index.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(); + // @ts-ignore + const redirectTo = (searchParams.size > 0 ? "/explore?" + searchParams.toString() : "/explore") + const data = useLoaderData(); + 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; + if (diff < 10000) { + toast({ + description: t("toast_user_creation_success"), + }); + setTimeout(() => { + toast({ + description: t("toast_login_success"), + }); + }, 100); + } else { + 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/nav-bar/nav-bar.tsx b/app/components/header/nav-bar/nav-bar.tsx index b569895cb..e0e3970a2 100644 --- a/app/components/header/nav-bar/nav-bar.tsx +++ b/app/components/header/nav-bar/nav-bar.tsx @@ -1,22 +1,36 @@ import React, { useEffect, useRef } from "react"; import Search from "~/components/search"; -import { - SunIcon, - CalendarDaysIcon, - MagnifyingGlassIcon, -} from "@heroicons/react/24/outline"; -import { Calendar } from "@/components/ui/calendar"; +import { SunIcon, CalendarDaysIcon, MagnifyingGlassIcon } 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 */ @@ -58,8 +72,14 @@ 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 ? (
-
Temperatur
+
+ {t("temperature_label")} +
-
+
Suche @@ -81,7 +103,43 @@ export default function NavBar(props: NavBarProps) {
- 01.01.2022 - 05.01-2022 + {timeState === "live" ? ( + {t("live_label")} + ) : 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}
@@ -89,7 +147,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..fb181d3d1 --- /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(); + + const { 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 ? ( +
+ {t("date_picker_label")} +
+ ) : ( +
+
+ {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 ? ( +
+ {t("date_range_picker_label")} +
+ ) : ( +
+
+
+ {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/map/map.tsx b/app/components/map/map.tsx index c78e73aff..1d153c5f0 100644 --- a/app/components/map/map.tsx +++ b/app/components/map/map.tsx @@ -25,7 +25,7 @@ const Map = forwardRef( style={{ width: "100%", height: "100%", - position: "absolute", + position: "fixed", top: 0, left: 0, }} diff --git a/app/components/search/index.tsx b/app/components/search/index.tsx index 0234bab65..2e8a6577f 100644 --- a/app/components/search/index.tsx +++ b/app/components/search/index.tsx @@ -171,6 +171,7 @@ export default function Search(props: SearchProps) {
+

diff --git a/app/components/search/search-list-item.tsx b/app/components/search/search-list-item.tsx index 54fe6221a..93d1c4a08 100644 --- a/app/components/search/search-list-item.tsx +++ b/app/components/search/search-list-item.tsx @@ -1,5 +1,6 @@ // import type { LngLatBounds, LngLatLike } from "react-map-gl"; import { useNavigate } from "@remix-run/react"; +import { useSearchParams } from "@remix-run/react"; import { useMap } from "react-map-gl"; import { goTo } from "~/lib/search-map-helper"; @@ -18,6 +19,11 @@ interface SearchListItemProps { export default function SearchListItem(props: SearchListItemProps) { const navigate = useNavigate(); const { osem } = useMap(); + const [searchParams] = useSearchParams(); + const navigateTo = + (props.type === "device" ? `/explore/${props.item.deviceId}` : "/explore") + + // @ts-ignore + (searchParams.size > 0 ? "?" + searchParams.toString() : ""); // console.log(props.index) @@ -27,11 +33,7 @@ export default function SearchListItem(props: SearchListItemProps) { onClick={() => { goTo(osem, props.item); props.setShowSearch(false); - navigate( - props.type === "device" - ? `/explore/${props.item.deviceId}` - : "/explore" - ); + navigate(navigateTo); }} data-active={props.active} onMouseEnter={() => { diff --git a/app/components/search/search-list.tsx b/app/components/search/search-list.tsx index 8a422dbe4..c4993fed0 100644 --- a/app/components/search/search-list.tsx +++ b/app/components/search/search-list.tsx @@ -1,6 +1,6 @@ import { useState, useEffect, useCallback } from "react"; import { useMap } from "react-map-gl"; -import { useNavigate } from "@remix-run/react"; +import { useNavigate, useSearchParams } from "@remix-run/react"; import { CpuChipIcon, @@ -62,7 +62,31 @@ export default function SearchList(props: SearchListProps) { var searchResultsAll = props.searchResultsDevice.concat( props.searchResultsLocation ); - var selected = searchResultsAll[cursor]; + const [selected, setSelected] = useState(searchResultsAll[cursor]); + + const [searchParams] = useSearchParams(); + const [navigateTo, setNavigateTo] = useState( + (selected.type === "device" + ? `/explore/${selected.deviceId}` + : "/explore") + + // @ts-ignore + (searchParams.size > 0 ? "?" + searchParams.toString() : "") + ); + + useEffect(() => { + setSelected(searchResultsAll[cursor]); + }, [cursor, searchResultsAll]); + + useEffect(() => { + if (selected.type === "device") { + // @ts-ignore + setNavigateTo(`/explore/${selected.deviceId}` + (searchParams.size > 0 ? "?" + searchParams.toString() : "")); + } else if (selected.type === "location") { + // @ts-ignore + setNavigateTo("/explore" + (searchParams.size > 0 ? "?" + searchParams.toString() : "")); + } + console.log(navigateTo); + }, [selected, searchParams, navigateTo]); const setShowSearchCallback = useCallback((state: boolean) => { props.setShowSearch(state); @@ -83,13 +107,17 @@ export default function SearchList(props: SearchListProps) { if (length !== 0 && enterPress) { goTo(osem, selected); setShowSearchCallback(false); - navigate( - selected.type === "device" - ? `/explore/${selected.deviceId}` - : "/explore" - ); + navigate(navigateTo); } - }, [enterPress, length, osem, navigate, selected, setShowSearchCallback]); + }, [ + enterPress, + length, + osem, + navigate, + selected, + setShowSearchCallback, + navigateTo, + ]); const handleDigitPress = (event: any) => { if ( @@ -97,16 +125,11 @@ export default function SearchList(props: SearchListProps) { Number(event.key) <= length && event.ctrlKey ) { - selected = searchResultsAll[Number(event.key) - 1]; event.preventDefault(); setCursor(Number(event.key) - 1); goTo(osem, selected); - setTimeout(() => setShowSearchCallback(false), 500); - navigate( - selected.type === "device" - ? `/explore/${selected.deviceId}` - : "/explore" - ); + setTimeout(() => {setShowSearchCallback(false); navigate(navigateTo);}, 500); + } }; 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/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/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/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/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..b672dcf2c --- /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 = 2 +const TOAST_REMOVE_DELAY = 3000 + +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 } diff --git a/app/routes/explore.tsx b/app/routes/explore.tsx index f916a0ae2..220d60cd7 100644 --- a/app/routes/explore.tsx +++ b/app/routes/explore.tsx @@ -26,10 +26,15 @@ import { } from "~/components/map/layers"; import type { Device } from "@prisma/client"; import OverlaySearch from "~/components/search/overlay-search"; +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 = () => { @@ -119,13 +124,8 @@ export default function Explore() { - {showSearch ? ( - - ) : null} + + { showSearch ? : null }
diff --git a/app/routes/explore/login.tsx b/app/routes/explore/login.tsx new file mode 100644 index 000000000..93d6b3270 --- /dev/null +++ b/app/routes/explore/login.tsx @@ -0,0 +1,229 @@ +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(); + // @ts-ignore + const redirectTo = (searchParams.size > 0 ? "/explore?" + searchParams.toString() : "/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..78372b91e --- /dev/null +++ b/app/routes/explore/register.tsx @@ -0,0 +1,221 @@ +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(); + // @ts-ignore + const redirectTo = (searchParams.size > 0 ? "/explore?" + searchParams.toString() : "/explore"); + 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/routes/logout.tsx b/app/routes/logout.tsx index 5def3ddfd..a22cd74f8 100644 --- a/app/routes/logout.tsx +++ b/app/routes/logout.tsx @@ -4,9 +4,12 @@ import { redirect } from "@remix-run/node"; import { logout } from "~/session.server"; export async function action({ request }: ActionArgs) { - return logout(request); + const formData = await request.formData(); + const redirectTo = formData.get("redirectTo")?.toString() || "/explore"; + console.log(redirectTo); + return logout({request, redirectTo}); } export async function loader() { - return redirect("/"); + return redirect("/explore/login"); } diff --git a/app/session.server.ts b/app/session.server.ts index 31a861e4b..210eec943 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; } @@ -39,7 +39,7 @@ export async function getUser(request: Request) { const user = await getUserById(userId); if (user) return user; - throw await logout(request); + throw await logout({request: request, redirectTo: "/explore"}); } export async function requireUserId( @@ -60,7 +60,7 @@ export async function requireUser(request: Request) { const user = await getUserById(userId); if (user) return user; - throw await logout(request); + throw await logout({request: request, redirectTo: "/explore"}); } export async function createUserSession({ @@ -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: { @@ -87,9 +87,15 @@ export async function createUserSession({ }); } -export async function logout(request: Request) { - const session = await getSession(request); - return redirect("/", { +export async function logout({ + request, + redirectTo, +}: { + request: Request; + redirectTo: string; +}) { + const session = await getUserSession(request); + return redirect(redirectTo, { headers: { "Set-Cookie": await sessionStorage.destroySession(session), }, diff --git a/package-lock.json b/package-lock.json index d8efb147e..2c296e1d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,8 +10,13 @@ "@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-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", "@radix-ui/react-toast": "^1.1.3", "@remix-run/express": "^1.12.0", "@remix-run/node": "^1.12.0", @@ -28,6 +33,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", @@ -2633,6 +2639,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", @@ -3048,6 +3080,35 @@ "@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-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", @@ -3140,6 +3201,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", @@ -3178,6 +3258,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", @@ -3204,6 +3327,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", @@ -3244,6 +3416,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", @@ -3256,6 +3449,26 @@ "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", @@ -3337,6 +3550,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", @@ -3350,6 +3587,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", @@ -4540,6 +4785,19 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.14.195", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz", + "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==" + }, + "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-gl": { "version": "2.7.10", "resolved": "https://registry.npmjs.org/@types/mapbox-gl/-/mapbox-gl-2.7.10.tgz", @@ -5477,38 +5735,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", @@ -7675,15 +7901,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", @@ -7765,23 +7982,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", @@ -10783,6 +10983,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", @@ -12342,6 +12554,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", @@ -12740,8 +12961,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", @@ -18634,20 +18854,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", @@ -18661,7 +18881,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", @@ -19597,6 +19818,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 08027d8af..2eaf4e3e1 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,13 @@ "@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-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", "@radix-ui/react-toast": "^1.1.3", "@remix-run/express": "^1.12.0", "@remix-run/node": "^1.12.0", @@ -57,6 +62,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/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/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/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/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/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 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;