Make fullPage actually fill the whole page

Fixes #52
This commit is contained in:
Stan Wohlwend 2024-05-30 15:47:14 +02:00
parent 207d85f5d3
commit 9423a2dc1a
20 changed files with 121 additions and 81 deletions

View File

@ -2,7 +2,7 @@ import { StackHandler } from "@stackframe/stack";
const { stackServerApp } = require("../../../stack");
function Handler(props: any) {
return <StackHandler app={stackServerApp} {...props} />;
return <StackHandler fullPage app={stackServerApp} {...props} />;
}
module.exports = Handler;

View File

@ -2,5 +2,5 @@ import { StackHandler } from "@stackframe/stack";
import { stackServerApp } from "src/stack";
export default function Handler(props) {
return <StackHandler app={stackServerApp} {...props} />;
return <StackHandler fullPage app={stackServerApp} {...props} />;
}

View File

@ -0,0 +1,9 @@
import { SignIn } from "@stackframe/stack";
export default function SimpleDivFullPageDemo() {
return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center" }}>
<SignIn fullPage />
</div>
);
}

View File

@ -0,0 +1,9 @@
import { SignIn } from "@stackframe/stack";
export default function SimpleDivFullPageDemo() {
return (
<div>
<SignIn fullPage />
</div>
);
}

View File

@ -2,5 +2,5 @@ import { StackHandler } from "@stackframe/stack";
import { stackServerApp } from "src/stack";
export default function Handler(props) {
return <StackHandler app={stackServerApp} {...props} />;
return <StackHandler fullPage app={stackServerApp} {...props} />;
}

View File

@ -4,7 +4,7 @@ import { stackServerApp } from "../../../stack";
export default function Handler(props: any) {
return (
<div style={{ backgroundColor: "white", borderRadius: 4 }}>
<StackHandler app={stackServerApp} {...props} />
<StackHandler fullPage app={stackServerApp} {...props} />
</div>
);
}

View File

@ -4,7 +4,7 @@ import { stackServerApp } from "../../../stack";
export default function Handler(props: any) {
return (
<div style={{ backgroundColor: "white", borderRadius: 4 }}>
<StackHandler app={stackServerApp} {...props} />
<StackHandler fullPage app={stackServerApp} {...props} />
</div>
);
}

View File

@ -76,7 +76,7 @@ npm install @stackframe/stack
import { stackServerApp } from "@/stack";
export default function Handler(props: any) {
return <StackHandler app={stackServerApp} {...props} />;
return <StackHandler fullPage app={stackServerApp} {...props} />;
}
```

View File

@ -96,7 +96,7 @@ async function main() {
console.log("Writing files...");
await writeFileIfNotExists(envLocalPath, "NEXT_PUBLIC_STACK_PROJECT_ID=\nNEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=\nSTACK_SECRET_SERVER_KEY=\n");
await writeFileIfNotExists(loadingPath, `export default function Loading() {\n${ind}// Stack uses React Suspense, which will render this page while user data is being fetched.\n${ind}// See: https://nextjs.org/docs/app/api-reference/file-conventions/loading\n${ind}return <></>;\n}\n`);
await writeFileIfNotExists(handlerPath, `import { StackHandler } from "@stackframe/stack";\nimport { stackServerApp } from "../../../stack";\n\nexport default function Handler(props${handlerFileExtension.includes("ts") ? ": any" : ""}) {\n${ind}return <StackHandler app={stackServerApp} {...props} />;\n}\n`);
await writeFileIfNotExists(handlerPath, `import { StackHandler } from "@stackframe/stack";\nimport { stackServerApp } from "../../../stack";\n\nexport default function Handler(props${handlerFileExtension.includes("ts") ? ": any" : ""}) {\n${ind}return <StackHandler fullPage app={stackServerApp} {...props} />;\n}\n`);
await writeFileIfNotExists(stackAppPath, `import "server-only";\n\nimport { StackServerApp } from "@stackframe/stack";\n\nexport const stackServerApp = new StackServerApp({\n${ind}tokenStore: "nextjs-cookie",\n});\n`);
await writeFile(layoutPath, updatedLayoutContent);
console.log("Files written successfully!");

View File

@ -2,5 +2,5 @@ import { StackHandler } from "@stackframe/stack";
import { stackServerApp } from "@/stack";
export default function Handler(props: any) {
return <StackHandler {...props} app={stackServerApp} />;
}
return <StackHandler fullPage {...props} app={stackServerApp} />;
}

View File

