Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 11 additions & 19 deletions app/app.css
Original file line number Diff line number Diff line change
@@ -1,40 +1,32 @@
@import 'tailwindcss';

/* Dark mode is toggled by adding/removing `.dark` on <html> (see ThemeModeProvider),
rather than following the OS preference, so it can stay in sync with the MUI theme. */
@custom-variant dark (&:where(.dark, .dark *));

@theme {
--font-sans:
'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';

/* Water/brand blue, anchored on the PHLASK logo color (#10B6FF = brand-500) */
:root {
/* Water/brand blue, anchored on the PHLASK logo color (#10B6FF = brand-500).
Mirrored in app/theme/theme.ts (kept in sync manually). */
--color-brand-50: #eafaff;
--color-brand-100: #d6f4ff;
--color-brand-200: #ade8ff;
--color-brand-300: #6fd6ff;
--color-brand-400: #38c1ff;
--color-brand-500: #10b6ff;
--color-brand-600: #0090de;
--color-brand-700: #0072b3;
--color-brand-800: #045c90;
--color-brand-900: #0a3a5c;

/* Deep ocean navy, used for dark-mode surfaces */
--color-navy-950: #071522;
--color-navy-900: #0a1929;
--color-navy-800: #10263a;
--color-navy-700: #17344c;
--color-navy-600: #1f4560;
}

html,
body {
@apply bg-brand-50 text-navy-900 dark:bg-navy-950 dark:text-brand-50;
background-color: var(--color-brand-50);
color: var(--color-navy-900);
color-scheme: light;
}

html.dark,
html.dark body {
background-color: var(--color-navy-950);
color: var(--color-brand-50);
}

html.dark {
color-scheme: dark;
}
Expand Down
36 changes: 26 additions & 10 deletions app/components/WaveDivider.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Box, type SxProps, type Theme } from "@mui/material";

// Two periods of the same wave back-to-back so a -50% translateX loops seamlessly.
const WAVE_PATH =
"M0,40 C150,90 350,0 600,40 C850,80 1050,0 1200,40 C1350,80 1550,0 1800,40 C1950,80 2150,0 2400,40 L2400,120 L0,120 Z";
Expand All @@ -8,7 +10,7 @@ type WaveDividerProps = {
opacity?: number;
duration?: number;
reverse?: boolean;
className?: string;
sx?: SxProps<Theme>;
};

