diff --git a/www/editgame b/www/editgame index a9ecf49b..b6d4d01d 100644 --- a/www/editgame +++ b/www/editgame @@ -521,7 +521,6 @@ if ($errMsg) { scriptSrc('/gridform.js'), false); - initStarControls(); imageUploadScripts(); ?> @@ -1037,7 +1036,7 @@ function saveReviewPopup() rowDisp = ""; } } -function setExtRevRating(rating) +function setExtRevRating(gameId, rating) { document.getElementById("extrevRating").value = rating; } @@ -1737,8 +1736,7 @@ function onComboSelectReviewSite(val)

Rating:
If the site uses a different rating diff --git a/www/ifdb.css b/www/ifdb.css index 2618b79b..80ce5c38 100644 --- a/www/ifdb.css +++ b/www/ifdb.css @@ -104,6 +104,13 @@ form.sortingControls { margin: 1em 0 1em 0; } +/* A box that has the sorting controls and other search result controls */ +.searchControls { + display: flex; + align-items: baseline; + column-gap: 5em; +} + /* The default style for cells within tables. We use the same style here as in the main page body. @@ -564,6 +571,21 @@ td.coverart { white-space: nowrap; } +.haveYouPlayed_playedSection { + margin: 1.25ex 0 0.75ex 0; +} + +.searchRatingBox { + width: 20rem !important; /* Overrides the 'width: 100%' in '.haveYouPlayed' */ + margin-left: auto !important; + margin-right: 100px !important; +} + +.searchRatingBox .haveYouPlayed_title, +.searchRatingBox .howLong { + display: none; +} + /* We use the "authorProfileLink" style for hyperlinks to author profiles, as shown in the author section of a game listing. We show these without @@ -634,27 +656,25 @@ the page. } /* - The "gamerightbar" table is the box on a game's listing page, + The "haveYouPlayed" class is the box on a game's listing page, underneath the Download box, where we display quick rating input links ("Have you played this game?"). */ - table.gamerightbar { - padding: 0 0 0 0; - margin: 0 0 0 0; + .haveYouPlayed { + padding: 3px; + margin: 0; background: #F0F0F0; border: thin dotted #e0e0e0; width: 100%; font-size: 80%; + height: fit-content; } - table.gamerightbar h3 { + .haveYouPlayed h3 { font: 100%/140% Verdana, Arial, Helvetica, Sans-Serif; font-weight: bold; color: #000080; margin: 0 0 0.5em 0; } - table.gamerightbar td { - padding: 0 0 0.5em 0; - } } /* @@ -671,6 +691,10 @@ on the viewgame page display: none; } +.submitTimeSection > div { + margin-top: 1px; +} + /* The "downloads" section is for external links. */ @@ -2239,14 +2263,14 @@ span.sfl-save-autodesc { #externalLinks.downloads h3 { color: #d0d0ff; } - table.gamerightbar { + .haveYouPlayed { background: #101030; border: thin dotted #303030; } - table.gamerightbar td { + .haveYouPlayed { color: #d8d8d8; } - table.gamerightbar td h3 { + .haveYouPlayed h3 { color: #d0d0ff; } } diff --git a/www/ifdbutil.js b/www/ifdbutil.js index 0695224b..a372af70 100644 --- a/www/ifdbutil.js +++ b/www/ifdbutil.js @@ -224,7 +224,7 @@ function forceDarkMode(force) { } } -async function jsonSend(url, statusSpanID, cbFunc, content, silentMode) +async function jsonSend(url, statusSpanID, cbFunc, content, silentMode, cbFuncArgs) { if (statusSpanID) document.getElementById(statusSpanID).innerHTML = ""; @@ -267,7 +267,7 @@ async function jsonSend(url, statusSpanID, cbFunc, content, silentMode) + "Please try again later."); } if (cbFunc && response?.ok) { - cbFunc(jsonResponse); + cbFunc(jsonResponse, cbFuncArgs); } } @@ -280,3 +280,215 @@ async function check_ifid_in_ifwiki(ifid) { } catch (e) {} return false; } + +function setStarCtlValue(id, value) { + if (value) { + document.getElementById(`${id}__rating${value}`).checked = true; + } else { + const checked = [...document.querySelectorAll(`#${id} input[type=radio]`)].filter(i => i.checked)[0]; + if (checked) checked.checked = false; + } +} + +// Used by the have-you-played widget + +function ckboxGetObj(id) +{ + return document.getElementById('ckBox' + id); +} +function ckboxClick(id, onUpdateFunc) +{ + const elem = ckboxGetObj(id); + if (onUpdateFunc) + onUpdateFunc(id, elem.checked); +} +function updateRating(game_id, rating) +{ + const submitRating = document.getElementById(`submitRating_${game_id}`); + if (rating) { + if (submitRating.style.display === 'block') return; + } else { + submitRating.style.display = 'none'; + } + jsonSend(`setrating?game=${game_id}&rating=${rating}`, + `rating_${game_id}_SaveMsg`); +} +function updatePlayed(id, stat) +{ + const elem = ckboxGetObj(id); + const game_id = elem.closest('[data-gameid]').dataset.gameid; + jsonSend(`setplayed?game=${game_id}&played=` + + (elem.checked ? "1" : "0"), + `${id}_SaveMsg`, (d) => {if (typeof updatePlaylistCount == "function") updatePlaylistCount(d.newCount);}); +} +function updateWishlist(id, stat) +{ + const elem = ckboxGetObj(id); + const game_id = elem.closest('[data-gameid]').dataset.gameid; + jsonSend(`setwishlist?game=${game_id}&add=` + + (elem.checked ? "1" : "0"), + `${id}_SaveMsg`, (d) => {if (typeof updateWishlistCount == "function") updateWishlistCount(d.newCount)}); +} +function updateUnwishlist(id, stat) +{ + const elem = ckboxGetObj(id); + const game_id = elem.closest('[data-gameid]').dataset.gameid; + jsonSend(`setunwishlist?game=${game_id}&add=` + + (elem.checked ? "1" : "0"), + `${id}_SaveMsg`); +} + +// When the user clicks the "Submit Time" button, get the input +// from the hours and minutes fields, validate it, convert it to +// minutes only, and save it to the database (with the note, if present). +function submitTime() { + const gameElem = event.target.closest('[data-gameid]'); + const game_id = gameElem.dataset.gameid; + + let new_hours = gameElem.querySelector('.timeSection_hours').value; + let new_minutes = gameElem.querySelector('.timeSection_minutes').value; + const time_note = gameElem.querySelector('.timeNote').value.trim(); + if (new_hours == "") { // If the hours field was blank, interpret it as 0 + new_hours = 0; + } + if (new_minutes == "") { // If the minutes field was blank, interpret it as 0 + new_minutes = 0; + } + + const saveMsgId = `time_SaveMsg_${game_id}`; + const saveMsgElem = document.getElementById(saveMsgId); + + const hours_are_digits = isOnlyDigits(new_hours); + const minutes_are_digits = isOnlyDigits(new_minutes); + if (hours_are_digits == false || minutes_are_digits == false) { + saveMsgElem.innerHTML="Please enter valid numbers."; + return; + } + if (new_hours > 200) { // Negative numbers will hopefully be eliminated by "Digits only" + saveMsgElem.innerHTML="For hours, please enter a number from 0 to 200."; + return; + } + if (new_minutes > 59) { + saveMsgElem.innerHTML="For minutes, please enter a number from 0 to 59."; + return; + } + const new_time = convertToMinutes(new_hours, new_minutes); + if (new_time < 1) { + saveMsgElem.innerHTML="The total time must be at least one minute."; + return; + } + // Time is valid, as far as we can tell. + // If the game isn't already on the user's list of played games, add it. + const played_it_checkbox = document.getElementById(`ckBoxckPlayed_${game_id}`); + if (!played_it_checkbox.checked) { + played_it_checkbox.checked = true; + updatePlayed(`ckPlayed_${game_id}`); + } + // Submit the time. + jsonSend(`settime?game=${game_id}&newtime=${new_time}¬e=${encodeURIComponent(time_note)}`, saveMsgId, swapTimeSections, null, false, {'gameid': game_id}); +} + +// Remove the user's time for this game from the database. +function removeTime() { + const game_id = event.target.closest('[data-gameid]').dataset.gameid; + jsonSend(`settime?game=${game_id}&newtime=0`, `time_SaveMsg_${game_id}`, swapTimeSections, null, false, {'gameid': game_id}); +} + +// Check whether a string contains only digits. +function isOnlyDigits(a_string) { + if (/^\d+$/.test(a_string)) { + return true; + } + return false; +} + + +// Take hours and minutes and convert them into just minutes +// so we can store the time as minutes in the database. +function convertToMinutes(hours, minutes) { + // They need to be integers so they don't get concatenated when we're trying to add them + hours = Number(hours); + minutes = Number(minutes); + hours_into_minutes = hours * 60; + total_minutes = hours_into_minutes + minutes; + return total_minutes; +} + + +// The Estimated Play Time control panel has a "submitTimeSection," a "removeTimeSection," and +// a "timeSavedSection." Only one of the sections should be visible at a time. These functions +// show and hide the sections. + +function showSubmitTimeSection(game_id) { + document.querySelector(`[data-gameid='${game_id}'] .submitTimeSection`).style.display = "block"; + document.querySelector(`[data-gameid='${game_id}'] .removeTimeSection`).style.display = "none"; + document.querySelector(`[data-gameid='${game_id}'] .timeSavedSection`).style.display = "none"; +} + +function showRemoveTimeSection(game_id) { + document.querySelector(`[data-gameid='${game_id}'] .submitTimeSection`).style.display = "none"; + document.querySelector(`[data-gameid='${game_id}'] .removeTimeSection`).style.display = "block"; + document.querySelector(`[data-gameid='${game_id}'] .timeSavedSection`).style.display = "none"; +} + +function showTimeSavedSection(game_id) { + document.querySelector(`[data-gameid='${game_id}'] .submitTimeSection`).style.display = "none"; + document.querySelector(`[data-gameid='${game_id}'] .removeTimeSection`).style.display = "none"; + document.querySelector(`[data-gameid='${game_id}'] .timeSavedSection`).style.display = "block"; +} + +// After successfully submitting or removing a time, hide or show +// the appropriate sections of the estimated play time control panel. +function swapTimeSectionsEventListener(event) { + const game_id = event.target.closest('[data-gameid]').dataset.gameid; + swapTimeSections(null, {'gameid': game_id}); +} + +function swapTimeSections(_response, args) { + const game_id = args.gameid; + + if (document.getElementById(`time_SaveMsg_${game_id}`).innerHTML == "Saved") { + if (document.querySelector(`[data-gameid='${game_id}'] .submitTimeSection`).style.display == "block") { + //The submit time section is visible, which means we just successfully saved a new time. + showTimeSavedSection(game_id); + } else if (document.querySelector(`[data-gameid='${game_id}'] .removeTimeSection`).style.display == "block") { + //The remove time section is visible, which means we just successfully removed our time. + showSubmitTimeSection(game_id); + } + } // Don't swap the time controls if there was an error saving. +} + +function prepareHaveYouPlayedBox(game_id) { + // Set up star rating control so it submits as soon as a star is clicked + const arrowKeys = new Set(['ArrowRight', 'ArrowLeft', 'ArrowUp', 'ArrowDown']); + const ratingStars = document.getElementById(`ratingStars_${game_id}`); + const submitRating = document.getElementById(`submitRating_${game_id}`); + ratingStars.addEventListener('keydown', e => { + if (arrowKeys.has(e.key)) submitRating.style.display = 'block'; + }); + submitRating.addEventListener('click', () => { + submitRating.style.display = 'none'; + updateRating(game_id, ratingStars.querySelector('input:checked').value); + }); + + // Attach the submitTime function to the "Submit Time" button. + document.querySelector(`[data-gameid='${game_id}'] .submitTimeButton`).addEventListener("click", submitTime); + + + // Attach the removeTime function to the Remove Time button. + document.querySelector(`[data-gameid='${game_id}'] .removeTimeButton`).addEventListener("click", removeTime); + + + // Attach the swapTimeSections function to the timeVoteControls div. + document.querySelector(`[data-gameid='${game_id}'] .timeVoteControls`).addEventListener("load", swapTimeSectionsEventListener); + + + // Show the relevant time controls based on whether the user has a time already stored for this game. + // To find out, we check whether there's a message displaying the user's time. + const your_time_message = document.querySelector(`[data-gameid='${game_id}'] .yourTime`).innerHTML; + if (your_time_message == "") { + showSubmitTimeSection(game_id); + } else { + showRemoveTimeSection(game_id); + } +} diff --git a/www/pagetpl.php b/www/pagetpl.php index 092c9273..1acb85d4 100644 --- a/www/pagetpl.php +++ b/www/pagetpl.php @@ -44,11 +44,6 @@ function basePageHeader($title, $focusCtl, $extraOnLoad, $extraHead, <?php echo $title ?> - code for setting up for active checkboxes. -// -function ckboxSetup() -{ -?> - - tag to replace inline event attributes function addEventListener($event, $code) { global $nonce; diff --git a/www/review b/www/review index 100dab41..613f9be5 100644 --- a/www/review +++ b/www/review @@ -752,8 +752,7 @@ if (isset($missingFields['rating'])) 1. Rate this game on a scale of 1 to 5 stars (5 is best):
You don't have to rate the game to diff --git a/www/search b/www/search index c3c97359..ff9a37b1 100644 --- a/www/search +++ b/www/search @@ -9,9 +9,11 @@ checkPersistentLogin(); include_once "pagetpl.php"; include_once "util.php"; include_once "searchutil.php"; +include_once "viewgame-components/have-you-played.php"; include_once "dbconnect.php"; $db = dbConnect(); +$curuser = checkPersistentLogin(); // check for API requests $api_mode = null; @@ -1126,7 +1128,8 @@ else if ($term || $browse) . ($searchType == "poll" ? "&poll" : "") . ($searchType == "member" ? "&member" : "") . ($searchType == "comp" ? "&comp" : "") - . ($searchType == "tag" ? "&tag" : ""), + . ($searchType == "tag" ? "&tag" : "") + . (isset($_GET['showRatingBoxes']) ? "&showRatingBoxes" : ""), $pg, $lastPage, $firstOnPage, $lastOnPage, $rowcnt, true, $rowcnt < $showAllMaxRows, $pgAll) @@ -1249,8 +1252,11 @@ else if ($term || $browse) $hids[$searchType] = '1'; if ($urlXL) $hids['xlink'] = $extraLink; + echo "
"; showSortingControls("searchOrder", "sortby", $sortList, $sortby, $hids, "search"); + $checked = isset($_GET['showRatingBoxes']) ? "checked" : ""; + echo "
"; // show the page controls at the top of the results; // if all of the results fit on one page, don't bother with @@ -1424,9 +1430,16 @@ else if ($term || $browse) if ($playtime != "") echo "
$playtime
"; - echo ""; + echo ""; + + if ($curuser) { + showHaveYouPlayedThisCtl($db, $curuser, $id, $row['current_user_rating'], $row['current_user_has_review'], + 'searchRatingBox' . (isset($_GET['showRatingBoxes']) ? '' : ' hidden')); + } + + echo ""; break; - + case "list": // get the row data $id = $row['id']; @@ -1583,6 +1596,36 @@ else if ($term || $browse) if (!$api_mode) { ?> + + + 0 as current_user_has_review"; + } $baseWhere = ""; $groupBy = ""; $baseOrderBy = ""; $tableList = "games join ".getGameRatingsView($db)." on games.id = gameid"; + if ($curuser) { + $tableList .= " left outer join reviews on games.id = reviews.gameid and reviews.userid = '$curuser'"; + } $matchCols = "title, author, `desc`, tags"; $likeCol = "title"; $summaryDesc = "Games"; diff --git a/www/starctl.php b/www/starctl.php index 1295e08e..e8b86ac5 100644 --- a/www/starctl.php +++ b/www/starctl.php @@ -1,24 +1,6 @@ - - " + $str .= "" . ""; } - $str .= addEventListener("change", "$clickFunc(event.target.value)"); + $str .= addEventListener("change", "$clickFunc('$gameid', event.target.value)"); $str .= ""; diff --git a/www/viewgame b/www/viewgame index 93a4d7e6..6f4c78ad 100644 --- a/www/viewgame +++ b/www/viewgame @@ -14,6 +14,7 @@ include_once "game-rss.php"; include_once "gameinfo.php"; include_once "commentutil.php"; include_once "news.php"; +include_once "viewgame-components/have-you-played.php"; $db = dbConnect(); $curuser = checkPersistentLogin(); @@ -896,19 +897,12 @@ echo helpWinLink("help-ifid", "IFID");
- - - - - - - -

Have you played this game?

+ + +
" data-gameid=""> +

Have you played this game?

0; - ?> - - \n" - . ".viewgame__playedSection { margin: 1.25ex 0 0.75ex 0; }\n" - . "#submitRating { display: none; }\n" - . "\n"; - echo "
" - . "
" - . "Rate it: " - . showStarCtl("ratingStars", $currentUserRating, - "updateRating", "mouseOutRating") - . "" - . "" - . "
" + . "" - . "
"; - checkboxWrite("ckPlayed", "I've played it", $played, "updatePlayed"); + . "
"; + checkboxWrite("ckPlayed_$id", "I've played it", $played, "updatePlayed"); echo " (view)" - . "   " + . "   " . "
"; - checkboxWrite("ckWishList", "It's on my wish list", $wishlist, - "updateWishList"); + checkboxWrite("ckWishlist_$id", "It's on my wish list", $wishlist, + "updateWishlist"); echo " (view) " - . "   " + . "   " . "
"; - checkboxWrite("ckUnwishList", "I'm not interested", $unwishlist, - "updateUnwishList"); + checkboxWrite("ckUnwishlist_$id", "I'm not interested", $unwishlist, + "updateUnwishlist"); echo " (view) " - . "   " + . "   " . "
"; @@ -137,12 +89,12 @@ function updateUnwishList(id, stat) ?>
-
-Estimated play time: +
+Estimated play time: -
-Vote on how long it takes to finish this game.
+
+
Vote on how long it takes to finish this game.
Tips
  • Vote only if you've played the game.
  • @@ -151,159 +103,29 @@ function updateUnwishList(id, stat)
  • To help players gauge how long a game with puzzles might take, you can note if your time is with or without hints.
  • Times don't need to be exact. Good-faith estimates are fine.
  • Your vote will be publicly attributed to you.
  • -
-

- - -

-

-

- +
+
+
+
+
-
-
- +
+
+
-
+
-
-

Your time has been submitted.

+
+

Your time has been submitted.

@@ -318,6 +140,8 @@ function prepareTimeControls() { } ?> - - - +
+ +