feat(dashboard): login and invite redesign (#9214)

**What**
- new UI for login and invite pages

---

![Screenshot 2024-09-20 at 15 24 31](https://github.com/user-attachments/assets/abaea120-6e93-4962-9865-bd52c0f67fb9)

![Screenshot 2024-09-20 at 15 24 40](https://github.com/user-attachments/assets/037fa81c-66a5-4764-aff1-5b5f9ac102d2)

---

CLOSES CC-131
This commit is contained in:
Frane Polić
2024-09-23 09:12:20 +02:00
committed by GitHub
parent cebffc7a11
commit 89bf88ee23
6 changed files with 167 additions and 80 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,51 @@
import { motion } from "framer-motion"
import { IconAvatar } from "../icon-avatar"
export default function AvatarBox({ checked }: { checked?: boolean }) {
return (
<IconAvatar
size="xlarge"
className="bg-ui-button-neutral shadow-buttons-neutral after:button-neutral-gradient relative mb-4 flex size-14 items-center justify-center rounded-xl after:inset-0 after:content-['']"
>
{checked && (
<motion.div
className="absolute -right-[5px] -top-1 flex size-5 items-center justify-center rounded-full border-[0.5px] border-[rgba(3,7,18,0.2)] bg-[#3B82F6] bg-gradient-to-b from-white/0 to-white/20 shadow-[0px_1px_2px_0px_rgba(3,7,18,0.12),0px_1px_2px_0px_rgba(255,255,255,0.10)_inset,0px_-1px_5px_0px_rgba(255,255,255,0.10)_inset,0px_0px_0px_0px_rgba(3,7,18,0.06)_inset]"
initial={{ opacity: 0, scale: 0.5 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
duration: 1.2,
delay: 0.8,
ease: [0, 0.71, 0.2, 1.01],
}}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
>
<motion.path
d="M5.8335 10.4167L9.16683 13.75L14.1668 6.25"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
initial={{ pathLength: 0, opacity: 0 }}
animate={{ pathLength: 1, opacity: 1 }}
transition={{
duration: 1.3,
delay: 1.1,
bounce: 0.6,
ease: [0.1, 0.8, 0.2, 1.01],
}}
/>
</svg>
</motion.div>
)}
<img width={44} height={44} src="/medusa-avatar.png" />
</IconAvatar>
)
}

View File

@@ -1 +1,2 @@
export * from "./logo-box"
export * from "./avatar-box"

View File

@@ -115,6 +115,7 @@
"close": "Close",
"showMore": "Show more",
"continue": "Continue",
"continueWithEmail": "Continue with Email",
"addReason": "Add Reason",
"addNote": "Add Note",
"reset": "Reset",
@@ -546,7 +547,7 @@
"inventory": {
"notManaged": "Not managed",
"manageItems": "Manage inventory items",
"notManagedDesc":"Inventory is not managed for this variant. Turn on Manage Inventory to track the variant's inventory.",
"notManagedDesc": "Inventory is not managed for this variant. Turn on Manage Inventory to track the variant's inventory.",
"manageKit": "Manage inventory kit",
"navigateToItem": "Go to inventory item",
"actions": {
@@ -2338,19 +2339,20 @@
},
"login": {
"forgotPassword": "Forgot password? - <0>Reset</0>",
"title": "Log in",
"hint": "to continue to Medusa"
"title": "Welcome to Medusa",
"hint": "Sign in to access the account area"
},
"invite": {
"title": "Create your account",
"hint": "to continue to Medusa",
"title": "Welcome to Medusa",
"hint": "Create you account below",
"backToLogin": "Back to login",
"createAccount": "Create account",
"alreadyHaveAccount": "Already have an account? - <0>Log in</0>",
"emailTooltip": "Your email cannot be changed. If you would like to use another email, a new invite must be sent.",
"invalidInvite": "The invite is invalid or has expired.",
"successTitle": "Your account has been created",
"successTitle": "Your account has is registered",
"successHint": "Get started with Medusa Admin right away.",
"successAction": "Sign in to start using Medusa",
"successAction": "Start Medusa Admin",
"invalidTokenTitle": "Your invite token is invalid",
"invalidTokenHint": "Try requesting a new invite link.",
"passwordMismatch": "Passwords do not match",

View File

@@ -1,18 +1,18 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Alert, Button, Heading, Input, Text, toast } from "@medusajs/ui"
import { Alert, Button, Heading, Hint, Input, Text, toast } from "@medusajs/ui"
import { AnimatePresence, motion } from "framer-motion"
import i18n from "i18next"
import { useState } from "react"
import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
import { useTranslation } from "react-i18next"
import { decodeToken } from "react-jwt"
import { Link, useSearchParams } from "react-router-dom"
import * as z from "zod"
import { Form } from "../../components/common/form"
import { LogoBox } from "../../components/common/logo-box"
import { useSignUpWithEmailPass } from "../../hooks/api/auth"
import { useAcceptInvite } from "../../hooks/api/invites"
import { isFetchError } from "../../lib/is-fetch-error"
import AvatarBox from "../../components/common/logo-box/avatar-box"
const CreateAccountSchema = z
.object({
@@ -50,23 +50,9 @@ export const Invite = () => {
const isValidInvite = invite && validateDecodedInvite(invite)
return (
<div className="bg-ui-bg-base relative flex min-h-dvh w-dvw items-center justify-center p-4">
<div className="flex w-full max-w-[300px] flex-col items-center">
<LogoBox
className="mb-4"
checked={success}
containerTransition={{
duration: 1.2,
delay: 0.8,
ease: [0, 0.71, 0.2, 1.01],
}}
pathTransition={{
duration: 1.3,
delay: 1.1,
bounce: 0.6,
ease: [0.1, 0.8, 0.2, 1.01],
}}
/>
<div className="bg-ui-bg-subtle relative flex min-h-dvh w-dvw items-center justify-center p-4">
<div className="flex w-full max-w-[280px] flex-col items-center">
<AvatarBox checked={success} />
<div className="max-h-[557px] w-full will-change-contents">
{isValidInvite ? (
<AnimatePresence>
@@ -150,19 +136,13 @@ const LoginLink = () => {
return (
<div className="flex w-full flex-col items-center">
<div className="my-6 h-px w-full border-b border-dotted" />
<span className="text-ui-fg-subtle txt-small">
<Trans
t={t}
i18nKey="invite.alreadyHaveAccount"
components={[
<Link
key="login-link"
to="/login"
className="text-ui-fg-interactive transition-fg hover:text-ui-fg-interactive-hover focus-visible:text-ui-fg-interactive-hover outline-none"
/>,
]}
/>
</span>
<Link
key="login-link"
to="/login"
className="text-ui-fg-interactive txt-small !text-ui-fg-base font-medium transition-fg hover:text-ui-fg-interactive-hover focus-visible:text-ui-fg-interactive-hover outline-none"
>
{t("invite.backToLogin")}
</Link>
</div>
)
}
@@ -250,6 +230,14 @@ const CreateView = ({
}
})
const serverError = form.formState.errors.root?.message
const validationError =
form.formState.errors.email?.message ||
form.formState.errors.password?.message ||
form.formState.errors.repeat_password?.message ||
form.formState.errors.first_name?.message ||
form.formState.errors.last_name?.message
return (
<div className="flex w-full flex-col items-center">
<div className="mb-4 flex flex-col items-center">
@@ -260,18 +248,21 @@ const CreateView = ({
</div>
<Form {...form}>
<form onSubmit={handleSubmit} className="flex w-full flex-col gap-y-6">
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-2">
<Form.Field
control={form.control}
name="email"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.email")}</Form.Label>
<Form.Control>
<Input autoComplete="off" {...field} />
<Input
autoComplete="off"
{...field}
className="bg-ui-bg-field-component"
placeholder={t("fields.email")}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
@@ -282,11 +273,14 @@ const CreateView = ({
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.firstName")}</Form.Label>
<Form.Control>
<Input autoComplete="given-name" {...field} />
<Input
autoComplete="given-name"
{...field}
className="bg-ui-bg-field-component"
placeholder={t("fields.firstName")}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
@@ -297,11 +291,14 @@ const CreateView = ({
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.lastName")}</Form.Label>
<Form.Control>
<Input autoComplete="family-name" {...field} />
<Input
autoComplete="family-name"
{...field}
className="bg-ui-bg-field-component"
placeholder={t("fields.lastName")}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
@@ -312,15 +309,15 @@ const CreateView = ({
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.password")}</Form.Label>
<Form.Control>
<Input
autoComplete="new-password"
type="password"
{...field}
className="bg-ui-bg-field-component"
placeholder={t("fields.password")}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
@@ -331,22 +328,33 @@ const CreateView = ({
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.repeatPassword")}</Form.Label>
<Form.Control>
<Input autoComplete="off" type="password" {...field} />
<Input
autoComplete="off"
type="password"
{...field}
className="bg-ui-bg-field-component"
placeholder={t("fields.repeatPassword")}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
{form.formState.errors.root && (
{validationError && (
<div className="text-center mt-6">
<Hint className="inline-flex" variant={"error"}>
{validationError}
</Hint>
</div>
)}
{serverError && (
<Alert
className="p-2 bg-white items-center"
dismissible
variant="error"
dismissible={false}
className="text-balance"
>
{form.formState.errors.root.message}
{serverError}
</Alert>
)}
</div>
@@ -381,6 +389,14 @@ const SuccessView = () => {
{t("invite.successAction")}
</Link>
</Button>
<Link
key="login-link"
to="/login"
className="text-ui-fg-interactive txt-small !text-ui-fg-base transition-fg hover:text-ui-fg-interactive-hover focus-visible:text-ui-fg-interactive-hover mt-3 font-medium outline-none"
>
{t("invite.backToLogin")}
</Link>
</div>
)
}

View File

@@ -1,18 +1,18 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { Alert, Button, Heading, Input, Text } from "@medusajs/ui"
import { Alert, Button, Heading, Hint, Input, Text } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import { Trans, useTranslation } from "react-i18next"
import { Link, useLocation, useNavigate } from "react-router-dom"
import * as z from "zod"
import { Divider } from "../../components/common/divider"
import { Form } from "../../components/common/form"
import { LogoBox } from "../../components/common/logo-box"
import { useSignInWithEmailPassword } from "../../hooks/api/auth"
import after from "virtual:medusa/widgets/login/after"
import before from "virtual:medusa/widgets/login/before"
import { isFetchError } from "../../lib/is-fetch-error"
import AvatarBox from "../../components/common/logo-box/avatar-box"
const LoginSchema = z.object({
email: z.string().email(),
@@ -68,11 +68,14 @@ export const Login = () => {
})
const serverError = form.formState.errors?.root?.serverError?.message
const validationError =
form.formState.errors.email?.message ||
form.formState.errors.password?.message
return (
<div className="bg-ui-bg-base flex min-h-dvh w-dvw items-center justify-center">
<div className="m-4 flex w-full max-w-[300px] flex-col items-center">
<LogoBox className="mb-4" />
<div className="bg-ui-bg-subtle flex min-h-dvh w-dvw items-center justify-center">
<div className="m-4 flex w-full max-w-[280px] flex-col items-center">
<AvatarBox />
<div className="mb-4 flex flex-col items-center">
<Heading>{t("login.title")}</Heading>
<Text size="small" className="text-ui-fg-subtle text-center">
@@ -92,18 +95,21 @@ export const Login = () => {
onSubmit={handleSubmit}
className="flex w-full flex-col gap-y-6"
>
<div className="flex flex-col gap-y-4">
<div className="flex flex-col gap-y-1">
<Form.Field
control={form.control}
name="email"
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.email")}</Form.Label>
<Form.Control>
<Input autoComplete="email" {...field} />
<Input
autoComplete="email"
{...field}
className="bg-ui-bg-field-component"
placeholder={t("fields.email")}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
@@ -114,29 +120,41 @@ export const Login = () => {
render={({ field }) => {
return (
<Form.Item>
<Form.Label>{t("fields.password")}</Form.Label>
<Form.Label>{}</Form.Label>
<Form.Control>
<Input
type="password"
autoComplete="current-password"
{...field}
className="bg-ui-bg-field-component"
placeholder={t("fields.password")}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
{validationError && (
<div className="text-center">
<Hint className="inline-flex" variant={"error"}>
{validationError}
</Hint>
</div>
)}
{serverError && (
<Alert
className="p-2 bg-white items-center"
dismissible
variant="error"
>
{serverError}
</Alert>
)}
<Button className="w-full" type="submit" isLoading={isPending}>
{t("actions.continue")}
{t("actions.continueWithEmail")}
</Button>
</form>
{serverError && (
<Alert className="mt-4" dismissible variant="error">
{serverError}
</Alert>
)}
</Form>
{after.widgets.map((w, i) => {
return (
@@ -146,15 +164,14 @@ export const Login = () => {
)
})}
</div>
<Divider variant="dashed" className="my-6" />
<span className="text-ui-fg-subtle txt-small">
<span className="text-ui-fg-muted txt-small my-6">
<Trans
i18nKey="login.forgotPassword"
components={[
<Link
key="reset-password-link"
to="/reset-password"
className="text-ui-fg-interactive transition-fg hover:text-ui-fg-interactive-hover focus-visible:text-ui-fg-interactive-hover outline-none"
className="text-ui-fg-interactive transition-fg hover:text-ui-fg-interactive-hover focus-visible:text-ui-fg-interactive-hover font-medium outline-none"
/>,
]}
/>