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

184 lines
5.5 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 {NuqsAdapter} from 'nuqs/adapters/next/pages';
import {Loader} from '@plunk/ui';
import {ActiveProjectProvider} from '../lib/contexts/ActiveProjectProvider';
import {CommandPalette} from '../components/CommandPalette';
import {useProjects} from '../lib/hooks/useProject';
import {useUser} from '../lib/hooks/useUser';
import {network} from '../lib/network';
import {addRecentPage, getFallbackLabel} from '../lib/recentPages';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import advancedFormat from 'dayjs/plugin/advancedFormat';
import Script from 'next/script';
// 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 (
<NuqsAdapter>
<SWRConfig
value={{
fetcher: (url: string) => network.fetch('GET', url),
shouldRetryOnError: false,
}}
>
<DefaultSeo titleTemplate="%s | Plunk" defaultTitle="Plunk | Email Platform Dashboard" />
<Script
defer
src="https://analytics.driaug.com/script.js"
data-website-id="5880df93-9025-41ae-8e33-7c3da865f764"
data-domains="next-app.useplunk.com"
/>
<ActiveProjectProvider>
<Root {...props} />
</ActiveProjectProvider>
</SWRConfig>
</NuqsAdapter>
);
}
function Root(props: AppProps) {
const router = useRouter();
const isPublicRoute = PUBLIC_ROUTES.some(
route => router.pathname === route || router.pathname.startsWith(`${route}/`),
);
useEffect(() => {
const handleRouteChange = () => {
const pathname = window.location.pathname;
// Pass 1: record immediately with a route-pattern label so detail pages
// are always captured even before their data loads.
const fallback = getFallbackLabel(pathname);
if (fallback) {
addRecentPage({label: fallback, href: pathname});
}
// Pass 2: after data loads, replace the label with the page's own title
// (e.g. contact email set by NextSeo after the API response arrives).
setTimeout(() => {
const titleLabel = document.title.split(' | ')[0]?.trim();
if (titleLabel && titleLabel !== 'Plunk' && titleLabel !== fallback) {
addRecentPage({label: titleLabel, href: pathname});
}
}, 800);
};
router.events.on('routeChangeComplete', handleRouteChange);
return () => router.events.off('routeChangeComplete', handleRouteChange);
}, [router.events]);
return (
<>
<Toaster position={'top-right'} />
<div>
<AuthGuard>
{isPublicRoute ? (
<App {...props} />
) : (
<ProjectGuard>
<CommandPalette />
<App {...props} />
</ProjectGuard>
)}
</AuthGuard>
</div>
</>
);
}