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
69 changes: 69 additions & 0 deletions app/api/resource-revisions/methods.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import type { SupabaseClient } from "@supabase/supabase-js";
import type {
ResourceRevision,
RevisionStatus,
} from "~/types/ResourceRevision";

const TABLE_NAME = "resource_revisions";

export const getResourceRevisionAPI = (client: SupabaseClient) => {
const table = client.from(TABLE_NAME);

return {
getList: async (params: { status?: RevisionStatus } = {}) => {
let query = table.select("*").order("date_created", { ascending: false });

if (params.status) {
query = query.eq("status", params.status);
}

const { data, error } = await query;

if (error) {
throw error;
}

return (data ?? []) as ResourceRevision[];
},
getById: async (id: number) => {
const { data, error } = await table
.select("*")
.eq("id", id)
.single<ResourceRevision>();

if (error) {
throw error;
}

return data;
},
updateStatus: async (id: number, status: RevisionStatus) => {
const { data, error } = await table
.update({ status })
.eq("id", id)
.select()
.single<ResourceRevision>();

if (error) {
throw error;
}

return data;
},
updateFields: async (id: number, values: Partial<ResourceRevision>) => {
const { data, error } = await table
.update(values)
.eq("id", id)
.select()
.single<ResourceRevision>();

if (error) {
throw error;
}

return data;
},
};
};

export default getResourceRevisionAPI;
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
19 changes: 15 additions & 4 deletions app/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { useThemeMode } from "~/theme/ThemeModeProvider";
import { brand, navy } from "~/theme/theme";

// Styling and icon visibility are driven purely by the `.dark` class (via
// theme.applyStyles / Tailwind's dark: variant) rather than the `mode` value
// from React state, so this can never mismatch between SSR and hydration.
// theme.applyStyles) rather than the `mode` value from React state, so this
// can never mismatch between SSR and hydration.
export function ThemeToggle() {
const { toggleMode } = useThemeMode();

Expand All @@ -26,8 +26,19 @@ export function ThemeToggle() {
}),
})}
>
<LightMode fontSize="small" className="dark:hidden" />
<DarkMode fontSize="small" className="hidden dark:inline-block" />
<LightMode
fontSize="small"
sx={(theme) => ({
...theme.applyStyles("dark", { display: "none" }),
})}
/>
<DarkMode
fontSize="small"
sx={(theme) => ({
display: "none",
...theme.applyStyles("dark", { display: "inline-block" }),
})}
/>
</IconButton>
</Tooltip>
);
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>
);
}
12 changes: 8 additions & 4 deletions app/root.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Box } from "@mui/material";
import {
isRouteErrorResponse,
Links,
Expand Down Expand Up @@ -82,14 +83,17 @@ export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
}

return (
<main className="pt-16 p-4 container mx-auto">
<Box
component="main"
sx={{ pt: 8, p: 2, maxWidth: "lg", mx: "auto", width: "100%" }}
>
<h1>{message}</h1>
<p>{details}</p>
{stack && (
<pre className="w-full p-4 overflow-x-auto">
<Box component="pre" sx={{ width: "100%", p: 2, overflowX: "auto" }}>
<code>{stack}</code>
</pre>
</Box>
)}
</main>
</Box>
);
}
1 change: 1 addition & 0 deletions app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
export default [
layout("routes/authenticated/_layout.tsx", [
index("routes/authenticated/dashboard.tsx"),
route("reviews", "routes/authenticated/reviews/index.tsx"),
route("logout", "routes/authenticated/logout.tsx"),
]),
route("auth", "routes/unauthenticated/_layout.tsx", [
Expand Down
118 changes: 87 additions & 31 deletions app/routes/authenticated/_layout.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,106 @@
import { Box } from "@mui/material";
import { styled } from "@mui/material/styles";
import { Link, NavLink, Outlet } from "react-router";

import phlasklogo from "~/assets/PHLASK_v2.svg";

export const action = () => {};

const SidebarNavLink = styled(NavLink)({
display: "block",
borderRadius: 8,
paddingInline: 16,
paddingBlock: 10,
fontSize: "0.875rem",
fontWeight: 500,
textDecoration: "none",
color: "#4b5563",
transition: "background-color 0.2s ease, color 0.2s ease",
"&:hover": { backgroundColor: "#f3f4f6" },
"&.active": {
color: "#1d4ed8",
backgroundColor: "#eff6ff",
},
});

export default function DashboardLayout() {
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">
<Box
sx={{
display: "flex",
height: "100vh",
bgcolor: "#f9fafb",
color: "#111827",
}}
>
<Box
component="aside"
sx={{
display: "flex",
width: 256,
flexDirection: "column",
bgcolor: "#ffffff",
borderRight: "1px solid #e5e7eb",
}}
>
<Box sx={{ p: 3, borderBottom: "1px solid #f3f4f6" }}>
<Box
component="h1"
sx={{
m: 0,
fontSize: "1.25rem",
fontWeight: 700,
color: "#2563eb",
letterSpacing: "-0.025em",
}}
>
<img src={phlasklogo} alt="PHLASK Logo" />
</h1>
</div>
</Box>
</Box>

<nav className="flex flex-col justify-between flex-1 p-4 space-y-1">
<ul>
<NavLink
to="/"
end
className={({ isActive }) =>
`block px-4 py-2.5 rounded-lg text-sm font-medium transition-colors ${
isActive
? "bg-blue-50 text-blue-700"
: "text-gray-600 hover:bg-gray-100"
}`
}
>
<Box
component="nav"
sx={{
display: "flex",
flex: 1,
flexDirection: "column",
justifyContent: "space-between",
p: 2,
gap: 0.5,
}}
>
<Box component="ul" sx={{ listStyle: "none", m: 0, p: 0 }}>
<SidebarNavLink to="/" end>
Dashboard
</NavLink>
</ul>
</SidebarNavLink>
</Box>

<Link
<Box
component={Link}
to="/logout"
className="block px-4 py-2.5 rounded-lg text-sm font-medium transition-colors"
sx={{
display: "block",
borderRadius: 2,
px: 2,
py: 1.25,
fontSize: "0.875rem",
fontWeight: 500,
textDecoration: "none",
color: "inherit",
transition: "background-color 0.2s ease",
"&:hover": { bgcolor: "#f3f4f6" },
}}
>
Logout
</Link>
</nav>
</aside>
</Box>
</Box>
</Box>

<main className="flex-1 overflow-auto">
<div className="p-8">
<Box component="main" sx={{ flex: 1, overflow: "auto" }}>
<Box sx={{ p: 4 }}>
<Outlet />
</div>
</main>
</div>
</Box>
</Box>
</Box>
);
}
Loading
Loading