Skip to content

Commit 0b9331d

Browse files
committed
simple clerk setup for hackathon voting app
1 parent 59ae490 commit 0b9331d

9 files changed

Lines changed: 686 additions & 17 deletions

File tree

hackathon-app/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Origin
2+
ORIGIN="http://localhost:5173"
3+
4+
# Clerk
5+
CLERK_PUBLISHABLE_KEY=
6+
CLERK_SECRET_KEY=

hackathon-app/app/app.css

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
@import "tailwindcss";
22

33
@theme {
4-
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
5-
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
4+
--font-sans:
5+
"Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
6+
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
67
}
78

89
html,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { z } from "zod";
2+
3+
const ClientConfigSchema = z.object({
4+
origin: z.string().url(),
5+
clerk: z.object({
6+
publishableKey: z.string(),
7+
}),
8+
});
9+
10+
export function getConfig() {
11+
const configMeta: HTMLMetaElement | null = document.querySelector(
12+
"meta[name='config']",
13+
);
14+
if (!configMeta) {
15+
throw new Error("Config meta not found");
16+
}
17+
18+
const config = ClientConfigSchema.parse(JSON.parse(configMeta.content));
19+
return config;
20+
}
21+
22+
export type ClientConfig = ReturnType<typeof getConfig>;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { z } from "zod";
2+
import type { Schema, ZodTypeDef } from "zod";
3+
4+
type PreValidate<ConfigData> = {
5+
[K in keyof ConfigData]: ConfigData[K] extends object
6+
? PreValidate<ConfigData[K]> | undefined
7+
: ConfigData[K] extends string
8+
? string | undefined // use string instead of enum values
9+
: ConfigData[K] | undefined;
10+
};
11+
12+
// Validation
13+
const validateConfigOrExit = <T, I>(
14+
schema: Schema<T, ZodTypeDef, I>,
15+
data: PreValidate<I>,
16+
): T => {
17+
try {
18+
return schema.parse(data);
19+
} catch (exception: any) {
20+
if (exception instanceof z.ZodError) {
21+
console.error("Configuration validation failed. Exit is forced.");
22+
exception.issues.forEach((issue) => {
23+
console.error(`\t- issue: ${issue.path.join(".")}: ${issue.message}`);
24+
});
25+
} else {
26+
console.error(exception);
27+
}
28+
process.exit(1);
29+
}
30+
};
31+
32+
// Definitions
33+
const InstanceSchema = z.object({
34+
environment: z.enum(["development", "production", "test"]),
35+
origin: z.string().url(),
36+
});
37+
38+
const ClerkSchema = z.object({
39+
publishableKey: z.string(),
40+
secretKey: z.string(),
41+
});
42+
43+
const MainConfigSchema = z
44+
.object({
45+
clerk: ClerkSchema,
46+
})
47+
.merge(InstanceSchema);
48+
49+
export type MainConfig = z.infer<typeof MainConfigSchema>;
50+
51+
const port = process.env.PORT ? Number.parseInt(process.env.PORT, 10) : 5173;
52+
export const mainConfig: MainConfig = validateConfigOrExit(MainConfigSchema, {
53+
environment: process.env.NODE_ENV || "development",
54+
origin: process.env.ORIGIN || `http://localhost:${port}`,
55+
clerk: {
56+
publishableKey: process.env.CLERK_PUBLISHABLE_KEY,
57+
secretKey: process.env.CLERK_SECRET_KEY,
58+
},
59+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { SignedIn, SignedOut, SignInButton, UserButton } from "@clerk/clerk-react";
2+
3+
export default function Header() {
4+
return (
5+
<header className="flex items-center justify-center py-8 px-4">
6+
<SignedOut>
7+
<SignInButton />
8+
</SignedOut>
9+
<SignedIn>
10+
<UserButton />
11+
</SignedIn>
12+
</header>
13+
);
14+
}

hackathon-app/app/root.tsx

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import {
66
Scripts,
77
ScrollRestoration,
88
} from "react-router";
9+
import { mainConfig } from "./modules/config/env.server";
10+
import type { ClientConfig } from "./modules/config/env.client";
11+
import { rootAuthLoader } from '@clerk/react-router/ssr.server'
12+
import { ClerkProvider } from "@clerk/react-router";
13+
import Header from "./modules/nav/header";
914

1015
import type { Route } from "./+types/root";
1116
import "./app.css";
@@ -23,6 +28,33 @@ export const links: Route.LinksFunction = () => [
2328
},
2429
];
2530

31+
export async function loader(args: Route.LoaderArgs) {
32+
return rootAuthLoader(args, () => {
33+
const clientConfig: ClientConfig = {
34+
origin: mainConfig.origin,
35+
clerk: {
36+
publishableKey: mainConfig.clerk.publishableKey,
37+
},
38+
};
39+
return {
40+
clientConfig,
41+
};
42+
}, {
43+
signUpFallbackRedirectUrl: "/",
44+
signInFallbackRedirectUrl: "/",
45+
});
46+
}
47+
48+
export function meta({ data }: Route.MetaArgs): ReturnType<Route.MetaFunction> {
49+
const { clientConfig } = data;
50+
return [
51+
{
52+
name: "config",
53+
content: JSON.stringify(clientConfig),
54+
},
55+
];
56+
}
57+
2658
export function Layout({ children }: { children: React.ReactNode }) {
2759
return (
2860
<html lang="en">
@@ -41,8 +73,18 @@ export function Layout({ children }: { children: React.ReactNode }) {
4173
);
4274
}
4375

44-
export default function App() {
45-
return <Outlet />;
76+
export default function App({ loaderData }: Route.ComponentProps) {
77+
return (
78+
<ClerkProvider
79+
publishableKey={loaderData.clientConfig.clerk.publishableKey}
80+
loaderData={loaderData}
81+
signUpFallbackRedirectUrl="/"
82+
signInFallbackRedirectUrl="/"
83+
>
84+
<Header />
85+
<Outlet />
86+
</ClerkProvider>
87+
);
4688
}
4789

4890
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {

0 commit comments

Comments
 (0)