-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
00f632c
commit ec60894
Showing
17 changed files
with
25,718 additions
and
25,441 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,225 @@ | ||
import { | ||
json, | ||
type LoaderFunctionArgs, | ||
type HeadersFunction, | ||
type LinksFunction, | ||
type MetaFunction, | ||
} from '@remix-run/node' | ||
import { | ||
Links, | ||
Meta, | ||
Outlet, | ||
Scripts, | ||
ScrollRestoration, | ||
useLoaderData, | ||
} from '@remix-run/react' | ||
import { withSentry } from '@sentry/remix' | ||
import { HoneypotProvider } from 'remix-utils/honeypot/react' | ||
// eslint-disable-next-line no-warning-comments | ||
// FIXME: This and the file should be removed when sonnerStyles are fixed (https://github.com/epicweb-dev/epic-stack/issues/842) | ||
import sonnerStyles from '#app/styles/sonner.css?url' | ||
import appleTouchIconAssetUrl from './assets/favicons/apple-touch-icon.png' | ||
import faviconAssetUrl from './assets/favicons/favicon.svg' | ||
import Banner from './components/banner.tsx' | ||
import { GeneralErrorBoundary } from './components/error-boundary.tsx' | ||
import { EpicProgress } from './components/progress-bar.tsx' | ||
import { useToast } from './components/toaster.tsx' | ||
import { href as iconsHref } from './components/ui/icon.tsx' | ||
import { EpicToaster } from './components/ui/sonner.tsx' | ||
import { | ||
ThemeSwitch, | ||
useOptionalTheme, | ||
useTheme, | ||
} from './routes/resources+/theme-switch.tsx' | ||
import tailwindStyleSheetUrl from './styles/tailwind.css?url' | ||
import { getUserId, logout } from './utils/auth.server.ts' | ||
import { ClientHintCheck, getHints } from './utils/client-hints.tsx' | ||
import { prisma } from './utils/db.server.ts' | ||
import { getEnv } from './utils/env.server.ts' | ||
import { honeypot } from './utils/honeypot.server.ts' | ||
import { combineHeaders, getDomainUrl } from './utils/misc.tsx' | ||
import { useNonce } from './utils/nonce-provider.ts' | ||
import { type Theme, getTheme } from './utils/theme.server.ts' | ||
import { makeTimings, time } from './utils/timing.server.ts' | ||
import { getToast } from './utils/toast.server.ts' | ||
|
||
export const links: LinksFunction = () => { | ||
return [ | ||
// Preload svg sprite as a resource to avoid render blocking | ||
{ rel: 'preload', href: iconsHref, as: 'image' }, | ||
{ | ||
rel: 'icon', | ||
href: '/favicon.ico', | ||
sizes: '48x48', | ||
}, | ||
{ rel: 'icon', type: 'image/svg+xml', href: faviconAssetUrl }, | ||
{ rel: 'apple-touch-icon', href: appleTouchIconAssetUrl }, | ||
{ | ||
rel: 'manifest', | ||
href: '/site.webmanifest', | ||
crossOrigin: 'use-credentials', | ||
} as const, // necessary to make typescript happy | ||
{ rel: 'stylesheet', href: tailwindStyleSheetUrl }, | ||
{ rel: 'stylesheet', href: sonnerStyles }, | ||
].filter(Boolean) | ||
} | ||
|
||
export const meta: MetaFunction<typeof loader> = ({ data }) => { | ||
return [ | ||
{ title: data ? 'Metabase' : 'Error | Metabase' }, | ||
{ name: 'description', content: `Your own captain's log` }, | ||
] | ||
} | ||
|
||
export async function loader({ request }: LoaderFunctionArgs) { | ||
const timings = makeTimings('root loader') | ||
const userId = await time(() => getUserId(request), { | ||
timings, | ||
type: 'getUserId', | ||
desc: 'getUserId in root', | ||
}) | ||
|
||
const user = userId | ||
? await time( | ||
() => | ||
prisma.user.findUniqueOrThrow({ | ||
select: { | ||
id: true, | ||
name: true, | ||
username: true, | ||
image: { select: { id: true } }, | ||
initials: true, | ||
roles: { | ||
select: { | ||
name: true, | ||
permissions: { | ||
select: { entity: true, action: true, access: true }, | ||
}, | ||
}, | ||
}, | ||
}, | ||
where: { id: userId }, | ||
}), | ||
{ timings, type: 'find user', desc: 'find user in root' }, | ||
) | ||
: null | ||
if (userId && !user) { | ||
console.info('something weird happened') | ||
// something weird happened... The user is authenticated but we can't find | ||
// them in the database. Maybe they were deleted? Let's log them out. | ||
await logout({ request, redirectTo: '/' }) | ||
} | ||
const { toast, headers: toastHeaders } = await getToast(request) | ||
const honeyProps = honeypot.getInputProps() | ||
|
||
return json( | ||
{ | ||
user, | ||
requestInfo: { | ||
hints: getHints(request), | ||
origin: getDomainUrl(request), | ||
path: new URL(request.url).pathname, | ||
userPrefs: { | ||
theme: getTheme(request), | ||
}, | ||
}, | ||
ENV: getEnv(), | ||
toast, | ||
honeyProps, | ||
}, | ||
{ | ||
headers: combineHeaders( | ||
{ 'Server-Timing': timings.toString() }, | ||
toastHeaders, | ||
), | ||
}, | ||
) | ||
} | ||
|
||
export const headers: HeadersFunction = ({ loaderHeaders }) => { | ||
const headers = { | ||
'Server-Timing': loaderHeaders.get('Server-Timing') ?? '', | ||
} | ||
return headers | ||
} | ||
|
||
function Document({ | ||
children, | ||
nonce, | ||
theme = 'light', | ||
env = {}, | ||
}: { | ||
children: React.ReactNode | ||
nonce: string | ||
theme?: Theme | ||
env?: Record<string, string> | ||
}) { | ||
const allowIndexing = ENV.ALLOW_INDEXING !== 'false' | ||
return ( | ||
<html lang="en" className={`${theme} h-full overflow-x-hidden`}> | ||
<head> | ||
<ClientHintCheck nonce={nonce} /> | ||
<Meta /> | ||
<meta charSet="utf-8" /> | ||
<meta name="viewport" content="width=device-width,initial-scale=1" /> | ||
{allowIndexing ? null : ( | ||
<meta name="robots" content="noindex, nofollow" /> | ||
)} | ||
<Links /> | ||
</head> | ||
<body className="bg-background text-foreground"> | ||
{children} | ||
<script | ||
nonce={nonce} | ||
dangerouslySetInnerHTML={{ | ||
__html: `window.ENV = ${JSON.stringify(env)}`, | ||
}} | ||
/> | ||
<ScrollRestoration nonce={nonce} /> | ||
<Scripts nonce={nonce} /> | ||
</body> | ||
</html> | ||
) | ||
} | ||
|
||
export function Layout({ children }: { children: React.ReactNode }) { | ||
// if there was an error running the loader, data could be missing | ||
const data = useLoaderData<typeof loader | null>() | ||
const nonce = useNonce() | ||
const theme = useOptionalTheme() | ||
return ( | ||
<Document nonce={nonce} theme={theme} env={data?.ENV}> | ||
{children} | ||
</Document> | ||
) | ||
} | ||
|
||
function App() { | ||
const data = useLoaderData<typeof loader>() | ||
const theme = useTheme() | ||
useToast(data.toast) | ||
|
||
return ( | ||
<> | ||
<Banner /> | ||
<Outlet /> | ||
<EpicToaster closeButton position="top-center" theme={theme} /> | ||
<EpicProgress /> | ||
</> | ||
) | ||
} | ||
|
||
function AppWithProviders() { | ||
const data = useLoaderData<typeof loader>() | ||
return ( | ||
<HoneypotProvider {...data.honeyProps}> | ||
<App /> | ||
</HoneypotProvider> | ||
) | ||
} | ||
|
||
export default withSentry(AppWithProviders) | ||
|
||
// this is a last resort error boundary. There's not much useful information we | ||
// can offer at this level. | ||
export const ErrorBoundary = GeneralErrorBoundary |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.