From 467f89c104df40904ef4c6b408507e90fe661724 Mon Sep 17 00:00:00 2001 From: Ellie Huxtable Date: Thu, 30 May 2024 12:49:22 +0100 Subject: feat(ui): add login/register dialog (#2056) --- ui/src/components/Button.tsx | 20 ++ ui/src/components/LoginOrRegister.tsx | 341 ++++++++++++++++++++++++++++++++++ ui/src/components/history/Stats.tsx | 9 +- ui/src/components/ui/dialog.tsx | 120 ++++++++++++ 4 files changed, 488 insertions(+), 2 deletions(-) create mode 100644 ui/src/components/Button.tsx create mode 100644 ui/src/components/LoginOrRegister.tsx create mode 100644 ui/src/components/ui/dialog.tsx (limited to 'ui/src/components') diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx new file mode 100644 index 00000000..5f7e1160 --- /dev/null +++ b/ui/src/components/Button.tsx @@ -0,0 +1,20 @@ +export enum ButtonStyle { + PrimarySm = "bg-emerald-500 hover:bg-emerald-600", + PrimarySmFill = "bg-emerald-500 hover:bg-emerald-600 w-full text-sm", +} + +interface ButtonProps { + text: string; + style: ButtonStyle; +} + +export default function Button(props: ButtonProps) { + return ( + + ); +} diff --git a/ui/src/components/LoginOrRegister.tsx b/ui/src/components/LoginOrRegister.tsx new file mode 100644 index 00000000..c13c314c --- /dev/null +++ b/ui/src/components/LoginOrRegister.tsx @@ -0,0 +1,341 @@ +import Logo from "@/assets/logo-light.svg"; +import { useState } from "react"; + +import { login, register } from "@/state/client"; +import { useStore } from "@/state/store"; + +interface LoginProps { + toggleRegister: () => void; +} + +function Login(props: LoginProps) { + const refreshUser = useStore((state) => state.refreshUser); + const [errors, setErrors] = useState(null); + + const doLogin = async (e: React.FormEvent) => { + e.preventDefault(); + + const form = e.currentTarget; + const username = form.username.value; + const password = form.password.value; + const key = form.key.value; + + console.log("Logging in..."); + try { + await login(username, password, key); + refreshUser(); + console.log("Logged in"); + } catch (e) { + console.error(e); + setErrors(e); + } + }; + + return ( + <> +
+
+ Atuin + +

+ Sign in to your account +

+ +

+ Backup and sync your data across devices. All data is end-to-end + encrypted and stored securely in the cloud. +

+
+ +
+
+
+ +
+ +
+
+ +
+
+ +
+ {/* You can't right now. Sorry. Validate emails first. + + Forgot password? + + */} +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ +
+
+ + {errors && ( +

{errors}

+ )} + +

+ Not a member?{" "} + { + e.preventDefault(); + props.toggleRegister(); + }} + > + Register + +

+
+
+ + ); +} + +interface RegisterProps { + toggleLogin: () => void; +} + +function Register(props: RegisterProps) { + const refreshUser = useStore((state) => state.refreshUser); + const [errors, setErrors] = useState(null); + + const doRegister = async (e: React.FormEvent) => { + e.preventDefault(); + + const form = e.currentTarget; + const username = form.username.value; + const email = form.email.value; + const password = form.password.value; + + console.log("Logging in..."); + try { + await register(username, email, password); + refreshUser(); + console.log("Logged in"); + } catch (e) { + console.error(e); + setErrors(e); + } + }; + + return ( + <> +
+
+ Atuin + +

+ Register for an account +

+ +

+ Backup and sync your data across devices. All data is end-to-end + encrypted and stored securely in the cloud. +

+
+ +
+
+
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+ {/* You can't right now. Sorry. Validate emails first. + + Forgot password? + + */} +
+
+
+ +
+
+ +
+ +
+
+ + {errors && ( +

{errors}

+ )} + +

+ Already have an account?{" "} + { + e.preventDefault(); + props.toggleLogin(); + }} + > + Login + +

+
+
+ + ); +} + +export default function LoginOrRegister() { + let [login, setLogin] = useState(false); + + if (login) { + return setLogin(false)} />; + } + + return setLogin(true)} />; +} diff --git a/ui/src/components/history/Stats.tsx b/ui/src/components/history/Stats.tsx index 9e2c9a64..f399eaf0 100644 --- a/ui/src/components/history/Stats.tsx +++ b/ui/src/components/history/Stats.tsx @@ -13,8 +13,13 @@ import { function renderLoading() { return ( -
- +
+
+ +
+
+

Crunching the latest numbers...

+
); } diff --git a/ui/src/components/ui/dialog.tsx b/ui/src/components/ui/dialog.tsx new file mode 100644 index 00000000..c23630eb --- /dev/null +++ b/ui/src/components/ui/dialog.tsx @@ -0,0 +1,120 @@ +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogClose = DialogPrimitive.Close + +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, + DialogPortal, + DialogOverlay, + DialogClose, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} -- cgit v1.3.1