From a65d6d9127c3fa7f6a8ecaec5d1ffd1f47c2bc98 Mon Sep 17 00:00:00 2001 From: Arda Serdar Pektezol <1669855+pektezol@users.noreply.github.com> Date: Tue, 3 Sep 2024 00:08:53 +0300 Subject: refactor: port to typescript --- frontend/src/App.css | 17 + frontend/src/App.js | 49 - frontend/src/App.tsx | 40 + frontend/src/api/Api.tsx | 157 ++ frontend/src/components/Discussions.tsx | 151 ++ frontend/src/components/GameEntry.tsx | 49 + frontend/src/components/Leaderboards.tsx | 105 ++ frontend/src/components/Login.tsx | 1931 +++++++++++++++++++++ frontend/src/components/ModMenu.tsx | 324 ++++ frontend/src/components/Sidebar.tsx | 183 ++ frontend/src/components/Summary.tsx | 169 ++ frontend/src/components/login.css | 26 - frontend/src/components/login.js | 61 - frontend/src/components/main.css | 17 - frontend/src/components/main.js | 17 - frontend/src/components/news.css | 29 - frontend/src/components/news.js | 21 - frontend/src/components/pages/about.css | 17 - frontend/src/components/pages/about.js | 32 - frontend/src/components/pages/game.js | 46 - frontend/src/components/pages/games.css | 99 -- frontend/src/components/pages/games.js | 62 - frontend/src/components/pages/home.css | 92 - frontend/src/components/pages/home.js | 242 --- frontend/src/components/pages/maplist.css | 403 ----- frontend/src/components/pages/maplist.js | 890 ---------- frontend/src/components/pages/profile.css | 239 --- frontend/src/components/pages/profile.js | 382 ---- frontend/src/components/pages/summary.css | 720 -------- frontend/src/components/pages/summary.js | 650 ------- frontend/src/components/pages/summary_modview.css | 112 -- frontend/src/components/pages/summary_modview.js | 254 --- frontend/src/components/record.css | 15 - frontend/src/components/record.js | 56 - frontend/src/components/sidebar.css | 208 --- frontend/src/components/sidebar.js | 203 --- frontend/src/css/Games.css | 99 ++ frontend/src/css/Login.css | 26 + frontend/src/css/Maps.css | 726 ++++++++ frontend/src/css/ModMenu.css | 112 ++ frontend/src/css/Profile.css | 239 +++ frontend/src/css/Sidebar.css | 208 +++ frontend/src/images/Images.tsx | 44 + frontend/src/images/png/1.png | Bin 0 -> 2011 bytes frontend/src/images/png/10.png | Bin 0 -> 1601 bytes frontend/src/images/png/11.png | Bin 0 -> 1294 bytes frontend/src/images/png/12.png | Bin 0 -> 1545 bytes frontend/src/images/png/13.png | Bin 0 -> 1251 bytes frontend/src/images/png/14.png | Bin 0 -> 1363 bytes frontend/src/images/png/15.png | Bin 0 -> 2988 bytes frontend/src/images/png/16.png | Bin 0 -> 3078 bytes frontend/src/images/png/17.png | Bin 0 -> 4943 bytes frontend/src/images/png/18.png | Bin 0 -> 2434 bytes frontend/src/images/png/19.png | Bin 0 -> 1266 bytes frontend/src/images/png/2.png | Bin 0 -> 1833 bytes frontend/src/images/png/3.png | Bin 0 -> 1517 bytes frontend/src/images/png/4.png | Bin 0 -> 4517 bytes frontend/src/images/png/5.png | Bin 0 -> 4112 bytes frontend/src/images/png/6.png | Bin 0 -> 2715 bytes frontend/src/images/png/7.png | Bin 0 -> 1608 bytes frontend/src/images/png/8.png | Bin 0 -> 1584 bytes frontend/src/images/png/9.png | Bin 0 -> 6037 bytes frontend/src/images/png/login.png | Bin 0 -> 4871 bytes frontend/src/images/png/logo.png | Bin 0 -> 67124 bytes frontend/src/imgs/1.png | Bin 2011 -> 0 bytes frontend/src/imgs/10.png | Bin 1601 -> 0 bytes frontend/src/imgs/11.png | Bin 1294 -> 0 bytes frontend/src/imgs/12.png | Bin 1545 -> 0 bytes frontend/src/imgs/13.png | Bin 1251 -> 0 bytes frontend/src/imgs/14.png | Bin 1363 -> 0 bytes frontend/src/imgs/15.png | Bin 2988 -> 0 bytes frontend/src/imgs/16.png | Bin 3078 -> 0 bytes frontend/src/imgs/17.png | Bin 4943 -> 0 bytes frontend/src/imgs/18.png | Bin 2434 -> 0 bytes frontend/src/imgs/19.png | Bin 1266 -> 0 bytes frontend/src/imgs/2.png | Bin 1833 -> 0 bytes frontend/src/imgs/3.png | Bin 1517 -> 0 bytes frontend/src/imgs/4.png | Bin 4517 -> 0 bytes frontend/src/imgs/5.png | Bin 4112 -> 0 bytes frontend/src/imgs/6.png | Bin 2715 -> 0 bytes frontend/src/imgs/7.png | Bin 1608 -> 0 bytes frontend/src/imgs/8.png | Bin 1584 -> 0 bytes frontend/src/imgs/9.png | Bin 6037 -> 0 bytes frontend/src/imgs/login.png | Bin 4871 -> 0 bytes frontend/src/imgs/logo.png | Bin 67124 -> 0 bytes frontend/src/index.js | 8 - frontend/src/index.tsx | 17 + frontend/src/pages/Games.tsx | 51 + frontend/src/pages/Maps.tsx | 91 + frontend/src/pages/Profile.tsx | 326 ++++ frontend/src/pages/User.tsx | 320 ++++ frontend/src/react-app-env.d.ts | 2 + frontend/src/types/Content.tsx | 18 + frontend/src/types/Game.tsx | 37 + frontend/src/types/Map.tsx | 103 ++ frontend/src/types/Pagination.tsx | 6 + frontend/src/types/Profile.tsx | 63 + frontend/src/types/Search.tsx | 13 + frontend/src/utils/Time.tsx | 42 + 99 files changed, 5669 insertions(+), 4950 deletions(-) delete mode 100644 frontend/src/App.js create mode 100644 frontend/src/App.tsx create mode 100644 frontend/src/api/Api.tsx create mode 100644 frontend/src/components/Discussions.tsx create mode 100644 frontend/src/components/GameEntry.tsx create mode 100644 frontend/src/components/Leaderboards.tsx create mode 100644 frontend/src/components/Login.tsx create mode 100644 frontend/src/components/ModMenu.tsx create mode 100644 frontend/src/components/Sidebar.tsx create mode 100644 frontend/src/components/Summary.tsx delete mode 100644 frontend/src/components/login.css delete mode 100644 frontend/src/components/login.js delete mode 100644 frontend/src/components/main.css delete mode 100644 frontend/src/components/main.js delete mode 100644 frontend/src/components/news.css delete mode 100644 frontend/src/components/news.js delete mode 100644 frontend/src/components/pages/about.css delete mode 100644 frontend/src/components/pages/about.js delete mode 100644 frontend/src/components/pages/game.js delete mode 100644 frontend/src/components/pages/games.css delete mode 100644 frontend/src/components/pages/games.js delete mode 100644 frontend/src/components/pages/home.css delete mode 100644 frontend/src/components/pages/home.js delete mode 100644 frontend/src/components/pages/maplist.css delete mode 100644 frontend/src/components/pages/maplist.js delete mode 100644 frontend/src/components/pages/profile.css delete mode 100644 frontend/src/components/pages/profile.js delete mode 100644 frontend/src/components/pages/summary.css delete mode 100644 frontend/src/components/pages/summary.js delete mode 100644 frontend/src/components/pages/summary_modview.css delete mode 100644 frontend/src/components/pages/summary_modview.js delete mode 100644 frontend/src/components/record.css delete mode 100644 frontend/src/components/record.js delete mode 100644 frontend/src/components/sidebar.css delete mode 100644 frontend/src/components/sidebar.js create mode 100644 frontend/src/css/Games.css create mode 100644 frontend/src/css/Login.css create mode 100644 frontend/src/css/Maps.css create mode 100644 frontend/src/css/ModMenu.css create mode 100644 frontend/src/css/Profile.css create mode 100644 frontend/src/css/Sidebar.css create mode 100644 frontend/src/images/Images.tsx create mode 100644 frontend/src/images/png/1.png create mode 100644 frontend/src/images/png/10.png create mode 100644 frontend/src/images/png/11.png create mode 100644 frontend/src/images/png/12.png create mode 100644 frontend/src/images/png/13.png create mode 100644 frontend/src/images/png/14.png create mode 100644 frontend/src/images/png/15.png create mode 100644 frontend/src/images/png/16.png create mode 100644 frontend/src/images/png/17.png create mode 100644 frontend/src/images/png/18.png create mode 100644 frontend/src/images/png/19.png create mode 100644 frontend/src/images/png/2.png create mode 100644 frontend/src/images/png/3.png create mode 100644 frontend/src/images/png/4.png create mode 100644 frontend/src/images/png/5.png create mode 100644 frontend/src/images/png/6.png create mode 100644 frontend/src/images/png/7.png create mode 100644 frontend/src/images/png/8.png create mode 100644 frontend/src/images/png/9.png create mode 100644 frontend/src/images/png/login.png create mode 100644 frontend/src/images/png/logo.png delete mode 100644 frontend/src/imgs/1.png delete mode 100644 frontend/src/imgs/10.png delete mode 100644 frontend/src/imgs/11.png delete mode 100644 frontend/src/imgs/12.png delete mode 100644 frontend/src/imgs/13.png delete mode 100644 frontend/src/imgs/14.png delete mode 100644 frontend/src/imgs/15.png delete mode 100644 frontend/src/imgs/16.png delete mode 100644 frontend/src/imgs/17.png delete mode 100644 frontend/src/imgs/18.png delete mode 100644 frontend/src/imgs/19.png delete mode 100644 frontend/src/imgs/2.png delete mode 100644 frontend/src/imgs/3.png delete mode 100644 frontend/src/imgs/4.png delete mode 100644 frontend/src/imgs/5.png delete mode 100644 frontend/src/imgs/6.png delete mode 100644 frontend/src/imgs/7.png delete mode 100644 frontend/src/imgs/8.png delete mode 100644 frontend/src/imgs/9.png delete mode 100644 frontend/src/imgs/login.png delete mode 100644 frontend/src/imgs/logo.png delete mode 100644 frontend/src/index.js create mode 100644 frontend/src/index.tsx create mode 100644 frontend/src/pages/Games.tsx create mode 100644 frontend/src/pages/Maps.tsx create mode 100644 frontend/src/pages/Profile.tsx create mode 100644 frontend/src/pages/User.tsx create mode 100644 frontend/src/react-app-env.d.ts create mode 100644 frontend/src/types/Content.tsx create mode 100644 frontend/src/types/Game.tsx create mode 100644 frontend/src/types/Map.tsx create mode 100644 frontend/src/types/Pagination.tsx create mode 100644 frontend/src/types/Profile.tsx create mode 100644 frontend/src/types/Search.tsx create mode 100644 frontend/src/utils/Time.tsx (limited to 'frontend/src') diff --git a/frontend/src/App.css b/frontend/src/App.css index 65e35de..3b732f0 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -1,3 +1,20 @@ +main { + overflow: auto; + overflow-x: hidden; + position: relative; + + width: calc(100% - 380px); + height: 100vh; + left: 350px; + + padding-right: 30px; + + font-size: 40px; + font-family: BarlowSemiCondensed-Regular; + color: #cdcfdf; + +} + body { overflow: hidden; background-color: #141520; diff --git a/frontend/src/App.js b/frontend/src/App.js deleted file mode 100644 index d96fa88..0000000 --- a/frontend/src/App.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import { BrowserRouter, Routes, Route} from "react-router-dom"; - -import Sidebar from "./components/sidebar.js" -import Main from "./components/main.js" -import "./App.css"; - -import Summary from "./components/pages/summary.js" -import Profile from "./components/pages/profile.js" -import About from './components/pages/about.js'; -import Games from "./components/pages/games.js"; -import Maplist from './components/pages/maplist.js'; -import Home from "./components/pages/maplist.js"; -import Homepage from './components/pages/home.js'; - - -export default function App() { - const [token, setToken] = React.useState(null); - const [mod,setMod] = React.useState(false) - React.useEffect(()=>{ - if(token!==null){ - setMod(JSON.parse(atob(token.split(".")[1])).mod) - } - },[token]) - - return ( - <> - - - - }> - }> - }> - }> - }> - }> - }> - }> - }> - }> - }> - }> - }> - }> - - - - ) -} \ No newline at end of file diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..555ce4c --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Routes, Route } from "react-router-dom"; + +import { UserProfile } from './types/Profile'; +import Sidebar from './components/Sidebar'; +import "./App.css"; + +import Profile from './pages/Profile'; +import Games from './pages/Games'; +import Maps from './pages/Maps'; +import User from './pages/User'; + + +const App: React.FC = () => { + const [token, setToken] = React.useState(undefined); + const [profile, setProfile] = React.useState(undefined); + const [isModerator, setIsModerator] = React.useState(true); + + // React.useEffect(() => { + // if (token) { + // setIsModerator(JSON.parse(atob(token.split(".")[1])).mod) + // } + // }, [token]); + + return ( + <> + + + yo} /> + } /> + } /> + } /> + } /> + + + + ); +}; + +export default App; diff --git a/frontend/src/api/Api.tsx b/frontend/src/api/Api.tsx new file mode 100644 index 0000000..9e45bc4 --- /dev/null +++ b/frontend/src/api/Api.tsx @@ -0,0 +1,157 @@ +import axios from 'axios'; + +import { Game } from '../types/Game'; +import { MapDiscussion, MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map'; +import { MapDiscussionCommentContent, MapDiscussionContent, ModMenuContent } from '../types/Content'; +import { Search } from '../types/Search'; +import { UserProfile } from '../types/Profile'; + +// add new api call function entries here +// example usage: API.get_games(); +export const API = { + user_logout: () => user_logout(), + + get_user: (user_id: string) => get_user(user_id), + get_games: () => get_games(), + get_search: (q: string) => get_search(q), + get_map_summary: (map_id: string) => get_map_summary(map_id), + get_map_leaderboard: (map_id: string) => get_map_leaderboard(map_id), + get_map_discussions: (map_id: string) => get_map_discussions(map_id), + get_map_discussion: (map_id: string, discussion_id: number) => get_map_discussion(map_id, discussion_id), + + post_map_summary: (map_id: string, content: ModMenuContent) => post_map_summary(map_id, content), + post_map_discussion: (map_id: string, content: MapDiscussionContent) => post_map_discussion(map_id, content), + post_map_discussion_comment: (map_id: string, discussion_id: number, content: MapDiscussionCommentContent) => post_map_discussion_comment(map_id, discussion_id, content), + + put_map_image: (map_id: string, image: string) => put_map_image(map_id, image), + put_map_summary: (map_id: string, content: ModMenuContent) => put_map_summary(map_id, content), + + delete_map_summary: (map_id: string, route_id: number) => delete_map_summary(map_id, route_id), + delete_map_discussion: (map_id: string, discussion_id: number) => delete_map_discussion(map_id, discussion_id), +}; + +const BASE_API_URL: string = "https://lp.ardapektezol.com/api/v1/" + +function url(path: string): string { + return BASE_API_URL + path; +} + +// USER + +const user_logout = async () => { + await axios.delete(url("token")); +}; + +const get_user = async (user_id: string): Promise => { + const response = await axios.get(url(`users/${user_id}`)) + return response.data.data; +}; + + +// GAMES + +const get_games = async (): Promise => { + const response = await axios.get(url("games")) + return response.data.data; +}; + +// SEARCH + +const get_search = async (q: string): Promise => { + const response = await axios.get(url(`search?q=${q}`)) + return response.data.data; +}; + +// MAP SUMMARY + +const put_map_image = async (map_id: string, image: string): Promise => { + const response = await axios.put(url(`maps/${map_id}/image`), { + "image": image, + }); + return response.data.success; +}; + +const get_map_summary = async (map_id: string): Promise => { + const response = await axios.get(url(`maps/${map_id}/summary`)) + return response.data.data; +}; + +const post_map_summary = async (map_id: string, content: ModMenuContent): Promise => { + const response = await axios.post(url(`maps/${map_id}/summary`), { + "user_name": content.name, + "score_count": content.score, + "record_date": content.date, + "showcase": content.showcase, + "description": content.description, + }); + return response.data.success; +}; + +const put_map_summary = async (map_id: string, content: ModMenuContent): Promise => { + const response = await axios.put(url(`maps/${map_id}/summary`), { + "route_id": content.id, + "user_name": content.name, + "score_count": content.score, + "record_date": content.date, + "showcase": content.showcase, + "description": content.description, + }); + return response.data.success; +}; + +const delete_map_summary = async (map_id: string, route_id: number): Promise => { + const response = await axios.delete(url(`maps/${map_id}/summary`), { + data: { + "route_id": route_id, + } + }); + return response.data.success; +}; + +// MAP LEADERBOARDS + +const get_map_leaderboard = async (map_id: string): Promise => { + const response = await axios.get(url(`maps/${map_id}/leaderboards`)) + if (!response.data.success) { + return undefined; + } + return response.data.data; +}; + +// MAP DISCUSSIONS + +const get_map_discussions = async (map_id: string): Promise => { + const response = await axios.get(url(`maps/${map_id}/discussions`)); + if (!response.data.data.discussions) { + return undefined; + } + return response.data.data; +}; + +const get_map_discussion = async (map_id: string, discussion_id: number): Promise => { + const response = await axios.get(url(`maps/${map_id}/discussions/${discussion_id}`)); + if (!response.data.data.discussion) { + return undefined; + } + return response.data.data; +}; + +const post_map_discussion = async (map_id: string, content: MapDiscussionContent): Promise => { + const response = await axios.post(url(`maps/${map_id}/discussions`), { + "title": content.title, + "content": content.content, + }); + return response.data.success; +}; + +const post_map_discussion_comment = async (map_id: string, discussion_id: number, content: MapDiscussionCommentContent): Promise => { + const response = await axios.post(url(`maps/${map_id}/discussions/${discussion_id}`), { + "comment": content.comment, + }); + return response.data.success; +}; + +const delete_map_discussion = async (map_id: string, discussion_id: number): Promise => { + const response = await axios.delete(url(`maps/${map_id}/discussions/${discussion_id}`)); + return response.data.success; +}; diff --git a/frontend/src/components/Discussions.tsx b/frontend/src/components/Discussions.tsx new file mode 100644 index 0000000..1cd3523 --- /dev/null +++ b/frontend/src/components/Discussions.tsx @@ -0,0 +1,151 @@ +import React from 'react'; + +import { MapDiscussion, MapDiscussions, MapDiscussionsDetail } from '../types/Map'; +import { MapDiscussionCommentContent, MapDiscussionContent } from '../types/Content'; +import { time_ago } from '../utils/Time'; +import { API } from '../api/Api'; +import "../css/Maps.css" + +interface DiscussionsProps { + data?: MapDiscussions; + isModerator: boolean; + mapID: string; + onRefresh: () => void; +} + +const Discussions: React.FC = ({ data, isModerator, mapID, onRefresh }) => { + + const [discussionThread, setDiscussionThread] = React.useState(undefined); + const [discussionSearch, setDiscussionSearch] = React.useState(""); + + const [createDiscussion, setCreateDiscussion] = React.useState(false); + const [createDiscussionContent, setCreateDiscussionContent] = React.useState({ + title: "", + content: "", + }); + const [createDiscussionCommentContent, setCreateDiscussionCommentContent] = React.useState({ + comment: "", + }); + + const _open_map_discussion = async (discussion_id: number) => { + const mapDiscussion = await API.get_map_discussion(mapID, discussion_id); + setDiscussionThread(mapDiscussion); + }; + + const _create_map_discussion = async () => { + await API.post_map_discussion(mapID, createDiscussionContent); + setCreateDiscussion(false); + onRefresh(); + }; + + const _create_map_discussion_comment = async (discussion_id: number) => { + await API.post_map_discussion_comment(mapID, discussion_id, createDiscussionCommentContent); + await _open_map_discussion(discussion_id); + }; + + const _delete_map_discussion = async (discussion: MapDiscussionsDetail) => { + if (window.confirm(`Are you sure you want to remove post: ${discussion.title}?`)) { + await API.delete_map_discussion(mapID, discussion.id); + onRefresh(); + } + }; + + return ( +
+ + + { // janky ternary operators here, could divide them to more components? + createDiscussion ? + ( +
+ Create Post + +
+ setCreateDiscussionContent({ + ...createDiscussionContent, + title: e.target.value, + })} /> + setCreateDiscussionContent({ + ...createDiscussionContent, + title: e.target.value, + })} /> +
+
+ +
+
+ ) + : + discussionThread ? + ( +
+
+ {discussionThread.discussion.title} + +
+ +
+ +
+ {discussionThread.discussion.creator.user_name} + {time_ago(new Date(discussionThread.discussion.created_at.replace("T", " ").replace("Z", "")))} + {discussionThread.discussion.content} +
+ {discussionThread.discussion.comments ? + discussionThread.discussion.comments.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) + .map(e => ( + <> + +
+ {e.user.user_name} + {time_ago(new Date(e.date.replace("T", " ").replace("Z", "")))} + {e.comment} +
+ + )) : "" + } +
+
+ e.key === "Enter" && _create_map_discussion_comment(discussionThread.discussion.id)} onChange={(e) => setCreateDiscussionCommentContent({ + ...createDiscussionContent, + comment: e.target.value, + })} /> +
+
+ +
+ ) + : + ( + data ? + (<> + {data.discussions.filter(f => f.title.includes(discussionSearch)).sort((a, b) => new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()) + .map((e, i) => ( +
+ + : + } + {e.creator.user_name}: {e.content} + Last Updated: {time_ago(new Date(e.updated_at.replace("T", " ").replace("Z", "")))} + +
+ ))} + ) + : + (No Discussions...) + ) + } +
+ ); +}; + +export default Discussions; diff --git a/frontend/src/components/GameEntry.tsx b/frontend/src/components/GameEntry.tsx new file mode 100644 index 0000000..8e58ce9 --- /dev/null +++ b/frontend/src/components/GameEntry.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import { Link } from "react-router-dom"; + +import { Game } from '../types/Game'; +import "../css/Games.css" + +interface GameEntryProps { + game: Game; +} + +const GameEntry: React.FC = ({ game }) => { + + React.useEffect(() => { + game.category_portals.forEach(catInfo => { + const itemBody = document.createElement("div"); + const itemTitle = document.createElement("span"); + const spacing = document.createElement("br"); + const itemNum = document.createElement("span"); + + itemTitle.innerText = catInfo.category.name; + itemNum.innerText = catInfo.portal_count as any as string; + itemTitle.classList.add("games-page-item-body-item-title"); + itemNum.classList.add("games-page-item-body-item-num"); + itemBody.appendChild(itemTitle); + itemBody.appendChild(spacing); + itemBody.appendChild(itemNum); + itemBody.className = "games-page-item-body-item"; + + // itemBody.innerHTML = ` + // ${catInfo.category.name}
+ // ${catInfo.portal_count}` + + document.getElementById(`${game.id}`)!.appendChild(itemBody); + }); + }, []); + + return ( +
+
+
+ {game.name} +
+
+
+
+ ); +}; + +export default GameEntry; diff --git a/frontend/src/components/Leaderboards.tsx b/frontend/src/components/Leaderboards.tsx new file mode 100644 index 0000000..badff37 --- /dev/null +++ b/frontend/src/components/Leaderboards.tsx @@ -0,0 +1,105 @@ +import React from 'react'; + +import { DownloadIcon, ThreedotIcon } from '../images/Images'; +import { MapLeaderboard } from '../types/Map'; +import { ticks_to_time, time_ago } from '../utils/Time'; +import "../css/Maps.css" + +interface LeaderboardsProps { + data?: MapLeaderboard; +} + +const Leaderboards: React.FC = ({ data }) => { + + const [pageNumber, setPageNumber] = React.useState(1); + + if (!data) { + return ( +
+

Map is not available for competitive boards.

+
+ ); + }; + + if (data.records.length === 0) { + return ( +
+

No records found.

+
+ ); + }; + + return ( +
+ +
+ Place + + {data.map.is_coop ? ( +
+ Host + Partner +
+ ) : ( + Runner + )} + + Portals + Time + Date +
+
+ + + {data.pagination.current_page}/{data.pagination.total_pages} + +
+
+
+
+
+ {data.records.map((r, index) => ( + + {r.placement} + + {r.kind === "multiplayer" ? ( +
+   {r.host.user_name} +   {r.partner.user_name} +
+ ) : ( +
  {r.user.user_name}
+ )} + + {r.score_count} + + {ticks_to_time(r.score_time)} + {time_ago(new Date(r.record_date.replace("T", " ").replace("Z", "")))} + + {r.kind === "multiplayer" ? ( + + + + + + ) : ( + + + + + + )} +
+ ))} +
+
+ ); +}; + +export default Leaderboards; diff --git a/frontend/src/components/Login.tsx b/frontend/src/components/Login.tsx new file mode 100644 index 0000000..adfa718 --- /dev/null +++ b/frontend/src/components/Login.tsx @@ -0,0 +1,1931 @@ +import React from 'react'; +import { Link, useNavigate } from 'react-router-dom'; + +import { ExitIcon, UserIcon, LoginIcon } from '../images/Images'; +import { UserProfile } from '../types/Profile'; +import { API } from '../api/Api'; +import "../css/Login.css"; + +interface LoginProps { + token?: string; + setToken: React.Dispatch>; + profile?: UserProfile; + setProfile: React.Dispatch>; +}; + +const Login: React.FC = ({ token, setToken, profile, setProfile }) => { + + const navigate = useNavigate(); + + const _logout = () => { + setProfile(undefined); + setToken(undefined); + API.user_logout(); + navigate("/"); + } + + return ( + <> + {profile + ? + ( + <> + + + + + + ) + : + ( + + + + )} + + ); +}; + +export default Login; diff --git a/frontend/src/components/ModMenu.tsx b/frontend/src/components/ModMenu.tsx new file mode 100644 index 0000000..1fe4239 --- /dev/null +++ b/frontend/src/components/ModMenu.tsx @@ -0,0 +1,324 @@ +import React from 'react'; +import ReactMarkdown from 'react-markdown'; + +import { MapSummary } from '../types/Map'; +import { ModMenuContent } from '../types/Content'; +import { API } from '../api/Api'; +import "../css/ModMenu.css" + +interface ModMenuProps { + data: MapSummary; + selectedRun: number; + mapID: string; +} + +const ModMenu: React.FC = ({ data, selectedRun, mapID }) => { + + const [menu, setMenu] = React.useState(0); + const [showButton, setShowButton] = React.useState(1) + + const [routeContent, setRouteContent] = React.useState({ + id: 0, + name: "", + score: 0, + date: "", + showcase: "", + description: "No description available.", + category_id: 1, + }); + + const [image, setImage] = React.useState(""); + const [md, setMd] = React.useState(""); + + function compressImage(file: File): Promise { + const reader = new FileReader(); + reader.readAsDataURL(file); + return new Promise(resolve => { + reader.onload = () => { + const img = new Image(); + if (typeof reader.result === "string") { + img.src = reader.result; + img.onload = () => { + let { width, height } = img; + if (width > 550) { + height *= 550 / width; + width = 550; + } + if (height > 320) { + width *= 320 / height; + height = 320; + } + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + canvas.getContext('2d')!.drawImage(img, 0, 0, width, height); + resolve(canvas.toDataURL(file.type, 0.6)); + }; + } + }; + }); + }; + + const _edit_map_summary_image = async () => { + if (window.confirm("Are you sure you want to submit this to the database?")) { + await API.put_map_image(mapID, image); + } + }; + + const _edit_map_summary_route = async () => { + if (window.confirm("Are you sure you want to submit this to the database?")) { + await API.put_map_summary(mapID, routeContent); + } + }; + + const _create_map_summary_route = async () => { + if (window.confirm("Are you sure you want to submit this to the database?")) { + await API.post_map_summary(mapID, routeContent); + } + }; + + const _delete_map_summary_route = async () => { + if (window.confirm(`Are you sure you want to delete this run from the database? + ${data.summary.routes[selectedRun].category.name} ${data.summary.routes[selectedRun].history.score_count} portals ${data.summary.routes[selectedRun].history.runner_name}`)) { + await API.delete_map_summary(mapID, data.summary.routes[selectedRun].route_id); + } + }; + + React.useEffect(() => { + if (menu === 3) { // add route + setRouteContent({ + id: 0, + name: "", + score: 0, + date: "", + showcase: "", + description: "No description available.", + category_id: 1, + }); + setMd("No description available."); + } + if (menu === 2) { // edit route + setRouteContent({ + id: data.summary.routes[selectedRun].route_id, + name: data.summary.routes[selectedRun].history.runner_name, + score: data.summary.routes[selectedRun].history.score_count, + date: data.summary.routes[selectedRun].history.date.split("T")[0], + showcase: data.summary.routes[selectedRun].showcase, + description: data.summary.routes[selectedRun].description, + category_id: data.summary.routes[selectedRun].category.id, + }); + setMd(data.summary.routes[selectedRun].description); + } + }, [menu]); + + React.useEffect(() => { + const modview = document.querySelector("div#modview") as HTMLElement + if (modview) { + showButton ? modview.style.transform = "translateY(-68%)" + : modview.style.transform = "translateY(0%)" + } + + const modview_block = document.querySelector("#modview_block") as HTMLElement + if (modview_block) { + showButton === 1 ? modview_block.style.display = "none" : modview_block.style.display = "block"// eslint-disable-next-line + } + }, [showButton]) + + return ( +
+ +
+
+ + + + +
+
+ {showButton ? ( + + ) : ( + + )} +
+
+ +
+ { // Edit Image + menu === 1 && ( +
+
+ Current Image: + missing +
+ +
+ New Image: + { + if (e.target.files) { + compressImage(e.target.files[0]) + .then(d => setImage(d)) + } + } + } /> + {image ? () : } + + +
+
+ ) + } + + { // Edit Route + menu === 2 && ( +
+
+ Route ID: + +
+
+ Runner Name: + { + setRouteContent({ + ...routeContent, + name: e.target.value, + }); + }} /> +
+
+ Score: + { + setRouteContent({ + ...routeContent, + score: parseInt(e.target.value), + }); + }} /> +
+
+ Date: + { + setRouteContent({ + ...routeContent, + date: e.target.value, + }); + }} /> +
+
+ Showcase Video: + { + setRouteContent({ + ...routeContent, + showcase: e.target.value, + }); + }} /> +
+
+ Description: + -
- -
- ):menu===3?( - // add route -
-
- category: - -
-
- runner name: - -
-
- score: - -
-
- date: - -
-
- showcase video: - -
-
- description: - -
- -
- ):("error")} - - {menu!==1?( -
- Markdown preview - documentation - demo -

- {md} - -

-
- ):""} -
):""} - - -) -} - diff --git a/frontend/src/components/record.css b/frontend/src/components/record.css deleted file mode 100644 index 60d47ee..0000000 --- a/frontend/src/components/record.css +++ /dev/null @@ -1,15 +0,0 @@ -.record-container { - --padding: 20px; - width: calc(100% - calc(var(--padding * 2))); - height: 42px; - background-color: #2B2E46; - border-radius: 200px; - font-size: 18px; - display: grid; - grid-template-columns: 20% 25% 15% 15% 25%; - text-align: center; - padding: 0px var(--padding); - vertical-align: middle; - align-items: center; - margin-bottom: 6px; -} \ No newline at end of file diff --git a/frontend/src/components/record.js b/frontend/src/components/record.js deleted file mode 100644 index 80e084d..0000000 --- a/frontend/src/components/record.js +++ /dev/null @@ -1,56 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { useLocation, Link } from "react-router-dom"; - -import "./record.css" - -export default function Record({ name, place, portals, time, date }) { - // const {token} = prop; - const [record, setRecord] = useState(null); - const location = useLocation(); - - // useEffect(() => { - // console.log(name, place, portals, time, date); - // }) - - function timeSince() { - const now = new Date(); - const dateNew = new Date(date); - - const secondsPast = Math.floor((now - dateNew) / 1000); - - if (secondsPast < 60) { - return `${secondsPast} seconds ago`; - } - if (secondsPast < 3600) { - const minutes = Math.floor(secondsPast / 60); - return `${minutes} minutes ago`; - } - if (secondsPast < 86400) { - const hours = Math.floor(secondsPast / 3600); - return `${hours} hours ago`; - } - if (secondsPast < 2592000) { - const days = Math.floor(secondsPast / 86400); - return `${days} days ago`; - } - if (secondsPast < 31536000) { - const months = Math.floor(secondsPast / 2592000); - return `${months} months ago`; - } - const years = Math.floor(secondsPast / 31536000); - return `${years} years ago`; - } - - return( -
- {place} -
- - {name} -
- {portals} - {time} - {timeSince()} -
- ) -} diff --git a/frontend/src/components/sidebar.css b/frontend/src/components/sidebar.css deleted file mode 100644 index 34ede80..0000000 --- a/frontend/src/components/sidebar.css +++ /dev/null @@ -1,208 +0,0 @@ -#sidebar { - overflow: hidden; - position: absolute; - background-color: #2b2e46; - width: 320px; height: 100vh; - min-height: 670px; - -} - - /* logo */ -#logo{ - display: grid; - grid-template-columns: 60px 200px; - - - height: 80px; - padding: 20px 0 20px 30px; - cursor: pointer; - user-select: none; -} - -#logo-text{ - font-family: BarlowCondensed-Regular; - font-size: 42px; - color: #FFF; - line-height: 38px; -} -span>b{ - font-family: BarlowCondensed-Bold; - font-size: 56px; -} - - /* Sidelist */ -#sidebar-list{ - z-index: 2; - background-color: #2b2e46; - position: relative; - height: calc(100vh - 120px); - width: 320px; - /* min-height: 670px; */ - transition: width .3s; -} -#sidebar-toplist>button:nth-child(1){margin-top: 5px;} -#sidebar-toplist{ - display: grid; - - margin: 0 5px 0 5px; - justify-items: left; - height: 400px; - grid-template-rows: 45px 50px 50px 50px 50px 50px 50px 50px auto; -} - -#sidebar-bottomlist{ - display: grid; - - margin: 0 5px 0 5px; - justify-items: left; - grid-template-rows: calc(100vh - 670px) 50px 50px 50px; -} -.sidebar-button>span{ - font-family: BarlowSemiCondensed-Regular; - font-size: 18px; - color: #CDCFDF; - height: 32px; - line-height: 28px; - transition: opacity .1s; -} -.sidebar-button{ - display: grid; - grid-template-columns: 50px auto; - place-items: left; - text-align: left; - - background-color: inherit; - cursor: pointer; - border: none; - width: 310px; - height: 40px; - border-radius: 20px; - padding: 0.4em 0 0 11px; - - transition: - width .3s, - background-color .15s, - padding .3s; -} - -.sidebar-button-selected { - background-color: #202232; -} - -.sidebar-button-deselected { - background-color: #20223200; -} - -.sidebar-button-deselected:hover { - background-color: #202232aa; -} - -button>img { - scale: 1.1; - width: 20px; - padding: 5px; -} - - /* Maplist */ -#sidebar>div:nth-child(3){ - position: relative; - background-color: #202232; - color: #424562; - z-index: 1; - - left: 52px; - top: calc(-100vh + 120px); - width: 268px; height: calc(100vh - 120px); - min-height: 550px; -} -input#searchbar[type=text]{ - margin: 10px 0 0 6px; - padding: 1px 0px 1px 16px; - width: 240px; - height: 30px; - - font-family: BarlowSemiCondensed-Regular; - font-size: 20px; - - background-color: #161723; - color:#CDCFDF; - - border: 0; - border-radius: 20px; - -} -input[type=text]::placeholder{color:#2b2e46} -input[type=text]:focus{outline: inherit;} -a{text-decoration: none;height: 40px;} - - -#search-data{ - margin: 8px 0 8px 0; - overflow-y: auto; - max-height: calc(100vh - 172px); - scrollbar-width: thin; -} -#search-data::-webkit-scrollbar{display: none;} -.search-map{ - margin: 10px 6px 0 6px; - height: 80px; - - border-radius: 20px; - text-align: center; - - display: grid; - - border: 0; - transition: background-color .1s; - background-color: #2b2e46; - grid-template-rows: 20% 20% 60%; - width: calc(100% - 15px); -} -.search-map>span{ - color: #888; - font-size: 16px; - font-family: BarlowSemiCondensed-Regular; -} -.search-map>span:nth-child(3){ - font-size: 30px; - color: #CDCFDF; -} - -.search-player{ - overflow: hidden; - margin: 10px 6px 0 6px; - height: 80px; - - border-radius: 20px; - text-align: center; - color: #CDCFDF; - font-family: BarlowSemiCondensed-Regular; - - display: grid; - place-items: center; - grid-template-columns: 20% 80%; - padding: 0 16px 0 16px; - - border: 0; - transition: background-color .1s; - background-color: #2b2e46; -} -.search-player>img{ - height: 60px; - border-radius: 20px; -} -.search-player>span{ - width:154px; - font-size: 26px; -} - - - - - - - - - - - diff --git a/frontend/src/components/sidebar.js b/frontend/src/components/sidebar.js deleted file mode 100644 index 1ca17e6..0000000 --- a/frontend/src/components/sidebar.js +++ /dev/null @@ -1,203 +0,0 @@ -import React, { useEffect } from 'react'; -import { Link, useLocation } from "react-router-dom"; - -import "../App.css" -import "./sidebar.css"; -import logo from "../imgs/logo.png" -import img1 from "../imgs/1.png" -import img2 from "../imgs/2.png" -import img3 from "../imgs/3.png" -import img4 from "../imgs/4.png" -import img5 from "../imgs/5.png" -import img6 from "../imgs/6.png" -import img7 from "../imgs/7.png" -import img8 from "../imgs/8.png" -import img9 from "../imgs/9.png" -import Login from "./login.js" - -export default function Sidebar(prop) { -const {token,setToken} = prop -const [profile, setProfile] = React.useState(null); - -React.useEffect(() => { - fetch(`https://lp.ardapektezol.com/api/v1/profile`,{ - headers: { - 'Content-Type': 'application/json', - Authorization: token - }}) - .then(r => r.json()) - .then(d => setProfile(d.data)) - }, [token]); - -// Locks search button for 300ms before it can be clicked again, prevents spam -const [isLocked, setIsLocked] = React.useState(false); -function HandleLock(arg) { -if (!isLocked) { - setIsLocked(true); - setTimeout(() => setIsLocked(false), 300); - SidebarHide(arg) - } -} - - -// The menu button -const [sidebar, setSidebar] = React.useState(); - -// Clicked buttons -function SidebarClick(x){ -const btn = document.querySelectorAll("button.sidebar-button"); - -if(sidebar===1){setSidebar(0);SidebarHide()} - -// clusterfuck -btn.forEach((e,i) =>{ - btn[i].classList.remove("sidebar-button-selected") - btn[i].classList.add("sidebar-button-deselected") -}) -btn[x].classList.add("sidebar-button-selected") -btn[x].classList.remove("sidebar-button-deselected") - -} - -function SidebarHide(){ -const btn = document.querySelectorAll("button.sidebar-button") -const span = document.querySelectorAll("button.sidebar-button>span"); -const side = document.querySelector("#sidebar-list"); -const login = document.querySelectorAll(".login>button")[1]; -const searchbar = document.querySelector("#searchbar"); - -if(sidebar===1){ - setSidebar(0) - side.style.width="320px" - btn.forEach((e, i) =>{ - e.style.width="310px" - e.style.padding = "0.4em 0 0 11px" - setTimeout(() => { - span[i].style.opacity="1" - login.style.opacity="1" - - }, 100) - }) - side.style.zIndex="2" -} else { - side.style.width="40px"; - searchbar.focus(); - setSearch(searchbar.value) - setSidebar(1) - btn.forEach((e,i) =>{ - e.style.width="40px" - e.style.padding = "0.4em 0 0 5px" - span[i].style.opacity="0" - }) - login.style.opacity="0" - setTimeout(() => { - side.style.zIndex="0" - }, 300); - } -} -// Links -const location = useLocation() -React.useEffect(()=>{ - if(location.pathname==="/"){SidebarClick(1)} - if(location.pathname.includes("news")){SidebarClick(2)} - if(location.pathname.includes("games")){SidebarClick(3)} - if(location.pathname.includes("leaderboards")){SidebarClick(4)} - if(location.pathname.includes("scorelog")){SidebarClick(5)} - if(location.pathname.includes("profile")){SidebarClick(6)} - if(location.pathname.includes("rules")){SidebarClick(8)} - if(location.pathname.includes("about")){SidebarClick(9)} - - // eslint-disable-next-line react-hooks/exhaustive-deps -}, [location.pathname]) - -const [search,setSearch] = React.useState(null) -const [searchData,setSearchData] = React.useState(null) - -React.useEffect(()=>{ - fetch(`https://lp.ardapektezol.com/api/v1/search?q=${search}`) - .then(r=>r.json()) - .then(d=>setSearchData(d.data)) - -}, [search]) - - -return ( - - ) -} - - diff --git a/frontend/src/css/Games.css b/frontend/src/css/Games.css new file mode 100644 index 0000000..ec57a71 --- /dev/null +++ b/frontend/src/css/Games.css @@ -0,0 +1,99 @@ +.games-page { + position: absolute; + left: 320px; + color: white; + width: calc(100% - 320px); + height: 100%; + font-family: BarlowSemiCondensed-Regular; + color: #ffffff; + overflow-y: scroll; + scrollbar-width: thin; +} + +.games-page-item-content { + position: absolute; + left: 50px; + width: calc(100% - 100px); +} + +.games-page-item-content a { + color: inherit; +} + +.games-page-header { + margin-top: 50px; + margin-left: 50px; +} + +span>b { + font-size: 56px; + font-family: BarlowCondensed-Bold; +} + +.loader-game { + width: 100%; + height: 256px; + border-radius: 24px; + overflow: hidden; + margin: 25px 0px; +} + +.games-page-item { + width: 100%; + height: 256px; + background: #202232; + border-radius: 24px; + overflow: hidden; + margin: 25px 0px; +} + +.games-page-item-header { + width: 100%; + height: 50%; + background-size: cover; + overflow: hidden; +} + +.games-page-item-header-img { + width: 100%; + height: 100%; + backdrop-filter: blur(4px); + filter: blur(4px); + background-size: cover; +} + +.games-page-item-header span>b { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + transform: translateY(-100%); +} + +.games-page-item-body { + display: flex; + justify-content: center; + align-items: center; + height: 50%; +} + +.games-page-item-body-item { + background: #2B2E46; + text-align: center; + width: max-content; + width: calc(100% - 24px); + height: 100px; + border-radius: 24px; + color: #CDCFDF; + margin: 12px; +} + +.games-page-item-body-item-title { + margin-top: 0px; + font-size: 26px; +} + +.games-page-item-body-item-num { + font-size: 50px; + font-family: BarlowCondensed-Bold; +} \ No newline at end of file diff --git a/frontend/src/css/Login.css b/frontend/src/css/Login.css new file mode 100644 index 0000000..aa75f98 --- /dev/null +++ b/frontend/src/css/Login.css @@ -0,0 +1,26 @@ +span>img { + scale: 1.1; + padding: 4px 0 0 8px; +} +.login>button>span{ + max-width: 22ch; + overflow: hidden; +} +.login>button:nth-child(2){ + position: relative; + left: 210px; + width: 50px !important; + + padding-left: 10px; + background-color: #00000000 !important; + /* transition: opacity .1s; */ +} + +.login{ + display: grid; + grid-template-columns: 50px auto 200px ; +} + +button:disabled { + display: none; +} \ No newline at end of file diff --git a/frontend/src/css/Maps.css b/frontend/src/css/Maps.css new file mode 100644 index 0000000..d164d3b --- /dev/null +++ b/frontend/src/css/Maps.css @@ -0,0 +1,726 @@ +#background-image{ + z-index: -1; + position: absolute; + opacity: 10%; + height: 50%; + width: 100% +} +#background-image>img{ + object-fit: cover; + width: 100%; + height: 100%; +} +#background-image::before{ + content: ""; + position: absolute; + width: 100%; + height: 100%; + background: linear-gradient(to top, #161723, #0000); +} + +/* Section 1: map name*/ + +#section1{ + margin: 20px 0 0 0; + cursor: default; +} + +.nav-button{ + height: 40px; + background-color: #2b2e46; + + color: inherit; + font-size: 18px; + font-family: inherit; + border: none; + transition: background-color .1s; +} +/* #section1>div>.nav-button:nth-child(1){border-radius: 0px;}:nth-child(1){border-radius: 20px 0 0 20px;} +#section1>div>.nav-button:nth-child(2){border-radius: 0 20px 20px 0;margin-left: 2px;} */ +.nav-button>span{padding: 0 8px 0 8px;} +.nav-button:hover{background-color: #202232;cursor: pointer;} + +/* Section 2: navbar */ +#section2{ + margin: 40px 0 0 0; + + display: grid; gap: 2px; + grid-template-columns: 1fr 1fr 1fr; +} + +#section2>.nav-button{ + height: 50px; + font-size: 22px; + display: flex; + justify-content: center; + place-items: center; +} +#section2>.nav-button>img{scale: 1.2;} +#section2>.nav-button:nth-child(1){border-radius: 30px 0 0 30px;} +#section2>.nav-button:nth-child(3){border-radius: 0 30px 30px 0;} + + +/* Section 3: category + history */ + +#section3{ + margin: 40px 0 0 0; + + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; +} + +#category{ + display: grid; + height: 350px; + border-radius: 24px; + overflow: hidden; + +} +#category>p{ + margin-bottom: 20px; + text-align: center; + font-size: 50px; + cursor: default; + color: white; +} + +p>span.portal-count{font-weight: bold;font-size: 100px;vertical-align: -15%;} + +#category-image{ + transform: translate(-20%, -15%); + z-index: -1; + overflow: hidden; + width: 125%; + margin: 22px; + filter: blur(4px) contrast(80%) brightness(80%); +} + +#category>span{ + margin-top: 70px; + background-color: #202232; + + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + gap: 2px; +} +#category>span>button{ + font-family: inherit; + font-size: 18px; + border: none; + height: 40px; + color: inherit; + + cursor: pointer; + transition: background-color .1s; +} + + + +#history>div>hr{border: 1px solid #2b2e46;margin: 8px 20px 0px 20px;} +#history{ + min-width: 560px; + background-color: #202232; + border-radius: 24px; + +} + +#records{overflow-y: auto; height: 256px;} +#records::-webkit-scrollbar{display: none;} + +.record-top, .record{ + font-size: 18px; + display: grid; + text-align: center; + grid-template-columns: 1fr 1fr 1fr; +} +.record-top{font-weight: bold;margin: 20px 20px 0 20px;cursor: default;} +.record{ + margin: 10px 20px 10px 20px; + height: 44px; width: calc(100% - 40px); + + color: inherit; + border-radius: 40px; + place-items: center; + + border: 0; + cursor: pointer; + transition: background-color .1s; +} +#history>span{ + border-top: #202232 solid 2px; + display: grid; + grid-template-columns: 1fr 1fr; +} +#history>span>button{ + width: 100%; height: 40px; + font-family: inherit; + font-size: 18px; + border: none; + color: inherit; + + cursor: pointer; + transition: background-color .1s; +} +#history>span>button:nth-child(1){border-radius: 0 0 0 24px;} +#history>span>button:nth-child(2){border-radius: 0 0 24px 0;} + +#graph{ + display: grid; + grid-template-columns: 20px 1fr; + grid-template-rows: 1fr 20px; + height: 293px; + + margin: 10px 10px 5px 10px; + overflow: hidden; +} +#graph>div:nth-child(1){ /* numbers */ + width: 20px; + display: grid; + place-items: center; + /* background-color: blue; */ +} +#graph>div:nth-child(1)>span{ + font-size: 12px; + line-height: 0; +} + +#graph>div:nth-child(2){ /* big graph */ + position: relative; + display: grid; +} +#graph>div:nth-child(2)>tr{ + display: flex; + align-items: center; + grid-template-columns: repeat(auto-fit, minmax(1px, 1fr)); +} +#graph>div:nth-child(2)>tr>td.graph_hor{ + width: 100%; + height: 0; + padding: 0; + + outline: 1px solid red; +} +#graph>div:nth-child(2)>tr>td.graph_ver{ + width: 0; + height: 100%; + padding: 0; + + outline: 1px solid blue; + transform: translateY(50%); + z-index: 0; + overflow: hidden; +} + +#graph>div:nth-child(3){ /* dates */ + padding-right: 20px; + z-index: 1; + height: 16px; + background-color: #202232; + grid-column: 1 /3; + font-size: 12px; + display: grid; + padding-top: 8px; + grid-template-columns: repeat(auto-fit, minmax(1px, 1fr)); +} + +.graph-button{ + position: absolute; + padding: 0; + border: 5px solid white; + border-radius: 20px; + cursor: pointer; + transform: translateX(-50%); +} + +#history>div>h5{text-align: center;height: 197px;} + + +/* Section 4: Difficulty + count */ + +#section4{ + display: grid; + grid-template-columns: 1fr 1fr; + gap: 20px; + margin: 40px 0 0 0; +} + +#difficulty, +#count { + background-color: #202232; + min-width: 250px; + text-align: center; + cursor: default; + + border-radius: 24px; + display: grid; + grid-template-rows: 20px 40px 40px; +} +#difficulty>span:nth-child(1), +#count>span:nth-child(1){ + padding-top:10px; + font-size: 18px; + color:#cdcfdf +} +#difficulty>span:nth-child(2){ + font-size: 40px; +} +#difficulty>div{ + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr 1fr; + padding: 0 calc(50% - 125px) 0 calc(50% - 125px); + place-items: center; +} + +.difficulty-rating{ + border-radius: 24px; + width: 40px; height: 3px; + background-color: #2b2e46; +} + +#count>div{ + padding-top:10px; + font-size: 50px; + color:white +} + +/* Section 5: route desc + video */ +#section5{ + margin: 40px 0 20px 0; + width: 100%; +} + +#description{ + width: 100%; height: auto; + min-height: 342px; +} + + + + +#description>iframe{ + margin: 4px; + float:right; + border: 0; + border-radius: 24px; + width: 608px; height: 342px; +} + +#description>h3{margin: 0 0 10px 0; color: white;} +#description-text{ + display: block; + font-size: 21px; + word-wrap: break-word; +} +#description-text>b{font-size: inherit;} +#description-text>a{font-size: inherit;color: #3c91e6;} + + +/* Section 6: leaderboards */ +#section6{ + margin: 40px 0 20px 0; + min-height: 600px; + background-color: #202232; + + border-radius: 24px; + padding: 10px 10px 0 10px; + +} + + +#section6>hr{border: 1px solid #2b2e46;margin: 8px 20px 0px 20px;} +#leaderboard-top{ + display: grid; + font-size: 20px; + height: 34px; + padding-left: 60px; + margin: 0 20px 0 20px; +} +#leaderboard-top>span{ + + display: flex; + place-items: flex-end; +} + +#runner{ + display: grid; + grid-template-columns: 50% 50%; + align-items: end; +} + +#page-number{ + display: flex; + width: auto; + flex-direction: row-reverse; +} +#page-number>div{ +width: 100px; +place-items: center; +display: grid; +grid-template-columns: 1fr 1fr 1fr; +text-align: center; +} +#page-number>div>button{ + width: 30px; + height: 30px; + background-color: #202232; + border: 0; + padding: 0; + cursor: pointer; +} + +.leaderboard-record{ + margin: 10px 20px 0px 20px; + height: 44px; width: calc(100% - 40px); + width: auto; + + color: inherit; + border-radius: 40px; + text-align: left; + padding: 0 0 0 60px; + font-size: 20px; + font-family: inherit; + + grid-template-columns: 3% 4.5% 40% 4% 3.5% 15% 15% 15%; + display: grid; + + border: 0; + transition: background-color .1s; + background-color: #2b2e46; +} + +.leaderboard-record>span:nth-child(1){display: grid;} +.leaderboard-record>span:nth-child(4){display: grid;} +.leaderboard-record>span:last-child{flex-direction: row-reverse;} +.leaderboard-record>span{ + display: flex; + place-items: center; + height: 44px; +} + +.leaderboard-record>div>span>img{ + height: 36px; + border-radius: 50px; + padding: 0; + scale: .95; +} +.leaderboard-record>div{ + display: grid; + grid-template-columns: 50% 50%; + place-items: left; +} +.leaderboard-record>div>span{ + display: flex; + place-items: center; + height: 44px; +} + +.leaderboard-record>span>button{ + background-color: #0000; + border: 0; + cursor: pointer; + transition: opacity 0.1s; +} + +.hover-popup { + position: relative; + display: inline-block; + } + + .hover-popup::after { + content: attr(popup-text); + position: absolute; + /* top: 0%; */ + /* left: 80%; */ + /* transform: translateX(-100%); */ + /* padding: 5px; */ + background-color: #2b2e46; + /* border: 1px solid #161723; */ + border-radius: 8px; + visibility: hidden; + opacity: 0; + color: #cdcfdf; + /* transition: visibility 0s, opacity 0.3s ease; */ + } + + .hover-popup:hover { + color: transparent; + } + + .hover-popup:hover::after { + visibility: visible; + opacity: 1; + } + +.leaderboard-record:last-child{margin: 10px 20px 10px 20px;} + + +#section7{ + margin: 40px 0 20px 0; + background-color: #202232; + border-radius: 24px; + padding: 10px; +} + +#discussion-search{ + height: 46px; + width: 100%; + display: grid; + grid-template-columns: 1fr 100px; + margin: 0 0 20px 0; +} +#discussion-search>input::placeholder{color: #aaa;} +#discussion-search>input{ + background-color: #2b2e46; + font-size: 20px; + padding-left: 10px; + color: white; + border: 0; + border-radius: 16px 0 0 16px; + font-family: inherit; +} +#discussion-search>div>button:hover{filter: brightness(75%);} +#discussion-search>div>button{ + padding: 7px 16px; + margin: 8px 0; + border: 0; + font-size: 16px; + border-radius: 24px; + display: block; + background-color:#3c91e6; + font-family: inherit; + font-weight: bold; + cursor: pointer; + color: white; + + transition: filter .2s; +} +#discussion-search>div{ + background-color: #2b2e46; + border-radius: 0 16px 16px 0; +} +#discussion-post>button:nth-child(1)>span>b{font-size: 18px;color:#cdcfdf;font-weight: lighter;} +#discussion-post>button:nth-child(1){ + background-color: #2b2e46; + display: grid; + grid-template-columns: minmax(0, 1fr) 150px; + + border-radius: 16px; + padding: 16px 12px; + margin: 8px 0 0 0; + border: 0; + width: 100%; height: 100px; + text-align: start; + white-space: nowrap; + color: #cdcfdf; + cursor: pointer; + overflow: hidden; +} +#discussion-post>button:nth-child(1)>span:nth-child(1){font-size: 32px;} +#discussion-post>button:nth-child(1)>span:nth-child(3){color: #aaa; font-size: 18px;} +#discussion-post>button:nth-child(1)>span:nth-child(4){ + opacity: .7; + height: 40px; + display: flex; + place-items: end; + justify-content: end; +} + +#discussion-post{height: 100px;} +#discussion-post>button>button:hover{filter: brightness(75%); } +#discussion-post>button>button{ + padding: 7px 16px; + + border: 0; + font-size: 16px; + border-radius: 24px; + background-color:#e52d04; + font-family: BarlowSemiCondensed-Regular; + font-weight: bold; + cursor: pointer; + color: white; + + transition: filter .2s; +} + + +#discussion-create>div{ + display: grid; + text-align: start; +} +#discussion-create{ + display: grid; + grid-template-columns: 1fr 40px; + height: auto; + word-wrap: break-word; +} + +#discussion-create>span{padding-left: 20px;} +#discussion-create>div>input::placeholder{color: #aaa;} +#discussion-create>div>input{ + background-color: #2b2e46; + font-size: 20px; + padding-left: 10px; + margin-top: 10px; + height: 32px; + color: white; + border: 0; + font-family: inherit; +} +#discussion-create>div>input:nth-child(2){font-size: 16px;} + +#discussion-create-button:hover{filter: brightness(75%);} +#discussion-create-button{ + padding: 7px 16px; + margin: 8px 0 0 0; + border: 0; + font-size: 16px; + border-radius: 24px; + + background-color:#3c91e6; + font-family: inherit; + font-weight: bold; + cursor: pointer; + color: white; + width: min-content; + grid-column: 1 / span 2; + + + transition: filter .2s; +} + + +#discussion-thread>div:nth-child(1){ + display: grid; + grid-template-columns: 1fr 40px; + height: auto; + padding: 0 0 10px 20px; + word-wrap: break-word; +} + +#discussion-create>button:nth-child(2), +#discussion-thread>div>button{ + height: 40px; + float:inline-end; + color:#cdcfdf; + background-color: #0000; + border: 0; + font-size: 38px; + cursor: pointer; +} + + +#discussion-thread>div:nth-child(2)>img{ + width: 60px; height: 60px; + border-radius: 100px; + margin: 20px 0 0 0; +} +#discussion-thread>div:nth-child(2)>div{ + height: max-content; + padding: 20px 0 0 10px; + display: inline-grid; + grid-template-columns: min-content 1fr ; + overflow: hidden; + +} +#discussion-thread>div:nth-child(2)>div>span:nth-child(1){font-weight: bold;height: 30px;} +#discussion-thread>div:nth-child(2)>div>span:nth-child(2){ + opacity: 0.6; + height: 30px; + font-size: 80%; + padding-left: 10px; +} +#discussion-thread>div:nth-child(2)>div>span:nth-child(3){ + grid-column: 1 / span 2; + height: max-content; + word-wrap: break-word; +} +#discussion-thread>div:nth-child(2){ + display: grid; + grid-template-columns: 60px 1fr; + font-size: 20px; + max-height: 522px; + overflow-y: auto; +} + + +#discussion-send{ + height: 48px; + width: 100%; + display: grid; + grid-template-columns: 1fr 80px; + margin: 10px 0 0 0; +} +#discussion-send>input::placeholder{color: #aaa;} +#discussion-send>input{ + background-color: #2b2e46; + padding-left: 10px; + color: white; + border: 0; + font-size: 20px; + border-radius: 16px 0 0 16px; + font-family: inherit; +} +#discussion-send>div{ + background-color: #2b2e46; + border-radius: 0 16px 16px 0; + +} +#discussion-send>div>button:hover{ filter: brightness(75%);} +#discussion-send>div>button{ + padding: 7px 20px; + margin: 8px 0; + font-size: 16px; + border: 0; + border-radius: 24px; + display: block; + background-color:#3c91e6; + font-family: inherit; + font-weight: bold; + cursor: pointer; + color: white; + + transition: filter .2s; +} + + + +.triangle{ + display: inline-block; + width: 8px; height: 0; + border-top: 7px solid transparent; + border-right: 8px solid #cdcfdf; + border-bottom: 7px solid transparent; +} + + /* such responsive, very mobile */ +@media screen and (max-width: 1480px) { + #section3.summary1{grid-template-columns: auto;} + #category{min-width: 608px;} + #history{min-width: 608px;} + #description{min-width: 608px;} + #section4.summary1{min-width: 588px;} + + #description>iframe{ + padding: 0 0 0 calc(50% - 304px); + float:none; + justify-content: center; + align-items: center; + } + + #section1.summary1{ + grid-template-columns: auto; + place-items: center; + text-align: center; + + } + + #section2.summary1{ + grid-template-columns: auto; + width: 450px; + margin: 40px auto 0 auto; + } + #section2.summary1>.nav-button:nth-child(1){border-radius: 30px 30px 0 0;} + #section2.summary1>.nav-button:nth-child(2){border-radius: 0;} + #section2.summary1>.nav-button:nth-child(3){border-radius: 0 0 30px 30px;} +} \ No newline at end of file diff --git a/frontend/src/css/ModMenu.css b/frontend/src/css/ModMenu.css new file mode 100644 index 0000000..c6d3d8d --- /dev/null +++ b/frontend/src/css/ModMenu.css @@ -0,0 +1,112 @@ +div#modview{ + position: absolute; + left: 50%; + z-index: 20; + width: 320px; height: auto; + /* background-color: red; */ + + transform: translateY(-68%); +} +div#modview>div>button{ + height: 30px; +} + +div#modview>div:nth-child(1){ + display: grid; + grid-template-columns: 50% 50%; +} + +div#modview>div:nth-child(2){ + display: grid; + place-items: center; +} + +#modview-menu{ + position: absolute; + left: calc(50% + 160px); top: 130px; + transform: translateX(-50%); + background-color: #2b2e46; + z-index: 2; color: white; +} + +#modview-menu-image{ + box-shadow: 0 0 40px 16px black; + outline: 8px solid #2b2e46; + border-radius: 20px; + font-size: 40px; + display: grid; + grid-template-columns: 50% 50%; + /* place-items: center; */ + +} +#modview-menu-image>div:nth-child(1){ + height: 400px; width: 500px; + display: grid; + grid-template-rows: 30% 70%; +} +#modview-menu-image>div:nth-child(2){ + height: 400px; width: 500px; + display: grid; + grid-template-rows: 20% 10%; +} + +#modview-menu-image>div>button{width: 300px;margin-left:100px;} +#modview-menu-image>div>img{width: 500px;} +#modview-menu-image>div>button{font-size: 20px;} +#modview-menu-image>div>span>input[type="file"]{font-size: 15px;} + + +#modview-menu-add, +#modview-menu-edit{ + box-shadow: 0 0 40px 16px black; + outline: 8px solid #2b2e46; + border-radius: 20px; + font-size: 40px; + display: grid; + grid-template-columns: 20% 20% 20% 20% 20%; +} + +#modview-menu-add>div, +#modview-menu-edit>div{ + display: grid; + margin: 20px; + width: 200px; + font-size: 20px; +} +#modview-route-description>textarea{ + resize: none; + height: 160px; + width: 1160px; +} +#modview-route-showcase>input::placeholder{opacity: .5;} +#modview_block{ + position: absolute; + background-color: black; + opacity: .3; + left: 320px; + width: calc(100% - 320px); + height: 100%; + z-index: 2; + cursor: no-drop; +} +#modview-md{ + box-shadow: 0 0 40px 16px black; + background-color: #2b2e46; + outline: 8px solid #2b2e46; + + border-radius: 20px; + position: absolute; + padding: 10px; top: 400px; + width: 1180px; height: 300px; + overflow-y: auto; + word-wrap: break-word; +} +#modview-md>span>a{ + padding-left: 20px; + color:aqua; +} +#modview-md>p{ + font-family: BarlowSemiCondensed-Regular; + color: #cdcfdf; + font-size: 21px; +} \ No newline at end of file diff --git a/frontend/src/css/Profile.css b/frontend/src/css/Profile.css new file mode 100644 index 0000000..4944ade --- /dev/null +++ b/frontend/src/css/Profile.css @@ -0,0 +1,239 @@ +#section1.profile{ + margin: 20px; + background: linear-gradient(0deg, #202232 50%, #2b2e46 50%); + border-radius: 24px; + height: 200px; + + display: grid; + grid-template-columns: 250px 1fr; + +} +#section1.profile>div:first-child{ + overflow: hidden; + border-radius: 100%; + display: grid; + + place-items: center; + + margin: 8px 33px 8px 33px; + scale: 0.9; + grid-row: 1 / 3; + + +} +#profile-image>img{ + border-radius: 100%; + transition: filter 0.3s; + cursor: pointer; +} + +#profile-image>span{ + z-index: 1; + position: absolute; + opacity: 0; + color:white; + transition: opacity 0.3s; + cursor: pointer; +} + +#profile-image:hover > img{filter: blur(5px) brightness(60%);z-index: 1;} +#profile-image:hover > span{opacity: 1;} + +#profile-top{ + height: 100px; + display: grid; + grid-template-columns: 80% 20%; +} +#profile-top>div:nth-child(1)>div>img{ + margin: 12px; + border-radius: 10px; +} + +#profile-top>div:nth-child(1){ + display: flex; + place-items: center; + font-size: 50px; + font-weight: bold; + color: white; +} +#profile-top>div:nth-child(1)>div{ + display: flex; + height: 60px; +} +span.titles{ + margin: 12px 12px 12px 0; + + font-size: 18px; + font-weight: 100; + + padding: 6px 20px 0px 20px; + border-radius: 10px; +} + +#profile-top>div:nth-child(2){ + display: flex; + flex-direction: row-reverse; + align-items: center; + padding-right: 10px; +} +#profile-top>div:nth-child(2)>a>img{ + height: 50px; + padding: 0 5px 0 5px; + scale: 0.9; + filter: brightness(200%); + +} + + +#profile-bottom{ + height: 100px; + display: grid; + grid-template-columns: 1fr 1fr 1fr; +} +#profile-bottom>div{ + margin: 12px; + background-color: #2b2e46; + border-radius: 20px; + display: grid; + place-items: center; + grid-template-rows: 40% 50%; +} +#profile-bottom>div>span:nth-child(1){ + color: inherit; + font-size: 18px; +} +#profile-bottom>div>span:nth-child(2){ + color: white; + font-size: 40px; +} +#profile-bottom>div>span:nth-child(2)>span{ + color: white; + font-size: 20px; +} +/* #section1.profile>div>div{outline: red 1px dashed;} */ + + +#section2.profile{ + margin: 20px; + height: 60px; + display: grid; + grid-template-columns: 1fr 1fr; +} +#section2.profile>button{ + display: flex; + justify-content: center; + align-items: center; + + background-color: #2b2e46; + border: 0; + color: inherit; + font-family: inherit; + font-size: 24px; + cursor: pointer; + + transition: background-color .1s; +} +#section2.profile>button:nth-child(1){border-radius: 24px 0 0 24px;} +#section2.profile>button:nth-child(2){border-radius: 0 24px 24px 0;} + +#section3.profile1>hr{border: 1px solid #2b2e46;margin: 8px 20px 0px 20px;} +#section3.profile1{ + margin: 20px; + display: block; + + background-color: #202232; + border-radius: 24px; +} + +#profileboard-nav{ + display: grid; + grid-template-columns: 1fr 1fr; +} + +#profileboard-nav>select{ + + /* appearance: none; */ + margin: 10px 20px 20px 20px; + height: 50px; + + border-radius: 24px; + text-align: center; + + color: inherit; + font-family: inherit; + font-size: 24px; + border: 0; + + background-color: #2b2e46; +} + + +#profileboard-top>span>img{height: 20px;scale: .8;} +#profileboard-top>span>img,#profileboard-top>span>span{cursor: pointer;} +#profileboard-top{ + height: 34px; + display: grid; + font-size: 20px; + padding-left: 40px; + margin: 0 20px; + grid-template-columns: 15% 15% 5% 15% 5% 15% 15% 15%; +} + +#profileboard-top>span{ + display: flex; + place-items: flex-end; +} + +#profileboard-records{ + padding-bottom: 10px; +} + +.profileboard-record{ + width: calc(100% - 40px); + margin: 10px 20px 0px 20px; + height: 44px; + + border-radius: 20px; + padding: 0 0 0 40px; + font-size: 20px; + + color: inherit; + font-family: inherit; + border: 0; + transition: background-color .1s; + background-color: #2b2e46; + display: grid; + grid-template-columns: 15% 15% 5% 15% 5% 15% 15% 15%; + overflow: hidden; + white-space: nowrap; + + transition: height .2s +} + +/* this right here should be illegal */ +.profileboard-record>span:nth-child(-n+8){filter: brightness(100%);} +.profileboard-record>span{ + display: flex; + place-items: flex-end; + filter: brightness(65%); +} + +.profileboard-record>hr{ + margin: 0 0 0 -60px; + border: 0; + height: 2px; + background-color: #202232; +} + +.profileboard-record>span:nth-child(4){display: grid;} +.profileboard-record>span{ + + display: flex; + place-items: center; + height: 44px; +} +.profileboard-record>span>button{ + background-color: #0000; + border: 0; + cursor: pointer; +} diff --git a/frontend/src/css/Sidebar.css b/frontend/src/css/Sidebar.css new file mode 100644 index 0000000..34ede80 --- /dev/null +++ b/frontend/src/css/Sidebar.css @@ -0,0 +1,208 @@ +#sidebar { + overflow: hidden; + position: absolute; + background-color: #2b2e46; + width: 320px; height: 100vh; + min-height: 670px; + +} + + /* logo */ +#logo{ + display: grid; + grid-template-columns: 60px 200px; + + + height: 80px; + padding: 20px 0 20px 30px; + cursor: pointer; + user-select: none; +} + +#logo-text{ + font-family: BarlowCondensed-Regular; + font-size: 42px; + color: #FFF; + line-height: 38px; +} +span>b{ + font-family: BarlowCondensed-Bold; + font-size: 56px; +} + + /* Sidelist */ +#sidebar-list{ + z-index: 2; + background-color: #2b2e46; + position: relative; + height: calc(100vh - 120px); + width: 320px; + /* min-height: 670px; */ + transition: width .3s; +} +#sidebar-toplist>button:nth-child(1){margin-top: 5px;} +#sidebar-toplist{ + display: grid; + + margin: 0 5px 0 5px; + justify-items: left; + height: 400px; + grid-template-rows: 45px 50px 50px 50px 50px 50px 50px 50px auto; +} + +#sidebar-bottomlist{ + display: grid; + + margin: 0 5px 0 5px; + justify-items: left; + grid-template-rows: calc(100vh - 670px) 50px 50px 50px; +} +.sidebar-button>span{ + font-family: BarlowSemiCondensed-Regular; + font-size: 18px; + color: #CDCFDF; + height: 32px; + line-height: 28px; + transition: opacity .1s; +} +.sidebar-button{ + display: grid; + grid-template-columns: 50px auto; + place-items: left; + text-align: left; + + background-color: inherit; + cursor: pointer; + border: none; + width: 310px; + height: 40px; + border-radius: 20px; + padding: 0.4em 0 0 11px; + + transition: + width .3s, + background-color .15s, + padding .3s; +} + +.sidebar-button-selected { + background-color: #202232; +} + +.sidebar-button-deselected { + background-color: #20223200; +} + +.sidebar-button-deselected:hover { + background-color: #202232aa; +} + +button>img { + scale: 1.1; + width: 20px; + padding: 5px; +} + + /* Maplist */ +#sidebar>div:nth-child(3){ + position: relative; + background-color: #202232; + color: #424562; + z-index: 1; + + left: 52px; + top: calc(-100vh + 120px); + width: 268px; height: calc(100vh - 120px); + min-height: 550px; +} +input#searchbar[type=text]{ + margin: 10px 0 0 6px; + padding: 1px 0px 1px 16px; + width: 240px; + height: 30px; + + font-family: BarlowSemiCondensed-Regular; + font-size: 20px; + + background-color: #161723; + color:#CDCFDF; + + border: 0; + border-radius: 20px; + +} +input[type=text]::placeholder{color:#2b2e46} +input[type=text]:focus{outline: inherit;} +a{text-decoration: none;height: 40px;} + + +#search-data{ + margin: 8px 0 8px 0; + overflow-y: auto; + max-height: calc(100vh - 172px); + scrollbar-width: thin; +} +#search-data::-webkit-scrollbar{display: none;} +.search-map{ + margin: 10px 6px 0 6px; + height: 80px; + + border-radius: 20px; + text-align: center; + + display: grid; + + border: 0; + transition: background-color .1s; + background-color: #2b2e46; + grid-template-rows: 20% 20% 60%; + width: calc(100% - 15px); +} +.search-map>span{ + color: #888; + font-size: 16px; + font-family: BarlowSemiCondensed-Regular; +} +.search-map>span:nth-child(3){ + font-size: 30px; + color: #CDCFDF; +} + +.search-player{ + overflow: hidden; + margin: 10px 6px 0 6px; + height: 80px; + + border-radius: 20px; + text-align: center; + color: #CDCFDF; + font-family: BarlowSemiCondensed-Regular; + + display: grid; + place-items: center; + grid-template-columns: 20% 80%; + padding: 0 16px 0 16px; + + border: 0; + transition: background-color .1s; + background-color: #2b2e46; +} +.search-player>img{ + height: 60px; + border-radius: 20px; +} +.search-player>span{ + width:154px; + font-size: 26px; +} + + + + + + + + + + + diff --git a/frontend/src/images/Images.tsx b/frontend/src/images/Images.tsx new file mode 100644 index 0000000..d2f6dfb --- /dev/null +++ b/frontend/src/images/Images.tsx @@ -0,0 +1,44 @@ +import logo from "./png/logo.png" +import login from "./png/login.png" +import img1 from './png/1.png'; +import img2 from './png/2.png'; +import img3 from './png/3.png'; +import img4 from './png/4.png'; +import img5 from './png/5.png'; +import img6 from './png/6.png'; +import img7 from './png/7.png'; +import img8 from './png/8.png'; +import img9 from './png/9.png'; +import img10 from './png/10.png'; +import img11 from './png/11.png'; +import img12 from './png/12.png'; +import img13 from './png/13.png'; +import img14 from './png/14.png'; +import img15 from './png/15.png'; +import img16 from './png/16.png'; +import img17 from './png/17.png'; +import img18 from './png/18.png'; +import img19 from './png/19.png'; + +export const LogoIcon = logo; +export const LoginIcon = login; + +export const SearchIcon = img1; +export const HomeIcon = img2; +export const NewsIcon = img3; +export const PortalIcon = img4; +export const FlagIcon = img5; +export const ChatIcon = img6; +export const TableIcon = img7; +export const BookIcon = img8; +export const HelpIcon = img9; +export const UserIcon = img10; +export const ExitIcon = img11; +export const DownloadIcon = img12; +export const ThreedotIcon = img13; +export const StatisticsIcon = img14; +export const TwitchIcon = img15; +export const YouTubeIcon = img16; +export const SteamIcon = img17; +export const HistoryIcon = img18; +export const SortIcon = img19; \ No newline at end of file diff --git a/frontend/src/images/png/1.png b/frontend/src/images/png/1.png new file mode 100644 index 0000000..ea59d2f Binary files /dev/null and b/frontend/src/images/png/1.png differ diff --git a/frontend/src/images/png/10.png b/frontend/src/images/png/10.png new file mode 100644 index 0000000..d4b0863 Binary files /dev/null and b/frontend/src/images/png/10.png differ diff --git a/frontend/src/images/png/11.png b/frontend/src/images/png/11.png new file mode 100644 index 0000000..b493059 Binary files /dev/null and b/frontend/src/images/png/11.png differ diff --git a/frontend/src/images/png/12.png b/frontend/src/images/png/12.png new file mode 100644 index 0000000..abb7717 Binary files /dev/null and b/frontend/src/images/png/12.png differ diff --git a/frontend/src/images/png/13.png b/frontend/src/images/png/13.png new file mode 100644 index 0000000..28a67c5 Binary files /dev/null and b/frontend/src/images/png/13.png differ diff --git a/frontend/src/images/png/14.png b/frontend/src/images/png/14.png new file mode 100644 index 0000000..7be6359 Binary files /dev/null and b/frontend/src/images/png/14.png differ diff --git a/frontend/src/images/png/15.png b/frontend/src/images/png/15.png new file mode 100644 index 0000000..e5ae8aa Binary files /dev/null and b/frontend/src/images/png/15.png differ diff --git a/frontend/src/images/png/16.png b/frontend/src/images/png/16.png new file mode 100644 index 0000000..bf3ae0c Binary files /dev/null and b/frontend/src/images/png/16.png differ diff --git a/frontend/src/images/png/17.png b/frontend/src/images/png/17.png new file mode 100644 index 0000000..85e39f0 Binary files /dev/null and b/frontend/src/images/png/17.png differ diff --git a/frontend/src/images/png/18.png b/frontend/src/images/png/18.png new file mode 100644 index 0000000..048cda9 Binary files /dev/null and b/frontend/src/images/png/18.png differ diff --git a/frontend/src/images/png/19.png b/frontend/src/images/png/19.png new file mode 100644 index 0000000..0d97d16 Binary files /dev/null and b/frontend/src/images/png/19.png differ diff --git a/frontend/src/images/png/2.png b/frontend/src/images/png/2.png new file mode 100644 index 0000000..b8d108e Binary files /dev/null and b/frontend/src/images/png/2.png differ diff --git a/frontend/src/images/png/3.png b/frontend/src/images/png/3.png new file mode 100644 index 0000000..cfda6a4 Binary files /dev/null and b/frontend/src/images/png/3.png differ diff --git a/frontend/src/images/png/4.png b/frontend/src/images/png/4.png new file mode 100644 index 0000000..bbc01c4 Binary files /dev/null and b/frontend/src/images/png/4.png differ diff --git a/frontend/src/images/png/5.png b/frontend/src/images/png/5.png new file mode 100644 index 0000000..b63d2c3 Binary files /dev/null and b/frontend/src/images/png/5.png differ diff --git a/frontend/src/images/png/6.png b/frontend/src/images/png/6.png new file mode 100644 index 0000000..6ced542 Binary files /dev/null and b/frontend/src/images/png/6.png differ diff --git a/frontend/src/images/png/7.png b/frontend/src/images/png/7.png new file mode 100644 index 0000000..c20bcf4 Binary files /dev/null and b/frontend/src/images/png/7.png differ diff --git a/frontend/src/images/png/8.png b/frontend/src/images/png/8.png new file mode 100644 index 0000000..d640522 Binary files /dev/null and b/frontend/src/images/png/8.png differ diff --git a/frontend/src/images/png/9.png b/frontend/src/images/png/9.png new file mode 100644 index 0000000..3cd602a Binary files /dev/null and b/frontend/src/images/png/9.png differ diff --git a/frontend/src/images/png/login.png b/frontend/src/images/png/login.png new file mode 100644 index 0000000..6456c21 Binary files /dev/null and b/frontend/src/images/png/login.png differ diff --git a/frontend/src/images/png/logo.png b/frontend/src/images/png/logo.png new file mode 100644 index 0000000..774d55a Binary files /dev/null and b/frontend/src/images/png/logo.png differ diff --git a/frontend/src/imgs/1.png b/frontend/src/imgs/1.png deleted file mode 100644 index ea59d2f..0000000 Binary files a/frontend/src/imgs/1.png and /dev/null differ diff --git a/frontend/src/imgs/10.png b/frontend/src/imgs/10.png deleted file mode 100644 index d4b0863..0000000 Binary files a/frontend/src/imgs/10.png and /dev/null differ diff --git a/frontend/src/imgs/11.png b/frontend/src/imgs/11.png deleted file mode 100644 index b493059..0000000 Binary files a/frontend/src/imgs/11.png and /dev/null differ diff --git a/frontend/src/imgs/12.png b/frontend/src/imgs/12.png deleted file mode 100644 index abb7717..0000000 Binary files a/frontend/src/imgs/12.png and /dev/null differ diff --git a/frontend/src/imgs/13.png b/frontend/src/imgs/13.png deleted file mode 100644 index 28a67c5..0000000 Binary files a/frontend/src/imgs/13.png and /dev/null differ diff --git a/frontend/src/imgs/14.png b/frontend/src/imgs/14.png deleted file mode 100644 index 7be6359..0000000 Binary files a/frontend/src/imgs/14.png and /dev/null differ diff --git a/frontend/src/imgs/15.png b/frontend/src/imgs/15.png deleted file mode 100644 index e5ae8aa..0000000 Binary files a/frontend/src/imgs/15.png and /dev/null differ diff --git a/frontend/src/imgs/16.png b/frontend/src/imgs/16.png deleted file mode 100644 index bf3ae0c..0000000 Binary files a/frontend/src/imgs/16.png and /dev/null differ diff --git a/frontend/src/imgs/17.png b/frontend/src/imgs/17.png deleted file mode 100644 index 85e39f0..0000000 Binary files a/frontend/src/imgs/17.png and /dev/null differ diff --git a/frontend/src/imgs/18.png b/frontend/src/imgs/18.png deleted file mode 100644 index 048cda9..0000000 Binary files a/frontend/src/imgs/18.png and /dev/null differ diff --git a/frontend/src/imgs/19.png b/frontend/src/imgs/19.png deleted file mode 100644 index 0d97d16..0000000 Binary files a/frontend/src/imgs/19.png and /dev/null differ diff --git a/frontend/src/imgs/2.png b/frontend/src/imgs/2.png deleted file mode 100644 index b8d108e..0000000 Binary files a/frontend/src/imgs/2.png and /dev/null differ diff --git a/frontend/src/imgs/3.png b/frontend/src/imgs/3.png deleted file mode 100644 index cfda6a4..0000000 Binary files a/frontend/src/imgs/3.png and /dev/null differ diff --git a/frontend/src/imgs/4.png b/frontend/src/imgs/4.png deleted file mode 100644 index bbc01c4..0000000 Binary files a/frontend/src/imgs/4.png and /dev/null differ diff --git a/frontend/src/imgs/5.png b/frontend/src/imgs/5.png deleted file mode 100644 index b63d2c3..0000000 Binary files a/frontend/src/imgs/5.png and /dev/null differ diff --git a/frontend/src/imgs/6.png b/frontend/src/imgs/6.png deleted file mode 100644 index 6ced542..0000000 Binary files a/frontend/src/imgs/6.png and /dev/null differ diff --git a/frontend/src/imgs/7.png b/frontend/src/imgs/7.png deleted file mode 100644 index c20bcf4..0000000 Binary files a/frontend/src/imgs/7.png and /dev/null differ diff --git a/frontend/src/imgs/8.png b/frontend/src/imgs/8.png deleted file mode 100644 index d640522..0000000 Binary files a/frontend/src/imgs/8.png and /dev/null differ diff --git a/frontend/src/imgs/9.png b/frontend/src/imgs/9.png deleted file mode 100644 index 3cd602a..0000000 Binary files a/frontend/src/imgs/9.png and /dev/null differ diff --git a/frontend/src/imgs/login.png b/frontend/src/imgs/login.png deleted file mode 100644 index 6456c21..0000000 Binary files a/frontend/src/imgs/login.png and /dev/null differ diff --git a/frontend/src/imgs/logo.png b/frontend/src/imgs/logo.png deleted file mode 100644 index 774d55a..0000000 Binary files a/frontend/src/imgs/logo.png and /dev/null differ diff --git a/frontend/src/index.js b/frontend/src/index.js deleted file mode 100644 index f648298..0000000 --- a/frontend/src/index.js +++ /dev/null @@ -1,8 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom/client'; -import App from './App'; - -const root = ReactDOM.createRoot(document.getElementById('root')); -root.render( - -); diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx new file mode 100644 index 0000000..eec2ff4 --- /dev/null +++ b/frontend/src/index.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { BrowserRouter } from "react-router-dom"; + +import App from './App'; + +const root = ReactDOM.createRoot( + document.getElementById('root') as HTMLElement +); + +root.render( + + + + + +); diff --git a/frontend/src/pages/Games.tsx b/frontend/src/pages/Games.tsx new file mode 100644 index 0000000..e4b33e5 --- /dev/null +++ b/frontend/src/pages/Games.tsx @@ -0,0 +1,51 @@ +import React from 'react'; + +import GameEntry from '../components/GameEntry'; +import { Game } from '../types/Game'; +import { API } from '../api/Api'; +import "../css/Maps.css" + +const Games: React.FC = () => { + const [games, setGames] = React.useState([]); + + const _fetch_games = async () => { + const games = await API.get_games(); + setGames(games); + }; + + const _page_load = () => { + const loaders = document.querySelectorAll(".loader"); + loaders.forEach((loader) => { + (loader as HTMLElement).style.display = "none"; + }); + } + + React.useEffect(() => { + document.querySelectorAll(".games-page-item-body").forEach((game, index) => { + game.innerHTML = ""; + }); + + _fetch_games(); + _page_load(); + }, []); + + return ( +
+
+ Games list +
+ +
+
+
+ {games.map((game, index) => ( + + ))} +
+
+
+
+ ); +}; + +export default Games; diff --git a/frontend/src/pages/Maps.tsx b/frontend/src/pages/Maps.tsx new file mode 100644 index 0000000..707d865 --- /dev/null +++ b/frontend/src/pages/Maps.tsx @@ -0,0 +1,91 @@ +import React from 'react'; +import { Link, useLocation } from 'react-router-dom'; + +import { PortalIcon, FlagIcon, ChatIcon } from '../images/Images'; +import Summary from '../components/Summary'; +import Leaderboards from '../components/Leaderboards'; +import Discussions from '../components/Discussions'; +import ModMenu from '../components/ModMenu'; +import { MapDiscussions, MapLeaderboard, MapSummary } from '../types/Map'; +import { API } from '../api/Api'; +import "../css/Maps.css"; + +interface MapProps { + isModerator: boolean; +}; + +const Maps: React.FC = ({ isModerator }) => { + + const [selectedRun, setSelectedRun] = React.useState(0); + + const [mapSummaryData, setMapSummaryData] = React.useState(undefined); + const [mapLeaderboardData, setMapLeaderboardData] = React.useState(undefined); + const [mapDiscussionsData, setMapDiscussionsData] = React.useState(undefined) + + + const [navState, setNavState] = React.useState(0); + + const location = useLocation(); + + const mapID = location.pathname.split("/")[2]; + + const _fetch_map_summary = async () => { + const mapSummary = await API.get_map_summary(mapID); + setMapSummaryData(mapSummary); + }; + + const _fetch_map_leaderboards = async () => { + const mapLeaderboards = await API.get_map_leaderboard(mapID); + setMapLeaderboardData(mapLeaderboards); + }; + + const _fetch_map_discussions = async () => { + const mapDiscussions = await API.get_map_discussions(mapID); + setMapDiscussionsData(mapDiscussions); + }; + + React.useEffect(() => { + _fetch_map_summary(); + _fetch_map_leaderboards(); + _fetch_map_discussions(); + }, []); + + if (!mapSummaryData) { + return ( + <> + ); + } + + return ( + <> + {isModerator && } + +
+ +
+
+
+
+ + +
{mapSummaryData.map.map_name} +
+ + +
+ +
+ + + +
+ + {navState === 0 && } + {navState === 1 && } + {navState === 2 && _fetch_map_discussions()} />} +
+ + ); +}; + +export default Maps; diff --git a/frontend/src/pages/Profile.tsx b/frontend/src/pages/Profile.tsx new file mode 100644 index 0000000..8654a03 --- /dev/null +++ b/frontend/src/pages/Profile.tsx @@ -0,0 +1,326 @@ +import React from 'react'; +import { useLocation, useNavigate } from 'react-router-dom'; + +import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '../images/Images'; +import { UserProfile } from '../types/Profile'; +import { Game, GameChapters } from '../types/Game'; +import { Map } from '../types/Map'; +import "../css/Profile.css"; + +interface ProfileProps { + profile: UserProfile; +} + +const Profile: React.FC = ({ profile }) => { + + + const location = useLocation(); + const navigate = useNavigate(); + + React.useEffect(() => { + if (!profile) { + navigate("/"); + }; + }, [profile]); + + const [navState, setNavState] = React.useState(0); + const [pageNumber, setPageNumber] = React.useState(1); + const [pageMax, setPageMax] = React.useState(0); + + const [game, setGame] = React.useState("0") + const [gameData, setGameData] = React.useState([]); + const [chapter, setChapter] = React.useState("0") + const [chapterData, setChapterData] = React.useState(null); + const [maps, setMaps] = React.useState([]); + + function NavClick() { + if (profile) { + const btn = document.querySelectorAll("#section2 button"); + btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); + (btn[navState] as HTMLElement).style.backgroundColor = "#202232"; + + document.querySelectorAll("section").forEach((e, i) => i >= 2 ? e.style.display = "none" : "") + if (navState === 0) { document.querySelectorAll(".profile1").forEach((e) => { (e as HTMLElement).style.display = "block" }); } + if (navState === 1) { document.querySelectorAll(".profile2").forEach((e) => { (e as HTMLElement).style.display = "block" }); } + } + } + + function UpdateProfile() { + fetch(`https://lp.ardapektezol.com/api/v1/profile`, { + method: 'POST', + headers: { Authorization: "" } + }).then(r => r.json()) + .then(d => d.success ? window.alert("profile updated") : window.alert(`Error: ${d.message}`)) + } + + function TicksToTime(ticks: number) { + + let seconds = Math.floor(ticks / 60) + let minutes = Math.floor(seconds / 60) + let hours = Math.floor(minutes / 60) + + let milliseconds = Math.floor((ticks % 60) * 1000 / 60) + seconds = seconds % 60; + minutes = minutes % 60; + + return `${hours === 0 ? "" : hours + ":"}${minutes === 0 ? "" : hours > 0 ? minutes.toString().padStart(2, '0') + ":" : (minutes + ":")}${minutes > 0 ? seconds.toString().padStart(2, '0') : seconds}.${milliseconds.toString().padStart(3, '0')} (${ticks})`; + } + + React.useEffect(() => { + fetch("https://lp.ardapektezol.com/api/v1/games") + .then(r => r.json()) + .then(d => { + setGameData(d.data) + setGame("0") + }) + + }, [location]); + + React.useEffect(() => { + if (game && game !== "0") { + fetch(`https://lp.ardapektezol.com/api/v1/games/${game}`) + .then(r => r.json()) + .then(d => { + setChapterData(d.data) + setChapter("0"); + // (document.querySelector('#select-chapter') as HTMLInputElement).value = "0" + }) + + } else if (game && game === "0") { + setPageMax(Math.ceil(profile.records.length / 20)) + setPageNumber(1) + } + + }, [game, location]); + + React.useEffect(() => { + if (game !== "0") { + if (chapter === "0") { + fetch(`https://lp.ardapektezol.com/api/v1/games/${game}/maps`) + .then(r => r.json()) + .then(d => { + setMaps(d.data.maps); + setPageMax(Math.ceil(d.data.maps.length / 20)) + setPageNumber(1) + }) + } else { + fetch(`https://lp.ardapektezol.com/api/v1/chapters/${chapter}`) + .then(r => r.json()) + .then(d => { + setMaps(d.data.maps); + setPageMax(Math.ceil(d.data.maps.length / 20)) + setPageNumber(1) + }) + + } + } + }, [game, chapter, chapterData]) + + return ( +
+
+ + {profile.profile + ? ( +
UpdateProfile()}> + profile-image + Refresh +
+ ) : ( +
+ profile-image +
+ )} + +
+
+
{profile.user_name}
+
+ {profile.country_code === "XX" ? "" : {profile.country_code}} +
+
+ {profile.titles.map(e => ( + + {e.name} + + ))} +
+
+
+ {profile.links.steam === "-" ? "" : Steam} + {profile.links.twitch === "-" ? "" : Twitch} + {profile.links.youtube === "-" ? "" : Youtube} + {profile.links.p2sr === "-" ? "" : P2SR} +
+ +
+
+
+ Overall + {profile.rankings.overall.rank === 0 ? "N/A " : "#" + profile.rankings.overall.rank + " "} + ({profile.rankings.overall.completion_count}/{profile.rankings.overall.completion_total}) + +
+
+ Singleplayer + {profile.rankings.singleplayer.rank === 0 ? "N/A " : "#" + profile.rankings.singleplayer.rank + " "} + ({profile.rankings.singleplayer.completion_count}/{profile.rankings.singleplayer.completion_total}) + +
+
+ Cooperative + {profile.rankings.cooperative.rank === 0 ? "N/A " : "#" + profile.rankings.cooperative.rank + " "} + ({profile.rankings.cooperative.completion_count}/{profile.rankings.cooperative.completion_total}) + +
+
+
+ + +
+ + +
+ + + + + +
+
+ {gameData === null ? : + + + } + + {game === "0" ? + + : chapterData === null ? : + + + } +
+
+ Map Name + Portals + WRΔ + Time + + Rank + Date +
+
+ + {pageNumber}/{pageMax} + +
+
+
+
+
+ + {game === "0" + ? ( + + profile.records.sort((a, b) => a.map_id - b.map_id) + .map((r, index) => ( + + Math.ceil((index + 1) / 20) === pageNumber ? ( + + + {i === 0 && r.scores.length > 1 ? : ""} + + + ))} + + + ) : "" + ))) : maps ? + + maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) + .map((r, index) => { + if (Math.ceil((index + 1) / 20) === pageNumber) { + let record = profile.records.find((e) => e.map_id === r.id); + return record === undefined ? ( + + ) : ( + + + {i === 0 && record!.scores.length > 1 ? : ""} + + + ))} + + + ) + } else { return null } + }) : (<>{console.warn(maps)})} +
+
+
+ ); +}; + +export default Profile; diff --git a/frontend/src/pages/User.tsx b/frontend/src/pages/User.tsx new file mode 100644 index 0000000..1f6d8d0 --- /dev/null +++ b/frontend/src/pages/User.tsx @@ -0,0 +1,320 @@ +import React from 'react'; +import { useLocation } from 'react-router-dom'; + +import { SteamIcon, TwitchIcon, YouTubeIcon, PortalIcon, FlagIcon, StatisticsIcon, SortIcon, ThreedotIcon, DownloadIcon, HistoryIcon } from '../images/Images'; +import { UserProfile } from '../types/Profile'; +import { Game, GameChapters } from '../types/Game'; +import { Map } from '../types/Map'; +import { API } from '../api/Api'; +import { ticks_to_time } from '../utils/Time'; +import "../css/Profile.css"; + +const User: React.FC = () => { + const location = useLocation(); + + const [user, setUser] = React.useState(undefined); + + const [navState, setNavState] = React.useState(0); + const [pageNumber, setPageNumber] = React.useState(1); + const [pageMax, setPageMax] = React.useState(0); + + const [game, setGame] = React.useState("0") + const [gameData, setGameData] = React.useState([]); + const [chapter, setChapter] = React.useState("0") + const [chapterData, setChapterData] = React.useState(null); + const [maps, setMaps] = React.useState([]); + + function NavClick() { + if (user) { + const btn = document.querySelectorAll("#section2 button"); + btn.forEach((e) => { (e as HTMLElement).style.backgroundColor = "#2b2e46" }); + (btn[navState] as HTMLElement).style.backgroundColor = "#202232"; + + document.querySelectorAll("section").forEach((e, i) => i >= 2 ? e.style.display = "none" : "") + if (navState === 0) { document.querySelectorAll(".profile1").forEach((e) => { (e as HTMLElement).style.display = "block" }); } + if (navState === 1) { document.querySelectorAll(".profile2").forEach((e) => { (e as HTMLElement).style.display = "block" }); } + } + } + + function UpdateProfile() { + fetch(`https://lp.ardapektezol.com/api/v1/profile`, { + method: 'POST', + headers: { Authorization: "" } + }).then(r => r.json()) + .then(d => d.success ? window.alert("profile updated") : window.alert(`Error: ${d.message}`)) + } + + const _fetch_user = async () => { + const userData = await API.get_user(location.pathname.split("/")[2]); + setUser(userData); + }; + + React.useEffect(() => { + fetch("https://lp.ardapektezol.com/api/v1/games") + .then(r => r.json()) + .then(d => { + setGameData(d.data) + setGame("0") + }) + + }, [location]); + + React.useEffect(() => { + if (user) { + if (game && game !== "0") { + fetch(`https://lp.ardapektezol.com/api/v1/games/${game}`) + .then(r => r.json()) + .then(d => { + setChapterData(d.data) + setChapter("0"); + // (document.querySelector('#select-chapter') as HTMLInputElement).value = "0" + }) + + } else if (game && game === "0") { + setPageMax(Math.ceil(user.records.length / 20)) + setPageNumber(1) + } + } + }, [user, game, location]); + + React.useEffect(() => { + _fetch_user(); + }, []); + + React.useEffect(() => { + if (game !== "0") { + if (chapter === "0") { + fetch(`https://lp.ardapektezol.com/api/v1/games/${game}/maps`) + .then(r => r.json()) + .then(d => { + setMaps(d.data.maps); + setPageMax(Math.ceil(d.data.maps.length / 20)) + setPageNumber(1) + }) + } else { + fetch(`https://lp.ardapektezol.com/api/v1/chapters/${chapter}`) + .then(r => r.json()) + .then(d => { + setMaps(d.data.maps); + setPageMax(Math.ceil(d.data.maps.length / 20)) + setPageNumber(1) + }) + + } + } + }, [game, chapter, chapterData]) + + if (!user) { + return ( + <> + ); + }; + + return ( +
+
+ + {user.profile + ? ( +
UpdateProfile()}> + profile-image + Refresh +
+ ) : ( +
+ profile-image +
+ )} + +
+
+
{user.user_name}
+
+ {user.country_code === "XX" ? "" : {user.country_code}} +
+
+ {user.titles.map(e => ( + + {e.name} + + ))} +
+
+
+ {user.links.steam === "-" ? "" : Steam} + {user.links.twitch === "-" ? "" : Twitch} + {user.links.youtube === "-" ? "" : Youtube} + {user.links.p2sr === "-" ? "" : P2SR} +
+ +
+
+
+ Overall + {user.rankings.overall.rank === 0 ? "N/A " : "#" + user.rankings.overall.rank + " "} + ({user.rankings.overall.completion_count}/{user.rankings.overall.completion_total}) + +
+
+ Singleplayer + {user.rankings.singleplayer.rank === 0 ? "N/A " : "#" + user.rankings.singleplayer.rank + " "} + ({user.rankings.singleplayer.completion_count}/{user.rankings.singleplayer.completion_total}) + +
+
+ Cooperative + {user.rankings.cooperative.rank === 0 ? "N/A " : "#" + user.rankings.cooperative.rank + " "} + ({user.rankings.cooperative.completion_count}/{user.rankings.cooperative.completion_total}) + +
+
+
+ + +
+ + +
+ + + + + +
+
+ {gameData === null ? : + + + } + + {game === "0" ? + + : chapterData === null ? : + + + } +
+
+ Map Name + Portals + WRΔ + Time + + Rank + Date +
+
+ + {pageNumber}/{pageMax} + +
+
+
+
+
+ + {game === "0" + ? ( + + user.records.sort((a, b) => a.map_id - b.map_id) + .map((r, index) => ( + + Math.ceil((index + 1) / 20) === pageNumber ? ( + + + {i === 0 && r.scores.length > 1 ? : ""} + + + ))} + + + ) : "" + ))) : maps ? + + maps.filter(e => e.is_disabled === false).sort((a, b) => a.id - b.id) + .map((r, index) => { + if (Math.ceil((index + 1) / 20) === pageNumber) { + let record = user.records.find((e) => e.map_id === r.id); + return record === undefined ? ( + + ) : ( + + + {i === 0 && record!.scores.length > 1 ? : ""} + + + ))} + + + ) + } else { return null } + }) : (<>{console.warn(maps)})} +
+
+
+ ); +}; + +export default User; diff --git a/frontend/src/react-app-env.d.ts b/frontend/src/react-app-env.d.ts new file mode 100644 index 0000000..8265915 --- /dev/null +++ b/frontend/src/react-app-env.d.ts @@ -0,0 +1,2 @@ +declare module "*.png"; +declare module "*.css"; diff --git a/frontend/src/types/Content.tsx b/frontend/src/types/Content.tsx new file mode 100644 index 0000000..e593505 --- /dev/null +++ b/frontend/src/types/Content.tsx @@ -0,0 +1,18 @@ +export interface ModMenuContent { + id: number; + name: string; + score: number; + date: string; + showcase: string; + description: string; + category_id: number; +}; + +export interface MapDiscussionContent { + title: string; + content: string; +}; + +export interface MapDiscussionCommentContent { + comment: string; +}; diff --git a/frontend/src/types/Game.tsx b/frontend/src/types/Game.tsx new file mode 100644 index 0000000..eb435f6 --- /dev/null +++ b/frontend/src/types/Game.tsx @@ -0,0 +1,37 @@ +import { Map } from './Map'; + + +export interface Game { + id: number; + name: string; + image: string; + is_coop: boolean; + category_portals: GameCategoryPortals[]; +}; + +export interface GameChapters { + game: Game; + chapters: Chapter[]; +}; + +export interface GameMaps { + game: Game; + maps: Map[]; +}; + +export interface Category { + id: number; + name: string; +}; + +interface Chapter { + id: number; + name: string; + image: string; + is_disabled: boolean; +}; + +export interface GameCategoryPortals { + category: Category; + portal_count: number; +}; diff --git a/frontend/src/types/Map.tsx b/frontend/src/types/Map.tsx new file mode 100644 index 0000000..4a6b60e --- /dev/null +++ b/frontend/src/types/Map.tsx @@ -0,0 +1,103 @@ +import { Category, GameCategoryPortals } from './Game'; +import { Pagination } from './Pagination'; +import { UserShort } from './Profile'; + +export interface Map { + id: number; + name: string; + image: string; + is_disabled: boolean; + difficulty: number; + category_portals: GameCategoryPortals[]; +}; + +export interface MapDiscussion { + discussion: MapDiscussionsDetail; +}; + +export interface MapDiscussions { + discussions: MapDiscussionsDetail[]; +}; + +export interface MapDiscussionsDetail { + id: number; + title: string; + content: string; + creator: UserShort; + comments: MapDiscussionDetailComment[]; + created_at: string; + updated_at: string; +}; + +interface MapDiscussionDetailComment { + comment: string; + date: string; + user: UserShort; +}; + +export interface MapLeaderboard { + map: MapSummaryMap; + records: MapLeaderboardRecordSingleplayer[] | MapLeaderboardRecordMultiplayer[]; + pagination: Pagination; +}; + +export interface MapLeaderboardRecordSingleplayer { + kind: "singleplayer"; + placement: number; + record_id: number; + score_count: number; + score_time: number; + user: UserShort; + demo_id: string; + record_date: string; +}; + +export interface MapLeaderboardRecordMultiplayer { + kind: "multiplayer"; + placement: number; + record_id: number; + score_count: number; + score_time: number; + host: UserShort; + partner: UserShort; + host_demo_id: string; + partner_demo_id: string; + record_date: string; +}; + + +export interface MapSummary { + map: MapSummaryMap; + summary: MapSummaryDetails; +}; + +interface MapSummaryMap { + id: number; + image: string; + chapter_name: string; + game_name: string; + map_name: string; + is_coop: boolean; + is_disabled: boolean; +}; + +interface MapSummaryDetails { + routes: MapSummaryDetailsRoute[]; +}; + +interface MapSummaryDetailsRoute { + route_id: number; + category: Category; + history: MapSummaryDetailsRouteHistory; + rating: number; + completion_count: number; + description: string; + showcase: string; +}; + +interface MapSummaryDetailsRouteHistory { + runner_name: string; + score_count: number; + date: string; +}; + diff --git a/frontend/src/types/Pagination.tsx b/frontend/src/types/Pagination.tsx new file mode 100644 index 0000000..ccff04b --- /dev/null +++ b/frontend/src/types/Pagination.tsx @@ -0,0 +1,6 @@ +export interface Pagination { + total_records: number; + total_pages: number; + current_page: number; + page_size: number; +}; diff --git a/frontend/src/types/Profile.tsx b/frontend/src/types/Profile.tsx new file mode 100644 index 0000000..2bb037c --- /dev/null +++ b/frontend/src/types/Profile.tsx @@ -0,0 +1,63 @@ +import { Pagination } from "./Pagination"; + +export interface UserShort { + steam_id: string; + user_name: string; + avatar_link: string; +}; + +export interface UserProfile { + profile: boolean; + steam_id: string; + user_name: string; + avatar_link: string; + country_code: string; + titles: UserProfileTitles[]; + links: UserProfileLinks; + rankings: UserProfileRankings; + records: UserProfileRecords[]; + pagination: Pagination; +}; + +interface UserProfileTitles { + name: string; + color: string; +}; + +interface UserProfileLinks { + p2sr: string; + steam: string; + youtube: string; + twitch: string; +}; + +interface UserProfileRankings { + overall: UserProfileRankingsDetail; + singleplayer: UserProfileRankingsDetail; + cooperative: UserProfileRankingsDetail; +}; + +interface UserProfileRecords { + game_id: number; + category_id: number; + map_id: number; + map_name: string; + map_wr_count: number; + placement: number; + scores: UserProfileRecordsScores[] +}; + +interface UserProfileRecordsScores { + record_id: number; + demo_id: string; + score_count: number; + score_time: number; + date: string; +}; + +interface UserProfileRankingsDetail { + rank: number; + completion_count: number; + completion_total: number; +}; + diff --git a/frontend/src/types/Search.tsx b/frontend/src/types/Search.tsx new file mode 100644 index 0000000..766311a --- /dev/null +++ b/frontend/src/types/Search.tsx @@ -0,0 +1,13 @@ +import { UserShort } from "./Profile"; + +export interface Search { + players: UserShort[]; + maps: SearchMap[]; +}; + +interface SearchMap { + id: number; + game: string; + chapter: string; + map: string; +}; diff --git a/frontend/src/utils/Time.tsx b/frontend/src/utils/Time.tsx new file mode 100644 index 0000000..b83a7ed --- /dev/null +++ b/frontend/src/utils/Time.tsx @@ -0,0 +1,42 @@ +export function time_ago(date: any) { + const now = new Date().getTime(); + + const localDate = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)); + const seconds = Math.floor((now - localDate.getTime()) / 1000); + + let interval = Math.floor(seconds / 31536000); + if (interval === 1) {return interval + ' year ago';} + if (interval > 1) {return interval + ' years ago';} + + interval = Math.floor(seconds / 2592000); + if (interval === 1) {return interval + ' month ago';} + if (interval > 1) {return interval + ' months ago';} + + interval = Math.floor(seconds / 86400); + if (interval === 1) {return interval + ' day ago';} + if (interval > 1) {return interval + ' days ago';} + + interval = Math.floor(seconds / 3600); + if (interval === 1) {return interval + ' hour ago';} + if (interval > 1) {return interval + ' hours ago';} + + interval = Math.floor(seconds / 60); + if (interval === 1) {return interval + ' minute ago';} + if (interval > 1) {return interval + ' minutes ago';} + + if(seconds < 10) return 'just now'; + + return Math.floor(seconds) + ' seconds ago'; +}; + +export function ticks_to_time(ticks: number) { + let seconds = Math.floor(ticks / 60) + let minutes = Math.floor(seconds / 60) + let hours = Math.floor(minutes / 60) + + let milliseconds = Math.floor((ticks % 60) * 1000 / 60) + seconds = seconds % 60; + minutes = minutes % 60; + + return `${hours === 0 ? "" : hours + ":"}${minutes === 0 ? "" : hours > 0 ? minutes.toString().padStart(2, '0') + ":" : (minutes + ":")}${minutes > 0 ? seconds.toString().padStart(2, '0') : seconds}.${milliseconds.toString().padStart(3, '0')}`; +}; \ No newline at end of file -- cgit v1.2.3