Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
1ca94a3
Reimporting old header stylings
Jul 31, 2025
2fc27e1
Adding 'memberships' page
Jul 31, 2025
2af467e
Rewriting the profile - using tailwind
Aug 31, 2025
58f99c9
Pulling out shared components from members.membership.tsx
Sep 3, 2025
5c37b8d
Linting the ui-toolkit heading, add comments and de-duplicate in the
Sep 3, 2025
afe712e
Compacting components with no children to use the 'void elements' syntax
Sep 3, 2025
0fbb474
Added a shared links file + the Applications page w/ table sorting and
Sep 3, 2025
6965aa7
Update comments and added a simplified display flag
Sep 3, 2025
e061d02
Tuning sitewide heading sizes, and add textbox resizing
Sep 3, 2025
7996bd9
Added members home with placeholder table - added `copyable` and
Sep 3, 2025
600a9c6
Milestone: Added supabase fetching
Sep 10, 2025
20cfa22
Adds a POST method to /members/profile -
Sep 10, 2025
9dc740d
Added supabase dev DB setup
Sep 10, 2025
721f312
Added E2E Auth checking! Updated react-router to 7.9.1
Sep 13, 2025
d599e56
Add manual identity linking (multiple OAuth identities)
Sep 14, 2025
2ac2ae7
Change constraints on profile to draw directly from auth.users table
Sep 15, 2025
02f3576
Adding scaffolding for the application details page -
Sep 15, 2025
0c9fb21
Move header spacing to app.css, move width-constraint to root
Sep 15, 2025
db28a18
Add toasts functionality
Sep 15, 2025
d5f7c03
Nit fixes: add space, and fix flicker in navbar by using Link for
Sep 15, 2025
b12e1b6
Nit fixes: change animation timing on toasts
Sep 15, 2025
31c1c98
Added member tables UI
Sep 15, 2025
ebd13a3
Bulk change: Added profile page for individual members
Sep 15, 2025
92f900f
Added /members/:profile_id route directly
Sep 16, 2025
2a7fbc2
Move styling back to body so Errors render correctly
Sep 17, 2025
5141546
Added roles + permission tables, starting application flow
Sep 22, 2025
2cbde8d
New components (counting-text-area) and new routes(application-edit)
Sep 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 41 additions & 4 deletions app/app.css
Original file line number Diff line number Diff line change
@@ -1,17 +1,54 @@
@import "tailwindcss";

@theme {
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-sans:
"Inter", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji",
"Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--color-primary: #be2361;
--color-secondary: #cf276a;
--color-button-active: #d23876;
}

html,
body {
@apply bg-white dark:bg-gray-950;

@media (prefers-color-scheme: light) {
color-scheme: light;
}
}
a {
color: #337ab7;
}
a:hover {
color: #23527c;
text-decoration: underline;
}
a.navbar {
color: white;
}
a.navbar:hover {
color: rgb(245, 209, 209);
}
h1,
h2,
h3,
h4,
h5,
h6 {
color: #000;
font-family: "Bree Serif", sans-serif;
font-weight: 800;
a {
color: black;
}
margin-top: 20px;
margin-bottom: 10px;
}
h1 {
font-size: 30px;
}
h2 {
font-size: 24px;
}
h4 {
font-size: 20px;
}
108 changes: 98 additions & 10 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import {
data,
isRouteErrorResponse,
Links,
Meta,
Outlet,
Scripts,
ScrollRestoration,
redirect,
matchPath,
useLoaderData,
} from "react-router";

import type { Route } from "./+types/root";
import { MainNavbar } from "../components/main-navbar";
import { MainNavbar, type NavigationItem } from "../components/main-navbar";
import "./app.css";

import { supabaseClientFromRequest } from "components/auth/client";
import {
Role,
RoleContext,
type RoleData,
type SerializedRole,
} from "components/auth/roles";
import { ToastContainer, ToastProvider } from "components/core/toast";

export const links: Route.LinksFunction = () => [
{ rel: "preconnect", href: "https://fonts.googleapis.com" },
{
Expand All @@ -20,7 +33,7 @@ export const links: Route.LinksFunction = () => [
},
{
rel: "stylesheet",
href: "https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
href: "https://fonts.googleapis.com/css2?family=Bree+Serif&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap",
},
];

Expand All @@ -42,20 +55,95 @@ export function Layout({ children }: { children: React.ReactNode }) {
);
}

