Skip to content

Commit 63e9b29

Browse files
committed
Tag functionality going live
1 parent b09d6e1 commit 63e9b29

6 files changed

Lines changed: 182 additions & 16 deletions

File tree

package-lock.json

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
"dependencies": {
1616
"@abstractplay/gameslib": "latest",
1717
"@abstractplay/renderer": "latest",
18-
"@tanstack/react-table": "^8.9.3",
18+
"@tanstack/react-table": "^8.11.2",
1919
"@testing-library/jest-dom": "5.16.5",
2020
"@testing-library/user-event": "13.5.0",
2121
"aws-amplify": "5.3.12",

public/locales/en/apfront.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@
7979
"DurationHelp_one": "This challenge will expire after being accepted the first time.",
8080
"DurationHelp_other": "This challenge will expire after being accepted {{count}} times.",
8181
"DurationHelpPersistent": "This challenge will remain available until you revoke it.",
82+
"EditTags": "Edit Tags",
8283
"EMail": "Email address",
8384
"EMailCode": "Emailed code",
8485
"EMailCodeHelp": "The login system just sent a code to the email address you provided. Please enter it here.",
@@ -201,6 +202,7 @@
201202
"RevokeChallenge": "Revoke challenge",
202203
"RotateBoard": "Rotate board",
203204
"Save": "Save",
205+
"SaveChanges": "Save Changes",
204206
"Search": "Search",
205207
"Seating": "Seating",
206208
"Seating1": "I play first",

src/components/MetaContainer.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ function MetaContainer(props) {
1414
const [counts, countsSetter] = useState(null);
1515
const [users, usersSetter] = useState(null);
1616
const [summary, summarySetter] = useState(null);
17+
const [updateCounter, updateCounterSetter] = useState(0);
1718
const { metaGame } = useParams();
1819
const { i18n } = useTranslation();
1920
addResource(i18n.language);
@@ -36,7 +37,7 @@ function MetaContainer(props) {
3637
}
3738
}
3839
fetchData();
39-
}, []);
40+
}, [updateCounter]);
4041

