GameListItem.jsx 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. import React from "react";
  2. import styled from "styled-components";
  3. import { Link, useHistory } from "react-router-dom";
  4. import { useTranslation } from "react-i18next";
  5. import { deleteGame, getBestTranslationFromConfig } from "../utils/api";
  6. import { confirmAlert } from "react-confirm-alert";
  7. import { toast } from "react-toastify";
  8. import { useMutation, useQueryClient } from "react-query";
  9. import { media2Url } from "../components/mediaLibrary";
  10. const Game = styled.li`
  11. position: relative;
  12. padding: 0em;
  13. margin: 0px;
  14. & .game-name {
  15. max-width: 80%;
  16. line-height: 1.2em;
  17. overflow: hidden;
  18. margin-bottom: 3px;
  19. margin: 0.2em 0 0.5em 0;
  20. font-size: 2.3vw;
  21. }
  22. & .unpublished {
  23. position: absolute;
  24. top: 0.5em;
  25. left: 0.5em;
  26. padding: 0em;
  27. }
  28. & .button.play {
  29. margin: 0 2px;
  30. background-color: var(--color-secondary);
  31. }
  32. & .play {
  33. position: absolute;
  34. bottom: 0.5em;
  35. right: 0.5em;
  36. }
  37. & .extra-actions {
  38. position: absolute;
  39. top: 0.5em;
  40. right: 0.5em;
  41. display: none;
  42. z-index: 2;
  43. }
  44. &:hover .extra-actions,
  45. & .extra-actions:hover {
  46. display: block;
  47. }
  48. & .img-wrapper {
  49. display: block;
  50. position: relative;
  51. margin: 0;
  52. padding: 0;
  53. width: 100%;
  54. padding-top: 64.5%;
  55. & > span {
  56. background-color: var(--color-blueGrey);
  57. position: absolute;
  58. top: 0;
  59. left: 0;
  60. bottom: 0;
  61. right: 0;
  62. overflow: hidden;
  63. display: block;
  64. display: flex;
  65. border-radius: 5px;
  66. & > .back {
  67. filter: blur(5px);
  68. background-size: cover;
  69. position: absolute;
  70. top: 0;
  71. left: 0;
  72. bottom: 0;
  73. right: 0;
  74. }
  75. }
  76. }
  77. & .img {
  78. object-fit: contain;
  79. width: 100%;
  80. height: 100%;
  81. z-index: 1;
  82. }
  83. & .details {
  84. display: flex;
  85. flex-direction: row;
  86. color: var(--font-color2);
  87. font-size: 14px;
  88. padding-top: 1em;
  89. }
  90. & .details > span {
  91. display: flex;
  92. align-items: center;
  93. padding-right: 5px;
  94. margin-right: 5px;
  95. border-right: 1px solid var(--font-color2);
  96. }
  97. & .details > span:last-child {
  98. border: none;
  99. }
  100. & .details img {
  101. margin-right: 0.5em;
  102. }
  103. @media screen and (max-width: 1024px) {
  104. & {
  105. flex-basis: 45%;
  106. }
  107. & .details {
  108. font-size: 12px;
  109. }
  110. & .game-name {
  111. font-size: 28px;
  112. }
  113. }
  114. @media screen and (max-width: 640px) {
  115. & {
  116. flex-basis: 100%;
  117. }
  118. & .game-name {
  119. font-size: 24px;
  120. }
  121. }
  122. `;
  123. const GameListItem = ({
  124. game: {
  125. published,
  126. owner,
  127. id,
  128. minAge,
  129. materialLanguage,
  130. duration,
  131. playerCount,
  132. imageUrl,
  133. },
  134. game,
  135. userId,
  136. onClick: propOnClick,
  137. onDelete,
  138. }) => {
  139. const { t, i18n } = useTranslation();
  140. const history = useHistory();
  141. const queryClient = useQueryClient();
  142. const deleteMutation = useMutation((gameId) => deleteGame(gameId), {
  143. onSuccess: () => {
  144. queryClient.invalidateQueries("ownGames");
  145. queryClient.invalidateQueries("games");
  146. },
  147. });
  148. const translation = React.useMemo(
  149. () => getBestTranslationFromConfig(game, i18n.languages),
  150. [game, i18n.languages]
  151. );
  152. const onClick = React.useCallback(() => {
  153. if (propOnClick) {
  154. return propOnClick(id);
  155. } else {
  156. history.push(`/playgame/${id}`);
  157. }
  158. }, [history, id, propOnClick]);
  159. const deleteGameHandler = async () => {
  160. confirmAlert({
  161. title: t("Confirmation"),
  162. message: t("Do you really want to remove this game?"),
  163. buttons: [
  164. {
  165. label: t("Yes"),
  166. onClick: async () => {
  167. try {
  168. deleteMutation.mutate(id);
  169. if (onDelete) onDelete(id);
  170. toast.success(t("Game deleted"), { autoClose: 1500 });
  171. } catch (e) {
  172. if (e.message === "Forbidden") {
  173. toast.error(t("Action forbidden. Try logging in again."));
  174. } else {
  175. console.log(e);
  176. toast.error(t("Error while deleting game. Try again later..."));
  177. }
  178. }
  179. },
  180. },
  181. {
  182. label: t("No"),
  183. onClick: () => {},
  184. },
  185. ],
  186. });
  187. };
  188. let playerCountDisplay = undefined;
  189. if (playerCount && playerCount.length) {
  190. const [min, max] = playerCount;
  191. if (min === max) {
  192. if (max === 9) {
  193. playerCountDisplay = ["9+"];
  194. } else {
  195. playerCountDisplay = [max];
  196. }
  197. } else {
  198. if (max === 9) {
  199. playerCountDisplay = [min, "9+"];
  200. } else {
  201. playerCountDisplay = [min, max];
  202. }
  203. }
  204. }
  205. let durationDisplay = undefined;
  206. if (duration && duration.length) {
  207. const [min, max] = duration;
  208. if (min === max) {
  209. if (max === 90) {
  210. durationDisplay = "90+";
  211. } else {
  212. durationDisplay = `~${max}`;
  213. }
  214. } else {
  215. if (max === 90) {
  216. durationDisplay = `${min}~90+`;
  217. } else {
  218. durationDisplay = `${min}~${max}`;
  219. }
  220. }
  221. }
  222. let materialLanguageDisplay = t(materialLanguage);
  223. const realImageUrl = media2Url(imageUrl);
  224. return (
  225. <Game>
  226. <div onClick={onClick} className="img-wrapper button">
  227. <span>
  228. {realImageUrl && (
  229. <>
  230. <span
  231. className="back"
  232. style={{ backgroundImage: `url(${realImageUrl})` }}
  233. />
  234. <img className="img" src={realImageUrl} />
  235. </>
  236. )}
  237. </span>
  238. </div>
  239. {userId && (userId === owner || !owner) && (
  240. <span className="extra-actions">
  241. <button
  242. onClick={deleteGameHandler}
  243. className="button edit icon-only error"
  244. >
  245. <img
  246. src="https://icongr.am/feather/trash.svg?size=16&color=ffffff"
  247. alt={t("Delete")}
  248. />
  249. </button>
  250. <Link to={`/game/${id}/edit`} className="button edit icon-only ">
  251. <img
  252. src="https://icongr.am/feather/edit.svg?size=16&color=ffffff"
  253. alt={t("Edit")}
  254. />
  255. </Link>
  256. </span>
  257. )}
  258. {!published && (
  259. <img
  260. className="unpublished"
  261. src="https://icongr.am/entypo/eye-with-line.svg?size=32&color=888886"
  262. alt={t("Unpublished")}
  263. />
  264. )}
  265. <div className="details">
  266. {playerCountDisplay && (
  267. <span>
  268. {playerCountDisplay.length === 2 &&
  269. t("{{min}} - {{max}} players", {
  270. min: playerCountDisplay[0],
  271. max: playerCountDisplay[1],
  272. })}
  273. {playerCountDisplay.length === 1 &&
  274. t("{{count}} player", {
  275. count: playerCountDisplay[0],
  276. })}
  277. </span>
  278. )}
  279. {durationDisplay && <span>{durationDisplay} mins</span>}
  280. {minAge && <span>age {minAge}+</span>}
  281. {materialLanguageDisplay && <span>{materialLanguageDisplay}</span>}
  282. </div>
  283. <h2 className="game-name">{translation.name}</h2>
  284. <p className="baseline">{translation.baseline}</p>
  285. </Game>
  286. );
  287. };
  288. export default GameListItem;