mirror of
https://github.com/lukevella/rallly.git
synced 2025-06-09 06:01:49 +02:00
♻️ Avoid getServerSideProps for login/register (#755)
This commit is contained in:
parent
becc7a7930
commit
438c4ec35b
12 changed files with 98 additions and 197 deletions
|
@ -68,7 +68,6 @@
|
||||||
"no": "No",
|
"no": "No",
|
||||||
"noDatesSelected": "No dates selected",
|
"noDatesSelected": "No dates selected",
|
||||||
"notificationsDisabled": "Notifications have been disabled for <b>{title}</b>",
|
"notificationsDisabled": "Notifications have been disabled for <b>{title}</b>",
|
||||||
"notRegistered": "Create a new account →",
|
|
||||||
"noVotes": "No one has voted for this option",
|
"noVotes": "No one has voted for this option",
|
||||||
"ok": "Ok",
|
"ok": "Ok",
|
||||||
"optional": "optional",
|
"optional": "optional",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { trpc } from "@rallly/backend";
|
import { trpc } from "@rallly/backend";
|
||||||
|
import { ArrowRightIcon } from "@rallly/icons";
|
||||||
import { Button } from "@rallly/ui/button";
|
import { Button } from "@rallly/ui/button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Trans, useTranslation } from "next-i18next";
|
import { Trans, useTranslation } from "next-i18next";
|
||||||
|
@ -98,7 +99,7 @@ const VerifyCode: React.FunctionComponent<{
|
||||||
{t("verificationCodeHelp")}
|
{t("verificationCodeHelp")}
|
||||||
</p>
|
</p>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div className="mt-6 space-y-4 sm:flex sm:space-x-3 sm:space-y-0">
|
<div className="mt-6 flex flex-col gap-2 sm:flex-row">
|
||||||
<Button
|
<Button
|
||||||
loading={formState.isSubmitting || formState.isSubmitSuccessful}
|
loading={formState.isSubmitting || formState.isSubmitSuccessful}
|
||||||
type="submit"
|
type="submit"
|
||||||
|
@ -128,7 +129,7 @@ type RegisterFormData = {
|
||||||
|
|
||||||
export const RegisterForm: React.FunctionComponent<{
|
export const RegisterForm: React.FunctionComponent<{
|
||||||
onClickLogin?: React.MouseEventHandler;
|
onClickLogin?: React.MouseEventHandler;
|
||||||
onRegistered: () => void;
|
onRegistered?: () => void;
|
||||||
defaultValues?: Partial<RegisterFormData>;
|
defaultValues?: Partial<RegisterFormData>;
|
||||||
}> = ({ onClickLogin, onRegistered, defaultValues }) => {
|
}> = ({ onClickLogin, onRegistered, defaultValues }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
@ -157,7 +158,7 @@ export const RegisterForm: React.FunctionComponent<{
|
||||||
|
|
||||||
queryClient.invalidate();
|
queryClient.invalidate();
|
||||||
|
|
||||||
onRegistered();
|
onRegistered?.();
|
||||||
posthog?.identify(res.user.id, {
|
posthog?.identify(res.user.id, {
|
||||||
email: res.user.email,
|
email: res.user.email,
|
||||||
name: res.user.name,
|
name: res.user.name,
|
||||||
|
@ -252,7 +253,7 @@ export const RegisterForm: React.FunctionComponent<{
|
||||||
loading={formState.isSubmitting}
|
loading={formState.isSubmitting}
|
||||||
type="submit"
|
type="submit"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="h-12 px-6"
|
size="lg"
|
||||||
>
|
>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -280,7 +281,7 @@ export const LoginForm: React.FunctionComponent<{
|
||||||
e: React.MouseEvent<HTMLAnchorElement>,
|
e: React.MouseEvent<HTMLAnchorElement>,
|
||||||
email: string,
|
email: string,
|
||||||
) => void;
|
) => void;
|
||||||
onAuthenticated: () => void;
|
onAuthenticated?: () => void;
|
||||||
}> = ({ onAuthenticated, onClickRegister }) => {
|
}> = ({ onAuthenticated, onClickRegister }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { register, handleSubmit, getValues, formState, setError } = useForm<{
|
const { register, handleSubmit, getValues, formState, setError } = useForm<{
|
||||||
|
@ -305,7 +306,7 @@ export const LoginForm: React.FunctionComponent<{
|
||||||
if (!res.user) {
|
if (!res.user) {
|
||||||
throw new Error("Failed to authenticate user");
|
throw new Error("Failed to authenticate user");
|
||||||
} else {
|
} else {
|
||||||
onAuthenticated();
|
onAuthenticated?.();
|
||||||
queryClient.invalidate();
|
queryClient.invalidate();
|
||||||
posthog?.identify(res.user.id, {
|
posthog?.identify(res.user.id, {
|
||||||
email: res.user.email,
|
email: res.user.email,
|
||||||
|
@ -395,25 +396,27 @@ export const LoginForm: React.FunctionComponent<{
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div className="space-y-3">
|
<div className="flex flex-col gap-2">
|
||||||
<Button
|
<Button
|
||||||
loading={formState.isSubmitting}
|
loading={formState.isSubmitting}
|
||||||
type="submit"
|
type="submit"
|
||||||
size="lg"
|
size="lg"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="h-12 w-full px-6"
|
className=""
|
||||||
>
|
>
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size="lg" asChild>
|
||||||
<Link
|
<Link
|
||||||
href="/register"
|
href="/register"
|
||||||
className="btn-default h-12 w-full px-6"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
onClickRegister?.(e, getValues("email"));
|
onClickRegister?.(e, getValues("email"));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t("notRegistered")}
|
{t("createAnAccount")}
|
||||||
|
<ArrowRightIcon className="h-4 w-4" />
|
||||||
</Link>
|
</Link>
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,92 +0,0 @@
|
||||||
import { Dialog, DialogContent } from "@rallly/ui/dialog";
|
|
||||||
import Image from "next/image";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { LoginForm, RegisterForm } from "./login-form";
|
|
||||||
|
|
||||||
export const LoginModal: React.FunctionComponent<{
|
|
||||||
open: boolean;
|
|
||||||
defaultView?: "login" | "register";
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
}> = ({ open, onOpenChange, defaultView = "login" }) => {
|
|
||||||
const [newAccount, setNewAccount] = React.useState(
|
|
||||||
defaultView === "register",
|
|
||||||
);
|
|
||||||
const [defaultEmail, setDefaultEmail] = React.useState("");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent className="p-0">
|
|
||||||
<div
|
|
||||||
data-testid="login-modal"
|
|
||||||
className="border-t-primary max-w-full overflow-hidden border-t-4 shadow-sm"
|
|
||||||
>
|
|
||||||
<div className="bg-pattern flex justify-center py-8">
|
|
||||||
<Image
|
|
||||||
src="/static/logo.svg"
|
|
||||||
width={140}
|
|
||||||
height={30}
|
|
||||||
alt="Rallly"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="p-4 sm:p-6">
|
|
||||||
{newAccount ? (
|
|
||||||
<RegisterForm
|
|
||||||
defaultValues={{ email: defaultEmail }}
|
|
||||||
onRegistered={() => onOpenChange(false)}
|
|
||||||
onClickLogin={(e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setNewAccount(false);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<LoginForm
|
|
||||||
onAuthenticated={() => onOpenChange(false)}
|
|
||||||
onClickRegister={(e, email) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setDefaultEmail(email);
|
|
||||||
setNewAccount(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LoginModalProvider = ({ children }: React.PropsWithChildren) => {
|
|
||||||
const [open, setOpen] = React.useState(false);
|
|
||||||
const [view, setView] = React.useState<"login" | "register">("login");
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const handleClick = (event: MouseEvent) => {
|
|
||||||
const target = event.target as HTMLElement;
|
|
||||||
const href = target.getAttribute("href");
|
|
||||||
if (
|
|
||||||
target.tagName === "A" &&
|
|
||||||
(href === "/login" || href === "/register")
|
|
||||||
) {
|
|
||||||
// Handle the click event here
|
|
||||||
event.preventDefault();
|
|
||||||
setView(href === "/login" ? "login" : "register");
|
|
||||||
setOpen(true);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("click", handleClick, { capture: true });
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("click", handleClick, { capture: true });
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{open ? (
|
|
||||||
<LoginModal open={open} defaultView={view} onOpenChange={setOpen} />
|
|
||||||
) : null}
|
|
||||||
{children}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,24 +0,0 @@
|
||||||
import { SpinnerIcon } from "@rallly/icons";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
interface FullPageLoaderProps {
|
|
||||||
className?: string;
|
|
||||||
children?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
const FullPageLoader: React.FunctionComponent<FullPageLoaderProps> = ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className={clsx("flex h-full items-center justify-center", className)}>
|
|
||||||
<div className="bg-primary-600 flex items-center rounded-lg px-4 py-3 text-sm text-white shadow-sm">
|
|
||||||
<SpinnerIcon className="mr-3 h-5 animate-spin" />
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default FullPageLoader;
|
|
|
@ -11,7 +11,6 @@ import { useInterval } from "react-use";
|
||||||
import spacetime from "spacetime";
|
import spacetime from "spacetime";
|
||||||
import soft from "timezone-soft";
|
import soft from "timezone-soft";
|
||||||
|
|
||||||
import { LoginModalProvider } from "@/components/auth/login-modal";
|
|
||||||
import { Container } from "@/components/container";
|
import { Container } from "@/components/container";
|
||||||
import FeedbackButton from "@/components/feedback";
|
import FeedbackButton from "@/components/feedback";
|
||||||
import { Spinner } from "@/components/spinner";
|
import { Spinner } from "@/components/spinner";
|
||||||
|
@ -154,13 +153,11 @@ export const StandardLayout: React.FunctionComponent<{
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<ModalProvider>
|
<ModalProvider>
|
||||||
<LoginModalProvider>
|
|
||||||
<div className="flex min-h-screen flex-col" {...rest}>
|
<div className="flex min-h-screen flex-col" {...rest}>
|
||||||
<MainNav />
|
<MainNav />
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
{process.env.NEXT_PUBLIC_FEEDBACK_EMAIL ? <FeedbackButton /> : null}
|
{process.env.NEXT_PUBLIC_FEEDBACK_EMAIL ? <FeedbackButton /> : null}
|
||||||
</LoginModalProvider>
|
|
||||||
</ModalProvider>
|
</ModalProvider>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { SpinnerIcon } from "@rallly/icons";
|
import { Loader2Icon } from "@rallly/icons";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export const Spinner = (props: { className?: string }) => {
|
export const Spinner = (props: { className?: string }) => {
|
||||||
return (
|
return (
|
||||||
<SpinnerIcon
|
<Loader2Icon
|
||||||
className={clsx("inline-block h-5 animate-spin", props.className)}
|
className={clsx("inline-block h-5 animate-spin", props.className)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { useTranslation } from "next-i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { PostHogProvider } from "@/contexts/posthog";
|
import { PostHogProvider } from "@/contexts/posthog";
|
||||||
|
import { useWhoAmI } from "@/contexts/whoami";
|
||||||
|
|
||||||
import { useRequiredContext } from "./use-required-context";
|
import { useRequiredContext } from "./use-required-context";
|
||||||
|
|
||||||
|
@ -48,7 +49,7 @@ export const UserProvider = (props: { children?: React.ReactNode }) => {
|
||||||
|
|
||||||
const queryClient = trpc.useContext();
|
const queryClient = trpc.useContext();
|
||||||
|
|
||||||
const { data: user } = trpc.whoami.get.useQuery();
|
const user = useWhoAmI();
|
||||||
const billingQuery = trpc.user.getBilling.useQuery();
|
const billingQuery = trpc.user.getBilling.useQuery();
|
||||||
const { data: userPreferences } = trpc.userPreferences.get.useQuery();
|
const { data: userPreferences } = trpc.userPreferences.get.useQuery();
|
||||||
|
|
||||||
|
|
6
apps/web/src/contexts/whoami.ts
Normal file
6
apps/web/src/contexts/whoami.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { trpc } from "@rallly/backend";
|
||||||
|
|
||||||
|
export const useWhoAmI = () => {
|
||||||
|
const { data: whoAmI } = trpc.whoami.get.useQuery();
|
||||||
|
return whoAmI;
|
||||||
|
};
|
|
@ -15,24 +15,23 @@ export async function middleware(req: NextRequest) {
|
||||||
const newUrl = nextUrl.clone();
|
const newUrl = nextUrl.clone();
|
||||||
const res = NextResponse.next();
|
const res = NextResponse.next();
|
||||||
const session = await getSession(req, res);
|
const session = await getSession(req, res);
|
||||||
if (
|
|
||||||
|
// a protected path is one that requires to be logged in
|
||||||
|
const isProtectedPath = protectedPaths.some((protectedPath) =>
|
||||||
|
req.nextUrl.pathname.includes(protectedPath),
|
||||||
|
);
|
||||||
|
|
||||||
|
const isProtectedPathDueToRequiredAuth =
|
||||||
process.env.AUTH_REQUIRED &&
|
process.env.AUTH_REQUIRED &&
|
||||||
session.user?.isGuest !== false &&
|
|
||||||
!publicPaths.some((publicPath) =>
|
!publicPaths.some((publicPath) =>
|
||||||
req.nextUrl.pathname.startsWith(publicPath),
|
req.nextUrl.pathname.startsWith(publicPath),
|
||||||
)
|
);
|
||||||
) {
|
|
||||||
newUrl.pathname = "/login";
|
|
||||||
return NextResponse.redirect(newUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
const isGuest = session.user?.isGuest !== false;
|
||||||
session.user?.isGuest !== false &&
|
|
||||||
protectedPaths.some((protectedPath) =>
|
if (isGuest && (isProtectedPathDueToRequiredAuth || isProtectedPath)) {
|
||||||
req.nextUrl.pathname.includes(protectedPath),
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
newUrl.pathname = "/login";
|
newUrl.pathname = "/login";
|
||||||
|
newUrl.searchParams.set("redirect", req.nextUrl.pathname);
|
||||||
return NextResponse.redirect(newUrl);
|
return NextResponse.redirect(newUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
import {
|
import { Loader2Icon } from "@rallly/icons";
|
||||||
composeGetServerSideProps,
|
import { NextPage } from "next";
|
||||||
withSessionSsr,
|
|
||||||
} from "@rallly/backend/next";
|
|
||||||
import { GetServerSideProps, NextPage } from "next";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useTranslation } from "next-i18next";
|
import { useTranslation } from "next-i18next";
|
||||||
|
@ -10,37 +7,45 @@ import React from "react";
|
||||||
|
|
||||||
import { AuthLayout } from "@/components/auth/auth-layout";
|
import { AuthLayout } from "@/components/auth/auth-layout";
|
||||||
import { LoginForm } from "@/components/auth/login-form";
|
import { LoginForm } from "@/components/auth/login-form";
|
||||||
|
import { PageDialog } from "@/components/page-dialog";
|
||||||
|
import { useWhoAmI } from "@/contexts/whoami";
|
||||||
|
|
||||||
import { withPageTranslations } from "../utils/with-page-translations";
|
import { getStaticTranslations } from "../utils/with-page-translations";
|
||||||
|
|
||||||
const Page: NextPage<{ referer: string | null }> = () => {
|
const Redirect = () => {
|
||||||
const { t } = useTranslation();
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
router.replace((router.query.redirect as string) ?? "/");
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthLayout>
|
<PageDialog>
|
||||||
<Head>
|
<Loader2Icon className="h-10 w-10 animate-spin text-gray-400" />
|
||||||
<title>{t("login")}</title>
|
</PageDialog>
|
||||||
</Head>
|
|
||||||
<LoginForm
|
|
||||||
onAuthenticated={async () => {
|
|
||||||
router.replace("/polls");
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</AuthLayout>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps = withSessionSsr(
|
const Page: NextPage<{ referer: string | null }> = () => {
|
||||||
composeGetServerSideProps(async (ctx) => {
|
const { t } = useTranslation();
|
||||||
if (ctx.req.session.user?.isGuest === false) {
|
const whoami = useWhoAmI();
|
||||||
return {
|
|
||||||
redirect: { destination: "/polls" },
|
if (whoami?.isGuest === false) {
|
||||||
props: {},
|
return <Redirect />;
|
||||||
};
|
|
||||||
}
|
}
|
||||||
return { props: {} };
|
|
||||||
}, withPageTranslations()),
|
return (
|
||||||
);
|
<>
|
||||||
|
<Head>
|
||||||
|
<title>{t("login")}</title>
|
||||||
|
</Head>
|
||||||
|
<AuthLayout>
|
||||||
|
<LoginForm />
|
||||||
|
</AuthLayout>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default Page;
|
export default Page;
|
||||||
|
|
||||||
|
export const getStaticProps = getStaticTranslations;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { withSessionSsr } from "@rallly/backend/next";
|
|
||||||
import { NextPage } from "next";
|
import { NextPage } from "next";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
@ -6,11 +5,11 @@ import { useTranslation } from "next-i18next";
|
||||||
|
|
||||||
import { AuthLayout } from "../components/auth/auth-layout";
|
import { AuthLayout } from "../components/auth/auth-layout";
|
||||||
import { RegisterForm } from "../components/auth/login-form";
|
import { RegisterForm } from "../components/auth/login-form";
|
||||||
import { withSession } from "../components/user-provider";
|
import { getStaticTranslations } from "../utils/with-page-translations";
|
||||||
import { withPageTranslations } from "../utils/with-page-translations";
|
|
||||||
|
|
||||||
const Page: NextPage = () => {
|
const Page: NextPage = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<AuthLayout>
|
<AuthLayout>
|
||||||
|
@ -19,13 +18,13 @@ const Page: NextPage = () => {
|
||||||
</Head>
|
</Head>
|
||||||
<RegisterForm
|
<RegisterForm
|
||||||
onRegistered={() => {
|
onRegistered={() => {
|
||||||
router.replace("/polls");
|
router.replace("/");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</AuthLayout>
|
</AuthLayout>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps = withSessionSsr(withPageTranslations());
|
export const getStaticProps = getStaticTranslations;
|
||||||
|
|
||||||
export default withSession(Page);
|
export default Page;
|
||||||
|
|
|
@ -6,17 +6,25 @@ const getVercelUrl = () => {
|
||||||
: null;
|
: null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function joinPath(baseUrl: string, subpath = "") {
|
||||||
|
if (subpath) {
|
||||||
|
const url = new URL(subpath, baseUrl);
|
||||||
|
return url.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
export function absoluteUrl(subpath = "") {
|
export function absoluteUrl(subpath = "") {
|
||||||
const baseUrl =
|
const baseUrl =
|
||||||
process.env.NEXT_PUBLIC_BASE_URL ??
|
process.env.NEXT_PUBLIC_BASE_URL ??
|
||||||
getVercelUrl() ??
|
getVercelUrl() ??
|
||||||
`http://localhost:${port}`;
|
`http://localhost:${port}`;
|
||||||
const url = new URL(subpath, baseUrl);
|
|
||||||
return url.href;
|
return joinPath(baseUrl, subpath);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function shortUrl(subpath = "") {
|
export function shortUrl(subpath = "") {
|
||||||
const baseUrl = process.env.NEXT_PUBLIC_SHORT_BASE_URL ?? absoluteUrl();
|
const baseUrl = process.env.NEXT_PUBLIC_SHORT_BASE_URL ?? absoluteUrl();
|
||||||
const url = new URL(subpath, baseUrl);
|
return joinPath(baseUrl, subpath);
|
||||||
return url.href;
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue