Skip to content

Commit

Permalink
improve react code
Browse files Browse the repository at this point in the history
  • Loading branch information
Chriscbr committed Feb 14, 2024
1 parent 7e2161a commit d655778
Show file tree
Hide file tree
Showing 11 changed files with 170 additions and 83 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ You can test the web app locally using Wing Console.
2. Run `wing it main.w` to launch the Wing Console in your browser.
3. In the Wing Console, locate the website resource, and click on it to see its properties on the right sidebar. Click on the URL property to open visit the website in your browser.

For working on the React app, you can `cd` into the `website` directory and run `npm run start` to start the React app, which will automatically connect to the Wing simulator if you have the Wing Console running.
The page will automatically reload if you make changes to the React code.

### Deployment

To deploy your own copy of the app, first make sure you have AWS credentials configured in your terminal for the account and region you want to deploy to.
Expand Down
43 changes: 29 additions & 14 deletions dynamodb.w
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,6 @@ pub struct Attribute {
value: Json;
}

pub class Util {
extern "./util.js" pub static inflight jsonToMutArray(json: Json): MutArray<Map<Attribute>>;
extern "./util.js" pub static inflight jsonToArray(json: Json): Array<Map<Attribute>>;
extern "./util.js" pub static inflight mutArrayToJson(json: MutArray<Map<Attribute>>): Json;
}