@ -3,7 +3,7 @@
import CredentialSignIn from '../components/credential-sign-in';
import SeparatorWithText from '../components/separator-with-text';
import OAuthGroup from '../components/oauth-group';
import CardFrame from '../components/card-frame';
import MaybeFullPage from '../components/maybe-full-page';
import { useUser, useStackApp, CredentialSignUp } from '..';
import RedirectMessageCard from '../components/redirect-message-card';
import { Link, Tabs, TabsContent, TabsList, TabsTrigger, Text } from "../components-core";
@ -21,7 +21,8 @@ export default function AuthPage({
}) {
const stackApp = useStackApp();
const user = useUser();
const project = mockProject || stackApp.useProject();
const projectFromHook = stackApp.useProject();
const project = mockProject || projectFromHook;
if (user && !mockProject) {
return <RedirectMessageCard type='signedIn' fullPage={fullPage} />;
@ -30,7 +31,7 @@ export default function AuthPage({
const enableSeparator = (project.credentialEnabled || project.magicLinkEnabled) && project.oauthProviders.filter(p => p.enabled).length > 0;
return (
<CardFrame fullPage={fullPage}>
<MaybeFullPage fullPage={fullPage}>
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
<Text size="xl" as='h2' style={{ fontWeight: 500 }}>
{type === 'sign-in' ? 'Sign in to your account' : 'Create a new account'}
@ -71,6 +72,6 @@ export default function AuthPage({
) : project.magicLinkEnabled ? (
<MagicLinkSignIn/>
) : null}
</CardFrame>
</MaybeFullPage>
);
}

View File

@ -1,7 +1,7 @@
'use client';
import ForgotPasswordElement from "../components/forgot-password";
import CardFrame from "../components/card-frame";
import MaybeFullPage from "../components/maybe-full-page";
import { useUser, useStackApp } from "..";
import RedirectMessageCard from "../components/redirect-message-card";
import { useState } from "react";
@ -22,7 +22,7 @@ export default function ForgotPassword({ fullPage=false }: { fullPage?: boolean
}
return (
<CardFrame fullPage={fullPage}>
<MaybeFullPage fullPage={fullPage}>
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
<Text size="xl" as='h2'>Reset Your Password</Text>
<Text>
@ -33,6 +33,6 @@ export default function ForgotPassword({ fullPage=false }: { fullPage?: boolean
</Text>
</div>
<ForgotPasswordElement onSent={() => setSent(true)} />
</CardFrame>
</MaybeFullPage>
);
};

View File

@ -5,7 +5,7 @@ import { useStackApp } from "..";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
import MessageCard from "../components/message-card";
export default function OAuthCallback () {
export default function OAuthCallback(props: { fullPage?: boolean }) {
const app = useStackApp();
const called = useRef(false);
const [error, setError] = useState<unknown>(null);
@ -29,7 +29,7 @@ export default function OAuthCallback () {
setTimeout(() => setShowRedirectLink(true), 3000);
}, []);
return <MessageCard title='Redirecting...' fullPage>
return <MessageCard title='Redirecting...' fullPage={props.fullPage}>
{showRedirectLink ? <p>If you are not redirected automatically, <a href={app.urls.home}>click here</a>.</p> : null}
{error ? <div>
<p>Something went wrong while processing the OAuth callback:</p>

View File

@ -1,4 +1,3 @@
'use client';
import AuthPage from './auth-page';
export default function SignIn({ fullPage=false }: { fullPage?: boolean }) {

View File

@ -4,12 +4,12 @@ import { use } from "react";
import { useUser } from "..";
import GoHomeMessageCard from "../components/redirect-message-card";
export default function Signout() {
export default function SignOut(props: { fullPage?: boolean }) {
const user = useUser();
if (user) {
use(user.signOut());
}
return <GoHomeMessageCard type='signedOut' fullPage />;
}
return <GoHomeMessageCard type='signedOut' fullPage={props.fullPage} />;
}

View File

@ -15,14 +15,22 @@ export default async function StackHandler<HasTokenStore extends boolean>({
app,
params: { stack } = {},
searchParams = {},
// TODO set default to false like on the other components (may break old code)
fullPage = "deprecated-unset",
}: {
app: StackServerApp<HasTokenStore>,
params?: { stack?: string[] },
searchParams?: Record<string, string>,
fullPage?: boolean | "deprecated-unset",
}) {
if (fullPage === "deprecated-unset") {
console.warn("You are not passing `fullPage` to Stack's Handler. The default behaviour will soon change from `true` to `false`. Please update your Handler component in handler/[...stack]/page.tsx by adding the `fullPage` prop.");
fullPage = true;
}
if (!stack) {
return (
<MessageCard title="Invalid Stack Handler Setup" fullPage>
<MessageCard title="Invalid Stack Handler Setup" fullPage={fullPage}>
<p>Can't use Stack handler at this location. Make sure that the file is in a folder called [...stack].</p>
</MessageCard>
);
@ -52,40 +60,40 @@ export default async function StackHandler<HasTokenStore extends boolean>({
case 'signin': {
redirectIfNotHandler('signIn');
await redirectIfHasUser();
return <SignIn fullPage/>;
return <SignIn fullPage={fullPage} />;
}
case 'signup': {
redirectIfNotHandler('signUp');
await redirectIfHasUser();
return <SignUp fullPage/>;
return <SignUp fullPage={fullPage} />;
}
case 'email-verification': {
redirectIfNotHandler('emailVerification');
return <EmailVerification searchParams={searchParams} fullPage/>;
return <EmailVerification searchParams={searchParams} fullPage={fullPage} />;
}
case 'password-reset': {
redirectIfNotHandler('passwordReset');
return <PasswordReset searchParams={searchParams} fullPage />;
return <PasswordReset searchParams={searchParams} fullPage={fullPage} />;
}
case 'forgot-password': {
redirectIfNotHandler('forgotPassword');
return <ForgotPassword fullPage />;
return <ForgotPassword fullPage={fullPage} />;
}
case 'signout': {
redirectIfNotHandler('signOut');
return <SignOut/>;
return <SignOut fullPage={fullPage} />;
}
case 'oauth-callback': {
redirectIfNotHandler('oauthCallback');
return <OAuthCallback />;
return <OAuthCallback fullPage={fullPage} />;
}
case 'account-settings': {
redirectIfNotHandler('accountSettings');
return <AccountSettings fullPage />;
return <AccountSettings fullPage={fullPage} />;
}
case 'magic-link-callback': {
redirectIfNotHandler('magicLinkCallback');
return <MagicLinkCallback searchParams={searchParams} fullPage />;
return <MagicLinkCallback searchParams={searchParams} fullPage={fullPage} />;
}
default: {
return notFound();

View File

@ -1,43 +0,0 @@
'use client';
import { Container } from "../components-core";
import React, { useEffect, useState } from "react";
export default function CardFrame({
children,
fullPage=true
}: {
children: React.ReactNode,
fullPage?: boolean,
}) {
const [hasNoParent, setHasNoParent] = useState(false);
useEffect(() => {
const component = document.getElementById('stack-card-frame');
setHasNoParent(
!component?.parentElement ||
component?.parentElement === document.body ||
component?.parentElement === document.documentElement
);
}, []);
if (fullPage) {
return (
<div
id='stack-card-frame'
style={{
height: hasNoParent ? '100vh' : '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Container size={380} style={{ padding: '1rem 1rem' }}>
{children}
</Container>
</div>
);
} else {
return children;
}
}

View File

@ -0,0 +1,57 @@
"use client";
import { Container } from "../components-core";
import React, { useEffect, useId } from "react";
export default function MaybeFullPage({
children,
fullPage=true
}: {
children: React.ReactNode,
fullPage?: boolean,
}) {
const uniqueId = useId();
const id = `stack-card-frame-${uniqueId}`;
const scriptString = `(([id]) => {
const el = document.getElementById(id);
const offset = el.getBoundingClientRect().top + document.documentElement.scrollTop;
el.style.minHeight = \`calc(100vh - \${offset}px)\`;
})(${JSON.stringify([id])})`;
useEffect(() => {
// React has a bug where it doesn't run the script on the first CSR render if SSR has been skipped due to suspense
// As a workaround, we run the script in the <script> tag again after the first render
eval(scriptString);
}, []);
if (fullPage) {
return (
<>
<div
id={id}
suppressHydrationWarning
style={{
minHeight: '100vh',
alignSelf: 'stretch',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
<Container size={380} style={{ padding: '1rem 1rem' }}>
{children}
</Container>
</div>
<script dangerouslySetInnerHTML={{
__html: scriptString,
}} />
</>
);
} else {
return <>
{children}
</>;
}
}

View File

@ -1,7 +1,7 @@
'use client';
import React from "react";
import CardFrame from "./card-frame";
import MaybeFullPage from "./maybe-full-page";
import { Text } from "../components-core";
export default function MessageCard(
@ -9,11 +9,11 @@ export default function MessageCard(
{ children?: React.ReactNode, title: string, fullPage?: boolean}
) {
return (
<CardFrame fullPage={fullPage}>
<MaybeFullPage fullPage={fullPage}>
<div style={{ textAlign: 'center' }}>
<Text size="xl" as='h2' style={{ marginBottom: '24px' }}>{title}</Text>
{children}
</div>
</CardFrame>
</MaybeFullPage>
);
}

View File

@ -10,7 +10,7 @@ import PasswordField from "./password-field";
import FormWarningText from "./form-warning";
import RedirectMessageCard from "./redirect-message-card";
import MessageCard from "./message-card";
import CardFrame from "./card-frame";
import MaybeFullPage from "./maybe-full-page";
import { Button, Label, Text } from "../components-core";
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
@ -64,7 +64,7 @@ export default function PasswordResetInner(
}
return (
<CardFrame fullPage={fullPage}>
<MaybeFullPage fullPage={fullPage}>
<div style={{ textAlign: 'center', marginBottom: '1.5rem' }}>
<Text size="xl" as='h2'>Reset Your Password</Text>
</div>
@ -100,6 +100,6 @@ export default function PasswordResetInner(
Reset Password
</Button>
</form>
</CardFrame>
</MaybeFullPage>
);
}