Files
plunk/apps/web/src/pages/_app.tsx
T

144 lines
4.0 KiB
TypeScript

import '../styles/globals.css';
import type {AppProps} from 'next/app';
import {useRouter} from 'next/router';
import React, {useEffect} from 'react';
import {Toaster} from 'sonner';
import {SWRConfig} from 'swr';
import {DefaultSeo} from 'next-seo';
import {Loader} from '@plunk/ui';
import {ActiveProjectProvider} from '../lib/contexts/ActiveProjectProvider';
import {useProjects} from '../lib/hooks/useProject';
import {useUser} from '../lib/hooks/useUser';
import {network} from '../lib/network';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import advancedFormat from 'dayjs/plugin/advancedFormat';
// Configure dayjs plugins globally
dayjs.extend(relativeTime);
dayjs.extend(advancedFormat);
// Routes that don't require authentication
const PUBLIC_ROUTES = [
'/auth/login',
'/auth/signup',
'/auth/reset-password',
'/auth/verify-email',
'/unsubscribe',
'/subscribe',
'/manage',
];
// Routes that don't require a project
const NO_PROJECT_ROUTES = ['/projects/create'];
function App({Component, pageProps}: AppProps) {
return (
<>
<Component {...pageProps} />
</>
);
}
function AuthGuard({children}: {children: React.ReactNode}) {
const {data: user, isLoading} = useUser();
const router = useRouter();
const isPublicRoute = PUBLIC_ROUTES.some(
route => router.pathname === route || router.pathname.startsWith(`${route}/`),
);
useEffect(() => {
// If not loading, no user, and trying to access a protected route, redirect to login
if (!isLoading && !user && !isPublicRoute) {
void router.push('/auth/login');
}
// If user is logged in and trying to access login/signup, redirect to home
if (!isLoading && user && (router.pathname === '/auth/login' || router.pathname === '/auth/signup')) {
void router.push('/');
}
}, [user, isLoading, router, isPublicRoute]);
// Show loading state while checking authentication (only for protected routes)
if (isLoading && !isPublicRoute) {
return <Loader message="Authenticating..." />;
}
// Don't render protected content if redirecting
if (!user && !isPublicRoute) {
return null;
}
return <>{children}</>;
}
function ProjectGuard({children}: {children: React.ReactNode}) {
const {data: projects, isLoading} = useProjects();
const router = useRouter();
const isNoProjectRoute = NO_PROJECT_ROUTES.includes(router.pathname);
useEffect(() => {
// If not loading, user has no projects, and not already on project creation page
if (!isLoading && projects && projects.length === 0 && !isNoProjectRoute) {
void router.push('/projects/create');
}
}, [projects, isLoading, router, isNoProjectRoute]);
// Show loading state while checking projects (only for routes that need a project)
if (isLoading && !isNoProjectRoute) {
return <Loader message="Loading project..." />;
}
// Don't render protected content if redirecting
if (!isLoading && projects && projects.length === 0 && !isNoProjectRoute) {
return null;
}
return <>{children}</>;
}
/**
* Main app root component that houses all components
* @param props Default nextjs props
*/
export default function WithProviders(props: AppProps) {
return (
<SWRConfig
value={{
fetcher: (url: string) => network.fetch('GET', url),
shouldRetryOnError: false,
}}
>
<DefaultSeo titleTemplate="%s | Plunk" defaultTitle="Plunk | Email Platform Dashboard" />
<ActiveProjectProvider>
<Root {...props} />
</ActiveProjectProvider>
</SWRConfig>
);
}
function Root(props: AppProps) {
const router = useRouter();
const isPublicRoute = PUBLIC_ROUTES.some(
route => router.pathname === route || router.pathname.startsWith(`${route}/`),
);
return (
<>
<Toaster position={'top-right'} />
<div>
<AuthGuard>
{isPublicRoute ? (
<App {...props} />
) : (
<ProjectGuard>
<App {...props} />
</ProjectGuard>
)}
</AuthGuard>
</div>
</>
);
}