interface IDynamoDBTable {
inflight getItem(map: Map<Attribute>): Map<Attribute>?;
inflight putItem(item: Map<Attribute>): void;
Expand All @@ -38,23 +32,44 @@ struct DynamoDBTableProps {
pub class DynamoDBTableSim impl IDynamoDBTable {
key: str;
data: cloud.Bucket;
hashKey: str;

new(props: DynamoDBTableProps) {
this.key = "data.json";
this.data = new cloud.Bucket();
this.data.addObject(this.key, "[]");
this.hashKey = props.hashKey;
}

pub inflight putItem(item: Map<Attribute>) {
let items = this.data.getJson(this.key);
let itemsMut = Util.jsonToMutArray(items);
itemsMut.push(item);
this.data.putJson(this.key, Util.mutArrayToJson(itemsMut));
// Check if the item has the hash key
if !item.has(this.hashKey) {
throw("Item does not have the hash key");
}

let items: Json = this.data.getJson(this.key);
let itemsMut: MutArray<MutMap<Attribute>> = unsafeCast(items);

// Check if the item already exists by looking in the array for an item with the same hash key
for existingItem in itemsMut {
if existingItem.get(this.hashKey).value == item.get(this.hashKey).value {
// If it does, update the item
for key in item.keys() {
existingItem.set(key, item.get(key));
}
this.data.putJson(this.key, unsafeCast(itemsMut));
return;
}
}

// If the item doesn't exist, add it to the array
itemsMut.push(item.copyMut());
this.data.putJson(this.key, unsafeCast(itemsMut));
}

pub inflight getItem(map: Map<Attribute>): Map<Attribute>? {
let items = this.data.getJson(this.key);
let itemsMut = Util.jsonToMutArray(items);
let items: Json = this.data.getJson(this.key);
let itemsMut: MutArray<Map<Attribute>> = unsafeCast(items);
for item in itemsMut {
let var matches = true;
for key in map.keys() {
Expand All @@ -73,8 +88,8 @@ pub class DynamoDBTableSim impl IDynamoDBTable {
}

pub inflight scan(): Array<Map<Attribute>> {
let items = this.data.getJson(this.key);
return Util.jsonToArray(items);
let items: Json = this.data.getJson(this.key);
return unsafeCast(items);
}

pub onLift(host: std.IInflightHost, ops: Array<str>) {
Expand Down
18 changes: 13 additions & 5 deletions main.w
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
bring "./dynamodb.w" as ddb;
bring cloud;
bring fs;
bring math;
bring util;

// --- types ---

Expand Down Expand Up @@ -40,8 +42,6 @@ struct SelectWinnerResponse {
}

class Util {
extern "./util.js" static inflight jsonToSelectWinnerRequest(json: Json): SelectWinnerRequest;

pub static inflight clamp(value: num, min: num, max: num): num {
if value < min {
return min;
Expand Down Expand Up @@ -116,8 +116,8 @@ class Store {
// probability that the winner should have won
let pWinner = 1.0 / (1.0 + 10 ** ((loserScore - winnerScore) / 400.0));

let winnerNewScore = Util.clamp(winnerScore + 32 * (1.0 - pWinner), 1000, 2000);
let loserNewScore = Util.clamp(loserScore + 32 * (pWinner - 1.0), 1000, 2000);
let winnerNewScore = Util.clamp(winnerScore + 32 * (1.0 - pWinner), 0, 2000);
let loserNewScore = Util.clamp(loserScore + 32 * (pWinner - 1.0), 0, 2000);

this.setEntry(Entry { name: winner, score: winnerNewScore });
this.setEntry(Entry { name: loser, score: loserNewScore });
Expand Down Expand Up @@ -187,7 +187,7 @@ new cloud.OnDeploy(inflight () => {
if !store.getEntry(food)? {
store.setEntry(Entry {
name: food,
score: 1500,
score: 1000 + math.floor(math.random() * 100) - 50, // 1000 +/- 50
});
}
}
Expand All @@ -198,6 +198,14 @@ let api = new cloud.Api(cors: true) as "VotingAppApi";
let website = new cloud.Website(path: "./website/build");
website.addJson("config.json", { apiUrl: api.url });

// A hack to expose the api url to the React app for local development
if util.env("WING_TARGET") == "sim" {
new cloud.OnDeploy(inflight () => {
fs.writeFile("node_modules/.votingappenv", api.url);
}) as "ReactAppSetup";
}


// Select two random items from the list of items for the user to choose between
api.post("/requestChoices", inflight (_) => {
let entries = store.getRandomPair();
Expand Down
11 changes: 0 additions & 11 deletions util.js

This file was deleted.

33 changes: 18 additions & 15 deletions website/src/components/VoteItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,21 +76,24 @@ counter-reset: ${safeName} ${score};
}
@keyframes ${safeName}-anim {
0% { counter-reset: ${safeName} ${safeScore + 15 * isWinner}; }
3% { counter-reset: ${safeName} ${safeScore + 14 * isWinner}; }
6% { counter-reset: ${safeName} ${safeScore + 13 * isWinner}; }
9% { counter-reset: ${safeName} ${safeScore + 12 * isWinner}; }
12% { counter-reset: ${safeName} ${safeScore + 11 * isWinner}; }
15% { counter-reset: ${safeName} ${safeScore + 10 * isWinner}; }
18% { counter-reset: ${safeName} ${safeScore + 9 * isWinner}; }
21% { counter-reset: ${safeName} ${safeScore + 8 * isWinner}; }
24% { counter-reset: ${safeName} ${safeScore + 7 * isWinner}; }
27% { counter-reset: ${safeName} ${safeScore + 6 * isWinner}; }
30% { counter-reset: ${safeName} ${safeScore + 5 * isWinner}; }
33% { counter-reset: ${safeName} ${safeScore + 4 * isWinner}; }
36% { counter-reset: ${safeName} ${safeScore + 3 * isWinner}; }
43% { counter-reset: ${safeName} ${safeScore + 2 * isWinner}; }
66% { counter-reset: ${safeName} ${safeScore + 1 * isWinner}; }
0% { counter-reset: ${safeName} ${safeScore + 18 * isWinner}; }
3% { counter-reset: ${safeName} ${safeScore + 17 * isWinner}; }
6% { counter-reset: ${safeName} ${safeScore + 16 * isWinner}; }
9% { counter-reset: ${safeName} ${safeScore + 15 * isWinner}; }
12% { counter-reset: ${safeName} ${safeScore + 14 * isWinner}; }
15% { counter-reset: ${safeName} ${safeScore + 13 * isWinner}; }
18% { counter-reset: ${safeName} ${safeScore + 12 * isWinner}; }
21% { counter-reset: ${safeName} ${safeScore + 11 * isWinner}; }
24% { counter-reset: ${safeName} ${safeScore + 10 * isWinner}; }
27% { counter-reset: ${safeName} ${safeScore + 9 * isWinner}; }
30% { counter-reset: ${safeName} ${safeScore + 8 * isWinner}; }
33% { counter-reset: ${safeName} ${safeScore + 7 * isWinner}; }
36% { counter-reset: ${safeName} ${safeScore + 6 * isWinner}; }
39% { counter-reset: ${safeName} ${safeScore + 5 * isWinner}; }
42% { counter-reset: ${safeName} ${safeScore + 4 * isWinner}; }
50% { counter-reset: ${safeName} ${safeScore + 3 * isWinner}; }
60% { counter-reset: ${safeName} ${safeScore + 2 * isWinner}; }
80% { counter-reset: ${safeName} ${safeScore + 1 * isWinner}; }
100% { counter-reset: ${safeName} ${safeScore}; }
}`}
</style>
Expand Down
40 changes: 39 additions & 1 deletion website/src/services/fetchChoices.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { fetchConfig } from "./fetchConfig";

export interface Choice {
Expand All @@ -19,7 +20,7 @@ const getImageSvg = async (label: string): Promise<string> => {
});
};

export const fetchChoices = async () => {
const fetchChoices = async (): Promise<Array<Choice>> => {
const apiUrl = (await fetchConfig()).apiUrl;
const response = await fetch(apiUrl + "/requestChoices", {
method: "POST",
Expand All @@ -37,3 +38,40 @@ export const fetchChoices = async () => {
);
return choices;
};

export const useFetchChoices = () => {
const [choices, setChoices] = useState<Choice[]>([
{ label: "" },
{ label: "" },
]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const hasFetchedInitialChoices = useRef(false);

const fetchNewChoices = async (userInitiated: boolean) => {
// Prevent automatic second fetch in development mode
if (hasFetchedInitialChoices.current && !userInitiated) {
return;
}

hasFetchedInitialChoices.current = true;

setChoices([{ label: "" }, { label: "" }]);
setIsLoading(true);
setError(null);
try {
const newChoices = await fetchChoices();
setChoices(newChoices);
} catch (err) {
setError((err as any).message);
} finally {
setIsLoading(false);
}
}

useEffect(() => {
fetchNewChoices(false);
}, []);

return { choices, isLoading, error, fetchNewChoices: () => fetchNewChoices(true) };
};
13 changes: 13 additions & 0 deletions website/src/services/fetchConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,24 @@ export interface Config {
apiUrl: string;
}

let cachedConfig: Config | null = null;

export const fetchConfig = async () => {
// In production, use the cached config if available
if (process.env.NODE_ENV === 'production' && cachedConfig) {
return cachedConfig;
}

const response = await fetch("./config.json");
if (!response.ok) {
throw new Error("Failed to fetch config");
}
const config: Config = await response.json();

// Cache the config if in production mode
if (process.env.NODE_ENV === 'production') {
cachedConfig = config;
}

return config;
};
30 changes: 29 additions & 1 deletion website/src/services/fetchLeaderboard.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEffect, useRef, useState } from "react";
import { fetchConfig } from "./fetchConfig";

export interface Entry {
name: string;
score: number;
}

export const fetchLeaderboard = async () => {
const fetchLeaderboard = async () => {
const apiUrl = (await fetchConfig()).apiUrl;
const response = await fetch(apiUrl + "/leaderboard");
if (!response.ok) {
Expand All @@ -14,3 +15,30 @@ export const fetchLeaderboard = async () => {
const jsonData: Entry[] = await response.json();
return jsonData;
};

export const useFetchLeaderboard = () => {
const [entries, setEntries] = useState<Entry[]>([]);
const [isLoading, setIsLoading] = useState(true);

// Prevent automatic second fetch in development mode
const hasFetchedInitialEntries = useRef(false);

useEffect(() => {
const fetchEntries = async () => {
if (!hasFetchedInitialEntries.current) {
hasFetchedInitialEntries.current = true;
setIsLoading(true);
try {
const data = await fetchLeaderboard();
setEntries(data);
} finally {
setIsLoading(false);
}
}
};

fetchEntries();
}, []);

return { entries, isLoading };
};
15 changes: 15 additions & 0 deletions website/src/setupProxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const fs = require('fs');

module.exports = function(app) {
app.use(
'/config.json',
function(req, res) {
const apiUrl = fs.readFileSync("../node_modules/.votingappenv", "utf8");
if (!apiUrl) {
res.status(500).send("No API URL set. Are you running `wing it`?");
return;
}
res.send({ apiUrl });
}
);
};
15 changes: 3 additions & 12 deletions website/src/views/LeaderboardView.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,8 @@
import { useEffect, useState } from "react";
import { Entry, fetchLeaderboard } from "../services/fetchLeaderboard";
import { useFetchLeaderboard } from "../services/fetchLeaderboard";
import { SpinnerLoader } from "../components/SpinnerLoader";

export const LeaderboardView = () => {
const [entries, setEntries] = useState<Entry[]>([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
fetchLeaderboard().then((items) => {
setEntries(items);
setLoading(false);
});
}, []);
const { entries, isLoading } = useFetchLeaderboard();

return (
<div className="max-h-[22rem] overflow-y-auto h-full">
Expand All @@ -33,7 +24,7 @@ export const LeaderboardView = () => {
</tr>
</thead>
<tbody className="divide-y divide-slate-400">
{loading && (
{isLoading && (
<tr>
<td
colSpan={2}
Expand Down
Loading

0 comments on commit d655778

Please sign in to comment.