Skip to content
This repository has been archived by the owner on Jan 3, 2025. It is now read-only.

Commit

Permalink
Create a React Frontend and deployment for it (#43)
Browse files Browse the repository at this point in the history
Added a simple React frontend and a cloudfront distribution to serve it
  • Loading branch information
FinnIckler authored May 23, 2023
1 parent dd9df2d commit 8eff1eb
Show file tree
Hide file tree
Showing 30 changed files with 494 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,6 @@ infra
storage/*
docker-compose.dev.yml
dockerfile.dev
dockerfile.handler
dockerfile.worker
Frontend
8 changes: 8 additions & 0 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,11 @@ jobs:
- name: Deploy worker
run: |
aws ecs update-service --cluster wca-registration --service wca-registration-worker --force-new-deployment
- name: Deploy Frontend
run: |
cd ./Frontend
npm install
npm run build-prod
aws s3 cp dist/bundle.js s3://assets.registration.worldcubeassociation.org/bundle.js
aws s3 cp dist/bundle.css s3://assets.registration.worldcubeassociation.org/bundle.css
aws cloudfront create-invalidation --distribution-id E322K044MBR5FG --paths "/bundle.js" "/bundle.css" --output text
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ tmp
node_modules
localstack
yarn.lock

*/package-lock.json
18 changes: 18 additions & 0 deletions Frontend/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const { configure, presets } = require('eslint-kit')

module.exports = configure({
mode: 'only-errors',
presets: [
presets.imports(),
presets.prettier(),
presets.node(),
presets.react(),
],
extend: {
rules: {
'import/no-default-export': 'off',
"react/jsx-uses-vars": "error",
"react/jsx-uses-react": "error"
},
}
})
1 change: 1 addition & 0 deletions Frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist
6 changes: 6 additions & 0 deletions Frontend/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"semi": false,
"singleQuote": true,
"tabWidth": 2,
"quoteProps": "consistent"
}
7 changes: 7 additions & 0 deletions Frontend/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Frontend
The Frontend is written in React and served by AWS Cloudfront. Whenever you push changes to the frontend
the js bundle is built and the cache is invalidated.

## How to run

Go to the root folder and run `docker compose -f docker-compose.dev.yml up`
12 changes: 12 additions & 0 deletions Frontend/dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Set the base image to the official Node.js image
FROM node:18 as build

# Create a working directory for the app
WORKDIR /app

# Copy the package.json and package-lock.json files to the working directory
COPY package*.json ./
COPY index.html ./

# Install dependencies using npm
RUN npm install
19 changes: 19 additions & 0 deletions Frontend/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>WCA Registration Test Page</title>

<link rel="stylesheet" href="./dist/bundle.css">
<script>
// For Live Reload
new EventSource('/esbuild').addEventListener('change', () => location.reload())
</script>
</head>
<body>
<div id="root"></div>
<script src="./dist/bundle.js"></script>
</body>
</html>
33 changes: 33 additions & 0 deletions Frontend/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{
"name": "wca-registration-frontend",
"version": "1.0.0",
"description": "The Frontend for the WCA Registration service",
"repository": {
"type": "git",
"url": "git+https://github.com/thewca/wca-registration.git"
},
"author": "WCA Software Team",
"license": "GPL-3.0-or-later",
"bugs": {
"url": "https://github.com/thewca/wca-registration/issues"
},
"homepage": "https://github.com/thewca/wca-registration/Frontend/README.md",
"scripts": {
"build-dev": "API_URL=\"http://localhost:3001\" node src/build.js",
"build-prod": "API_URL=\"https://registration.worldcubeassociation.org\" node src/build.js",
"watch": "node src/watch.mjs",
"lint": "eslint src --ext .js,.jsx,.ts,.tsx",
"lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix"
},
"dependencies": {
"esbuild": "^0.17.19",
"esbuild-scss-modules-plugin": "^1.1.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"eslint": "^8.40.0",
"eslint-kit": "^8.5.0",
"prettier": "^2.8.8"
}
}
19 changes: 19 additions & 0 deletions Frontend/src/api/helper/backend_fetch.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
export default async function backendFetch(route, method, body = {}) {
try {
let init = {}
if (method !== 'GET') {
init = {
method,
body,
}
}
const response = await fetch(`${process.env.API_URL}/${route}`, init)

if (response.ok) {
return await response.json()
}
return { error: response.statusText, statusCode: response.status }
} catch (error) {
return { error, statusCode: 500 }
}
}
9 changes: 9 additions & 0 deletions Frontend/src/api/registration/delete/delete_registration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import backendFetch from '../../helper/backend_fetch'

export default async function deleteRegistration(competitorID, competitionID) {
const formData = new FormData()
formData.append('competitor_id', competitorID)
formData.append('competition_id', competitionID)

return backendFetch('/register', 'DELETE', formData)
}
5 changes: 5 additions & 0 deletions Frontend/src/api/registration/get/get_registrations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import backendFetch from '../../helper/backend_fetch'

