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: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ Before the session starts, please clone this repository and run `npm install` an
1) Install JS dependencies with `npm install`.
2) Install Python dependencies with `uv sync`. If `uv` is unavailable, first run `pip install uv`.
3) Run `npm run start`. You should be able to view the app at [http://localhost:3000/](http://localhost:3000/). It will hot reload as you make changes.

**A note on the API port**
The API will attempt to run on port 5000.
If 5000 is already in use, the next available port is used automatically (logged to the terminal on startup).
You can pin the API_PORT if needed (rare): `API_PORT=5010 npm start`.
24 changes: 22 additions & 2 deletions api/app.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
from typing import Callable
from flask import Flask, jsonify, request
from flask_cors import CORS
from pagination import get_page, get_page_filtered
from models import Project
import json
import os
import traceback
from pathlib import Path

# Wrap candidate-edited module imports so a syntax error doesn't kill the
# Werkzeug reloader (its parent process exits when the child exits with a
# non-restart code, and there is nothing to bring it back).
_pagination_error: str | None = None
try:
from pagination import get_page, get_page_filtered
except Exception:
_pagination_error = traceback.format_exc()
get_page = None # type: ignore[assignment]
get_page_filtered = None # type: ignore[assignment]
Comment thread
cursor[bot] marked this conversation as resolved.

app = Flask(__name__)

CORS(app)
Expand All @@ -31,6 +43,9 @@ def get_users():

@app.route("/api/projects", methods=["GET"])
def get_projects():
if get_page is None:
return jsonify({"error": f"pagination module failed to load:\n{_pagination_error}"}), 500
Comment thread
datananda marked this conversation as resolved.
Dismissed

projects = load_json("projects.json")

# Handle pagination using startAfterId
Expand Down Expand Up @@ -61,4 +76,9 @@ def get_projects():


if __name__ == "__main__":
app.run(port=5000, debug=True)
port = int(os.environ.get("API_PORT") or 5000)
# Watch all .py files in the api/ directory so the reloader picks up fixes
# to files that failed to import (and therefore aren't in sys.modules).
api_dir = Path(__file__).parent
extra = [str(p) for p in api_dir.glob("*.py")]
app.run(port=port, debug=True, extra_files=extra)
Comment thread
datananda marked this conversation as resolved.
Dismissed
109 changes: 0 additions & 109 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@
"@types/lodash": "^4.14.195",
"@types/react": "^17.0.38",
"@types/react-dom": "^17.0.11",
"concurrently": "^8.2.2",
"react-scripts": "^5.0.1",
"typescript": "^4.5.4"
},
"scripts": {
"start": "concurrently \"npm run start-frontend\" \"npm run start-api\"",
"start": "bash scripts/start.sh",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject",
Expand Down
31 changes: 31 additions & 0 deletions scripts/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/bin/bash
trap 'kill 0' EXIT

# Find a free port for the API, starting from 5000.
# Override: API_PORT=5010 npm start
if [ -z "$API_PORT" ]; then
API_PORT=$(python3 -c "
import socket, sys
for port in range(5000, 5020):
s = socket.socket()
try:
s.bind(('127.0.0.1', port))
print(port)
s.close()
sys.exit(0)
except OSError:
s.close()
print('ERROR: no free port in 5000-5019', file=sys.stderr)
sys.exit(1)
") || exit 1
fi

export API_PORT
export REACT_APP_API_PORT=$API_PORT

echo ""
echo " API port: $API_PORT (override with API_PORT=<port> npm start)"
echo ""

npm run start-api &
npm run start-frontend
15 changes: 13 additions & 2 deletions src/Projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,19 @@ interface ProjectsProps {
export default function Projects({ selectedUser, nameById }: ProjectsProps) {
const [projects, setProjects] = React.useState<ProjectData[] | null>(() => null);
const [hasMoreResults, setHasMoreResults] = React.useState<boolean>(false);
const [error, setError] = React.useState<string | null>(null);

const fetchProjects = React.useCallback((startAfter?: ProjectData, overwrite = false) => {
SERVER.getProjects({ pageSize: 5, startAfter, userId: selectedUser?.toString() }).then((page) => {
setError(null);
if (overwrite) {
setProjects(_ => ([...(page.projects ?? [])]));
} else {
setProjects(projects => [...(projects ?? []), ...page.projects]);
}
setHasMoreResults(page.hasMoreResults);
}).catch(() => {
alert("Something went wrong...");
}).catch((err) => {
setError(err.message);
});
}, [selectedUser]);

Expand All @@ -36,6 +38,15 @@ export default function Projects({ selectedUser, nameById }: ProjectsProps) {
fetchProjects(undefined, true);
}, [selectedUser, fetchProjects]);

if (error) {
return (
<div className="error-overlay">
<h1>Server error:</h1>
<pre>{error}</pre>
</div>
);
}

return (
<div className="projects">
{projects?.length === 0 && (<div>No projects found.</div>)}
Expand Down
10 changes: 8 additions & 2 deletions src/api/apiImpl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ export interface ProjectsResponse {
hasMoreResults: boolean;
}

const API_BASE = `http://127.0.0.1:${process.env.REACT_APP_API_PORT || "5000"}`;

class DefaultServer {
async getUsers(): Promise<UserData[]> {
const response = await fetch('http://127.0.0.1:5000/api/users');
const response = await fetch(`${API_BASE}/api/users`);
return response.json();
}

Expand All @@ -16,7 +18,7 @@ class DefaultServer {
startAfter?: ProjectData;
pageSize?: number;
}): Promise<ProjectsResponse> {
const url = new URL('http://127.0.0.1:5000/api/projects');
const url = new URL(`${API_BASE}/api/projects`);

if (options?.userId != null) {
url.searchParams.append('userId', options.userId);
Expand All @@ -29,6 +31,10 @@ class DefaultServer {
}

const response = await fetch(url);
if (!response.ok) {
const body = await response.json().catch(() => null);
throw new Error(body?.error ?? `API error ${response.status}`);
}
return response.json();
}
}
Expand Down
31 changes: 31 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,35 @@ body {
flex-direction: row;
gap: 10px;
justify-content: space-between;
}

.error-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgb(35, 33, 32);
color: rgb(232, 232, 232);
padding: 2rem;
overflow: auto;
z-index: 9999;
font-family: sans-serif;
}

.error-overlay h1 {
color: rgb(252, 98, 93);
font-size: 1.4rem;
margin: 0 0 1.5rem 0;
}

.error-overlay pre {
color: rgb(252, 98, 93);
background: rgba(206, 17, 38, 0.1);
padding: 1rem;
border-radius: 4px;
font-size: 13px;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.5;
}
Loading