wip: Sign in / Role fixes
This commit is contained in:
parent
944bc5a1d1
commit
ce7f7c65e0
2 changed files with 94 additions and 84 deletions
|
@ -8,22 +8,23 @@ import { useBackend } from '@freesewing/react/hooks/useBackend'
|
||||||
import { Link as DefaultLink } from '@freesewing/react/components/Link'
|
import { Link as DefaultLink } from '@freesewing/react/components/Link'
|
||||||
import { LockIcon, PlusIcon } from '@freesewing/react/components/Icon'
|
import { LockIcon, PlusIcon } from '@freesewing/react/components/Icon'
|
||||||
import { Spinner } from '@freesewing/react/components/Spinner'
|
import { Spinner } from '@freesewing/react/components/Spinner'
|
||||||
|
import { H1, H2, H3 } from '@freesewing/react/components/Heading'
|
||||||
|
|
||||||
//import { ConsentForm, ns as gdprNs } from 'shared/components/gdpr/form.mjs'
|
//import { ConsentForm, ns as gdprNs } from 'shared/components/gdpr/form.mjs'
|
||||||
|
|
||||||
const ConsentForm = () => null
|
const ConsentForm = () => null
|
||||||
|
|
||||||
const Wrap = ({ children }) => (
|
const Wrap = ({ children }) => (
|
||||||
<div className="m-auto max-w-xl text-center mt-8 p-8">{children}</div>
|
<div className="tw-m-auto tw-max-w-xl tw-text-center tw-mt-8 tw-p-8">{children}</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
const ContactSupport = ({ Link = false }) => {
|
const ContactSupport = ({ Link = false }) => {
|
||||||
if (!Link) Link = DefaultLink
|
if (!Link) Link = DefaultLink
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center justify-center gap-4 mt-8">
|
<div className="tw-flex tw-flex-row tw-items-center tw-justify-center tw-gap-4 tw-mt-8">
|
||||||
<Link href="/support" className="btn btn-success w-full">
|
<Link href="/support" className="tw-daisy-btn tw-daisy-btn-success tw-w-full">
|
||||||
{t('contactSupport')}
|
Contact Support
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -35,16 +36,19 @@ const AuthRequired = ({ Link, banner }) => {
|
||||||
return (
|
return (
|
||||||
<Wrap>
|
<Wrap>
|
||||||
{banner}
|
{banner}
|
||||||
<h2>Authentication Required</h2>
|
<H3>Authentication Required</H3>
|
||||||
<p>This functionality requires a FreeSewing account</p>
|
<p>This functionality requires a FreeSewing account</p>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 mt-8">
|
<div className="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-2 tw-mt-8">
|
||||||
<Link href="/signup" className={`${horFlexClasses} daisy-btn daisy-btn-secondary w-full`}>
|
<Link
|
||||||
|
href="/signup"
|
||||||
|
className={`${horFlexClasses} tw-daisy-btn tw-daisy-btn-secondary tw-w-full`}
|
||||||
|
>
|
||||||
<PlusIcon />
|
<PlusIcon />
|
||||||
Sign Up
|
Sign Up
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/signin"
|
href="/signin"
|
||||||
className={`${horFlexClasses} daisy-btn daisy-btn-secondary daisy-btn-outline w-full`}
|
className={`${horFlexClasses} tw-daisy-btn tw-daisy-btn-secondary tw-daisy-btn-outline tw-w-full`}
|
||||||
>
|
>
|
||||||
<LockIcon />
|
<LockIcon />
|
||||||
Sign In
|
Sign In
|
||||||
|
@ -60,12 +64,12 @@ const AccountInactive = ({ Link, banner }) => {
|
||||||
return (
|
return (
|
||||||
<Wrap>
|
<Wrap>
|
||||||
{banner}
|
{banner}
|
||||||
<h1>{t('accountInactive')}</h1>
|
<H3>Account Inactive</H3>
|
||||||
<p>{t('accountInactiveMsg')}</p>
|
<p>You must activate your account via the signup link we sent you.</p>
|
||||||
<p>{t('signupAgain')}</p>
|
<p>If you cannot find the link, you can receive a new one by signing up again.</p>
|
||||||
<div className="flex flex-row items-center justify-center gap-4 mt-8">
|
<div className="tw-flex tw-flex-row tw-items-center tw-justify-center tw-gap-4 tw-mt-8">
|
||||||
<Link href="/signup" className="btn btn-primary w-full">
|
<Link href="/signup" className="tw-daisy-btn tw-daisy-btn-primary tw-w-full">
|
||||||
{t('signUp')}
|
Sign Up
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</Wrap>
|
</Wrap>
|
||||||
|
@ -75,36 +79,42 @@ const AccountInactive = ({ Link, banner }) => {
|
||||||
const AccountDisabled = ({ banner }) => (
|
const AccountDisabled = ({ banner }) => (
|
||||||
<Wrap>
|
<Wrap>
|
||||||
{banner}
|
{banner}
|
||||||
<h1>{t('accountDisabled')}</h1>
|
<H3>Acccount Disabled</H3>
|
||||||
<p>{t('accountDisabledMsg')}</p>
|
<p>
|
||||||
<ContactSupport t={t} />
|
You cannot re-enable a disabled account. You need to contact support to resolve this
|
||||||
|
situation.
|
||||||
|
</p>
|
||||||
|
<ContactSupport />
|
||||||
</Wrap>
|
</Wrap>
|
||||||
)
|
)
|
||||||
|
|
||||||
const AccountProhibited = ({ banner }) => (
|
const AccountProhibited = ({ banner }) => (
|
||||||
<Wrap>
|
<Wrap>
|
||||||
{banner}
|
{banner}
|
||||||
<h1>{t('accountProhibited')}</h1>
|
<H3>Your account has been disabled</H3>
|
||||||
<p>{t('accountProhibitedMsg')}</p>
|
<p>Your account has been administratively disabled.</p>
|
||||||
<ContactSupport t={t} />
|
<ContactSupport />
|
||||||
</Wrap>
|
</Wrap>
|
||||||
)
|
)
|
||||||
|
|
||||||
const AccountStatusUnknown = ({ t, banner }) => (
|
const AccountStatusUnknown = ({ banner }) => (
|
||||||
<Wrap>
|
<Wrap>
|
||||||
{banner}
|
{banner}
|
||||||
<h1>{t('statusUnknown')}</h1>
|
<H3>Account status warning</H3>
|
||||||
<p>{t('statusUnknownMsg')}</p>
|
<p>Your account status prohibits us from processing your data. Please contact support.</p>
|
||||||
<ContactSupport t={t} />
|
<ContactSupport />
|
||||||
</Wrap>
|
</Wrap>
|
||||||
)
|
)
|
||||||
|
|
||||||
const RoleLacking = ({ t, requiredRole, role, banner }) => (
|
const RoleLacking = ({ t, requiredRole, role, banner }) => (
|
||||||
<Wrap>
|
<Wrap>
|
||||||
{banner}
|
{banner}
|
||||||
<h1>{t('roleLacking')}</h1>
|
<H3>You lack the required role to access this content</H3>
|
||||||
<p dangerouslySetInnerHTML={{ __html: t('roleLackingMsg', { requiredRole, role }) }} />
|
<p>
|
||||||
<ContactSupport t={t} />
|
This content requires the <b>{requiredRole}</b> role. Your role is <b>{role}</b> which does
|
||||||
|
not grant you access to this content.
|
||||||
|
</p>
|
||||||
|
<ContactSupport />
|
||||||
</Wrap>
|
</Wrap>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -132,7 +142,7 @@ const ConsentLacking = ({ banner, refresh }) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrap>
|
<Wrap>
|
||||||
<div className="text-left">
|
<div className="tw-text-left">
|
||||||
{banner}
|
{banner}
|
||||||
<ConsentForm submit={updateConsent} />
|
<ConsentForm submit={updateConsent} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -140,8 +150,6 @@ const ConsentLacking = ({ banner, refresh }) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const t = (input) => input
|
|
||||||
|
|
||||||
export const RoleBlock = ({ children, user = false, Link = false }) => {
|
export const RoleBlock = ({ children, user = false, Link = false }) => {
|
||||||
if (!Link) Link = DefaultLink
|
if (!Link) Link = DefaultLink
|
||||||
let requiredRole = 'admin'
|
let requiredRole = 'admin'
|
||||||
|
@ -181,9 +189,8 @@ export const RoleBlock = ({ children, user = false, Link = false }) => {
|
||||||
} else {
|
} else {
|
||||||
if (data?.error?.error) setError(data.error.error)
|
if (data?.error?.error) setError(data.error.error)
|
||||||
else {
|
else {
|
||||||
console.log('WOULD SIGN OUT', data)
|
signOut()
|
||||||
}
|
}
|
||||||
//else signOut()
|
|
||||||
}
|
}
|
||||||
setReady(true)
|
setReady(true)
|
||||||
}
|
}
|
||||||
|
@ -200,26 +207,20 @@ export const RoleBlock = ({ children, user = false, Link = false }) => {
|
||||||
setError(false)
|
setError(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ready)
|
if (!ready) return <Spinner />
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<p>not ready</p>
|
|
||||||
<Spinner />
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
|
|
||||||
const banner = impersonating ? (
|
const banner = impersonating ? (
|
||||||
<div className="bg-warning rounded-lg shadow py-4 px-6 flex flex-row items-center gap-4 justify-between">
|
<div className="tw-bg-warning tw-rounded-lg tw-shadow tw-py-4 tw-px-6 tw-flex tw-flex-row tw-items-center tw-gap-4 tw-justify-between">
|
||||||
<span className="text-base-100 text-left">
|
<span className="tw-text-base-100 tw-text-left">
|
||||||
Hi <b>{impersonating.admin}</b>, you are currently impersonating <b>{impersonating.user}</b>
|
Hi <b>{impersonating.admin}</b>, you are currently impersonating <b>{impersonating.user}</b>
|
||||||
</span>
|
</span>
|
||||||
<button className="btn btn-neutral" onClick={stopImpersonating}>
|
<button className="tw-daisy-btn tw-daisy-btn-neutral" onClick={stopImpersonating}>
|
||||||
Stop Impersonating
|
Stop Impersonating
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : null
|
) : null
|
||||||
|
|
||||||
const childProps = { t, banner }
|
const childProps = { banner }
|
||||||
|
|
||||||
if (!token || !account.username) return <AuthRequired {...childProps} />
|
if (!token || !account.username) return <AuthRequired {...childProps} />
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {
|
||||||
UserIcon,
|
UserIcon,
|
||||||
} from '@freesewing/react/components/Icon'
|
} from '@freesewing/react/components/Icon'
|
||||||
import { MfaInput, StringInput, PasswordInput } from '@freesewing/react/components/Input'
|
import { MfaInput, StringInput, PasswordInput } from '@freesewing/react/components/Input'
|
||||||
|
import { H1, H2, H3, H4 } from '@freesewing/react/components/Heading'
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* This SignIn component holds the entire sign-in form
|
* This SignIn component holds the entire sign-in form
|
||||||
|
@ -126,43 +127,49 @@ export const SignIn = ({ onSuccess = false }) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const btnClasses = `daisy-btn capitalize w-full mt-4 ${
|
const btnClasses = `tw-daisy-btn tw-capitalize tw-w-full tw-mt-4 ${
|
||||||
signInFailed ? 'daisy-btn-warning' : 'daisy-btn-primary'
|
signInFailed ? 'tw-daisy-btn-warning' : 'tw-daisy-btn-primary'
|
||||||
} transition-colors ease-in-out duration-300 ${horFlexClasses}`
|
} tw-transition-colors tw-ease-in-out tw-duration-300 ${horFlexClassesNoSm}`
|
||||||
const noBueno = (
|
const noBueno = (
|
||||||
<>
|
<>
|
||||||
<WarningIcon />
|
<WarningIcon />
|
||||||
<span className="pl-2">{signInFailed}</span>
|
<span className="tw-pl-2">{signInFailed}</span>
|
||||||
<WarningIcon />
|
<WarningIcon />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (magicLinkSent)
|
if (magicLinkSent)
|
||||||
return (
|
return (
|
||||||
<>
|
<WrapForm>
|
||||||
<h1 className="text-inherit text-3xl lg:text-5xl mb-4 pb-0 text-center">Email Sent</h1>
|
<H1>Email Sent</H1>
|
||||||
<p className="text-inherit text-lg text-center">
|
<p className="tw-text-inherit tw-text-lg tw-text-center">
|
||||||
Go check your inbox for an email from <b>FreeSewing.org</b>
|
Go check your inbox for an email from <b>FreeSewing.org</b>
|
||||||
</p>
|
</p>
|
||||||
<p className="text-inherit text-lg text-center">
|
<p className="tw-text-inherit tw-text-lg tw-text-center">
|
||||||
Click the sign-in link in that email to sign in to your FreeSewing account.
|
Click the sign-in link in that email to sign in to your FreeSewing account.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-row gap-4 items-center justify-center p-8">
|
<div className="tw-flex tw-flex-row tw-gap-4 tw-items-center tw-justify-center tw-p-8">
|
||||||
<button className="daisy-btn daisy-btn-ghost" onClick={() => setMagicLinkSent(false)}>
|
<button
|
||||||
|
className="tw-daisy-btn tw-daisy-btn-outline tw-daisy-btn-sm"
|
||||||
|
onClick={() => setMagicLinkSent(false)}
|
||||||
|
>
|
||||||
Back
|
Back
|
||||||
</button>
|
</button>
|
||||||
<Link href="/support" className="daisy-btn daisy-btn-ghost">
|
<Link
|
||||||
|
href="/support"
|
||||||
|
className="tw-daisy-btn tw-daisy-btn-outline tw-daisy-btn-sm hover:tw-no-underline"
|
||||||
|
>
|
||||||
Contact support
|
Contact support
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</WrapForm>
|
||||||
)
|
)
|
||||||
|
|
||||||
if (mfa)
|
if (mfa)
|
||||||
return (
|
return (
|
||||||
<>
|
<WrapForm>
|
||||||
<h1 className="text-inherit text-3xl lg:text-5xl mb-4 pb-0 text-center">MFA Code</h1>
|
<H1>MFA Code</H1>
|
||||||
<p className="text-inherit text-lg text-center">
|
<p className="tw-text-inherit tw-text-lg tw-text-center">
|
||||||
Please provide a one-time MFA code, or a backup scratch code
|
Please provide a one-time MFA code, or a backup scratch code
|
||||||
</p>
|
</p>
|
||||||
<MfaInput
|
<MfaInput
|
||||||
|
@ -175,31 +182,31 @@ export const SignIn = ({ onSuccess = false }) => {
|
||||||
noBueno
|
noBueno
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="hidden lg:block">
|
<span className="tw-hidden lg:tw-block">
|
||||||
<KeyIcon />
|
<KeyIcon />
|
||||||
</span>
|
</span>
|
||||||
<span className="pl-2">Sign In</span>
|
<span className="tw-pl-2">Sign In</span>
|
||||||
<span className="hidden lg:block">
|
<span className="tw-hidden lg:tw-block">
|
||||||
<LockIcon />
|
<LockIcon />
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
<div className="flex flex-row gap-4 items-center justify-center p-8">
|
<div className="tw-flex tw-flex-row tw-gap-4 tw-items-center tw-justify-center tw-p-8">
|
||||||
<button className="daisy-btn daisy-btn-ghost" onClick={() => setMfa(false)}>
|
<button className="tw-daisy-btn tw-daisy-btn-ghost" onClick={() => setMfa(false)}>
|
||||||
Back
|
Back
|
||||||
</button>
|
</button>
|
||||||
<Link href="/support" className="daisy-btn daisy-btn-ghost">
|
<Link href="/support" className="tw-daisy-btn tw-daisy-btn-ghost">
|
||||||
Contact support
|
Contact support
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</WrapForm>
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tailwind-container">
|
<WrapForm>
|
||||||
<h1>{seenBefore ? `Welcome back ${seenUser}` : 'Welcome'}</h1>
|
<H1>{seenBefore ? `Welcome back ${seenUser}` : 'Welcome'}</H1>
|
||||||
<h3>Sign in to FreeSewing</h3>
|
<H4>Sign in to FreeSewing</H4>
|
||||||
{!seenBefore && (
|
{!seenBefore && (
|
||||||
<StringInput
|
<StringInput
|
||||||
label="Your Email address, Username, or User #"
|
label="Your Email address, Username, or User #"
|
||||||
|
@ -211,7 +218,7 @@ export const SignIn = ({ onSuccess = false }) => {
|
||||||
)}
|
)}
|
||||||
{magicLink ? (
|
{magicLink ? (
|
||||||
<button
|
<button
|
||||||
className={`${btnClasses} daisy-btn-lg`}
|
className={`${btnClasses} tw-daisy-btn-lg`}
|
||||||
tabIndex="-1"
|
tabIndex="-1"
|
||||||
role="button"
|
role="button"
|
||||||
onClick={signinHandler}
|
onClick={signinHandler}
|
||||||
|
@ -220,11 +227,11 @@ export const SignIn = ({ onSuccess = false }) => {
|
||||||
noBueno
|
noBueno
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="hidden lg:block">
|
<span className="tw-hidden lg:tw-block">
|
||||||
<EmailIcon />
|
<EmailIcon />
|
||||||
</span>
|
</span>
|
||||||
<span className="pl-2">Email me a sign-in link</span>
|
<span className="tw-pl-2">Email me a sign-in link</span>
|
||||||
<span className="hidden lg:block">
|
<span className="tw-hidden lg:tw-block">
|
||||||
<EmailIcon />
|
<EmailIcon />
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
|
@ -244,11 +251,11 @@ export const SignIn = ({ onSuccess = false }) => {
|
||||||
noBueno
|
noBueno
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="hidden lg:block">
|
<span className="tw-hidden lg:tw-block">
|
||||||
<KeyIcon />
|
<KeyIcon />
|
||||||
</span>
|
</span>
|
||||||
<span className="pl-2">Sign in</span>
|
<span className="tw-pl-2">Sign in</span>
|
||||||
<span className="hidden lg:block">
|
<span className="tw-hidden lg:tw-block">
|
||||||
<LockIcon />
|
<LockIcon />
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
|
@ -257,19 +264,19 @@ export const SignIn = ({ onSuccess = false }) => {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
className={`block md:flex md:flex-row md:justify-between md:items-center daisy-btn daisy-btn-primary daisy-btn-outline w-full mt-8`}
|
className={`tw-block md:tw-flex md:tw-flex-row md:tw-justify-between md:tw-items-center tw-daisy-btn tw-daisy-btn-primary tw-daisy-btn-outline tw-w-full tw-mt-8`}
|
||||||
onClick={() => setMagicLink(!magicLink)}
|
onClick={() => setMagicLink(!magicLink)}
|
||||||
>
|
>
|
||||||
<span className="hidden lg:block">{magicLink ? <LockIcon /> : <EmailIcon />}</span>
|
<span className="tw-hidden lg:tw-block">{magicLink ? <LockIcon /> : <EmailIcon />}</span>
|
||||||
{magicLink ? 'Use your password' : 'Email me a sign-in link'}
|
{magicLink ? 'Use your password' : 'Email me a sign-in link'}
|
||||||
<span className="hidden lg:block">{magicLink ? <KeyIcon /> : <EmailIcon />}</span>
|
<span className="tw-hidden lg:tw-block">{magicLink ? <KeyIcon /> : <EmailIcon />}</span>
|
||||||
</button>
|
</button>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-2 items-center mt-2">
|
<div className="tw-grid tw-grid-cols-1 md:tw-grid-cols-2 tw-gap-2 tw-items-center tw-mt-2">
|
||||||
{['Google', 'Github'].map((provider) => (
|
{['Google', 'Github'].map((provider) => (
|
||||||
<button
|
<button
|
||||||
key={provider}
|
key={provider}
|
||||||
id={provider}
|
id={provider}
|
||||||
className={`${horFlexClasses} daisy-btn daisy-btn-secondary`}
|
className={`${horFlexClasses} tw-daisy-btn tw-daisy-btn-secondary`}
|
||||||
onClick={() => initOauth(provider)}
|
onClick={() => initOauth(provider)}
|
||||||
>
|
>
|
||||||
{provider === 'Google' ? <GoogleIcon stroke={0} /> : <GitHubIcon />}
|
{provider === 'Google' ? <GoogleIcon stroke={0} /> : <GitHubIcon />}
|
||||||
|
@ -279,7 +286,7 @@ export const SignIn = ({ onSuccess = false }) => {
|
||||||
</div>
|
</div>
|
||||||
{seenBefore ? (
|
{seenBefore ? (
|
||||||
<button
|
<button
|
||||||
className={`${horFlexClassesNoSm} daisy-btn daisy-btn-neutral daisy-btn-outline mt-2 w-full`}
|
className={`${horFlexClasses} tw-daisy-btn tw-daisy-btn-neutral tw-daisy-btn-outline tw-mt-2 tw-w-full`}
|
||||||
onClick={() => setSeenUser(false)}
|
onClick={() => setSeenUser(false)}
|
||||||
>
|
>
|
||||||
<UserIcon />
|
<UserIcon />
|
||||||
|
@ -287,13 +294,15 @@ export const SignIn = ({ onSuccess = false }) => {
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link
|
||||||
className={`${horFlexClasses} daisy-btn daisy-btn-lg daisy-btn-neutral mt-2`}
|
className={`${horFlexClasses} tw-daisy-btn tw-daisy-btn-lg tw-daisy-btn-neutral tw-mt-2 hover:tw-text-neutral-content hover:tw-no-underline`}
|
||||||
href="/signup"
|
href="/signup"
|
||||||
>
|
>
|
||||||
<FreeSewingIcon className="h-10 w-10" />
|
<FreeSewingIcon className="tw-h-10 tw-w-10" />
|
||||||
Sign up here
|
Sign up here
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</WrapForm>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WrapForm = ({ children }) => <div className="tw-text-center tw-py-12">{children}</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue