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/App.tsx | 39 +++- 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 ++++++++++++ ui/src/state/client.ts | 16 ++ ui/src/state/models.ts | 16 +- ui/src/state/store.ts | 3 +- 8 files changed, 546 insertions(+), 18 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') diff --git a/ui/src/App.tsx b/ui/src/App.tsx index ae6ebdb1..54b62c46 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -1,6 +1,19 @@ import "./App.css"; import { useState, ReactElement } from "react"; +import { useStore } from "@/state/store"; + +import Button, { ButtonStyle } from "@/components/Button"; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; + import { Cog6ToothIcon, HomeIcon, @@ -16,6 +29,7 @@ function classNames(...classes: any) { import Home from "./pages/Home.tsx"; import History from "./pages/History.tsx"; import Dotfiles from "./pages/Dotfiles.tsx"; +import LoginOrRegister from "./components/LoginOrRegister.tsx"; enum Section { Home, @@ -39,6 +53,8 @@ function App() { // I think hashrouter may work, but I'd rather avoiding thinking of them as // pages const [section, setSection] = useState(Section.Home); + const user = useStore((state) => state.user); + console.log(user); const navigation = [ { @@ -96,16 +112,19 @@ function App() {
  • - - + {user && !user.isLoggedIn() && ( + + + + )}
  • 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, +} diff --git a/ui/src/state/client.ts b/ui/src/state/client.ts index f43683c1..5ec0d8a7 100644 --- a/ui/src/state/client.ts +++ b/ui/src/state/client.ts @@ -11,3 +11,19 @@ export async function sessionToken(): Promise { export async function settings(): Promise { return await invoke("config"); } + +export async function login( + username: string, + password: string, + key: string, +): Promise { + return await invoke("login", { username, password, key }); +} + +export async function register( + username: string, + email: string, + password: string, +): Promise { + return await invoke("register", { username, email, password }); +} diff --git a/ui/src/state/models.ts b/ui/src/state/models.ts index 57db44ae..c1d97f4b 100644 --- a/ui/src/state/models.ts +++ b/ui/src/state/models.ts @@ -1,12 +1,18 @@ import Database from "@tauri-apps/plugin-sql"; -export interface User { - username: string; +export class User { + username: string | null; + + constructor(username: string) { + this.username = username; + } + + isLoggedIn(): boolean { + return this.username !== "" && this.username !== null; + } } -export const DefaultUser: User = { - username: "", -}; +export const DefaultUser: User = new User(""); export interface HomeInfo { historyCount: number; diff --git a/ui/src/state/store.ts b/ui/src/state/store.ts index 5e2570bb..6746c1fb 100644 --- a/ui/src/state/store.ts +++ b/ui/src/state/store.ts @@ -94,6 +94,7 @@ export const useStore = create()((set, get) => ({ session = await sessionToken(); } catch (e) { console.log("Not logged in, so not refreshing user"); + set({ user: DefaultUser }); return; } let url = config.sync_address + "/api/v0/me"; @@ -105,7 +106,7 @@ export const useStore = create()((set, get) => ({ }); let me = await res.json(); - set({ user: me }); + set({ user: new User(me.username) }); }, historyNextPage: (query?: string) => { -- cgit v1.3.1