diff --git a/utils-circuits.php b/utils-circuits.php index 12acaaf0..419bb5e2 100755 --- a/utils-circuits.php +++ b/utils-circuits.php @@ -187,7 +187,7 @@ function countRows($sql,&$params) { $query = toSQLSort($sql,$params); $countQuery = preg_replace('#^SELECT.+? FROM #', 'SELECT COUNT(*) AS nb FROM ', $query); $count = mysql_fetch_array(mysql_query($countQuery)); - return $count['nb']; + return +$count['nb']; } function escape($str) { return str_replace('', '<\/script>', str_replace('%u', '\\u', str_replace('"', '\\"', str_replace('\\', '\\\\', $str)))); diff --git a/v2/app/components/ClassicPage/ClassicPage.tsx b/v2/app/components/ClassicPage/ClassicPage.tsx index 0f5da444..6cc6c512 100644 --- a/v2/app/components/ClassicPage/ClassicPage.tsx +++ b/v2/app/components/ClassicPage/ClassicPage.tsx @@ -21,11 +21,11 @@ function Flag({ nLanguage, src: srcData, alt, page, homepage = false }) { let url; let language = useLanguage(); if (homepage) { - url = nLanguage ? 'en.php' : 'fr.php'; + url = nLanguage ? '/en.php' : '/fr.php'; alt = nLanguage ? 'Home - Mario Kart PC' : 'Accueil - Mario Kart PC'; } else - url = 'changeLanguage.php?nLanguage=' + nLanguage + '&page=' + page; + url = '/changeLanguage.php?nLanguage=' + nLanguage + '&page=' + page; const chosen = (nLanguage === language); function handleClick() { if (chosen) diff --git a/v2/app/components/Rating/Rating.module.scss b/v2/app/components/Rating/Rating.module.scss index 888452f5..6d72e295 100644 --- a/v2/app/components/Rating/Rating.module.scss +++ b/v2/app/components/Rating/Rating.module.scss @@ -1,4 +1,5 @@ .Rating { + border-collapse: collapse; .star0, .star1 { width: 15px; height: 15px; diff --git a/v2/app/components/Rating/Rating.tsx b/v2/app/components/Rating/Rating.tsx index 15421c9e..24e1bbc0 100644 --- a/v2/app/components/Rating/Rating.tsx +++ b/v2/app/components/Rating/Rating.tsx @@ -1,4 +1,4 @@ -import style from "./Rating.module.scss" +import styles from "./Rating.module.scss" import useLanguage, { plural } from "../../hooks/useLanguage"; type Props = { @@ -15,15 +15,15 @@ function Rating({ rating, nbRatings, label }: Props) { const rest = rating - lastRating; const restW = 3 + Math.round(9 * rest); - return + return
- {Object.keys([...Array(lastRating)]).map((i) => )} + {Object.keys([...Array(lastRating)]).map((i) => )} {(rest > 0) && <> - - + + } - {Object.keys([...Array(5 - nextRating)]).map((i) => )} + {Object.keys([...Array(5 - nextRating)]).map((i) => )} {label && } diff --git a/v2/app/components/TrackCreationCard/TrackCreationCard.module.scss b/v2/app/components/TrackCreationCard/TrackCreationCard.module.scss new file mode 100644 index 00000000..0ae97541 --- /dev/null +++ b/v2/app/components/TrackCreationCard/TrackCreationCard.module.scss @@ -0,0 +1,157 @@ +.circuit-poster { + background-position: center; + background-size: cover; + background-repeat: no-repeat; + + line-height: normal; + position: relative; + font-family: Helvetica, arial, sans-serif; + text-align: center; + width: 130px; + height: 130px; + margin: 8px 20px; + display: inline-block; + &:hover { + background-color: rgba(0,0,0,0.2); + } + + @media screen and (max-width: 880px) { + width: 110px; + height: 110px; + .circuit-name { + font-size: 0.9em; + } + } + + .cup-poster { + background-repeat: no-repeat; + background-position: top left, top right, bottom left, bottom right; + background-size: 50% 50%; + background-size: calc(50% + 1px) calc(50% + 1px); + } + .circuit-rate { + position: absolute; + left: 0; + bottom: 0; + width: 100%; + display: flex; + align-items: center; + justify-content: center; + background-color: rgba(255,255,255, 0.6); + opacity: 0.8; + padding: 2px 0 3px 0; + } + &:hover .circuit-rate { + opacity: 1; + } + .circuit-star { + width: 15px; + position: relative; + } + .circuit-star > div { + position: absolute; + left: 0; + top: 1px; + overflow: hidden; + } + .circuit-name { + display: none; + position: absolute; + left: 10%; + top: 50%; + width: 80%; + -webkit-transform: translateY(-50%); + -moz-transform: translateY(-50%); + -o-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); + background-color: white; + color: black; + opacity: 0.8; + padding-top: 2px; + padding-bottom: 2px; + > div:first-child { + word-wrap: break-word; + overflow: hidden; + text-overflow: ellipsis; + display: -webkit-box; + -webkit-line-clamp: 3; + line-clamp: 2; + -webkit-box-orient: vertical; + } + } + .circuit-author { + margin: 1px 3px; + font-size: 0.7em; + opacity: 0.6; + display: flex; + align-items: center; + justify-content: center; + } + .circuit-author img { + height: 0.8em; + margin-left: 2px; + margin-right: 3px; + } + .circuit-author span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .circuit-nbcomments { + position: absolute; + left: 0; + top: 0; + color: black; + background-color: rgba(255,255,255, 0.6); + padding: 2px 5px; + border-bottom: solid 1px #AAA; + border-right: solid 1px #AAA; + border-bottom-right-radius: 3px; + font-size: 12px; + } + .circuit-preview { + display: none; + position: absolute; + right: 0; + top: 0; + background-color: rgba(255,255,255, 0.6); + padding: 0px 5px; + border-bottom: solid 1px #AAC; + border-left: solid 1px #AAC; + border-bottom-left-radius: 3px; + cursor: zoom-in; + } + .circuit-preview:hover { + background-color: rgba(230,230,230, 0.6); + } + .circuit-suppr { + position: absolute; + left: 40%; + width: 20%; + background-color: #FEE; + opacity: 0.7; + color: #F00; + } + .circuit-suppr:hover { + opacity: 1; + } + .circuit-nbcomments img { + height: 12px; + position: relative; + top: 2px; + } + .circuit-preview img { + height: 12px; + } + &:hover .circuit-name { + display: inline-block; + } + &:hover .circuit-preview { + display: inline-block; + } + .circuit-star { + display: inline-block; + padding-top: 1px; + } +} \ No newline at end of file diff --git a/v2/app/components/TrackCreationCard/TrackCreationCard.tsx b/v2/app/components/TrackCreationCard/TrackCreationCard.tsx new file mode 100644 index 00000000..c4adbe8a --- /dev/null +++ b/v2/app/components/TrackCreationCard/TrackCreationCard.tsx @@ -0,0 +1,57 @@ +import styles from "./TrackCreationCard.module.scss" +import useLanguage, { plural } from "../../hooks/useLanguage"; + +import userIcon from "../../images/icons/user.png" +import commentIcon from "../../images/icons/comment.png" +import previewIcon from "../../images/icons/preview.png" +import Rating from "../Rating/Rating"; +import { MouseEvent } from "react"; + +export type TrackCreation = { + id: number, + author: string, + cicon: string, + icons: string[], + href: string, + isCup: boolean, + name: string, + nbComments: number, + publicationDate: number, + rating: number, + nbRatings: number +} + +type Props = { + creation: TrackCreation; + onPreview: (creation: TrackCreation) => void; +} +function TrackCreationCard({ creation, onPreview }: Props) { + const language = useLanguage(); + + function handlePreview(e: MouseEvent, creation: TrackCreation) { + e.preventDefault(); + onPreview(creation); + } + + return `url('images/creation_icons/${src}')`).join(",") : undefined }}> +
+
{creation.name || (language ? "Untitled" : "Sans titre")}
+ {creation.author &&
+ {language + {creation.author} +
} +
+
+ +
+
+ {language {creation.nbComments} +
+
handlePreview(e, creation)}> + {language +
+
+} + +export default TrackCreationCard; \ No newline at end of file diff --git a/v2/app/helpers/uris.ts b/v2/app/helpers/uris.ts new file mode 100644 index 00000000..ae88c7ac --- /dev/null +++ b/v2/app/helpers/uris.ts @@ -0,0 +1,12 @@ +export function buildQuery(params: Record) { + return Object.entries(params) + .filter(([_key, value]) => value != null) + .map(([key, value]) => { + if (Array.isArray(value)) { + return value.map((v, i) => `${key}[${i}]=${encodeURIComponent(v)}`).join("&"); + } else { + return `${key}=${encodeURIComponent(value)}`; + } + }) + .join("&"); +} \ No newline at end of file diff --git a/v2/app/images/icons/preview.png b/v2/app/images/icons/preview.png new file mode 100755 index 00000000..96a83f9c Binary files /dev/null and b/v2/app/images/icons/preview.png differ diff --git a/v2/app/images/icons/user.png b/v2/app/images/icons/user.png new file mode 100755 index 00000000..c578c0d4 Binary files /dev/null and b/v2/app/images/icons/user.png differ diff --git a/v2/app/pages/creations.tsx b/v2/app/pages/creations.tsx new file mode 100644 index 00000000..afb7557d --- /dev/null +++ b/v2/app/pages/creations.tsx @@ -0,0 +1,134 @@ +import { NextPage } from "next"; +import ClassicPage, { commonStyles } from "../components/ClassicPage/ClassicPage"; +import styles from "../styles/Creations.module.scss"; +import cx from "classnames"; +import useLanguage from "../hooks/useLanguage"; +import Ad from "../components/Ad/Ad"; +import WithAppContext from "../components/WithAppContext/WithAppContext"; +import useSmoothFetch, { Placeholder } from "../hooks/useSmoothFetch"; +import Skeleton from "../components/Skeleton/Skeleton"; +import { useMemo } from "react"; +import { useRouter } from "next/router"; +import TrackCreationCard from "../components/TrackCreationCard/TrackCreationCard"; +import { buildQuery } from "../helpers/uris"; +import Link from "next/link"; +import useCreations from "../hooks/useCreations"; + +const CreationsList: NextPage = () => { + const language = useLanguage(); + const router = useRouter(); + const { user, admin, tri, type, nom, auteur } = router.query; + const { data: creator } = useSmoothFetch<{ name: string }>(`/api/user/${user}`, { + disabled: !user + }); + const nTri = +tri || 0; + const singleType = (type !== ''); + const sortTabs = language ? ['By latest', 'Top rated', 'Trending'] : ['Les plus récents', 'Les mieux notés', 'Tendances']; + const types = language + ? ['Complete mode - multicups', 'Quick mode - multicups', 'Complete mode - cups', 'Quick mode - cups', 'Complete mode - circuits', 'Quick mode - circuits', 'Complete mode - arenas', 'Quick mode - arenas'] + : ['Mode complet - multicoupes', 'Mode simplifié - multicoupes', 'Mode complet - coupes', 'Mode simplifié - coupes', 'Mode complet - circuits', 'Mode simplifié - circuits', 'Mode complet - arènes', 'Mode simplifié - arènes'] + const creationParams = useMemo(() => { + const params = { + user, admin, tri, type, nom, auteur + }; + return buildQuery(params); + }, [router.query]); + const { data: creationsPayload, loading: creationsLoading } = useSmoothFetch(`/api/getCreations.php?${creationParams}`, { + placeholder: () => ({ + data: Placeholder.array(60, (id) => ({ + id, + author: "", + cicon: "", + icons: [], + href: "", + isCup: false, + name: Placeholder.text(15, 35), + nbComments: Placeholder.number(1, 100), + publicationDate: Placeholder.timestamp(), + rating: 0, + nbRatings: 0 + })), + count: 0, + countByType: [] + }) + }); + const creationCount = useMemo(() => { + if (creationsLoading) return; + return { + total: creationsPayload.count, + byType: creationsPayload.countByType + } + }, [creationsPayload, creationsLoading]); + + const { previewCreation } = useCreations(); + + function defile() { + } + function masque() { + } + function reduceAll() { + } + function handleTabSelect(e) { + e.preventDefault(); + } + function scrollToTop(e) { + e.preventDefault(); + window.scrollTo(0, 0); + } + + return ( + + +

{creator + ? (language ? 'Creations list of ' + creator.name : 'Liste des créations de ' + creator.name) + : (language ? 'Creations list of Mario Kart PC' : 'Liste des créations Mario Kart PC')}

+
+

{!user && ( + language ? <>Welcome to the list of circuits and courses shared by the Mario Kart PC community !
+ You too, share your circuit creations by clicking on "Share circuit" at the bottom-left of the circuit page. + : <>Bienvenue dans la liste des circuits et arènes partagés par la communauté de Mario Kart PC !
+ Vous aussi, partagez les circuits que vous créez en cliquant sur "Partager le circuit" en bas à gauche de la page du circuit. + )}

+
+
+ {sortTabs.map((sortTab, i) => { + return (i === nTri) ? {sortTab} : {sortTab} + })} +
+
{language ? 'Creation type' : 'Type de création '}{": "} +
+
{language ? 'Search' : 'Recherche '}{": "} + {admin && } + + + {" "} + {" "} + +
+ +
+ +
+
+ + {creationsPayload.data.map((creation) => )} + +
+

+ defile()} />{"   "} + masque()} />{"   "} + reduceAll()} /> +

+ +

+ {language ? 'Back to top' : 'Retour haut de page'}{" - "} + {language ? 'Back to Mario Kart PC' : 'Retour à Mario Kart PC'} +

+
+ ); +} + +export default WithAppContext(CreationsList); \ No newline at end of file diff --git a/v2/app/pages/index.tsx b/v2/app/pages/index.tsx index 7591112b..bc85613d 100644 --- a/v2/app/pages/index.tsx +++ b/v2/app/pages/index.tsx @@ -57,6 +57,7 @@ import ss12xs from "../images/main/screenshots/ss12xs.png" import { formatDate } from "../helpers/dates"; import { formatRank, formatTime } from "../helpers/records"; import { escapeHtml } from "../helpers/strings"; +import { buildQuery } from "../helpers/uris"; import { Fragment, useEffect, useMemo, useState } from "react"; import cx from "classnames"; import { uniqBy } from "../helpers/objects"; @@ -246,19 +247,13 @@ const Home: NextPage = () => { }, [lastNewsRead, newsPayload, lastNewsReadLoading]); const creationParams = useMemo(() => { const nbByType = [1, 1, 2, 2, 3, 3, 2, 2]; - let nbByTypeParams = {}; - for (let i = 0; i < nbByType.length; i++) - nbByTypeParams[`nbByType[${i}]`] = nbByType[i]; - return new URLSearchParams({ - ...nbByTypeParams - }).toString(); + return buildQuery({ nbByType }); }, []); const { data: creationsPayload, loading: creationsLoading } = useSmoothFetch(`/api/getCreations.php?${creationParams}`, { placeholder: () => ({ data: Placeholder.array(10, (id) => ({ id, author: "", - category: Placeholder.text(3, 8), cicon: "", icons: [], href: "", @@ -668,14 +663,14 @@ const Home: NextPage = () => {

Most of the modes from Mario Kart have been included: Grand Prix, VS, Battle mode, Time Trials, and more!
There's also a brand new mode: the track builder! Place straight lines and turns, add items, boost panels and more! Everything is customizable! The only limit is your own imagination!
- You can share your tracks, and try other people's tracks thanks to the sharing tool. Thousands of custom tracks are already available!

+ You can share your tracks, and try other people's tracks thanks to the sharing tool. Thousands of custom tracks are already available!

Finally, you can face players from the whole world thanks to the multiplayer online mode! Climb the rankings and become world champion!

: <>

Vous connaissez certainement Mario Kart, le jeu de course le plus fun de tous les temps ! Mario Kart PC reprend les mêmes principes que le jeu original mais il est jouable sur navigateur, et gratuitement.

La plupart des modes issus de Mario Kart ont été repris : Grand Prix, courses VS, batailles de ballons, contre-la-montre...
Et un dernier mode inédit : l'éditeur de circuits ! Placez les lignes droites et les virages, ajoutez les objets, insérez des accélérateurs... Tout est personnalisable ! Votre imagination est la seule limite !
- Vous pouvez également partager vos créations et essayer celles des autres grâce à l'outil de partage. + Vous pouvez également partager vos créations et essayer celles des autres grâce à l'outil de partage. Plusieurs milliers de circuits ont déjà été partagés !

Enfin, il est possible d'affronter les joueurs du monde entier grâce au mode multijoueurs en ligne ! Grimpez dans le classement et devenez champion du monde !

} @@ -778,7 +773,7 @@ const Home: NextPage = () => { {language ? 'All news' : 'Toutes les news'}
- +

{language ? 'Latest creations' : 'Dernières créations'}

{label}
@@ -802,7 +797,7 @@ const Home: NextPage = () => {
- {language ? 'Display all' : 'Afficher tout'} + {language ? 'Display all' : 'Afficher tout'}

{language ? 'Last challenges' : 'Derniers défis'}

{ diff --git a/v2/app/styles/Creations.module.scss b/v2/app/styles/Creations.module.scss new file mode 100644 index 00000000..efdda158 --- /dev/null +++ b/v2/app/styles/Creations.module.scss @@ -0,0 +1,143 @@ +.Creations { + position: relative; + + #form-search { + text-align: center; + margin-bottom: 10px; + } + #sort-tabs { + display: table; + margin-left: auto; + margin-right: auto; + margin-bottom: 6px; + text-align: center; + font-size: 0.9em; + } + #sort-tabs > * { + display: table-cell; + vertical-align: middle; + border-top: solid 1px #820; + border-bottom: solid 1px #820; + padding: 6px 10px; + } + @media screen and (max-width: 500px) { + #sort-tabs { + font-size: 0.7em; + } + #sort-tabs > * { + padding: 5px 8px; + } + } + #sort-tabs > a { + background-color: #FFE30C; + color: #F60; + text-decoration: none; + } + #sort-tabs > a:hover { + background-color: #FFD816; + color: #C30; + } + #sort-tabs > span { + background-color: #FFC02C; + color: #820; + font-weight: bold; + } + #sort-tabs > *:first-child { + border-left: solid 1px #820; + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; + } + #sort-tabs > *:last-child { + border-right: solid 1px #820; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; + } + .pub { + width: 100%; + overflow: hidden; + text-align: center; + } + .retour { + color: #F90; + font-weight: bold; + } + .retour:hover { + background-color: #FF9; + } + select { + background-color: #FC0; + color: black; + border: solid 1px maroon; + padding: 2px; + } + select:hover { + background-color: #F90; + } + select:active { + background-color: #F60; + } + input[type="text"] { + background-color: #FFEE99; + width: 100px; + padding: 2px; + } + input[type="text"]:hover { + background-color: #FFF3A9; + } + input[type="text"]:focus { + background-color: #FFF6CC; + } + form div { + margin: 2px 0px; + } + h1, h2 { + text-decoration: underline; + font-family: Verdana; + color: #560000; + } + .subbuttons { + margin-top: 12px; + } + @media screen and (min-width: 880px) { + h2, .subbuttons { + margin-left: 15%; + margin-left: calc(50% - 350px); + } + .subbuttons { + margin-right: 15%; + margin-right: calc(50% - 350px); + } + } + .hidden { + /*height: px;*/ + overflow: hidden; + text-align: center; + } + input.defiler { + margin-left: 5px; + } + .defiler { + position: relative; + top: -4px; + } + a.defiler::before { + content: "+"; + color: #FED; + margin-right: 5px; + } + a.defiler { + float: right; + background-color: #F90; + } + a.defiler:hover { + float: right; + background-color: #FB0; + } + .liste { + max-width: 851px; + margin-left: auto; + margin-right: auto; + text-align: center; + line-height: 0; + } +} \ No newline at end of file diff --git a/v2/node-api/src/user/user.controller.ts b/v2/node-api/src/user/user.controller.ts index fa0967e0..fa458c1d 100644 --- a/v2/node-api/src/user/user.controller.ts +++ b/v2/node-api/src/user/user.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, HttpCode } from '@nestjs/common'; +import { Controller, Get, HttpCode, Param } from '@nestjs/common'; import { Auth } from '../auth/auth.decorator'; import { EntityManager } from 'typeorm'; import { User } from './user.entity'; @@ -36,4 +36,13 @@ export class UserController { endDate: ban.endDate } } + + @Get("/:id") + async getUser(@Param("id") id: number) { + const user = await this.em.findOne(User, id); + return { + id: user.id, + name: user.name + } + } } diff --git a/v2/php-api/endpoints/getCreations.php b/v2/php-api/endpoints/getCreations.php index 6765f5ff..0c54ce55 100644 --- a/v2/php-api/endpoints/getCreations.php +++ b/v2/php-api/endpoints/getCreations.php @@ -63,5 +63,6 @@ $nbCreations = array_sum($nbByType); renderResponse(array( 'data' => $data, - 'count' => $nbCreations + 'count' => $nbCreations, + 'countByType' => $nbByType ));