feat(admin,admin-ui,medusa): Add Medusa Admin plugin (#3334)
This commit is contained in:
committed by
GitHub
parent
d6b1ad1ccd
commit
40de54b010
13
packages/admin-ui/ui/src/pages/404.tsx
Normal file
13
packages/admin-ui/ui/src/pages/404.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import React from "react"
|
||||
import SEO from "../components/seo"
|
||||
import Layout from "../components/templates/layout"
|
||||
|
||||
const NotFoundPage = () => (
|
||||
<Layout>
|
||||
<SEO title="404: Not found" />
|
||||
<h1>NOT FOUND</h1>
|
||||
<p>You just hit a route that doesn't exist... the sadness.</p>
|
||||
</Layout>
|
||||
)
|
||||
|
||||
export default NotFoundPage
|
||||
66
packages/admin-ui/ui/src/pages/a.tsx
Normal file
66
packages/admin-ui/ui/src/pages/a.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { DndProvider } from "react-dnd"
|
||||
import { HTML5Backend } from "react-dnd-html5-backend"
|
||||
import { useHotkeys } from "react-hotkeys-hook"
|
||||
import { Route, Routes, useNavigate } from "react-router-dom"
|
||||
import PrivateRoute from "../components/private-route"
|
||||
import SEO from "../components/seo"
|
||||
import Layout from "../components/templates/layout"
|
||||
import { WRITE_KEY } from "../constants/analytics"
|
||||
import Collections from "../domain/collections"
|
||||
import Customers from "../domain/customers"
|
||||
import Discounts from "../domain/discounts"
|
||||
import GiftCards from "../domain/gift-cards"
|
||||
import Inventory from "../domain/inventory"
|
||||
import Oauth from "../domain/oauth"
|
||||
import Orders from "../domain/orders"
|
||||
import DraftOrders from "../domain/orders/draft-orders"
|
||||
import Pricing from "../domain/pricing"
|
||||
import ProductsRoute from "../domain/products"
|
||||
import PublishableApiKeys from "../domain/publishable-api-keys"
|
||||
import SalesChannels from "../domain/sales-channels"
|
||||
import Settings from "../domain/settings"
|
||||
import { AnalyticsProvider } from "../providers/analytics-provider"
|
||||
|
||||
const IndexPage = () => {
|
||||
const navigate = useNavigate()
|
||||
useHotkeys("g + o", () => navigate("/a/orders"))
|
||||
useHotkeys("g + p", () => navigate("/a/products"))
|
||||
|
||||
return (
|
||||
<PrivateRoute>
|
||||
<DashboardRoutes />
|
||||
</PrivateRoute>
|
||||
)
|
||||
}
|
||||
|
||||
const DashboardRoutes = () => {
|
||||
return (
|
||||
<AnalyticsProvider writeKey={WRITE_KEY}>
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Layout>
|
||||
<SEO title="Medusa" />
|
||||
<Routes>
|
||||
<Route path="oauth/:app_name" element={<Oauth />} />
|
||||
<Route path="products/*" element={<ProductsRoute />} />
|
||||
<Route path="collections/*" element={<Collections />} />
|
||||
<Route path="gift-cards/*" element={<GiftCards />} />
|
||||
<Route path="orders/*" element={<Orders />} />
|
||||
<Route path="draft-orders/*" element={<DraftOrders />} />
|
||||
<Route path="discounts/*" element={<Discounts />} />
|
||||
<Route path="customers/*" element={<Customers />} />
|
||||
<Route path="pricing/*" element={<Pricing />} />
|
||||
<Route path="settings/*" element={<Settings />} />
|
||||
<Route path="sales-channels/*" element={<SalesChannels />} />
|
||||
<Route
|
||||
path="publishable-api-keys/*"
|
||||
element={<PublishableApiKeys />}
|
||||
/>
|
||||
<Route path="inventory/*" element={<Inventory />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
</DndProvider>
|
||||
</AnalyticsProvider>
|
||||
)
|
||||
}
|
||||
|
||||
export default IndexPage
|
||||
20
packages/admin-ui/ui/src/pages/index.tsx
Normal file
20
packages/admin-ui/ui/src/pages/index.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useEffect } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import Spinner from "../components/atoms/spinner"
|
||||
import SEO from "../components/seo"
|
||||
|
||||
const IndexPage = () => {
|
||||
const navigate = useNavigate()
|
||||
useEffect(() => {
|
||||
navigate("/a/orders")
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="bg-grey-5 text-grey-90 flex h-screen w-full items-center justify-center">
|
||||
<SEO title="Home" />
|
||||
<Spinner variant="secondary" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default IndexPage
|
||||
229
packages/admin-ui/ui/src/pages/invite.tsx
Normal file
229
packages/admin-ui/ui/src/pages/invite.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
import ConfettiGenerator from "confetti-js"
|
||||
import { useAdminAcceptInvite } from "medusa-react"
|
||||
import qs from "qs"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { decodeToken } from "react-jwt"
|
||||
import { Link, useLocation, useNavigate } from "react-router-dom"
|
||||
import Button from "../components/fundamentals/button"
|
||||
import LongArrowRightIcon from "../components/fundamentals/icons/long-arrow-right-icon"
|
||||
import MedusaIcon from "../components/fundamentals/icons/medusa-icon"
|
||||
import MedusaVice from "../components/fundamentals/icons/medusa-vice"
|
||||
import SigninInput from "../components/molecules/input-signin"
|
||||
import SEO from "../components/seo"
|
||||
import LoginLayout from "../components/templates/login-layout"
|
||||
import useNotification from "../hooks/use-notification"
|
||||
import { getErrorMessage } from "../utils/error-messages"
|
||||
|
||||
type formValues = {
|
||||
password: string
|
||||
repeat_password: string
|
||||
first_name: string
|
||||
last_name: string
|
||||
}
|
||||
|
||||
const InvitePage = () => {
|
||||
const location = useLocation()
|
||||
const parsed = qs.parse(location.search.substring(1))
|
||||
const [signUp, setSignUp] = useState(false)
|
||||
|
||||
let token: Object | null = null
|
||||
if (parsed?.token) {
|
||||
try {
|
||||
token = decodeToken(parsed.token as string)
|
||||
} catch (e) {
|
||||
token = null
|
||||
}
|
||||
}
|
||||
|
||||
const [passwordMismatch, setPasswordMismatch] = useState(false)
|
||||
const [ready, setReady] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
const confettiSettings = {
|
||||
target: "confetti-canvas",
|
||||
start_from_edge: true,
|
||||
size: 3,
|
||||
clock: 25,
|
||||
colors: [
|
||||
[251, 146, 60],
|
||||
[167, 139, 250],
|
||||
[251, 146, 60],
|
||||
[96, 165, 250],
|
||||
[45, 212, 191],
|
||||
[250, 204, 21],
|
||||
[232, 121, 249],
|
||||
],
|
||||
max: 26,
|
||||
}
|
||||
const confetti = new ConfettiGenerator(confettiSettings)
|
||||
confetti.render()
|
||||
|
||||
return () => confetti.clear()
|
||||
}, [])
|
||||
|
||||
const { register, handleSubmit, formState } = useForm<formValues>({
|
||||
defaultValues: {
|
||||
first_name: "",
|
||||
last_name: "",
|
||||
password: "",
|
||||
repeat_password: "",
|
||||
},
|
||||
})
|
||||
|
||||
const accept = useAdminAcceptInvite()
|
||||
const navigate = useNavigate()
|
||||
const notification = useNotification()
|
||||
|
||||
const handleAcceptInvite = (data: formValues) => {
|
||||
setPasswordMismatch(false)
|
||||
|
||||
if (data.password !== data.repeat_password) {
|
||||
setPasswordMismatch(true)
|
||||
return
|
||||
}
|
||||
|
||||
accept.mutate(
|
||||
{
|
||||
token: parsed.token as string,
|
||||
user: {
|
||||
first_name: data.first_name,
|
||||
last_name: data.last_name,
|
||||
password: data.password,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
navigate("/login")
|
||||
},
|
||||
onError: (err) => {
|
||||
notification("Error", getErrorMessage(err), "error")
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
formState.dirtyFields.password &&
|
||||
formState.dirtyFields.repeat_password &&
|
||||
formState.dirtyFields.first_name &&
|
||||
formState.dirtyFields.last_name
|
||||
) {
|
||||
setReady(true)
|
||||
} else {
|
||||
setReady(false)
|
||||
}
|
||||
}, [formState])
|
||||
|
||||
return (
|
||||
<>
|
||||
{signUp ? (
|
||||
<LoginLayout>
|
||||
<SEO title="Create Account" />
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="flex min-h-[600px] bg-grey-0 rounded-rounded justify-center">
|
||||
<form
|
||||
className="flex flex-col py-12 w-full px-[120px] items-center"
|
||||
onSubmit={handleSubmit(handleAcceptInvite)}
|
||||
>
|
||||
<MedusaIcon />
|
||||
{!token ? (
|
||||
<div className="h-full flex flex-col gap-y-2 text-center items-center justify-center">
|
||||
<span className="inter-large-semibold text-grey-90">
|
||||
You signup link is invalid
|
||||
</span>
|
||||
<span className="inter-base-regular mt-2 text-grey-50">
|
||||
Contact your administrator to obtain a valid signup link
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="inter-2xlarge-semibold mt-4 text-grey-90">
|
||||
Welcome to the team!
|
||||
</span>
|
||||
<span className="inter-base-regular text-grey-50 mt-2 mb-large">
|
||||
Create your account below👇🏼
|
||||
</span>
|
||||
<SigninInput
|
||||
placeholder="First name"
|
||||
{...register("first_name", { required: true })}
|
||||
autoComplete="given-name"
|
||||
/>
|
||||
<SigninInput
|
||||
placeholder="Last name"
|
||||
{...register("last_name", { required: true })}
|
||||
autoComplete="family-name"
|
||||
/>
|
||||
<SigninInput
|
||||
placeholder="Password"
|
||||
type={"password"}
|
||||
{...register("password", { required: true })}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<SigninInput
|
||||
placeholder="Repeat password"
|
||||
type={"password"}
|
||||
{...register("repeat_password", { required: true })}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
{passwordMismatch && (
|
||||
<span className="text-rose-50 w-full mt-2 inter-small-regular">
|
||||
The two passwords are not the same
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
className="w-full mt-base"
|
||||
loading={formState.isSubmitting}
|
||||
disabled={!ready}
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
<Link
|
||||
to="/login"
|
||||
className="inter-small-regular text-grey-50 mt-large"
|
||||
>
|
||||
Already signed up? Log in
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</LoginLayout>
|
||||
) : (
|
||||
<div className="bg-grey-90 h-screen w-full overflow-hidden">
|
||||
<div className="z-10 flex-grow flex flex-col items-center justify-center h-full absolute inset-0 max-w-[1080px] mx-auto">
|
||||
<MedusaVice className="mb-3xlarge" />
|
||||
<div className="flex flex-col items-center max-w-3xl text-center">
|
||||
<h1 className="inter-3xlarge-semibold text-grey-0 mb-base">
|
||||
You have been invited to join the team
|
||||
</h1>
|
||||
<p className="inter-xlarge-regular text-grey-50">
|
||||
You can now join the Medusa Store team. Sign up below and get
|
||||
started with your Medusa Admin account right away.
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4xlarge">
|
||||
<Button
|
||||
size="large"
|
||||
variant="primary"
|
||||
className="w-[280px]"
|
||||
onClick={() => setSignUp(true)}
|
||||
>
|
||||
Sign up
|
||||
<LongArrowRightIcon size={20} className="pt-1" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<canvas id="confetti-canvas" />
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default InvitePage
|
||||
38
packages/admin-ui/ui/src/pages/login.tsx
Normal file
38
packages/admin-ui/ui/src/pages/login.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import clsx from "clsx"
|
||||
import React, { useState } from "react"
|
||||
import MedusaIcon from "../components/fundamentals/icons/medusa-icon"
|
||||
import LoginCard from "../components/organisms/login-card"
|
||||
import ResetTokenCard from "../components/organisms/reset-token-card"
|
||||
import SEO from "../components/seo"
|
||||
import LoginLayout from "../components/templates/login-layout"
|
||||
|
||||
const LoginPage = () => {
|
||||
const [resetPassword, setResetPassword] = useState(false)
|
||||
|
||||
return (
|
||||
<LoginLayout>
|
||||
<SEO title="Login" />
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div
|
||||
className={clsx(
|
||||
"flex min-h-[600px] w-[640px] bg-grey-0 rounded-rounded justify-center transition-['min-height'] duration-300",
|
||||
{
|
||||
"min-h-[480px]": resetPassword,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex flex-col pt-12 w-full px-[120px] items-center">
|
||||
<MedusaIcon />
|
||||
{resetPassword ? (
|
||||
<ResetTokenCard goBack={() => setResetPassword(false)} />
|
||||
) : (
|
||||
<LoginCard toResetPassword={() => setResetPassword(true)} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</LoginLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginPage
|
||||
158
packages/admin-ui/ui/src/pages/reset-password.tsx
Normal file
158
packages/admin-ui/ui/src/pages/reset-password.tsx
Normal file
@@ -0,0 +1,158 @@
|
||||
import { useAdminResetPassword } from "medusa-react"
|
||||
import qs from "qs"
|
||||
import React, { useEffect, useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { decodeToken } from "react-jwt"
|
||||
import { useLocation, useNavigate } from "react-router-dom"
|
||||
import Button from "../components/fundamentals/button"
|
||||
import MedusaIcon from "../components/fundamentals/icons/medusa-icon"
|
||||
import SigninInput from "../components/molecules/input-signin"
|
||||
import SEO from "../components/seo"
|
||||
import LoginLayout from "../components/templates/login-layout"
|
||||
import { getErrorMessage } from "../utils/error-messages"
|
||||
|
||||
type formValues = {
|
||||
password: string
|
||||
repeat_password: string
|
||||
}
|
||||
|
||||
const ResetPasswordPage = () => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const parsed = qs.parse(location.search.substring(1))
|
||||
|
||||
let token: { email: string } | null = null
|
||||
if (parsed?.token) {
|
||||
try {
|
||||
token = decodeToken(parsed.token as string)
|
||||
} catch (e) {
|
||||
token = null
|
||||
}
|
||||
}
|
||||
|
||||
const [passwordMismatch, setPasswordMismatch] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [ready, setReady] = useState(false)
|
||||
const email = (token?.email || parsed?.email || "") as string
|
||||
|
||||
const { register, handleSubmit, formState } = useForm<formValues>({
|
||||
defaultValues: {
|
||||
password: "",
|
||||
repeat_password: "",
|
||||
},
|
||||
})
|
||||
const reset = useAdminResetPassword()
|
||||
|
||||
const handleAcceptInvite = (data: formValues) => {
|
||||
setPasswordMismatch(false)
|
||||
setError(null)
|
||||
|
||||
if (data.password !== data.repeat_password) {
|
||||
setPasswordMismatch(true)
|
||||
return
|
||||
}
|
||||
|
||||
reset.mutate(
|
||||
{
|
||||
token: parsed.token as string,
|
||||
password: data.password,
|
||||
email: email,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
navigate("/login")
|
||||
},
|
||||
onError: (err) => {
|
||||
setError(getErrorMessage(err))
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
formState.dirtyFields.password &&
|
||||
formState.dirtyFields.repeat_password
|
||||
) {
|
||||
setReady(true)
|
||||
} else {
|
||||
setReady(false)
|
||||
}
|
||||
}, [formState])
|
||||
|
||||
return (
|
||||
<LoginLayout>
|
||||
<SEO title="Reset Password" />
|
||||
<div className="flex h-full w-full items-center justify-center">
|
||||
<div className="flex min-h-[540px] bg-grey-0 rounded-rounded justify-center">
|
||||
<form
|
||||
className="flex flex-col py-12 w-full px-[120px] items-center"
|
||||
onSubmit={handleSubmit(handleAcceptInvite)}
|
||||
>
|
||||
<MedusaIcon />
|
||||
{!token ? (
|
||||
<div className="h-full flex flex-col gap-y-2 text-center items-center justify-center">
|
||||
<span className="inter-large-semibold text-grey-90">
|
||||
You reset link is invalid
|
||||
</span>
|
||||
<span className="inter-base-regular text-grey-50 mt-2">
|
||||
Please try resetting your password again
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="inter-2xlarge-semibold mt-4 text-grey-90">
|
||||
Reset your password
|
||||
</span>
|
||||
<span className="inter-base-regular text-grey-50 mt-2 mb-xlarge">
|
||||
Choose a new password below 👇🏼
|
||||
</span>
|
||||
<SigninInput
|
||||
placeholder="Email"
|
||||
name="first_name"
|
||||
value={email}
|
||||
readOnly
|
||||
/>
|
||||
<SigninInput
|
||||
placeholder="Password"
|
||||
type={"password"}
|
||||
{...register("password", { required: true })}
|
||||
autoComplete="new-password"
|
||||
/>
|
||||
<SigninInput
|
||||
placeholder="Confirm password"
|
||||
type={"password"}
|
||||
{...register("repeat_password", { required: true })}
|
||||
autoComplete="new-password"
|
||||
className="mb-0"
|
||||
/>
|
||||
{error && (
|
||||
<span className="text-rose-50 w-full mt-xsmall inter-small-regular">
|
||||
The two passwords are not the same
|
||||
</span>
|
||||
)}
|
||||
{passwordMismatch && (
|
||||
<span className="text-rose-50 w-full mt-xsmall inter-small-regular">
|
||||
The two passwords are not the same
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
className="w-full mt-base rounded-rounded"
|
||||
loading={formState.isSubmitting}
|
||||
disabled={!ready}
|
||||
>
|
||||
Reset Password
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</LoginLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResetPasswordPage
|
||||
Reference in New Issue
Block a user