Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
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
19 changes: 19 additions & 0 deletions scripts/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash
trap 'kill 0' EXIT

# Find a free port for the API and share it with the frontend.
# Prefer 5000 for consistency; fall back to a random port if taken.
export API_PORT=$(python3 -c "
import socket
s = socket.socket()
try:
s.bind(('127.0.0.1', 5000))
except OSError:
s.bind(('127.0.0.1', 0))
print(s.getsockname()[1])
s.close()
")
Comment thread
datananda marked this conversation as resolved.
Outdated
export REACT_APP_API_PORT=$API_PORT

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