feat(dashboard): login and invite redesign (#9214)
**What** - new UI for login and invite pages ---   --- CLOSES CC-131
This commit is contained in:
BIN
packages/admin/dashboard/public/medusa-avatar.png
Normal file
BIN
packages/admin/dashboard/public/medusa-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./logo-box"
|
||||
export * from "./avatar-box"
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
/>,
|
||||
]}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user