export default function App() {
export default function App({ params }: Route.ComponentProps) {
let roleJSON = useLoaderData<SerializedRole | null>();
let role =
roleJSON != null ? new Role(roleJSON.userId, roleJSON.roles) : null;

var navigationItems: NavigationItem[] = [];
if (role != null && role.isMember()) {
navigationItems = [
{ name: "Home", href: "/members" },
{ name: "Applications", href: "/members/applications" },
{ name: "Edit Profile", href: "/members/profile" },
{ name: "Manage Membership", href: "/members/membership" },
];
} else if (role != null && role.isProspectiveMember()) {
navigationItems = [{ name: "Application", href: `/members/applications` }];
}

return (
<div className="min-h-screen bg-[#ebebeb]">
<div className="mx-auto">
<MainNavbar />
<main className="px-4 sm:px-6 lg:px-8 py-8">
<div className="min-h-screen bg-[#ebebeb] mx-auto">
<ToastProvider>
<MainNavbar navigationItems={navigationItems} />
<main className="px-4 sm:px-6 lg:px-8 py-8 max-w-4xl mx-auto">
<ToastContainer />
<Outlet />
</main>
</div>
</ToastProvider>
</div>
);
}

export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
export async function loader({
request,
context,
}: Route.LoaderArgs): Promise<SerializedRole | null> {
let role: Role | null = context.get(RoleContext);
return role != null
? {
userId: role.userId,
roles: role.roles,
}
: null;
}

async function authMiddleware({ request, context }, next) {
const requestURL = new URL(request.url);
var { supabaseClient, headers } = supabaseClientFromRequest(request);

var userId: string | null = null;
var criticalAuthPath: boolean = false;
if (supabaseClient) {
const { data, error } = await supabaseClient.auth.getUser();
userId = data?.user?.id ?? null;
}
criticalAuthPath =
requestURL.pathname == "/signin" ||
requestURL.pathname == "/auth-callback" ||
requestURL.pathname == "/logout";

if (!criticalAuthPath) {
if (!userId) {
return redirect("/signin");
}

const { data: roleData, error: roleError } = await supabaseClient
.from("user_role_view")
.select();
let role: Role | null = null;
if (roleData && userId) {
role = new Role(userId, roleData as RoleData[]);
context.set(RoleContext, role);
}
if (!role) {
throw new Response(null, { status: 404, statusText: "Not Found" });
}
}
const response = await next();
for (const [key, value] of headers.entries()) {
response.headers.set(key, value);
}
return response;
}

export const middleware = [authMiddleware];

export function HydrateFallback() {
return <div>Loading...</div>;
}

export function ErrorBoundary({ params, error }: Route.ErrorBoundaryProps) {
let message = "Oops!";
let details = "An unexpected error occurred.";
let stack: string | undefined;
Expand All @@ -72,7 +160,7 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
}

return (
<main className="pt-16 p-4 container mx-auto">
<main className="pt-16 p-4 container mx-auto bg-white">
<h1>{message}</h1>
<p>{details}</p>
{stack && (
Expand Down
21 changes: 20 additions & 1 deletion app/routes.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
import { type RouteConfig, index, route, layout, prefix } from "@react-router/dev/routes";
import {
type RouteConfig,
index,
route,
layout,
prefix,
} from "@react-router/dev/routes";

export default [
index("routes/redirect.tsx"),
...prefix("members", [
layout("routes/members.tsx", [
index("routes/members.home.tsx"),
route("applications", "routes/members.applications.tsx"),
route(
"applications/:application_id",
"routes/members.applications.detail.tsx"
),
route(
"applications/:application_id/edit",
"routes/members.applications.edit.tsx"
),
route("profile", "routes/members.profile.tsx"),
route("/:profile_id", "routes/members.profile.detail.tsx"),
route("membership", "routes/members.membership.tsx"),
]),
]),
route("auth-callback", "routes/auth-callback.tsx"),
route("link-identity", "routes/link-identity.tsx"),
route("signin", "routes/signin.tsx"),
route("logout", "routes/logout.tsx"),
] satisfies RouteConfig;
21 changes: 21 additions & 0 deletions app/routes/auth-callback.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Route } from "./+types/auth-callback";

import { redirectDocument } from "react-router";

import { supabaseClientFromRequest } from "components/auth/client";

export async function loader({ request, params, context }: Route.LoaderArgs) {
const requestUrl = new URL(request.url);
const code = requestUrl.searchParams.get("code");
const next = requestUrl.searchParams.get("next") || "/";

if (code) {
const { supabaseClient, headers } = supabaseClientFromRequest(request);
const { error } = await supabaseClient.auth.exchangeCodeForSession(code);
if (error) {
console.log("authError: ", error);
}
return redirectDocument(next, { headers });
}
return redirectDocument(next);
}
10 changes: 5 additions & 5 deletions app/routes/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { Welcome } from "../welcome/welcome";
import { Link } from "../../components/ui-toolkit/link";
import { Button } from "../../components/ui-toolkit/button";

import { MembersLink } from "components/core/links";

export function meta() {
return [
{ title: "New React Router App" },
Expand All @@ -14,11 +16,9 @@ export default function Home() {
<div>
<Welcome />
<div className="mt-8 flex justify-center">
<Link href="/members">
<Button color="blue">
Enter Members Area
</Button>
</Link>
<MembersLink>
<Button color="blue">Enter Members Area</Button>
</MembersLink>
</div>
</div>
);
Expand Down
43 changes: 43 additions & 0 deletions app/routes/link-identity.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { redirectDocument } from "react-router";

import { supabaseClientFromRequest } from "components/auth/client";
import { LinkIdentityAuth } from "components/core/auth";

import type { Route as Route } from "./+types/link-identity";

type SupportedProvider = "google" | "github";

const SupportedProviders = ["google", "github"];

function isSupported(provider: string): provider is SupportedProvider {
return provider.length > 0 && SupportedProviders.includes(provider);
}

export async function action({ request, params, context }: Route.ActionArgs) {
const formData = await request.formData();
const provider = String(formData.get("oauth_provider"));

if (!provider || !isSupported(provider)) {
console.log("does not have oauthProvider");
return null;
}

const { supabaseClient, headers } = supabaseClientFromRequest(request);

const { data, error } = await supabaseClient.auth.getUser();
if (!data?.user) {
console.log("not logged in!");
return null;
}

const { data: oAuthData, error: oAuthError } =
await supabaseClient.auth.linkIdentity({ provider });

if (oAuthData?.url) {
return redirectDocument(oAuthData.url, { headers });
}
}

export default function Component() {
return <LinkIdentityAuth />;
}
13 changes: 13 additions & 0 deletions app/routes/logout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { Route as Route } from "./+types/logout";
import { redirectDocument } from "react-router";

import { supabaseClientFromRequest } from "components/auth/client";

export async function loader({ request }: Route.LoaderArgs) {
const { supabaseClient, headers } = supabaseClientFromRequest(request);
let { error } = await supabaseClient.auth.signOut();
if (error) {
console.error("logoutError: ", error);
}
return redirectDocument("/members/profile", { headers });
}
Loading