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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@
/.react-router/
/build/
/docker/build/

# Amplify build output
/.amplify-hosting/
19 changes: 19 additions & 0 deletions amplify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: 1
frontend:
phases:
preBuild:
commands:
- nvm use 22
- corepack enable
- corepack prepare pnpm@10.28.2 --activate
- pnpm install --frozen-lockfile
build:
commands:
- env | grep -e SUPABASE_URL >> .env
- env | grep -e SUPABASE_PUBLISHABLE_KEY >> .env
- pnpm run build
- bash bin/postbuild.sh
artifacts:
baseDirectory: .amplify-hosting
files:
- '**/*'
22 changes: 11 additions & 11 deletions app/api/resource-revisions/methods.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import type { SupabaseClient } from "@supabase/supabase-js";
import type {
ResourceRevision,
RevisionStatus,
ResourceEdit,
ResourceEditReviewStatus,
} from "~/types/ResourceRevision";

const TABLE_NAME = "resource_revisions";
const TABLE_NAME = "resource_edits";

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

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

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

const { data, error } = await query;
Expand All @@ -23,39 +23,39 @@ export const getResourceRevisionAPI = (client: SupabaseClient) => {
throw error;
}

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

if (error) {
throw error;
}

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

if (error) {
throw error;
}

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

if (error) {
throw error;
Expand Down
6 changes: 3 additions & 3 deletions app/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ type PaginatedResult<Entity> = {

export type GetModelAPI<Entity, Params> = (client: SupabaseClient) => {
getList: (params: Params) => Promise<PaginatedResult<Entity>>;
getById: (id: string) => Promise<Entity>;
getById: (id: number) => Promise<Entity>;
create: (values: Entity) => Promise<Entity>;
updateById: (id: string, values: Partial<Entity>) => Promise<Entity>;
delete: (id: string) => Promise<void>;
updateById: (id: number, values: Partial<Entity>) => Promise<Entity>;
delete: (id: number) => Promise<void>;
};
4 changes: 2 additions & 2 deletions app/routes/authenticated/dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const loader: LoaderFunction = async ({ request }) => {
const creator = revision.creator || "Unknown";
submitterCounts.set(creator, (submitterCounts.get(creator) ?? 0) + 1);

if (revision.status === "PENDING") {
if (revision.review_status === "PENDING") {
pendingCount += 1;
outstandingByType.set(
revision.resource_type,
Expand Down Expand Up @@ -218,7 +218,7 @@ const Dashboard = () => {
variant="outlined"
sx={{ p: 2.5, flex: 1, bgcolor: "action.hover" }}
>
<Tooltip title="resource_revisions has no reviewed_by column yet, so there's no record of who approved/rejected a given submission. Add that column (and set it in the approve/reject action) to unlock this.">
<Tooltip title="resource_edits has no reviewed_by column yet, so there's no record of who approved/rejected a given submission. Add that column (and set it in the approve/reject action) to unlock this.">
<Typography variant="subtitle1" fontWeight={600} gutterBottom>
Top approvers
</Typography>
Expand Down
20 changes: 10 additions & 10 deletions app/routes/authenticated/reviews/detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ import type {
ResourceType,
WaterTag,
} from "~/types/ResourceEntry";
import type { ResourceRevision } from "~/types/ResourceRevision";
import type { ResourceEdit } from "~/types/ResourceRevision";
import { statusChipColor } from "~/utils/chipColors";

export const middleware = [authMiddleware];
Expand Down Expand Up @@ -129,7 +129,7 @@ type EditableValues = {
bathroom: { tags: BathroomTag[] };
};

const toEditableValues = (revision: ResourceRevision): EditableValues => ({
const toEditableValues = (revision: ResourceEdit): EditableValues => ({
name: revision.name ?? "",
resource_type: revision.resource_type,
entry_type: revision.entry_type ?? "",
Expand Down Expand Up @@ -174,7 +174,7 @@ export const loader: LoaderFunction = async ({ request, params }) => {
let resource: ResourceEntry | null = null;
try {
const resourceAPI = getResourceEntryAPI(client);
resource = await resourceAPI.getById(String(revision.mapped_resources));
resource = await resourceAPI.getById(revision.mapped_resource);
} catch {
resource = null;
}
Expand Down Expand Up @@ -224,7 +224,7 @@ export const action: ActionFunction = async ({ request, params }) => {

const ReviewDetail = () => {
const { revision, resource } = useLoaderData<{
revision: ResourceRevision;
revision: ResourceEdit;
resource: ResourceEntry | null;
}>();
const actionData = useActionData<{ message?: string }>();
Expand All @@ -238,7 +238,7 @@ const ReviewDetail = () => {
setValues(toEditableValues(revision));
}, [revision]);

const isPending = revision.status === "PENDING";
const isPending = revision.review_status === "PENDING";
const isSaving = fetcher.state !== "idle";

const setField = <K extends keyof EditableValues>(
Expand Down Expand Up @@ -267,7 +267,7 @@ const ReviewDetail = () => {
) => setValues((v) => ({ ...v, bathroom: { ...v.bathroom, [key]: value } }));

const handleSave = () => {
const payload: Partial<ResourceRevision> = {
const payload: Partial<ResourceEdit> = {
name: values.name || null,
resource_type: values.resource_type,
entry_type: values.entry_type || null,
Expand Down Expand Up @@ -335,9 +335,9 @@ const ReviewDetail = () => {
</Typography>
<Stack direction="row" gap={1} alignItems="center">
<Chip
label={revision.status}
label={revision.review_status}
size="small"
color={statusChipColor(revision.status)}
color={statusChipColor(revision.review_status)}
/>
<Typography variant="body2" color="text.secondary">
Submitted{" "}
Expand All @@ -352,8 +352,8 @@ const ReviewDetail = () => {

{!resource && (
<Alert severity="warning">
The resource this revision targets (#{revision.mapped_resources})
could not be found — it may have been deleted.
The resource this revision targets (#{revision.mapped_resource}) could
not be found — it may have been deleted.
</Alert>
)}

Expand Down
12 changes: 6 additions & 6 deletions app/routes/authenticated/reviews/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { type LoaderFunction, useLoaderData, useNavigate } from "react-router";
import { getDatabaseClient } from "~/api/client.server";
import { getResourceRevisionAPI } from "~/api/resource-revisions/methods";
import { authMiddleware } from "~/middleware/auth";
import type { ResourceRevision } from "~/types/ResourceRevision";
import type { ResourceEdit } from "~/types/ResourceRevision";
import {
resourceTypeChipColor,
resourceTypeChipIcon,
Expand All @@ -35,7 +35,7 @@ export const loader: LoaderFunction = async ({ request }) => {
const revisionAPI = getResourceRevisionAPI(client);
const revisions = await revisionAPI.getList({ status: "PENDING" });

const resourceIds = [...new Set(revisions.map((r) => r.mapped_resources))];
const resourceIds = [...new Set(revisions.map((r) => r.mapped_resource))];
const resourceNames = new Map<number, string | null>();

if (resourceIds.length > 0) {
Expand All @@ -59,7 +59,7 @@ export const loader: LoaderFunction = async ({ request }) => {
};
};

type RowData = ResourceRevision & { resourceLabel: string };
type RowData = ResourceEdit & { resourceLabel: string };

const columns: ColumnDef<RowData>[] = [
{
Expand Down Expand Up @@ -96,7 +96,7 @@ const columns: ColumnDef<RowData>[] = [

const ReviewsQueue = () => {
const { revisions, resourceNames } = useLoaderData<{
revisions: ResourceRevision[];
revisions: ResourceEdit[];
resourceNames: Record<number, string | null>;
}>();
const navigate = useNavigate();
Expand All @@ -109,8 +109,8 @@ const ReviewsQueue = () => {
revisions.map((revision) => ({
...revision,
resourceLabel:
resourceNames[revision.mapped_resources] ||
`Resource #${revision.mapped_resources}`,
resourceNames[revision.mapped_resource] ||
`Resource #${revision.mapped_resource}`,
})),
[revisions, resourceNames],
);
Expand Down
35 changes: 10 additions & 25 deletions app/types/ResourceRevision.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,14 @@
import type { ResourceEntry } from "~/types/ResourceEntry";

/**
* Review status of a `resource_revisions` row. Note this table reuses the
* `status` column for review state rather than the resource's operational
* status (`ResourceStatus`) — the two are unrelated despite the shared
* column name.
*/
export type RevisionStatus = "PENDING" | "APPROVED" | "REJECTED";
export type ResourceEditReviewStatus = "PENDING" | "APPROVED" | "REJECTED";

/**
* A proposed edit to an existing resource, staged in `resource_revisions`.
*
* Shape mirrors `ResourceEntry` (it's a full proposed snapshot of the
* resource) plus:
* - `mapped_resources`: FK to `resources.id` — the resource this revision
* proposes changes to. Every revision maps to an existing resource; this
* table has no concept of a brand-new, not-yet-existing resource.
* - `mapped_resource`: a second, unconstrained (no FK) int column that
* exists on the table but isn't used by this feature.
* - `status`: review state (`PENDING` / `APPROVED` / `REJECTED`), not the
* resource's operational status.
*/
export type ResourceRevision = Omit<ResourceEntry, "id" | "status"> & {
id: number;
export type ResourceEdit = {
id?: number;
mapped_resource: number;
mapped_resources: number;
status: RevisionStatus;
};
review_status: ResourceEditReviewStatus;
submitted_by?: string;
submitted_at?: Date | string;
reviewed_by?: string;
reviewed_at?: Date | string;
review_notes?: string;
} & Omit<ResourceEntry, "id">;
4 changes: 2 additions & 2 deletions app/utils/chipColors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { LocalDining, Park, WaterDrop, Wc } from "@mui/icons-material";
import type { ChipProps } from "@mui/material";
import type { ReactElement } from "react";
import type { ResourceType } from "~/types/ResourceEntry";
import type { RevisionStatus } from "~/types/ResourceRevision";
import type { ResourceEditReviewStatus } from "~/types/ResourceRevision";

export const resourceTypeChipColor = (
type: ResourceType | string,
Expand Down Expand Up @@ -72,7 +72,7 @@ export const resourceTypeChipIcon = (
};

export const statusChipColor = (
status: RevisionStatus | string,
status: ResourceEditReviewStatus | string,
): ChipProps["color"] => {
switch (status) {
case "PENDING":
Expand Down
35 changes: 35 additions & 0 deletions bin/postbuild.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#!/bin/bash
set -euo pipefail

rm -rf .amplify-hosting

mkdir -p .amplify-hosting/compute/default/build
mkdir -p .amplify-hosting/static

# Server entry point and server-side build
cp server.js .amplify-hosting/compute/default/index.js
cp package.json .amplify-hosting/compute/default/package.json
cp pnpm-lock.yaml .amplify-hosting/compute/default/pnpm-lock.yaml
cp pnpm-workspace.yaml .amplify-hosting/compute/default/pnpm-workspace.yaml
cp -r build/server .amplify-hosting/compute/default/build/server

# Copy env vars baked in by the Amplify build phase so the runtime
# process can load them via process.loadEnvFile (see server.js)
if [ -f .env ]; then
cp .env .amplify-hosting/compute/default/.env
fi

# Install production dependencies directly in the compute bundle so pnpm
# creates a self-contained node_modules/.pnpm virtual store there.
# Copying pnpm's node_modules from the build root is not safe because its
# symlinks target the global content-addressable store which won't exist
# in the Lambda runtime.
cd .amplify-hosting/compute/default
pnpm install --prod --frozen-lockfile
cd -

# Static client assets served by CloudFront/S3 via the deployment manifest
cp -r build/client/. .amplify-hosting/static/

# Amplify deployment manifest
cp deploy-manifest.json .amplify-hosting/deploy-manifest.json
38 changes: 38 additions & 0 deletions deploy-manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"version": 1,
"framework": { "name": "react-router", "version": "1.0.0" },
"routes": [
{
"path": "/assets/*",
"target": {
"kind": "Static",
"cacheControl": "public, max-age=31536000, immutable"
}
},
{
"path": "/*.*",
"target": {
"kind": "Static",
"cacheControl": "public, max-age=3600"
},
"fallback": {
"kind": "Compute",
"src": "default"
}
},
{
"path": "/*",
"target": {
"kind": "Compute",
"src": "default"
}
}
],
"computeResources": [
{
"name": "default",
"runtime": "nodejs22.x",
"entrypoint": "index.js"
}
]
}
Loading