export function WaveDivider({
Expand All @@ -17,27 +19,41 @@ export function WaveDivider({
opacity = 1,
duration = 18,
reverse = false,
className,
sx,
}: WaveDividerProps) {
return (
<div
<Box
aria-hidden
className={`pointer-events-none absolute inset-x-0 overflow-hidden leading-none ${className ?? ""}`}
style={{ height }}
sx={[
{
pointerEvents: "none",
position: "absolute",
insetInline: 0,
overflow: "hidden",
lineHeight: 0,
height,
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
<svg
<Box
component="svg"
aria-hidden="true"
viewBox="0 0 2400 120"
preserveAspectRatio="none"
className="absolute inset-y-0 left-0 h-full w-[200%]"
style={{
sx={{
position: "absolute",
insetBlock: 0,
left: 0,
height: "100%",
width: "200%",
animation: `wave-drift ${duration}s linear infinite`,
animationDirection: reverse ? "reverse" : "normal",
opacity,
}}
>
<path d={WAVE_PATH} fill={color} />
</svg>
</div>
</Box>
</Box>
);
}
3 changes: 3 additions & 0 deletions app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,8 @@ export default [
]),
route("auth", "routes/unauthenticated/_layout.tsx", [
index("routes/unauthenticated/login.tsx"),
route("forgot-password", "routes/unauthenticated/forgot-password.tsx"),
route("confirm", "routes/unauthenticated/confirm.tsx"),
route("reset-password", "routes/unauthenticated/reset-password.tsx"),
]),
] satisfies RouteConfig;
136 changes: 123 additions & 13 deletions app/routes/unauthenticated/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,133 @@
import { Box } from "@mui/material";
import { Outlet } from "react-router";

import phlasklogo from "~/assets/PHLASK_v2.svg";
import { ThemeToggle } from "~/components/ThemeToggle";
import { WaveDivider } from "~/components/WaveDivider";
import { brand, navy } from "~/theme/theme";

const BUBBLES = [
{ left: "8%", size: 14, delay: 0, duration: 9 },
{ left: "18%", size: 8, delay: 2.5, duration: 7 },
{ left: "32%", size: 20, delay: 1, duration: 11 },
{ left: "62%", size: 10, delay: 3.5, duration: 8 },
{ left: "78%", size: 16, delay: 0.5, duration: 10 },
{ left: "90%", size: 9, delay: 2, duration: 7.5 },
];

export default function UnauthenticatedLayout() {
return (
<div className="flex h-screen bg-gray-50 text-gray-900">
<aside className="w-64 bg-white border-r border-gray-200 flex flex-col">
<div className="p-6 border-b border-gray-100">
<h1 className="text-xl font-bold text-blue-600 tracking-tight">
<img src={phlasklogo} alt="PHLASK Logo" />
</h1>
</div>
</aside>
<Box
sx={(theme) => ({
position: "relative",
display: "flex",
minHeight: "100vh",
alignItems: "center",
justifyContent: "center",
overflow: "hidden",
px: 2,
py: 6,
backgroundImage: `linear-gradient(to bottom, ${brand[100]}, ${brand[300]}, ${brand[600]})`,
...theme.applyStyles("dark", {
backgroundImage: `linear-gradient(to bottom, ${navy[950]}, ${navy[900]}, ${navy[800]})`,
}),
})}
>
<Box sx={{ position: "absolute", top: 16, right: 16, zIndex: 20 }}>
<ThemeToggle />
</Box>

{BUBBLES.map((bubble) => (
<Box
key={bubble.left}
aria-hidden
sx={(theme) => ({
pointerEvents: "none",
position: "absolute",
bottom: 0,
borderRadius: "9999px",
bgcolor: "rgba(255,255,255,0.4)",
...theme.applyStyles("dark", {
bgcolor: `${brand[200]}33`,
}),
})}
style={{
left: bubble.left,
width: bubble.size,
height: bubble.size,
animation: `bubble-rise ${bubble.duration}s ease-in infinite`,
animationDelay: `${bubble.delay}s`,
}}
/>
))}

<WaveDivider
color={brand[400]}
height={140}
opacity={0.35}
duration={26}
sx={(theme) => ({
bottom: 0,
...theme.applyStyles("dark", { opacity: 0.2 }),
})}
/>
<WaveDivider
color={brand[500]}
height={100}
opacity={0.45}
duration={20}
reverse
sx={(theme) => ({
bottom: 0,
...theme.applyStyles("dark", { opacity: 0.25 }),
})}
/>
<WaveDivider
color={brand[700]}
height={60}
opacity={0.55}
duration={15}
sx={(theme) => ({
bottom: 0,
...theme.applyStyles("dark", { opacity: 0.3 }),
})}
/>

<main className="flex-1 overflow-auto">
<div className="p-8">
<Box
sx={{ position: "relative", zIndex: 10, width: "100%", maxWidth: 448 }}
>
<Box sx={{ mb: 4, display: "flex", justifyContent: "center" }}>
<Box
sx={(theme) => ({
display: "inline-flex",
borderRadius: 4,
bgcolor: "rgba(255,255,255,0.8)",
p: 1.5,
boxShadow: 2,
backdropFilter: "blur(4px)",
...theme.applyStyles("dark", { bgcolor: `${navy[950]}99` }),
})}
>
<img src={phlasklogo} alt="PHLASK Logo" style={{ height: 36 }} />
</Box>
</Box>
<Box
sx={(theme) => ({
borderRadius: 4,
border: "1px solid rgba(255,255,255,0.5)",
bgcolor: "rgba(255,255,255,0.75)",
p: 4,
boxShadow: 8,
backdropFilter: "blur(8px)",
...theme.applyStyles("dark", {
borderColor: `${navy[700]}99`,
bgcolor: `${navy[900]}bf`,
}),
})}
>
<Outlet />
</div>
</main>
</div>
</Box>
</Box>
</Box>
);
}
20 changes: 20 additions & 0 deletions app/routes/unauthenticated/confirm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { type LoaderFunction, redirect } from "react-router";
import { getDatabaseClient } from "~/api/client.server";

export const loader: LoaderFunction = async ({ request }) => {
const url = new URL(request.url);
const code = url.searchParams.get("code");

if (!code) {
return redirect("/auth/forgot-password");
}

const { client, headers } = getDatabaseClient(request);
const { error } = await client.auth.exchangeCodeForSession(code);

if (error) {
return redirect("/auth/forgot-password");
}

return redirect("/auth/reset-password", { headers });
};
Loading
Loading