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
5 changes: 2 additions & 3 deletions .example.env
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
# Need access to the database? Message us in the #phlask-data channel on Slack.
VITE_DB_URL=https://wantycfbnzzocsbthqzs.supabase.co
VITE_DB_API_KEY="Enter the API key provided by the team here"
SUPABASE_URL=https://wantycfbnzzocsbthqzs.supabase.co
SUPABASE_PUBLISHABLE_KEY=
31 changes: 11 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

This is the admin dashboard for the [PHLASK](https://github.com/phlask/phlask-map/tree/develop) project. It provides a web interface for reviewing, editing, and managing community resource data using Supabase as the backend.

### Key Features
## Key Features

- View and manage resources from the Supabase database
- Review and approve/reject suggested edits to resources
Expand All @@ -26,34 +26,25 @@ Install dependencies with [pnpm](https://pnpm.io/installation):
pnpm install
```

### Development
### Environment Variables

Start the development server:
To run the app locally, you must create a `.env` file in the root directory with the variables defined in the `.env.example` file. You should duplicate this file and populate the missing fields with the API secrets.

```bash
pnpm run dev
cp .example.env .env
```

Visit [http://localhost:5174](http://localhost:5174) or as output in the terminal from pnpm run dev to view the app.
If any variables are reported as missing, message us in the `#phlask-data` channel on Slack (See [Code For Philly](https://codeforphilly.org/) for more details). Also, refer to the `.env.example` file for more details.

### Environment Variables
### Development

To see the data & tables create a `.env` file in the root directory with the following variables:
For reference, check and copy from the `.env.example` file.
Start the development server:

```bash
cp .example.env .env
```

Your `.env` file should look like this:

```env
VITE_DB_NAME="resources"
VITE_DB_URL="Check .example.env for the URL"
VITE_DB_API_KEY="Message us in the #phlask-data channel on Slack"
pnpm run dev
```

Need access to the database? Message us in the [#phlask-data](https://codeforphilly.org/chat) channel on Slack. Also, refer to the `.env.example` file for more details.
Visit [http://localhost:5174](http://localhost:5174) or as output in the terminal from pnpm run dev to view the app.

### Docker

Expand Down Expand Up @@ -95,9 +86,9 @@ app/

## How to Contribute / Next Steps

- Please refer to contributing guidelines [here](https://github.com/phlask/phlask-map?tab=readme-ov-file#want-to-add-something-new-or-developreport-a-fix-for-a-bug-you-found).
- Please refer to the [contributing guidelines](https://github.com/phlask/phlask-map/blob/develop/contributing.md).

- Please check our [https://github.com/phlask/admin-dashboard/issues](https://github.com/phlask/admin-dashboard/issues) for open issues and feature requests.
- Please check our [GitHub issues](https://github.com/phlask/admin-dashboard/issues) for open issues and feature requests.

- Before submitting a PR, please ensure that your code adheres to the project's coding standards and passes all tests. We recommend running the following command to check for linting errors and run tests:

Expand Down
38 changes: 38 additions & 0 deletions app/api/client.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {
createServerClient,
type GetAllCookies,
parseCookieHeader,
type SetAllCookies,
serializeCookieHeader,
} from "@supabase/ssr";
import { data } from "react-router";
import { databaseApiKey, databaseUrl } from "~/constants/db.server";

export const getDatabaseClient = (request: Request) => {
const headers = new Headers();

if (!databaseUrl || !databaseApiKey) {
const message = import.meta.env.DEV
? "Database credentials are missing! Make sure that `SUPABASE_URL` and `SUPABASE_PUBLISHABLE_KEY` are defined in a `.env` file"
: "An unexpected error have happened. Please try again later.";
throw data(new Error(message), { status: 500 });
}

const getAll: GetAllCookies = async () => {
return parseCookieHeader(request.headers.get("Cookie") ?? "").map(
(cookie) => ({ name: cookie.name, value: cookie.value ?? "" }),
);
};

const setAll: SetAllCookies = (cookiesToSet) => {
cookiesToSet.forEach(({ name, value }) => {
headers.append("Set-Cookie", serializeCookieHeader(name, value, {}));
});
};

const supabase = createServerClient(databaseUrl, databaseApiKey, {
cookies: { getAll, setAll },
});

return { client: supabase, headers };
};
4 changes: 0 additions & 4 deletions app/api/client.ts

This file was deleted.

170 changes: 86 additions & 84 deletions app/api/resources/methods.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { client } from "~/api/client";
import type { ModelAPI } from "~/api/types";
import type { GetModelAPI } from "~/api/types";
import type {
ResourceEntry,
ResourceStatus,
Expand All @@ -21,90 +20,93 @@ export type ResourceEntryGetListParams = {
};

const TABLE_NAME = "resources";
const table = client.from(TABLE_NAME);

export const ResourceEntryAPI: ModelAPI<
export const getResourceEntryAPI: GetModelAPI<
ResourceEntry,
ResourceEntryGetListParams
> = {
getList: async (params) => {
const { limit, offset, resourceType, status } = params;
const isPaginating =
typeof limit === "number" && typeof offset === "number";

let query = table.select("*", { count: "exact" });

if (isPaginating) {
query = query.range(offset, offset + limit - 1);
}

if (resourceType) {
query = query.eq("resource_type", resourceType);
}

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

const { data, error, count } = await query;

if (error) {
throw error;
}

if (!data?.length || !count) {
return { data: [], count: 0, hasMore: false };
}

return {
data,
count,
hasMore: isPaginating ? offset + limit < count : false,
};
},
getById: async (id) => {
const { data, error } = await table
.select("*")
.eq("id", id)
.single<ResourceEntry>();

if (error) {
throw error;
}

return data;
},
create: async (values) => {
const { data, error } = await table
.insert(values)
.select()
.single<ResourceEntry>();

if (error) {
throw error;
}

return data;
},
updateById: async (id, values) => {
const { data, error } = await table
.update(values)
.eq("id", id)
.single<ResourceEntry>();

if (error) {
throw error;
}

return data;
},
delete: async (id) => {
const { error } = await table.delete().eq("id", id);

if (error) {
throw error;
}
},
> = (client) => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only change here is injecting the client here, so that we can use it on the server

const table = client.from(TABLE_NAME);

return {
getList: async (params) => {
const { limit, offset, resourceType, status } = params;
const isPaginating =
typeof limit === "number" && typeof offset === "number";

let query = table.select("*", { count: "exact" });

if (isPaginating) {
query = query.range(offset, offset + limit - 1);
}

if (resourceType) {
query = query.eq("resource_type", resourceType);
}

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

const { data, error, count } = await query;

if (error) {
throw error;
}

if (!data?.length || !count) {
return { data: [], count: 0, hasMore: false };
}

return {
data,
count,
hasMore: isPaginating ? offset + limit < count : false,
};
},
getById: async (id) => {
const { data, error } = await table
.select("*")
.eq("id", id)
.single<ResourceEntry>();

if (error) {
throw error;
}

return data;
},
create: async (values) => {
const { data, error } = await table
.insert(values)
.select()
.single<ResourceEntry>();

if (error) {
throw error;
}

return data;
},
updateById: async (id, values) => {
const { data, error } = await table
.update(values)
.eq("id", id)
.single<ResourceEntry>();

if (error) {
throw error;
}

return data;
},
delete: async (id) => {
const { error } = await table.delete().eq("id", id);

if (error) {
throw error;
}
},
};
};

export default ResourceEntryAPI;
export default getResourceEntryAPI;
4 changes: 3 additions & 1 deletion app/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { SupabaseClient } from "@supabase/supabase-js";

/**
* Result of a paginated fetch operation
*/
Expand All @@ -10,7 +12,7 @@ type PaginatedResult<Entity> = {
hasMore: boolean;
};

export type ModelAPI<Entity, Params> = {
export type GetModelAPI<Entity, Params> = (client: SupabaseClient) => {
getList: (params: Params) => Promise<PaginatedResult<Entity>>;
getById: (id: string) => Promise<Entity>;
create: (values: Entity) => Promise<Entity>;
Expand Down
2 changes: 2 additions & 0 deletions app/constants/db.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const databaseUrl = process.env.SUPABASE_URL;
export const databaseApiKey = process.env.SUPABASE_PUBLISHABLE_KEY;
2 changes: 0 additions & 2 deletions app/constants/db.ts

This file was deleted.

4 changes: 4 additions & 0 deletions app/context/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { User } from "@supabase/supabase-js";
import { createContext } from "react-router";

export const userContext = createContext<User | null>(null);
19 changes: 19 additions & 0 deletions app/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { type MiddlewareFunction, redirect } from "react-router";
import { getDatabaseClient } from "~/api/client.server";
import { userContext } from "~/context/user";

export const authMiddleware: MiddlewareFunction = async (
{ request, context },
next,
) => {
const { client } = getDatabaseClient(request);

const response = await client.auth.getUser();
if (response.error) {
return redirect("/auth");
}

context.set(userContext, response.data.user);

return next();
};
15 changes: 13 additions & 2 deletions app/routes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
import { index, layout, type RouteConfig } from "@react-router/dev/routes";
import {
index,
layout,
type RouteConfig,
route,
} from "@react-router/dev/routes";

export default [
layout("routes/_layout.tsx", [index("routes/dashboard.tsx")]),
layout("routes/authenticated/_layout.tsx", [
index("routes/authenticated/dashboard.tsx"),
route("logout", "routes/authenticated/logout.tsx"),
]),
route("auth", "routes/unauthenticated/_layout.tsx", [
index("routes/unauthenticated/login.tsx"),
]),
] satisfies RouteConfig;
Loading