Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
b202c6b
Update docs, README, and project config
tayloraj10 Jul 14, 2026
8362722
Add theming system and shared UI components
tayloraj10 Jul 14, 2026
63d6b5c
Add resource reviews list page
tayloraj10 Jul 14, 2026
a8966f8
Add authenticated layout and dashboard redesign
tayloraj10 Jul 14, 2026
be735ba
Add resource review detail page
tayloraj10 Jul 14, 2026
69eeb1a
Refactor theming system to use cookies for persistence
tayloraj10 Jul 15, 2026
afd6656
Convert WaveDivider, ThemeToggle, and root ErrorBoundary from Tailwin…
tayloraj10 Jul 15, 2026
13fad62
Convert authenticated layout from Tailwind to MUI
tayloraj10 Jul 15, 2026
93f8ba7
Remove Tailwind CSS infrastructure
tayloraj10 Jul 15, 2026
e12aa63
Convert remaining pages to MUI and remove Tailwind CSS infrastructure
tayloraj10 Jul 15, 2026
50c7b46
Convert remaining pages to MUI and remove Tailwind CSS infrastructure
tayloraj10 Jul 15, 2026
ce71543
Convert remaining pages to MUI and remove Tailwind CSS infrastructure
tayloraj10 Jul 15, 2026
d9f738d
Merge branch 'pr/reviews-list' into pr/dashboard-layout
tayloraj10 Jul 15, 2026
7913066
Merge remote-tracking branch 'origin/pr/dashboard-layout' into pr/rev…
tayloraj10 Jul 15, 2026
b4c9690
Redesign auth pages and add password reset flow (#28)
tayloraj10 Jul 21, 2026
c608f9d
Merge pull request #29 from phlask/pr/reviews-list
tayloraj10 Jul 21, 2026
9f63117
Merge pull request #31 from phlask/pr/review-detail
tayloraj10 Jul 21, 2026
ef2b0ca
Merge remote-tracking branch 'origin/pr/theming-infra' into tmp/docs-…
tayloraj10 Jul 22, 2026
14569c3
Merge remote-tracking branch 'origin/pr/dashboard-layout' into tmp/do…
tayloraj10 Jul 22, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ jobs:
- name: Setup Biome
uses: biomejs/setup-biome@v2
with:
version: latest
version: 2.3.14
- name: Run Biome
run: biome ci .
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,4 @@
# React Router
/.react-router/
/build/


/docker/build/
49 changes: 37 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ This is the admin dashboard for the [PHLASK](https://github.com/phlask/phlask-ma

## Key Features

- Authenticate via Supabase (login, password reset, email confirmation)
- View and manage resources from the Supabase database
- Review and approve/reject suggested edits to resources
- View and resolve reports on resources
- View resource changelogs and roll back changes if needed
- Light/dark theme support

## Getting Started

Expand Down Expand Up @@ -65,23 +67,46 @@ Replace 3000:3000 with the actual port your application listens on if it is diff
app/
├── api/
│ ├── resources/
│ │ └── methods.ts # API methods related to resources
│ ├── client.ts # API client setup
│ └── types.ts # Shared API request/response types
│ │ └── methods.ts # API methods related to resources
│ ├── resource-revisions/
│ │ └── methods.ts # API methods related to resource revisions/changelogs
│ ├── client.server.ts # Supabase client setup (server-side)
│ └── types.ts # Shared API request/response types
├── assets/
│ └── PHILASK_v2.svg # Static assets (logos, images, etc.)
│ └── PHILASK_v2.svg # Static assets (logos, images, etc.)
├── components/
│ ├── ThemeToggle.tsx # Light/dark theme toggle control
│ └── WaveDivider.tsx # Decorative UI component
├── constants/
│ └── db.ts # Database-related constants/config
│ └── db.ts # Database-related constants/config
├── context/
│ └── user.ts # Authenticated user context
├── middleware/
│ └── auth.ts # Route authentication middleware
├── routes/
│ ├── _layout.tsx # Shared layout for route pages
│ ├── dashboard.tsx # Dashboard page component
│ ├── authenticated/ # Routes that require a logged-in user
│ │ ├── reviews/ # Review & approve/reject suggested edits
│ │ ├── _layout.tsx
│ │ ├── dashboard.tsx
│ │ └── logout.tsx
│ ├── unauthenticated/ # Public auth routes (login, password reset, etc.)
│ │ ├── _layout.tsx
│ │ ├── login.tsx
│ │ ├── forgot-password.tsx
│ │ ├── reset-password.tsx
│ │ └── confirm.tsx
├── schemas/
│ └── *.ts # Zod schemas for auth forms
├── theme/
│ ├── theme.ts # MUI theme configuration
│ └── ThemeModeProvider.tsx # Light/dark theme context provider
├── types/
│ └── ResourceEntry.ts # Domain-specific TypeScript types
│ └── ResourceEntry.ts # Domain-specific TypeScript types
├── utils/
│ └── distance.ts # Distance calculation utilities
├── app.css # Global application styles
├── root.tsx # App root component / Entry point
└── routes.ts # Route definitions and router configuration
│ └── distance.ts # Distance calculation utilities
├── app.css # Global application styles
├── root.tsx # App root component / Entry point
└── routes.ts # Route definitions and router configuration
```

## How to Contribute / Next Steps
Expand Down
Binary file added Screenshot 2026-07-07 at 8.06.36 PM.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
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;
105 changes: 97 additions & 8 deletions app/app.css
Original file line number Diff line number Diff line change
@@ -1,16 +1,105 @@
@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. */

@theme {
--font-sans:
'Inter', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
: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-300: #6fd6ff;

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

html,
body {
@apply bg-white dark:bg-gray-950;
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;
}

@keyframes wave-drift {
from {
transform: translateX(0);
}
to {
transform: translateX(-50%);
}
}

@keyframes bubble-rise {
from {
transform: translateY(0) scale(1);
opacity: 0.5;
}
to {
transform: translateY(-140px) scale(1.15);
opacity: 0;
}
}

/* Ambient per-type motion for resource-type chip icons (see chipColors.tsx) */
@keyframes chip-icon-bob {
0%,
100% {
transform: translateY(0);
}
50% {
transform: translateY(-2px);
}
}

@keyframes chip-icon-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.18);
}
}

@keyframes chip-icon-sway {
0%,
100% {
transform: rotate(-10deg);
}
50% {
transform: rotate(10deg);
}
}

@media (prefers-color-scheme: dark) {
color-scheme: dark;
@keyframes chip-icon-wiggle {
0%,
100% {
transform: rotate(0deg);
}
25% {
transform: rotate(-12deg);
}
75% {
transform: rotate(12deg);
}
}

/* Water-themed scrollbar */
* {
scrollbar-color: var(--color-brand-300) transparent;
}

html.dark * {
scrollbar-color: var(--color-navy-600) transparent;
}
45 changes: 45 additions & 0 deletions app/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { DarkMode, LightMode } from "@mui/icons-material";
import { IconButton, Tooltip } from "@mui/material";
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) rather than the `mode` value from React state, so this
// can never mismatch between SSR and hydration.
export function ThemeToggle() {
const { toggleMode } = useThemeMode();

return (
<Tooltip title="Toggle dark mode">
<IconButton
onClick={toggleMode}
aria-label="Toggle dark mode"
sx={(theme) => ({
bgcolor: brand[100],
color: brand[700],
transition: "transform 0.35s ease, background-color 0.2s ease",
"&:hover": { bgcolor: brand[200], transform: "rotate(-14deg)" },
...theme.applyStyles("dark", {
bgcolor: navy[800],
color: brand[300],
"&:hover": { bgcolor: navy[700] },
}),
})}
>
<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>
);
}
59 changes: 59 additions & 0 deletions app/components/WaveDivider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
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";

type WaveDividerProps = {
color: string;
height?: number;
opacity?: number;
duration?: number;
reverse?: boolean;
sx?: SxProps<Theme>;
};

export function WaveDivider({
color,
height = 60,
opacity = 1,
duration = 18,
reverse = false,
sx,
}: WaveDividerProps) {
return (
<Box
aria-hidden
sx={[
{
pointerEvents: "none",
position: "absolute",
insetInline: 0,
overflow: "hidden",
lineHeight: 0,
height,
},
...(Array.isArray(sx) ? sx : [sx]),
]}
>
<Box
component="svg"
aria-hidden="true"
viewBox="0 0 2400 120"
preserveAspectRatio="none"
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} />
</Box>
</Box>
);
}
Loading