4142
useEffect(() => {
4243
async function fetchData() {
@@ -163,6 +164,7 @@ function MetaContainer(props) {
163164
toggleStar={toggleStar.bind(this)}
164165
handleChallenge={handleNewChallenge.bind(this)}
165166
users={users}
167+
updateSetter={updateCounterSetter}
166168
/>
167169
</Fragment>
168170
);

src/components/MetaContainer/Table.js

Lines changed: 167 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import React, {
55
useMemo,
66
Fragment,
77
useCallback,
8+
useRef,
89
} from "react";
910
import { Link } from "react-router-dom";
1011
import { gameinfo } from "@abstractplay/gameslib";
@@ -28,6 +29,8 @@ import Modal from "../Modal";
2829
import NewChallengeModal from "../NewChallengeModal";
2930
import ExpandableDiv from "../ExpandableDiv";
3031
import { useStorageState } from "react-use-storage-state";
32+
import { Auth } from "aws-amplify";
33+
import { API_ENDPOINT_AUTH } from "../../config";
3134

3235
const allSize = Number.MAX_SAFE_INTEGER;
3336
// props:
@@ -37,18 +40,21 @@ const allSize = Number.MAX_SAFE_INTEGER;
3740
// - summary
3841
// - toggleStar
3942
// - handleChallenge
40-
function Table(props) {
41-
const [globalMe] = useContext(MeContext);
43+
function Table({toggleStar, handleChallenge, metaGame, updateSetter, ...props}) {
44+
const [globalMe, globalMeSetter] = useContext(MeContext);
4245
const [activeImgModal, activeImgModalSetter] = useState("");
4346
const [activeChallengeModal, activeChallengeModalSetter] = useState("");
47+
const [showTagModal, showTagModalSetter] = useState(false);
4448
const [expandedPara, expandedParaSetter] = useState([]);
49+
const [selectedGame, selectedGameSetter] = useState("");
50+
const [newTag, newTagSetter] = useState("");
51+
const [myTags, myTagsSetter] = useState([]);
4552
const { t, i18n } = useTranslation();
4653
const [sorting, setSorting] = useState([{ id: "gameName", desc: false }]);
4754
const [filterStars, filterStarsSetter] = useState(false);
48-
const [globalFilter, globalFilterSetter] = useState(props.metaGame);
55+
const [globalFilter, globalFilterSetter] = useState(metaGame);
4956
const [showState, showStateSetter] = useStorageState("allgames-show", 10);
50-
const toggleStar = props.toggleStar;
51-
const handleChallenge = props.handleChallenge;
57+
const tagInput = useRef(null);
5258
addResource(i18n.language);
5359

5460
useEffect(() => {
@@ -79,6 +85,14 @@ function Table(props) {
7985
[expandedPara, expandedParaSetter]
8086
);
8187

88+
useEffect(() => {
89+
if (globalMe !== undefined && globalMe !== null && globalMe.tags !== undefined) {
90+
myTagsSetter([...globalMe.tags]);
91+
} else {
92+
myTagsSetter([]);
93+
}
94+
}, [globalMe]);
95+
8296
/**
8397
* Table columns
8498
* Name
@@ -129,6 +143,10 @@ function Table(props) {
129143
globalMe.stars.includes(metaGame)
130144
? true
131145
: false,
146+
tags:
147+
props.counts !== null && metaGame in props.counts
148+
? props.counts[metaGame].tags.join("||")
149+
: "",
132150
stars:
133151
props.counts !== null && metaGame in props.counts
134152
? props.counts[metaGame].stars
@@ -270,6 +288,13 @@ function Table(props) {
270288
),
271289
enableSorting: false,
272290
}),
291+
columnHelper.accessor("tags", {
292+
header: "Tags",
293+
cell: (props) => props.getValue().split("||").map(tag => tag === "" ? null : (
294+
<span className="tag">{tag}</span>
295+
)).reduce((acc, x) => acc === null ? x : <>{acc} {x}</>, null),
296+
enableSorting: false,
297+
}),
273298
columnHelper.accessor("stars", {
274299
header: "Stars",
275300
}),
@@ -345,9 +370,67 @@ function Table(props) {
345370
]
346371
);
347372

373+
const addTag = () => {
374+
const idx = myTags.findIndex(t => t.meta === selectedGame);
375+
if (idx !== -1) {
376+
const unique = new Set();
377+
unique.add(newTag);
378+
for (const tag of myTags[idx].tags) {
379+
unique.add(tag);
380+
}
381+
myTags[idx] = {meta: selectedGame, tags: [...unique.values()].sort((a, b) => a.localeCompare(b))};
382+
} else {
383+
myTags.push({meta: selectedGame, tags: [newTag]})
384+
}
385+
newTagSetter("");
386+
tagInput.current.focus();
387+
}
388+
389+
const saveTags = async () => {
390+
try {
391+
const usr = await Auth.currentAuthenticatedUser();
392+
const res = await fetch(API_ENDPOINT_AUTH, {
393+
method: "POST",
394+
headers: {
395+
Accept: "application/json",
396+
"Content-Type": "application/json",
397+
Authorization: `Bearer ${usr.signInUserSession.idToken.jwtToken}`,
398+
},
399+
body: JSON.stringify({
400+
query: "save_tags",
401+
pars: {
402+
payload: myTags,
403+
},
404+
}),
405+
});
406+
if (res.status !== 200) {
407+
const result = await res.json();
408+
console.log(
409+
`An error occured while saving tags:\n${result}`
410+
);
411+
} else {
412+
// update globalMe tags
413+
const newMe = JSON.parse(JSON.stringify(globalMe));
414+
newMe.tags = myTags;
415+
globalMeSetter(newMe);
416+
// triger meta game refresh
417+
updateSetter(v => v + 1);
418+
}
419+
} catch (error) {
420+
console.log(error);
421+
}
422+
showTagModalSetter(false);
423+
}
424+
348425
const table = useReactTable({
349426
data,
350427
columns,
428+
// filterFns: {
429+
// customIncludesFilter: (row, columnId, filterValue) => {
430+
// // return the filtered rows
431+
// return row.getValue(columnId).includes(filterValue);
432+
// },
433+
// },
351434
state: {
352435
sorting,
353436
columnVisibility: {
@@ -531,6 +614,23 @@ function Table(props) {
531614
</span>
532615
</>
533616
)}
617+
{(header.id === "tags" && globalMe !== null && globalMe !== undefined) ? (
618+
<>
619+
{" "}
620+
<span
621+
style={{
622+
fontSize: "smaller",
623+
fontWeight: "normal",
624+
paddingTop: 0,
625+
textDecoration: "underline",
626+
cursor: "pointer",
627+
}}
628+
onClick={() => showTagModalSetter(true)}
629+
>
630+
(edit)
631+
</span>
632+
</>
633+
) : null}
534634
</div>
535635
)}
536636
</th>
@@ -552,6 +652,68 @@ function Table(props) {
552652
</table>
553653
{tableNavigation}
554654
</div>
655+
656+
<Modal
657+
show={showTagModal}
658+
title={t("EditTags")}
659+
buttons={[
660+
{ label: t("SaveChanges"), action: saveTags },
661+
{
662+
label: t("Close"),
663+
action: () => showTagModalSetter(false),
664+
},
665+
]}
666+
>
667+
<div className="content">
668+
<p>Tags are publicly visible. Please familiarize yourself with our <Link to="/legal">terms of service</Link>. Don't forget to save your changes before closing this dialog.</p>
669+
<p>The best tags are short (preferably one word) and lowercase.</p>
670+
</div>
671+
<div className="field is-grouped">
672+
<div className="control">
673+
<div className="select is-small">
674+
<select id="gameSelect" value={selectedGame} onChange={(e) => selectedGameSetter(e.target.value)}>
675+
<option value={""} key={`gameSelectBlank`}>--Choose a game--</option>
676+
{props.games.map(meta => {
677+
const info = gameinfo.get(meta);
678+
return (<option value={meta} key={`gameSelect|${meta}`}>{info.name}</option>)
679+
})}
680+
</select>
681+
</div>
682+
</div>
683+
<div className="control">
684+
<input className="input is-small" type="text" placeholder="Type new tag here" value={newTag} onChange={(e) => newTagSetter(e.target.value)} ref={tagInput} />
685+
</div>
686+
</div>
687+
<div className="control">
688+
<button className="button is-small apButton" onClick={addTag} disabled={newTag === "" || selectedGame === ""}>Add tag</button>
689+
</div>
690+
{myTags.length === 0 ? null :
691+
<table className="table">
692+
<thead>
693+
<tr>
694+
<th>Game</th>
695+
<th>Tags</th>
696+
</tr>
697+
</thead>
698+
<tbody>
699+
{myTags.sort((a,b) => a.meta.localeCompare(b.meta)).map(({meta, tags}) => {
700+
const info = gameinfo.get(meta);
701+
return (
702+
<tr key={`MyTagRow|${meta}`}>
703+
<td>{info.name}</td>
704+
<td>
705+
{tags.map(tag => (
706+
<span className="tag">{tag}</span>
707+
)).reduce((acc, x) => acc === null ? x : <>{acc} {x}</>, null)}
708+
</td>
709+
</tr>
710+
);
711+
})}
712+
</tbody>
713+
</table>
714+
}
715+
</Modal>
716+
555717
</article>
556718
);
557719
}

src/components/Stats.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const explanations = [
6262
["topPlayers", `This table lists each available game and variant combination along with the player with the highest Elo rating for that combination.`],
6363
["numPlays", `This table lists each game, how many times it was played, and by how many unique players. It also provides a histogram of games completed for that game. The preview shows the last 10 weeks, with the most recent week on the right. Clicking on the preview will show you the full history of games completed, in seven-day "buckets," with the most recent week on the right.`],
6464
["playerStats", `This shows each recorded player along with their total games played, a count of unique games, and the number of different opponents they played. It also provides an \`h-index\`, which represents the number of games you've played at least that many times (e.g., if you've played three different games only one time each, then your index is 1. As soon as you've played those three games at least three times *each*, the index will increase to 3). An activity histogram is also provided, showing the number of games completed each week, with the most recent week on the right. The preview shows the most recent ten weeks. Clicking on it will show you the full history.`],
65-
["gameStats", `This table shows each game and variant combination along with the number of recorded games, the average length of the game (number of moves), the median length, and the rate of first-player wins. **The first-player win stats should be treated with care!** The sample size is small and biased. There are many confounding factors that could explain an extreme rate. Note that the rate completely ignores whether the pie rule was invoked. This is a measure of how often the first player (regardless of how many times the person sitting in that chair may change) wins the game.`],
65+
["gameStats", `This table shows each game and variant combination along with the number of recorded games, the average length of the game (number of moves), the median length, and the rate of first-player wins. **The first-player win stats should be treated with care!** The sample size is small and biased. There are many confounding factors that could explain an extreme rate. Note that the rate completely ignores whether the pie rule was invoked. This is a measure of how often the first player (regardless of how many times the person sitting in that chair may change) wins the game. Draws are counted as half a win for the first player (so a game played three times with a win, a loss, and a draw would have a first-player win percentage of 50%).`],
6666
];
6767

6868
function Stats(props) {

0 commit comments

Comments
 (0)