export default async function getRegistrations(competitionID) {
return backendFetch(`/registrations?competition_id=${competitionID}`, 'GET')
}
14 changes: 14 additions & 0 deletions Frontend/src/api/registration/patch/update_registration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import backendFetch from '../../helper/backend_fetch'

export default async function updateRegistration(
competitorID,
competitionID,
status
) {
const formData = new FormData()
formData.append('competitor_id', competitorID)
formData.append('competition_id', competitionID)
formData.append('status', status)

return backendFetch('/register', 'PATCH', formData)
}
14 changes: 14 additions & 0 deletions Frontend/src/api/registration/post/submit_registration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import backendFetch from '../../helper/backend_fetch'

export default async function submitEventRegistration(
competitorId,
competitionId,
events
) {
const formData = new FormData()
formData.append('competitor_id', competitorId)
formData.append('competition_id', competitionId)
events.forEach((eventId) => formData.append('event_ids[]', eventId))

return backendFetch('/register', 'POST', formData)
}
21 changes: 21 additions & 0 deletions Frontend/src/build.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
const esbuild = require('esbuild')
const process = require('process')

esbuild
.build({
entryPoints: ['src/index.jsx'],
bundle: true,
outfile: 'dist/bundle.js',
jsxFactory: 'React.createElement',
jsxFragment: 'React.Fragment',
plugins: [
require('esbuild-scss-modules-plugin').ScssModulesPlugin({
inject: true,
minify: true,
}),
],
define: {
'process.env.API_URL': `"${process.env.API_URL}"`,
},
})
.catch(() => process.exit(1))
9 changes: 9 additions & 0 deletions Frontend/src/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createRoot } from 'react-dom/client'
import App from './register/pages'

// Clear the existing HTML content
document.body.innerHTML = '<div id="app"></div>'

// Render your React component instead
const root = createRoot(document.querySelector('#app'))
root.render(App())
34 changes: 34 additions & 0 deletions Frontend/src/register/components/EventSelection.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react'
import styles from './panel.module.scss'

function toggleElementFromArray(arr, element) {
const index = arr.indexOf(element)
if (index !== -1) {
arr.splice(index, 1)
} else {
arr.push(element)
}
return arr
}

const EVENTS = ['3x3', '4x4']

export default function EventSelection({ events, setEvents }) {
return (
<div className={styles.events}>
{EVENTS.map((wca_event) => (
<label key={wca_event}>
{wca_event}
<input
type="checkbox"
value="0"
name={`event-${wca_event}`}
onChange={(_) =>
setEvents(toggleElementFromArray(events, wca_event))
}
/>
</label>
))}
</div>
)
}
101 changes: 101 additions & 0 deletions Frontend/src/register/components/RegistrationList.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import React, { useState } from 'react'
import deleteRegistration from '../../api/registration/delete/delete_registration'
import getRegistrations from '../../api/registration/get/get_registrations'
import updateRegistration from '../../api/registration/patch/update_registration'
import styles from './list.module.scss'
import StatusDropdown from './StatusDropdown'

function RegistrationRow({
competitorId,
eventIDs,
serverStatus,
competitionID,
setRegistrationList,
registrationList,
}) {
const [status, setStatus] = useState(serverStatus)

return (
<tr>
<td>{competitorId}</td>
<td>{eventIDs.join(',')}</td>
<td>
<StatusDropdown status={status} setStatus={setStatus} />
</td>
<td>
<button
onClick={(_) => {
updateRegistration(competitorId, competitionID, status)
}}
>
{' '}
Apply
</button>
</td>
<td>
<button
onClick={(_) => {
deleteRegistration(competitorId, competitionID)
setRegistrationList(
registrationList.filter((r) => r.competitor_id !== competitorId)
)
}}
>
Delete
</button>
</td>
</tr>
)
}

export default function RegistrationList() {
const [competitionID, setCompetitionID] = useState('HessenOpen2023')
const [registrationList, setRegistrationList] = useState([])
return (
<div className={styles.list}>
<button
onClick={async (_) =>
setRegistrationList(await getRegistrations(competitionID))
}
>
{' '}
List Registrations
</button>
<label>
Competition_id
<input
type="text"
value={competitionID}
name="list_competition_id"
onChange={(e) => setCompetitionID(e.target.value)}
/>
</label>
<table>
<thead>
<tr>
<th> Competitor</th>
<th> Events </th>
<th> Status </th>
<th> Apply Changes </th>
<th> Delete </th>
</tr>
</thead>
<tbody>
{registrationList.map((registration) => {
return (
<RegistrationRow
key={registration.competitor_id}
competitorId={registration.competitor_id}
setRegistrationList={setRegistrationList}
eventIDs={registration.event_ids}
competitionID={competitionID}
serverStatus={registration.registration_status}
registrationList={registrationList}
/>
)
})}
</tbody>
</table>
</div>
)
}
Loading

0 comments on commit 8eff1eb

Please sign in to comment.