diff --git a/src/views/Game/GameDock.tsx b/src/views/Game/GameDock.tsx index 867e28d4a1..391a5450ef 100644 --- a/src/views/Game/GameDock.tsx +++ b/src/views/Game/GameDock.tsx @@ -20,7 +20,7 @@ import * as data from "data"; import * as preferences from "preferences"; import { MAX_DOCK_DELAY } from "SettingsCommon"; import { useUser } from "hooks"; -import { api1, post, del } from "requests"; +import { api1, post, del, get } from "requests"; import { Dock } from "Dock"; import { Link } from "react-router-dom"; import { toast } from "toast"; @@ -28,6 +28,7 @@ import { _, pgettext } from "translate"; import { openACLModal } from "ACLModal"; import { openGameLinkModal } from "./GameLinkModal"; import { openGameLogModal } from "./GameLogModal"; +import { openGameLibraryModal } from "./GameLibraryModal"; import { sfx } from "sfx"; import { alert } from "swal_config"; import { challengeFromBoardPosition } from "ChallengeModal/ForkModal"; @@ -311,6 +312,18 @@ export function GameDock({ ? engine.playerToMove() : engine.playerNotToMove(); + const showLibraryModal = () => { + // Pull user data prior to opening modal preventing component from rendering prior to obtaining needed data + const user = data.get("user"); + const promise = get("library/%%", user.id); + promise + .then(async (library) => { + const userLibrary = await library.collections; + openGameLibraryModal(user.id, userLibrary, game_id); + }) + .catch(errorAlerter); + }; + return ( {(tournament_id || null) && ( @@ -499,6 +512,9 @@ export function GameDock({ {_("Download SGF")} )} + + {/* Add To Library functionality */} + {sgf_download_enabled && sgf_with_ai_review_url && ( @@ -506,6 +522,13 @@ export function GameDock({ )} + { + + + {_("Save to Library")} + + + } {sgf_download_enabled && sgf_with_comments_url && ( diff --git a/src/views/Game/GameLibraryModal.styl b/src/views/Game/GameLibraryModal.styl new file mode 100644 index 0000000000..ca0fb04e65 --- /dev/null +++ b/src/views/Game/GameLibraryModal.styl @@ -0,0 +1,51 @@ +/* + * Copyright (C) Online-Go.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +.GameLibraryModal.Modal { + width: 100%; + max-width: 40rem; + padding: 1rem; + display: flex; + flex-direction: row; + align-items: center; + + .file-name-inputfield{ + margin: 1rem; + } +} +.collection-list { + display:flex; + width: 100%; + flex-direction: column; + align-content: stretch; + align-items: center; +} + +.collection-row { + width: 100% + display:flex; + justify-content: space-evenly; + align-items: center; +} + +.cell { + max-width: 20rem; + padding-left: 0.5em; + display: table-cell; + text-align: left; + position: relative; +} \ No newline at end of file diff --git a/src/views/Game/GameLibraryModal.tsx b/src/views/Game/GameLibraryModal.tsx new file mode 100644 index 0000000000..8603012d2a --- /dev/null +++ b/src/views/Game/GameLibraryModal.tsx @@ -0,0 +1,176 @@ +/* + * Copyright (C) Online-Go.com + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import * as React from "react"; +import { openModal, Modal } from "Modal"; +import { api1, post } from "requests"; +import { errorAlerter } from "misc"; +import { _ } from "translate"; + +interface Events {} + +interface GameLibraryModalProperties { + userID: number; + userLibrary: any; + gameID: number; +} + +export class GameLibraryModal extends Modal { + constructor(props) { + super(props); + this.state = { + gameName: "", + collectionName: "", + collections: [], + gameAdded: false, + }; + } + + componentDidMount(): void { + const userLibrary: any = JSON.stringify(this.props.userLibrary); + const userlibraryJSON = JSON.parse(userLibrary); + this.setState({ collections: userlibraryJSON }); + } + + setGameName = (ev) => { + this.setState({ gameName: ev.target.value }); + }; + + setCollectionName = (ev) => { + this.setState({ collectionName: ev.target.value }); + }; + + resetTextFields = () => { + this.setState({ gameName: "" }); + this.setState({ collectionName: "" }); + }; + + async addToLibrary(collection) { + const url = api1(`games/${this.props.gameID}/sgf`); + await fetch(url, { + method: "GET", + // Specify the content-type of the request otherwise the "games/%%/sgf" endpoint will respond with a 415 error code + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.blob()) + .then((data) => { + // Pull sgfName from value implemented by user setting a default value if empty + let gameName = this.state.gameName; + if (this.state.gameName === "") { + gameName = `My Game #${this.props.gameID}`; + } + const gameFile = new File([data as BlobPart], `${gameName}.sgf`, { + type: "application/x-go-sgf", + lastModified: new Date().getTime(), + }); + // Create post request adding SGF File to the given collection using collection's id + post("me/games/sgf/%%", collection[0], gameFile).catch(errorAlerter); + this.resetTextFields(); + this.close(); // Closes modal after creating post request + this.setState({ gameAdded: true }); + }) + .catch(errorAlerter); + } + + async createCollection() { + if (this.state.collectionName === "") { + await this.setState({ collectionName: "My_Collection" }); + } + // Create a new library within the user's libraries + post("library/%%/collections", this.props.userID, { + name: this.state.collectionName, + }) + .then((res) => { + // Pull the newly created collection's id to be used to identify which collection we add our SGF File too + const collection_id = [res.collection_id]; + this.addToLibrary(collection_id).catch(errorAlerter); + }) + .catch(errorAlerter); + } + + render() { + return ( +
+
+

Libraries

+ + {/* If user has collections in their library map and render them. Otherwise; allow user to create collection */} + {this.state.collections.length > 0 ? ( + <> + + + + + + {this.state.collections.map((data) => ( +
+ +

{data[1]}

+
+ + + +
+ ))} + + ) : ( +
+ + + + + + + + + + + +
+ )} +
+
+ ); + } +} + +export function openGameLibraryModal(userID: number, userLibrary: any, gameID: number): void { + openModal( + , + ); +}