From 35ed4dd8777309269b29b3131ea9758c01f5a962 Mon Sep 17 00:00:00 2001 From: alekswilc Date: Sat, 9 Nov 2024 19:54:29 +0100 Subject: [PATCH 1/2] feat(frontend): Add verification mark & UX Fixes --- packages/frontend/src/App.tsx | 97 +-- .../src/components/mini/alerts/Error.tsx | 8 +- .../src/components/mini/alerts/Success.tsx | 8 +- .../src/components/mini/alerts/Warning.tsx | 8 +- .../mini/header/DarkModeSwitcher.tsx | 35 +- .../src/components/mini/header/Header.tsx | 54 +- .../src/components/mini/icons/ArrowIcon.tsx | 6 +- .../components/mini/loaders/ContentLoader.tsx | 40 +- .../components/mini/loaders/PageLoader.tsx | 5 +- .../src/components/mini/sidebar/Sidebar.tsx | 749 +++++++++--------- .../mini/sidebar/SidebarLinkGroup.tsx | 15 +- .../components/mini/util/CardDataStats.tsx | 34 +- .../src/components/mini/util/PageMeta.tsx | 45 +- .../src/components/mini/util/Search.tsx | 13 +- .../pages/leaderboard/StationTable.tsx | 55 +- .../pages/leaderboard/TrainTable.tsx | 63 +- .../src/components/pages/log/StationLog.tsx | 62 +- .../src/components/pages/log/TrainLog.tsx | 69 +- .../components/pages/logs/StationTable.tsx | 64 +- .../src/components/pages/logs/TrainTable.tsx | 73 +- .../src/components/pages/profile/Profile.tsx | 103 +-- packages/frontend/src/hooks/useColorMode.tsx | 4 +- .../frontend/src/hooks/useLocalStorage.tsx | 10 +- packages/frontend/src/i18n/languages/en.json | 249 +++--- packages/frontend/src/i18n/languages/pl.json | 21 +- .../frontend/src/layout/DefaultLayout.tsx | 17 +- packages/frontend/src/main.tsx | 10 +- packages/frontend/src/pages/Home.tsx | 82 +- .../pages/leaderboard/StationsLeaderboard.tsx | 25 +- .../pages/leaderboard/TrainLeaderboard.tsx | 25 +- packages/frontend/src/pages/log/Log.tsx | 84 +- .../frontend/src/pages/logs/StationLogs.tsx | 24 +- .../frontend/src/pages/logs/TrainLogs.tsx | 26 +- .../frontend/src/pages/profile/Profile.tsx | 71 +- packages/frontend/src/types/leaderboard.ts | 1 + packages/frontend/src/types/log.ts | 4 +- packages/frontend/src/types/profile.ts | 1 + packages/frontend/src/types/station.ts | 1 + packages/frontend/src/types/train.ts | 1 + packages/frontend/src/util/time.ts | 14 + 40 files changed, 1217 insertions(+), 1059 deletions(-) create mode 100644 packages/frontend/src/util/time.ts diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 009e4af..bf1f298 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -1,51 +1,54 @@ -import { useEffect, useState } from 'react'; -import { Route, Routes, useLocation } from 'react-router-dom'; +import { useEffect, useState } from "react"; +import { Route, Routes, useLocation } from "react-router-dom"; -import { Loader } from './components/mini/loaders/PageLoader.tsx'; -import { Home } from './pages/Home'; -import DefaultLayout from './layout/DefaultLayout'; -import './i18n'; -import { TrainLeaderboard } from './pages/leaderboard/TrainLeaderboard.tsx'; -import { StationLeaderboard } from './pages/leaderboard/StationsLeaderboard.tsx'; -import { TrainLogs } from './pages/logs/TrainLogs.tsx'; -import { StationLogs } from './pages/logs/StationLogs.tsx'; -import { Profile } from './pages/profile/Profile.tsx'; -import { Log } from './pages/log/Log.tsx'; +import { Loader } from "./components/mini/loaders/PageLoader.tsx"; +import { Home } from "./pages/Home"; +import DefaultLayout from "./layout/DefaultLayout"; +import "./i18n"; +import { TrainLeaderboard } from "./pages/leaderboard/TrainLeaderboard.tsx"; +import { StationLeaderboard } from "./pages/leaderboard/StationsLeaderboard.tsx"; +import { TrainLogs } from "./pages/logs/TrainLogs.tsx"; +import { StationLogs } from "./pages/logs/StationLogs.tsx"; +import { Profile } from "./pages/profile/Profile.tsx"; +import { Log } from "./pages/log/Log.tsx"; -import 'react-toastify/dist/ReactToastify.css'; -import { ToastContainer } from 'react-toastify'; -import useColorMode from './hooks/useColorMode.tsx'; -import { HelmetProvider } from 'react-helmet-async'; -import { PageMeta } from './components/mini/util/PageMeta.tsx'; +import "react-toastify/dist/ReactToastify.css"; +import { ToastContainer } from "react-toastify"; +import useColorMode from "./hooks/useColorMode.tsx"; +import { HelmetProvider } from "react-helmet-async"; +import { PageMeta } from "./components/mini/util/PageMeta.tsx"; -function App() { - const [loading, setLoading] = useState(true); +function App() +{ + const [ loading, setLoading ] = useState(true); const { pathname } = useLocation(); - const [theme] = useColorMode(); - useEffect(() => { + const [ theme ] = useColorMode(); + useEffect(() => + { window.scrollTo(0, 0); - }, [pathname]); + }, [ pathname ]); - useEffect(() => { + useEffect(() => + { setTimeout(() => setLoading(false), 400); }, []); return - {loading ? ( - + { loading ? ( + ) : ( <> @@ -54,8 +57,8 @@ function App() { element={ <> - + description="Simrail Stats - The best SimRail logs and statistics site!"/> + } /> @@ -64,8 +67,8 @@ function App() { element={ <> - + description="Simrail Stats - The best SimRail logs and statistics site!"/> + } /> @@ -75,8 +78,8 @@ function App() { element={ <> - + description="Simrail Stats - The best SimRail logs and statistics site!"/> + } /> @@ -86,8 +89,8 @@ function App() { element={ <> - + description="Simrail Stats - The best SimRail logs and statistics site!"/> + } /> @@ -97,8 +100,8 @@ function App() { element={ <> - + description="Simrail Stats - The best SimRail logs and statistics site!"/> + } /> @@ -108,9 +111,9 @@ function App() { element={ <> - {/* page meta is modified in component! */} - + description="Simrail Stats - The best SimRail logs and statistics site!"/> + {/* page meta is modified in component! */ } + } /> @@ -120,16 +123,16 @@ function App() { element={ <> - {/* page title is modified after API response */} - + description="Simrail Stats - The best SimRail logs and statistics site!"/> + {/* page title is modified after API response */ } + } /> - )} + ) } ; } diff --git a/packages/frontend/src/components/mini/alerts/Error.tsx b/packages/frontend/src/components/mini/alerts/Error.tsx index 66298b5..e7429b5 100644 --- a/packages/frontend/src/components/mini/alerts/Error.tsx +++ b/packages/frontend/src/components/mini/alerts/Error.tsx @@ -1,17 +1,17 @@ -import { ErrorAlertIcon } from '../icons/AlertIcons.tsx'; +import { ErrorAlertIcon } from "../icons/AlertIcons.tsx"; export const ErrorAlert = ({ title, description }: { title: string, description: string }) =>
- +
- {title} + { title }
  • - {description} + { description }
diff --git a/packages/frontend/src/components/mini/alerts/Success.tsx b/packages/frontend/src/components/mini/alerts/Success.tsx index ff4ba9a..099ed44 100644 --- a/packages/frontend/src/components/mini/alerts/Success.tsx +++ b/packages/frontend/src/components/mini/alerts/Success.tsx @@ -1,17 +1,17 @@ -import { SuccessAlertIcon } from '../icons/AlertIcons.tsx'; +import { SuccessAlertIcon } from "../icons/AlertIcons.tsx"; export const SuccessAlert = ({ title, description }: { title: string, description: string }) =>
- +
- {title} + { title }

- {description} + { description }

; \ No newline at end of file diff --git a/packages/frontend/src/components/mini/alerts/Warning.tsx b/packages/frontend/src/components/mini/alerts/Warning.tsx index 39799f3..2341e19 100644 --- a/packages/frontend/src/components/mini/alerts/Warning.tsx +++ b/packages/frontend/src/components/mini/alerts/Warning.tsx @@ -1,17 +1,17 @@ -import { WarningAlertIcon } from '../icons/AlertIcons.tsx'; +import { WarningAlertIcon } from "../icons/AlertIcons.tsx"; export const WarningAlert = ({ title, description }: { title: string, description: string }) =>
- +
- {title} + { title }

- {description} + { description }

; diff --git a/packages/frontend/src/components/mini/header/DarkModeSwitcher.tsx b/packages/frontend/src/components/mini/header/DarkModeSwitcher.tsx index 0314095..a1f5243 100644 --- a/packages/frontend/src/components/mini/header/DarkModeSwitcher.tsx +++ b/packages/frontend/src/components/mini/header/DarkModeSwitcher.tsx @@ -1,35 +1,38 @@ -import useColorMode from '../../../hooks/useColorMode'; -import { DarkIcon, LightIcon } from '../icons/DarkModeSwitchIcons.tsx'; +import useColorMode from "../../../hooks/useColorMode"; +import { DarkIcon, LightIcon } from "../icons/DarkModeSwitchIcons.tsx"; -const DarkModeSwitcher = () => { - const [colorMode, setColorMode] = useColorMode(); +const DarkModeSwitcher = () => +{ + const [ colorMode, setColorMode ] = useColorMode(); return (
  • diff --git a/packages/frontend/src/components/mini/header/Header.tsx b/packages/frontend/src/components/mini/header/Header.tsx index e32678d..659d0c5 100644 --- a/packages/frontend/src/components/mini/header/Header.tsx +++ b/packages/frontend/src/components/mini/header/Header.tsx @@ -1,51 +1,53 @@ -import DarkModeSwitcher from './DarkModeSwitcher.tsx'; -import ReactCountryFlag from 'react-country-flag'; -import i18n from 'i18next'; +import DarkModeSwitcher from "./DarkModeSwitcher.tsx"; +import ReactCountryFlag from "react-country-flag"; +import i18n from "i18next"; export const Header = (props: { sidebarOpen: string | boolean | undefined; setSidebarOpen: (arg0: boolean) => void; -}) => { +}) => +{ return (
    diff --git a/packages/frontend/src/components/mini/icons/ArrowIcon.tsx b/packages/frontend/src/components/mini/icons/ArrowIcon.tsx index e6aa405..d68b8cc 100644 --- a/packages/frontend/src/components/mini/icons/ArrowIcon.tsx +++ b/packages/frontend/src/components/mini/icons/ArrowIcon.tsx @@ -1,8 +1,8 @@ export const ArrowIcon = ({ rotated }: { rotated?: boolean }) => { +export const LoadError = () => +{ const { t } = useTranslation(); return
    - +
    - {t('content_loader.error.header')} + { t("content_loader.error.header") }
    • - {t('content_loader.error.description')} + { t("content_loader.error.description") }
    • - {/* TODO: add git issue params */} + {/* TODO: add git issue params */ }
      {t('content_loader.error.report')} + to="https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/issues/new">{ t("content_loader.error.report") } window.location.reload()}>{t('content_loader.error.refresh')} + onClick={ () => window.location.reload() }>{ t("content_loader.error.refresh") }
      @@ -42,20 +43,23 @@ export const LoadError = () => {
    ; }; -export const ContentLoader = () => { - const [error, setError] = useState(false); - useEffect(() => { - new Promise(res => setTimeout(res, 5000)).then(() => { +export const ContentLoader = () => +{ + const [ error, setError ] = useState(false); + useEffect(() => + { + new Promise(res => setTimeout(res, 5000)).then(() => + { setError(true); }); }, []); return ( <> - {error ? :
    :
    -
    -
    } +
    +
    } ); }; diff --git a/packages/frontend/src/components/mini/loaders/PageLoader.tsx b/packages/frontend/src/components/mini/loaders/PageLoader.tsx index f3ad74c..23b5311 100644 --- a/packages/frontend/src/components/mini/loaders/PageLoader.tsx +++ b/packages/frontend/src/components/mini/loaders/PageLoader.tsx @@ -1,7 +1,8 @@ -export const Loader = () => { +export const Loader = () => +{ return (
    -
    +
    ); }; diff --git a/packages/frontend/src/components/mini/sidebar/Sidebar.tsx b/packages/frontend/src/components/mini/sidebar/Sidebar.tsx index 06d30e9..b92b50c 100644 --- a/packages/frontend/src/components/mini/sidebar/Sidebar.tsx +++ b/packages/frontend/src/components/mini/sidebar/Sidebar.tsx @@ -1,91 +1,104 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { NavLink, useLocation } from 'react-router-dom'; -import SidebarLinkGroup from './SidebarLinkGroup.tsx'; -import { useTranslation } from 'react-i18next'; -import { HamburgerGoBackIcon } from '../icons/SidebarIcons.tsx'; -import { ArrowIcon } from '../icons/ArrowIcon.tsx'; -import { FaHome, FaClipboardList } from 'react-icons/fa'; -import { FaChartSimple, FaTrain, FaBuildingFlag } from 'react-icons/fa6'; +import React, { useEffect, useRef, useState } from "react"; +import { NavLink, useLocation } from "react-router-dom"; +import SidebarLinkGroup from "./SidebarLinkGroup.tsx"; +import { useTranslation } from "react-i18next"; +import { HamburgerGoBackIcon } from "../icons/SidebarIcons.tsx"; +import { ArrowIcon } from "../icons/ArrowIcon.tsx"; +import { FaHome, FaClipboardList } from "react-icons/fa"; +import { FaChartSimple, FaTrain, FaBuildingFlag } from "react-icons/fa6"; -interface SidebarProps { +interface SidebarProps +{ sidebarOpen: boolean; setSidebarOpen: (arg: boolean) => void; } -export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => { +export const Sidebar = ({ sidebarOpen, setSidebarOpen }: SidebarProps) => +{ const location = useLocation(); const { pathname } = location; const trigger = useRef(null); const sidebar = useRef(null); - const storedSidebarExpanded = localStorage.getItem('sidebar-expanded'); - const [sidebarExpanded, setSidebarExpanded] = useState( - storedSidebarExpanded === null ? false : storedSidebarExpanded === 'true' + const storedSidebarExpanded = localStorage.getItem("sidebar-expanded"); + const [ sidebarExpanded, setSidebarExpanded ] = useState( + storedSidebarExpanded === null ? false : storedSidebarExpanded === "true", ); const { t } = useTranslation(); // close on click outside - useEffect(() => { - const clickHandler = ({ target }: MouseEvent) => { - if (!sidebar.current || !trigger.current) { + useEffect(() => + { + const clickHandler = ({ target }: MouseEvent) => + { + if (!sidebar.current || !trigger.current) + { return; } if ( !sidebarOpen || sidebar.current.contains(target) || trigger.current.contains(target) - ) { + ) + { return; } setSidebarOpen(false); }; - document.addEventListener('click', clickHandler); - return () => document.removeEventListener('click', clickHandler); + document.addEventListener("click", clickHandler); + return () => document.removeEventListener("click", clickHandler); }); // close if the esc key is pressed - useEffect(() => { - const keyHandler = ({ keyCode }: KeyboardEvent) => { - if (!sidebarOpen || keyCode !== 27) { + useEffect(() => + { + const keyHandler = ({ keyCode }: KeyboardEvent) => + { + if (!sidebarOpen || keyCode !== 27) + { return; } setSidebarOpen(false); }; - document.addEventListener('keydown', keyHandler); - return () => document.removeEventListener('keydown', keyHandler); + document.addEventListener("keydown", keyHandler); + return () => document.removeEventListener("keydown", keyHandler); }); - useEffect(() => { - localStorage.setItem('sidebar-expanded', sidebarExpanded.toString()); - if (sidebarExpanded) { - document.querySelector('body')?.classList.add('sidebar-expanded'); - } else { - document.querySelector('body')?.classList.remove('sidebar-expanded'); + useEffect(() => + { + localStorage.setItem("sidebar-expanded", sidebarExpanded.toString()); + if (sidebarExpanded) + { + document.querySelector("body")?.classList.add("sidebar-expanded"); } - }, [sidebarExpanded]); + else + { + document.querySelector("body")?.classList.remove("sidebar-expanded"); + } + }, [ sidebarExpanded ]); return (
    */} - {/* */}{/*{/* */} - {/* */} - {/* );*/} - {/* }}*/} - {/* */} - {/* */}{/*{/* */} - {/* */} - {/*
    }*/} + {/* */ }{/*{/* */ } + {/* */ } + {/* {(handleClick, open) => {*/ } + {/* return (*/ } + {/* */ } + {/* {*/ } + {/* e.preventDefault();*/ } + {/* sidebarExpanded*/ } + {/* ? handleClick()*/ } + {/* : setSidebarExpanded(true);*/ } + {/* }}*/ } + {/* >*/ } + {/* */ } + {/* */ } + {/* */ } + {/* */ } + {/* */ } + {/* */ } + {/* */ } + {/* */ } + {/* */ } + {/* */ } + {/* */ } + {/* Authentication*/ } + {/* */ } + {/* */ } + {/* */ } + {/* */ } + {/* */ }{/*{/* */ } + {/* */ } + {/*
      */ } + {/*
    • */ } + {/* */ } + {/* 'group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white ' +*/ } + {/* (isActive && '!text-white')*/ } + {/* }*/ } + {/* >*/ } + {/* Sign In*/ } + {/* */ } + {/*
    • */ } + {/*
    • */ } + {/* */ } + {/* 'group relative flex items-center gap-2.5 rounded-md px-4 font-medium text-bodydark2 duration-300 ease-in-out hover:text-white ' +*/ } + {/* (isActive && '!text-white')*/ } + {/* }*/ } + {/* >*/ } + {/* Sign Up*/ } + {/* */ } + {/*
    • */ } + {/*
    */ } + {/*
    */ } + {/* */ }{/*{/* */ } + {/* */ } + {/* );*/ } + {/* }}*/ } + {/* */ } + {/* */ }{/*{/* */ } + {/* */ } + {/*
    }*/ } - {/* */} + {/* */ }
    ); diff --git a/packages/frontend/src/components/mini/sidebar/SidebarLinkGroup.tsx b/packages/frontend/src/components/mini/sidebar/SidebarLinkGroup.tsx index affeef0..069c01c 100644 --- a/packages/frontend/src/components/mini/sidebar/SidebarLinkGroup.tsx +++ b/packages/frontend/src/components/mini/sidebar/SidebarLinkGroup.tsx @@ -1,18 +1,21 @@ -import { ReactNode, useState } from 'react'; +import { ReactNode, useState } from "react"; -interface SidebarLinkGroupProps { +interface SidebarLinkGroupProps +{ children: (handleClick: () => void, open: boolean) => ReactNode; activeCondition: boolean; } -const SidebarLinkGroup = ({ children, activeCondition }: SidebarLinkGroupProps) => { - const [open, setOpen] = useState(activeCondition); +const SidebarLinkGroup = ({ children, activeCondition }: SidebarLinkGroupProps) => +{ + const [ open, setOpen ] = useState(activeCondition); - const handleClick = () => { + const handleClick = () => + { setOpen(!open); }; - return
  • {children(handleClick, open)}
  • ; + return
  • { children(handleClick, open) }
  • ; }; export default SidebarLinkGroup; diff --git a/packages/frontend/src/components/mini/util/CardDataStats.tsx b/packages/frontend/src/components/mini/util/CardDataStats.tsx index 7718d6d..6486fc0 100644 --- a/packages/frontend/src/components/mini/util/CardDataStats.tsx +++ b/packages/frontend/src/components/mini/util/CardDataStats.tsx @@ -1,6 +1,7 @@ -import React from 'react'; +import React from "react"; -interface CardDataStatsProps { +interface CardDataStatsProps +{ title: string; total: string; rate?: string; @@ -13,27 +14,28 @@ export const CardDataStats: React.FC = ({ total, rate, levelUp, - levelDown - }) => { + levelDown, + }) => +{ return (

    - {total} + { total }

    - {title} + { title }
    - {rate && - {rate} + { rate } - {levelUp && ( + { levelUp && ( = ({ fill="" /> - )} - {levelDown && ( + ) } + { levelDown && ( = ({ fill="" /> - )} - } + ) } + }
    ); diff --git a/packages/frontend/src/components/mini/util/PageMeta.tsx b/packages/frontend/src/components/mini/util/PageMeta.tsx index 9601c9d..41ab86c 100644 --- a/packages/frontend/src/components/mini/util/PageMeta.tsx +++ b/packages/frontend/src/components/mini/util/PageMeta.tsx @@ -1,29 +1,30 @@ -import { Helmet } from 'react-helmet-async'; +import { Helmet } from "react-helmet-async"; // https://dev.to/facubotta/meta-data-in-react-1p93 -export const PageMeta = ({ title = '', description = '', image = '', name = '' }) => { +export const PageMeta = ({ title = "", description = "", image = "", name = "" }) => +{ return ( - { /* Standard metadata tags */} - {title} - - - { /* Open Graph tags (OG) */} - - - - - {/* OG image tags */} - - - - - - { /* Twitter tags */} - {name && } - - - + { /* Standard metadata tags */ } + { title } + + + { /* Open Graph tags (OG) */ } + + + + + {/* OG image tags */ } + + + + + + { /* Twitter tags */ } + { name && } + + + ); }; \ No newline at end of file diff --git a/packages/frontend/src/components/mini/util/Search.tsx b/packages/frontend/src/components/mini/util/Search.tsx index ef4c883..874f7e4 100644 --- a/packages/frontend/src/components/mini/util/Search.tsx +++ b/packages/frontend/src/components/mini/util/Search.tsx @@ -1,10 +1,11 @@ -import { ChangeEventHandler } from 'react'; -import { useTranslation } from 'react-i18next'; +import { ChangeEventHandler } from "react"; +import { useTranslation } from "react-i18next"; export const Search = ({ searchItem, handleInputChange }: { searchItem: string; handleInputChange: ChangeEventHandler -}) => { +}) => +{ const { t } = useTranslation(); return
    @@ -12,9 +13,9 @@ export const Search = ({ searchItem, handleInputChange }: {
    ; diff --git a/packages/frontend/src/components/pages/leaderboard/StationTable.tsx b/packages/frontend/src/components/pages/leaderboard/StationTable.tsx index 8bd0952..1a2f65e 100644 --- a/packages/frontend/src/components/pages/leaderboard/StationTable.tsx +++ b/packages/frontend/src/components/pages/leaderboard/StationTable.tsx @@ -1,70 +1,73 @@ -import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; -import { TLeaderboardRecord } from '../../../types/leaderboard.ts'; -import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx'; -import { WarningAlert } from '../../mini/alerts/Warning.tsx'; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import { TLeaderboardRecord } from "../../../types/leaderboard.ts"; +import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx"; +import { WarningAlert } from "../../mini/alerts/Warning.tsx"; +import { formatTime } from "../../../util/time.ts"; +import { FaCheck } from 'react-icons/fa6'; -export const StationTable = ({ stations, error }: { stations: TLeaderboardRecord[], error: number }) => { +export const StationTable = ({ stations, error }: { stations: TLeaderboardRecord[], error: number }) => +{ const { t } = useTranslation(); return ( <> - {error === 2 && } - {error === 0 && } - {error === 1 &&
    } + { error === 0 && } + { error === 1 &&
    - {t('leaderboard.user')} + { t("leaderboard.user") }
    - {t('leaderboard.time')} + { t("leaderboard.time") }
    - {t('leaderboard.actions')} + { t("leaderboard.actions") }
    - {stations.map((station, key) => ( + { stations.map((station, key) => (

    - {station.steamName} + { station.steamName } { station.verified && }

    -

    {Math.floor(station.dispatcherTime / 3600000)}h

    +

    { formatTime(station.dispatcherTime) }

    - {t('leaderboard.profile')} + { t("leaderboard.profile") }
    - ))} + )) }
    -
    } +
    } ); diff --git a/packages/frontend/src/components/pages/leaderboard/TrainTable.tsx b/packages/frontend/src/components/pages/leaderboard/TrainTable.tsx index f839afd..09f596d 100644 --- a/packages/frontend/src/components/pages/leaderboard/TrainTable.tsx +++ b/packages/frontend/src/components/pages/leaderboard/TrainTable.tsx @@ -1,88 +1,91 @@ -import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; -import { TLeaderboardRecord } from '../../../types/leaderboard.ts'; -import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx'; -import { WarningAlert } from '../../mini/alerts/Warning.tsx'; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import { TLeaderboardRecord } from "../../../types/leaderboard.ts"; +import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx"; +import { WarningAlert } from "../../mini/alerts/Warning.tsx"; +import { formatTime } from "../../../util/time.ts"; +import { FaCheck } from 'react-icons/fa6'; -export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], error: number }) => { +export const TrainTable = ({ trains, error }: { trains: TLeaderboardRecord[], error: number }) => +{ const { t } = useTranslation(); return ( <> - {error === 2 && } - {error === 0 && } - {error === 1 &&
    } + { error === 0 && } + { error === 1 &&
    - {t('leaderboard.user')} + { t("leaderboard.user") }
    - {t('leaderboard.points')} + { t("leaderboard.points") }
    - {t('leaderboard.distance')} + { t("leaderboard.distance") }
    - {t('leaderboard.time')} + { t("leaderboard.time") }
    - {t('leaderboard.actions')} + { t("leaderboard.actions") }
    - {trains.map((train, key) => ( + { trains.map((train, key) => (

    - {train.steamName} + { train.steamName } { train.verified && }

    -

    {train.trainPoints}

    +

    { train.trainPoints }

    -

    {(train.trainDistance / 1000).toFixed(2)}km

    +

    { (train.trainDistance / 1000).toFixed(2) }km

    -

    {Math.floor(train.trainTime / 3600000)}h

    +

    { formatTime(train.trainTime) }

    - {t('leaderboard.profile')} + { t("leaderboard.profile") }
    - ))} + )) }
    -
    } +
    } ); diff --git a/packages/frontend/src/components/pages/log/StationLog.tsx b/packages/frontend/src/components/pages/log/StationLog.tsx index 8fe3aa2..6a8c45b 100644 --- a/packages/frontend/src/components/pages/log/StationLog.tsx +++ b/packages/frontend/src/components/pages/log/StationLog.tsx @@ -1,21 +1,26 @@ -import { useTranslation } from 'react-i18next'; -import { TLogStationData } from '../../../types/log.ts'; -import dayjs from 'dayjs'; -import { toast } from 'react-toastify'; +import { useTranslation } from "react-i18next"; +import { TLogStationData } from "../../../types/log.ts"; +import dayjs from "dayjs"; +import { toast } from "react-toastify"; +import { Link } from "react-router-dom"; +import { FaCheck } from 'react-icons/fa6'; -export const StationLog = ({ data }: { data: TLogStationData }) => { +export const StationLog = ({ data }: { data: TLogStationData }) => +{ const { t } = useTranslation(); - const copyLink = () => { + const copyLink = () => + { void navigator.clipboard.writeText(location.href); - toast.success(t('log.toasts.copied')); + toast.success(t("log.toasts.copied")); }; - const report = () => { - toast.info(t('log.toasts.report'), { - autoClose: 5000 + const report = () => + { + toast.info(t("log.toasts.report"), { + autoClose: 5000, }); - void navigator.clipboard.writeText(`;user: \`${data.userUsername}\`\n;steam: \`${data.userSteamId}\`\n;left: ${data.joinedDate ? `\n;joined: ` : ''}\n;station: \`${data.stationName}\`\n;link: ${location.href}\n\n`); + void navigator.clipboard.writeText(`;user: \`${ data.userUsername }\`\n;steam: \`${ data.userSteamId }\`\n;left: ${ data.joinedDate ? `\n;joined: ` : "" }\n;station: \`${ data.stationName }\`\n;link: ${ location.href }\n\n`); }; return
    {
    - profile + profile

    - {data.userUsername} + { data.userUsername }{ data.verified && }

    @@ -37,31 +42,38 @@ export const StationLog = ({ data }: { data: TLogStationData }) => {
    -

    {t('log.station.header')}

    -

    {t('log.station.server', { server: data.server.toUpperCase() })}

    -

    {t('log.station.station', { name: data.stationName, short: data.stationShort })}

    +

    { t("log.station.header") }

    +

    { t("log.station.server", { server: data.server.toUpperCase() }) }

    +

    { t("log.station.station", { name: data.stationName, short: data.stationShort }) }

    - {data.joinedDate && -

    {t('log.station.joined', { date: dayjs(data.joinedDate).format('DD/MM/YYYY HH:mm') })}

    } -

    {t('log.station.left', { date: dayjs(data.leftDate).format('DD/MM/YYYY HH:mm') })}

    - {data.joinedDate && -

    {t('log.station.spent', { date: dayjs.duration(data.leftDate - data.joinedDate).format('H[h] m[m]') })}

    } + { data.joinedDate && +

    { t("log.station.joined", { date: dayjs(data.joinedDate).format("DD/MM/YYYY HH:mm") }) }

    } +

    { t("log.station.left", { date: dayjs(data.leftDate).format("DD/MM/YYYY HH:mm") }) }

    + { data.joinedDate && +

    { t("log.station.spent", { date: dayjs.duration(data.leftDate - data.joinedDate).format("H[h] m[m]") }) }

    }
    diff --git a/packages/frontend/src/components/pages/log/TrainLog.tsx b/packages/frontend/src/components/pages/log/TrainLog.tsx index 589cba9..c46d74a 100644 --- a/packages/frontend/src/components/pages/log/TrainLog.tsx +++ b/packages/frontend/src/components/pages/log/TrainLog.tsx @@ -1,21 +1,27 @@ -import { useTranslation } from 'react-i18next'; -import { TLogTrainData } from '../../../types/log.ts'; -import dayjs from 'dayjs'; -import { toast } from 'react-toastify'; +import { useTranslation } from "react-i18next"; +import { TLogTrainData } from "../../../types/log.ts"; +import dayjs from "dayjs"; +import { toast } from "react-toastify"; +import { Link } from "react-router-dom"; +import { FaCheck } from 'react-icons/fa6'; -export const TrainLog = ({ data }: { data: TLogTrainData }) => { + +export const TrainLog = ({ data }: { data: TLogTrainData }) => +{ const { t } = useTranslation(); - const copyLink = () => { + const copyLink = () => + { void navigator.clipboard.writeText(location.href); - toast.success(t('log.toasts.copied')); + toast.success(t("log.toasts.copied")); }; - const report = () => { - toast.info(t('log.toasts.report'), { - autoClose: 5000 + const report = () => + { + toast.info(t("log.toasts.report"), { + autoClose: 5000, }); - void navigator.clipboard.writeText(`;user: \`${data.userUsername}\`\n;steam: \`${data.userSteamId}\`\n;left: ${data.joinedDate ? `\n;joined: ` : ''}\n;train: \`${data.trainNumber}\`\n;link: ${location.href}\n\n`); + void navigator.clipboard.writeText(`;user: \`${ data.userUsername }\`\n;steam: \`${ data.userSteamId }\`\n;left: ${ data.joinedDate ? `\n;joined: ` : "" }\n;train: \`${ data.trainNumber }\`\n;link: ${ location.href }\n\n`); }; return
    {
    - profile + profile

    - {data.userUsername} + { data.userUsername } { data.verified && }

    @@ -37,35 +43,42 @@ export const TrainLog = ({ data }: { data: TLogTrainData }) => {
    -

    {t('log.train.header')}

    -

    {t('log.train.server', { server: data.server.toUpperCase() })}

    -

    {t('log.train.train', { name: data.trainName, number: data.trainNumber })}

    - {(data.distance || data.distance === 0) && -

    {t('log.train.distance', { distance: (data.distance / 1000).toFixed(2) })}

    } +

    { t("log.train.header") }

    +

    { t("log.train.server", { server: data.server.toUpperCase() }) }

    +

    { t("log.train.train", { name: data.trainName, number: data.trainNumber }) }

    + { (data.distance || data.distance === 0) && +

    { t("log.train.distance", { distance: (data.distance / 1000).toFixed(2) }) }

    } - {(data.points || data.points === 0) &&

    {t('log.train.points', { points: data.points })}

    } + { (data.points || data.points === 0) &&

    { t("log.train.points", { points: data.points }) }

    } - {data.joinedDate && -

    {t('log.train.joined', { date: dayjs(data.joinedDate).format('DD/MM/YYYY HH:mm') })}

    } -

    {t('log.train.left', { date: dayjs(data.leftDate).format('DD/MM/YYYY HH:mm') })}

    - {data.joinedDate && -

    {t('log.train.spent', { date: dayjs.duration(data.leftDate - data.joinedDate).format('H[h] m[m]') })}

    } + { data.joinedDate && +

    { t("log.train.joined", { date: dayjs(data.joinedDate).format("DD/MM/YYYY HH:mm") }) }

    } +

    { t("log.train.left", { date: dayjs(data.leftDate).format("DD/MM/YYYY HH:mm") }) }

    + { data.joinedDate && +

    { t("log.train.spent", { date: dayjs.duration(data.leftDate - data.joinedDate).format("H[h] m[m]") }) }

    }
    diff --git a/packages/frontend/src/components/pages/logs/StationTable.tsx b/packages/frontend/src/components/pages/logs/StationTable.tsx index 9f236af..89c7cae 100644 --- a/packages/frontend/src/components/pages/logs/StationTable.tsx +++ b/packages/frontend/src/components/pages/logs/StationTable.tsx @@ -1,91 +1,93 @@ -import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; -import dayjs from 'dayjs'; -import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx'; -import { TStationRecord } from '../../../types/station.ts'; -import { WarningAlert } from '../../mini/alerts/Warning.tsx'; +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import dayjs from "dayjs"; +import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx"; +import { TStationRecord } from "../../../types/station.ts"; +import { WarningAlert } from "../../mini/alerts/Warning.tsx"; +import { FaCheck } from 'react-icons/fa6'; // setSearchItem: Dispatch> export const StationTable = ({ stations, error }: { stations: TStationRecord[], error: number -}) => { +}) => +{ const { t } = useTranslation(); return ( <> - {error === 2 && } - {error === 0 && } + { error === 2 && } + { error === 0 && } - {error === 1 &&
    - {t('logs.user')} + { t("logs.user") }
    - {t('logs.station')} + { t("logs.station") }
    - {t('logs.time')} + { t("logs.time") }
    - {t('logs.actions')} + { t("logs.actions") }
    - {stations.map((station, key) => ( + { stations.map((station, key) => (

    - {station.userUsername} + { station.userUsername } { station.verified && }

    -

    {station.server.toUpperCase()} - {station.stationName ?? '--'}

    +

    { station.server.toUpperCase() } - { station.stationName ?? "--" }

    -

    {dayjs(station.leftDate).format('HH:mm DD/MM/YYYY')}

    +

    { dayjs(station.leftDate).format("HH:mm DD/MM/YYYY") }

    - {t('logs.profile')} + { t("logs.profile") } - {t('logs.record')} + { t("logs.record") }
    - ))} + )) }
    -
    } +
  • } ); }; \ No newline at end of file diff --git a/packages/frontend/src/components/pages/logs/TrainTable.tsx b/packages/frontend/src/components/pages/logs/TrainTable.tsx index 5c099ba..7b87f2f 100644 --- a/packages/frontend/src/components/pages/logs/TrainTable.tsx +++ b/packages/frontend/src/components/pages/logs/TrainTable.tsx @@ -1,108 +1,109 @@ -import { useTranslation } from 'react-i18next'; -import { Link } from 'react-router-dom'; -import { TTrainRecord } from '../../../types/train.ts'; -import dayjs from 'dayjs'; -import { ContentLoader } from '../../mini/loaders/ContentLoader.tsx'; -import { WarningAlert } from '../../mini/alerts/Warning.tsx'; - +import { useTranslation } from "react-i18next"; +import { Link } from "react-router-dom"; +import { TTrainRecord } from "../../../types/train.ts"; +import dayjs from "dayjs"; +import { ContentLoader } from "../../mini/loaders/ContentLoader.tsx"; +import { WarningAlert } from "../../mini/alerts/Warning.tsx"; +import { FaCheck } from 'react-icons/fa6'; // setSearchItem: Dispatch> export const TrainTable = ({ trains, error }: { trains: TTrainRecord[], error: number -}) => { +}) => +{ const { t } = useTranslation(); return ( <> - {error === 2 && } - {error === 0 && } - {error === 1 &&
    } + { error === 0 && } + { error === 1 &&
    - {t('logs.user')} + { t("logs.user") }
    - {t('logs.train')} + { t("logs.train") }
    - {t('logs.points')} + { t("logs.points") }
    - {t('logs.distance')} + { t("logs.distance") }
    - {t('logs.time')} + { t("logs.time") }
    - {t('logs.actions')} + { t("logs.actions") }
    - {trains.map((train, key) => ( + { trains.map((train, key) => (

    - {train.userUsername} + { train.userUsername } { train.verified && }

    -

    {train.server.toUpperCase()} - {train.trainNumber ?? '--'}

    +

    { train.server.toUpperCase() } - { train.trainNumber ?? "--" }

    -

    {train.distance ? train.points : '--'}

    +

    { train.distance ? train.points : "--" }

    -

    {train.distance ? `${(train.distance / 1000).toFixed(2)}km` : '--'}

    +

    { train.distance ? `${ (train.distance / 1000).toFixed(2) }km` : "--" }

    -

    {dayjs(train.leftDate).format('HH:mm DD/MM/YYYY')}

    +

    { dayjs(train.leftDate).format("HH:mm DD/MM/YYYY") }

    - {t('logs.profile')} + { t("logs.profile") } - {t('logs.record')} + { t("logs.record") }
    - ))} + )) }
    -
    } +
    } ); }; \ No newline at end of file diff --git a/packages/frontend/src/components/pages/profile/Profile.tsx b/packages/frontend/src/components/pages/profile/Profile.tsx index 6ce7a80..00a37a5 100644 --- a/packages/frontend/src/components/pages/profile/Profile.tsx +++ b/packages/frontend/src/components/pages/profile/Profile.tsx @@ -1,12 +1,15 @@ -import { useState } from 'react'; -import { TProfileData } from '../../../types/profile.ts'; -import { useTranslation } from 'react-i18next'; -import { ArrowIcon } from '../../mini/icons/ArrowIcon.tsx'; +import { useState } from "react"; +import { TProfileData } from "../../../types/profile.ts"; +import { useTranslation } from "react-i18next"; +import { ArrowIcon } from "../../mini/icons/ArrowIcon.tsx"; +import { formatTime } from "../../../util/time.ts"; +import { FaCheck } from 'react-icons/fa6'; -export const ProfileCard = ({ data }: { data: TProfileData }) => { +export const ProfileCard = ({ data }: { data: TProfileData }) => +{ - const [showTrains, setShowTrains] = useState(false); - const [showStations, setShowStations] = useState(false); + const [ showTrains, setShowTrains ] = useState(false); + const [ showStations, setShowStations ] = useState(false); const { t } = useTranslation(); return
    {
    - profile + profile

    - {data.steam.personname} + { data.steam.personname } { data.player.verified && }

    {
    - {Math.floor(data.player.trainDistance / 1000)}km + { Math.floor(data.player.trainDistance / 1000) }km - {t('profile.stats.distance')} + { t("profile.stats.distance") }
    - {Math.floor(data.player.dispatcherTime / 3600000)}h + { formatTime(data.player.dispatcherTime) } - {t('profile.stats.time')} + { t("profile.stats.time") }
    - {Object.keys(data.player.trainStats || {}).length > 0 && + { Object.keys(data.player.trainStats || {}).length > 0 &&
    -
    setShowTrains(val => !val)}> -

    {t('profile.trains.header')}

    - +
    setShowTrains(val => !val) }> +

    { t("profile.trains.header") }

    +
    - {showTrains && + { showTrains &&
    - {t('profile.trains.train')} + { t("profile.trains.train") }
    - {t('profile.trains.distance')} + { t("profile.trains.distance") }
    - {t('profile.trains.points')} + { t("profile.trains.points") }
    - {t('profile.trains.time')} + { t("profile.trains.time") }
    - {Object.keys(data.player.trainStats).map(trainName => { - const train = data.player.trainStats[trainName]; + { Object.keys(data.player.trainStats).map(trainName => + { + const train = data.player.trainStats[ trainName ]; return

    - {trainName} + { trainName }

    -

    {Math.floor(train.distance / 1000)}km

    +

    { Math.floor(train.distance / 1000) }km

    -

    {train.score}

    +

    { train.score }

    -

    {Math.floor(train.time / 3600000)}h

    +

    { formatTime(train.time) }

    ; - })} + }) } -
    } +
    } -
    } - {Object.keys(data.player.dispatcherStats || {}).length > 0 && + } + { Object.keys(data.player.dispatcherStats || {}).length > 0 &&
    -
    setShowStations(val => !val)}> -

    {t('profile.stations.header')}

    - +
    setShowStations(val => !val) }> +

    { t("profile.stations.header") }

    +
    - {showStations && + { showStations &&
    - {t('profile.stations.station')} + { t("profile.stations.station") }
    - {t('profile.stations.time')} + { t("profile.stations.time") }
    - {Object.keys(data.player.dispatcherStats).map(stationName => { - const station = data.player.dispatcherStats[stationName]; + { Object.keys(data.player.dispatcherStats).map(stationName => + { + const station = data.player.dispatcherStats[ stationName ]; return

    - {stationName} + { stationName }

    -

    {Math.floor(station.time / 3600000)}h

    +

    {formatTime(station.time) }

    ; - })} + }) } -
    } +
    } -
    } + } ; }; \ No newline at end of file diff --git a/packages/frontend/src/hooks/useColorMode.tsx b/packages/frontend/src/hooks/useColorMode.tsx index ae8e78b..d11d3c5 100644 --- a/packages/frontend/src/hooks/useColorMode.tsx +++ b/packages/frontend/src/hooks/useColorMode.tsx @@ -11,8 +11,8 @@ const useColorMode = () => const bodyClass = window.document.body.classList; colorMode === "dark" - ? bodyClass.add(className) - : bodyClass.remove(className); + ? bodyClass.add(className) + : bodyClass.remove(className); }, [ colorMode ]); return [ colorMode, setColorMode ]; diff --git a/packages/frontend/src/hooks/useLocalStorage.tsx b/packages/frontend/src/hooks/useLocalStorage.tsx index 5047e9a..9415f7f 100644 --- a/packages/frontend/src/hooks/useLocalStorage.tsx +++ b/packages/frontend/src/hooks/useLocalStorage.tsx @@ -3,8 +3,8 @@ import { useEffect, useState } from "react"; type SetValue = T | ((val: T) => T); function useLocalStorage( - key: string, - initialValue: T, + key: string, + initialValue: T, ): [ T, (value: SetValue) => void ] { const [ storedValue, setStoredValue ] = useState(() => @@ -25,9 +25,9 @@ function useLocalStorage( try { const valueToStore = - typeof storedValue === "function" - ? storedValue(storedValue) - : storedValue; + typeof storedValue === "function" + ? storedValue(storedValue) + : storedValue; window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { diff --git a/packages/frontend/src/i18n/languages/en.json b/packages/frontend/src/i18n/languages/en.json index 09f46a1..1cf9e6f 100644 --- a/packages/frontend/src/i18n/languages/en.json +++ b/packages/frontend/src/i18n/languages/en.json @@ -1,134 +1,137 @@ { - "_": { - "title": "Simrail Stats" + "_": { + "title": "Simrail Stats" + }, + "preview": { + "title": "Preview version!", + "description": "The site is in version V3-PREVIEW, may contain errors. I will be grateful for reporting all errors on the project page (git.alekswilc.dev) or in a private message on discord - alekswilc. Stay tuned!" + }, + "home": { + "stats": { + "trains": "Trains", + "dispatchers": "Dispatchers", + "profiles": "Profiles" }, - "preview": { - "title": "Preview version!", - "description": "The site is in version V3-PREVIEW, may contain errors. I will be grateful for reporting all errors on the project page (git.alekswilc.dev) or in a private message on discord - alekswilc. Stay tuned!" + "title": "Simrail Stats", + "description": "Simrail Stats - The best SimRail logs and statistics site!", + "buttons": { + "project": "Project page", + "forum": "Forum Page" }, - "home": { - "stats": { - "trains": "Trains", - "dispatchers": "Dispatchers", - "profiles": "Profiles" - }, - "title": "Simrail Stats", - "description": "Simrail Stats - The best SimRail logs and statistics site!", - "buttons": { - "project": "Project page", - "forum": "Forum Page" - }, - "footer": { - "license": "License:", - "powered": "Based on:", - "thanks": "Special thanks to BAHU.PRO hosting, Simrail ELITE discord, Simrail community and my girlfriend", - "author": "Created by {{author}} with ❤️ for the Simrail community" - } + "footer": { + "license": "License:", + "powered": "Based on:", + "thanks": "Special thanks to BAHU.PRO hosting, Simrail ELITE discord, Simrail community and my girlfriend", + "author": "Created by {{author}} with ❤️ for the Simrail community" + } + }, + "leaderboard": { + "user": "Player", + "time": "Time", + "distance": "Distance", + "points": "Points", + "profile": "Profile", + "actions": "Actions" + }, + "logs": { + "user": "Player", + "time": "Time", + "distance": "Distance", + "points": "Points", + "profile": "Profile", + "record": "Record", + "train": "Train", + "actions": "Actions", + "search": "Type to search", + "station": "Station" + }, + "content_loader": { + "error": { + "header": "Page loading error", + "description": "Check your internet connection and refresh the page", + "report": "Report a bug", + "refresh": "Refresh" }, - "leaderboard": { - "user": "Player", - "time": "Time", - "distance": "Distance", - "points": "Points", - "profile": "Profile", - "actions": "Actions" + "notfound": { + "header": "No data found", + "description": "Your search returned no results." + } + }, + "profile": { + "stats": { + "distance": "Driver experience", + "time": "Dispatcher experience" }, - "logs": { - "user": "Player", - "time": "Time", - "distance": "Distance", - "points": "Points", - "profile": "Profile", - "record": "Record", - "train": "Train", - "actions": "Actions", - "search": "Type to search", - "station": "Station" + "trains": { + "header": "Train Statistics", + "train": "Train", + "distance": "Distance", + "points": "Points", + "time": "Time" }, - "content_loader": { - "error": { - "header": "Page loading error", - "description": "Check your internet connection and refresh the page", - "report": "Report a bug", - "refresh": "Refresh" - }, - "notfound": { - "header": "No data found", - "description": "Your search returned no results." - } + "stations": { + "header": "Station Statistics", + "station": "Station", + "time": "Time" }, - "profile": { - "stats": { - "distance": "Kilometers traveled", - "time": "Dispatcher hours" - }, - "trains": { - "header": "Train Statistics", - "train": "Train", - "distance": "Distance", - "points": "Points", - "time": "Time" - }, - "stations": { - "header": "Station Statistics", - "station": "Station", - "time": "Time" - }, - "errors": { - "notfound": { - "title": "Profile not found", - "description": "Player's profile could not be found or the player has a private Steam profile." - }, - "blacklist": { - "title": "Unable to display profile", - "description": "This player's profile has been blocked." - } - } + "errors": { + "notfound": { + "title": "Profile not found", + "description": "Player's profile could not be found or the player has a private Steam profile." + }, + "blacklist": { + "title": "Unable to display profile", + "description": "This player's profile has been blocked." + } + } + }, + "log": { + "errors": { + "notfound": { + "title": "Record not found", + "description": "This record could not be found." + }, + "blacklist": { + "title": "The record cannot be displayed", + "description": "The record has been blocked." + } }, - "log": { - "errors": { - "title": "Record not found", - "description": "This record could not be found." - }, - "blacklist": { - "title": "The record cannot be displayed", - "description": "The record has been blocked." - }, - "station": { - "header": "Leaving the station", - "server": "Server: {{server}}", - "station": "Station: {{name}} - {{short}}", - "joined": "Date of entry: {{date}}", - "left": "Date of exit: {{date}}", - "spent": "Time spent: {{date}}" - }, - "train": { - "header": "Leaving the train", - "server": "Server: {{server}}", - "train": "Train: {{name}} {{number}}", - "joined": "Date of entry: {{date}}", - "left": "Date of exit: {{date}}", - "spent": "Time spent: {{date}}", - "distance": "Distance: {{distance}}km", - "points": "Points: {{points}}" - }, - "toasts": { - "copied": "Link copied to clipboard!", - "report": "The outpost exit data has been copied to your clipboard, you can use it to submit to the #multiplayer-help-requests channel on the official simrail Discord server. Don't forget to add a reason for the report!" - }, - "buttons": { - "report": "Report", - "copy": "Copy link" - } + "station": { + "header": "Leaving the station", + "server": "Server: {{server}}", + "station": "Station: {{name}} - {{short}}", + "joined": "Date of entry: {{date}}", + "left": "Date of exit: {{date}}", + "spent": "Time spent: {{date}}" }, - "sidebar": { - "home": "Home page", - "logs": "Logs", - "stations": "Stations", - "trains": "Trains", - "leaderboard": "Leaderboard", - "info": "INFO", - "admin": "ADMIN" + "train": { + "header": "Leaving the train", + "server": "Server: {{server}}", + "train": "Train: {{name}} {{number}}", + "joined": "Date of entry: {{date}}", + "left": "Date of exit: {{date}}", + "spent": "Time spent: {{date}}", + "distance": "Distance: {{distance}}km", + "points": "Points: {{points}}" }, - "admin": {} + "toasts": { + "copied": "Link copied to clipboard!", + "report": "The outpost exit data has been copied to your clipboard, you can use it to submit to the #multiplayer-help-requests channel on the official simrail Discord server. Don't forget to add a reason for the report!" + }, + "buttons": { + "report": "Report", + "copy": "Copy link", + "profile": "Profile" + } + }, + "sidebar": { + "home": "Home page", + "logs": "Logs", + "stations": "Stations", + "trains": "Trains", + "leaderboard": "Leaderboard", + "info": "INFO", + "admin": "ADMIN" + }, + "admin": {} } \ No newline at end of file diff --git a/packages/frontend/src/i18n/languages/pl.json b/packages/frontend/src/i18n/languages/pl.json index fd68169..f8c6c72 100644 --- a/packages/frontend/src/i18n/languages/pl.json +++ b/packages/frontend/src/i18n/languages/pl.json @@ -59,8 +59,8 @@ }, "profile": { "stats": { - "distance": "Przejechanych kilometrów", - "time": "Godzin dyżurów" + "distance": "Staż maszynisty", + "time": "Staż dyżurnego ruchu" }, "trains": { "header": "Statystyki pociągów", @@ -87,12 +87,14 @@ }, "log": { "errors": { - "title": "Nie znaleziono rekordu", - "description": "Ten rekord nie został znaleziony." - }, - "blacklist": { - "title": "Nie można wyświetlić rekordu", - "description": "Rekord został zablokowany." + "notfound": { + "title": "Nie znaleziono rekordu", + "description": "Ten rekord nie został znaleziony." + }, + "blacklist": { + "title": "Nie można wyświetlić rekordu", + "description": "Rekord został zablokowany." + } }, "station": { "header": "Wyjście z posterunku", @@ -118,7 +120,8 @@ }, "buttons": { "report": "Zgłoś", - "copy": "Kopiuj link" + "copy": "Kopiuj link", + "profile": "Profil" } }, "sidebar": { diff --git a/packages/frontend/src/layout/DefaultLayout.tsx b/packages/frontend/src/layout/DefaultLayout.tsx index 927037c..173c734 100644 --- a/packages/frontend/src/layout/DefaultLayout.tsx +++ b/packages/frontend/src/layout/DefaultLayout.tsx @@ -1,19 +1,20 @@ -import React, { useState, ReactNode } from 'react'; -import { Header } from '../components/mini/header/Header'; -import { Sidebar } from '../components/mini/sidebar/Sidebar'; +import React, { useState, ReactNode } from "react"; +import { Header } from "../components/mini/header/Header"; +import { Sidebar } from "../components/mini/sidebar/Sidebar"; -export const DefaultLayout: React.FC<{ children: ReactNode }> = ({ children }) => { - const [sidebarOpen, setSidebarOpen] = useState(false); +export const DefaultLayout: React.FC<{ children: ReactNode }> = ({ children }) => +{ + const [ sidebarOpen, setSidebarOpen ] = useState(false); return (
    - +
    -
    +
    - {children} + { children }
    diff --git a/packages/frontend/src/main.tsx b/packages/frontend/src/main.tsx index 2bcb3ea..30fd24c 100644 --- a/packages/frontend/src/main.tsx +++ b/packages/frontend/src/main.tsx @@ -16,9 +16,9 @@ dayjs.extend(relativeTime); ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - - - , + + + + + , ); diff --git a/packages/frontend/src/pages/Home.tsx b/packages/frontend/src/pages/Home.tsx index 5deb29e..3c241aa 100644 --- a/packages/frontend/src/pages/Home.tsx +++ b/packages/frontend/src/pages/Home.tsx @@ -1,21 +1,24 @@ -import React, { useEffect, useState } from 'react'; -import { useTranslation, Trans } from 'react-i18next'; -import { Link } from 'react-router-dom'; -import { TStatsResponse } from '../types/stats.ts'; -import { WarningAlert } from '../components/mini/alerts/Warning.tsx'; -import { CardDataStats } from '../components/mini/util/CardDataStats.tsx'; +import React, { useEffect, useState } from "react"; +import { useTranslation, Trans } from "react-i18next"; +import { Link } from "react-router-dom"; +import { TStatsResponse } from "../types/stats.ts"; +import { WarningAlert } from "../components/mini/alerts/Warning.tsx"; +import { CardDataStats } from "../components/mini/util/CardDataStats.tsx"; -export const Home: React.FC = () => { +export const Home: React.FC = () => +{ const { t } = useTranslation(); - const [commit, setCommit] = useState(''); - const [version, setVersion] = useState(''); - const [trains, setTrains] = useState(0); - const [dispatchers, setDispatchers] = useState(0); - const [profiles, setProfiles] = useState(0); + const [ commit, setCommit ] = useState(""); + const [ version, setVersion ] = useState(""); + const [ trains, setTrains ] = useState(0); + const [ dispatchers, setDispatchers ] = useState(0); + const [ profiles, setProfiles ] = useState(0); - useEffect(() => { - fetch(`${import.meta.env.VITE_API_URL}/stats/`).then(x => x.json()).then((data: TStatsResponse) => { + useEffect(() => + { + fetch(`${ import.meta.env.VITE_API_URL }/stats/`).then(x => x.json()).then((data: TStatsResponse) => + { data.data.git.commit && setCommit(data.data.git.commit); data.data.git.version && setVersion(data.data.git.version); @@ -29,14 +32,14 @@ export const Home: React.FC = () => { return ( <>
    - +
    - - - + + +
    @@ -45,22 +48,22 @@ export const Home: React.FC = () => {

    - {t('home.title')} + { t("home.title") }

    -

    {t('home.description')}

    +

    { t("home.description") }

    - {t('home.buttons.project')} + { t("home.buttons.project") } - {t('home.buttons.forum')} + { t("home.buttons.forum") }
    @@ -73,30 +76,31 @@ export const Home: React.FC = () => {

    - }} + i18nKey={ t("home.footer.author") } + values={ { author: "alekswilc" } } + components={ { + anchor: , + } } />

    , + i18nKey={ t("home.footer.thanks") } + components={ { + bahu: , simrailelite: - }} + to={ "https://discord.gg/yDhy3pDrVr" }/>, + } } />

    -

    {t('home.footer.license')} GNU +

    { t("home.footer.license") } GNU AGPL V3

    -

    {t('home.footer.powered')} TailAdmin

    +

    { t("home.footer.powered") } TailAdmin +

    -

    {version && {version}}{version && commit && ' | '}{commit && +

    { version && { version } }{ version && commit && " | " }{ commit && {commit}}

    + to={ `https://git.alekswilc.dev/simrail/simrail.alekswilc.dev/commit/${ commit }` }>{ commit } }

    diff --git a/packages/frontend/src/pages/leaderboard/StationsLeaderboard.tsx b/packages/frontend/src/pages/leaderboard/StationsLeaderboard.tsx index da421b7..4e79f7a 100644 --- a/packages/frontend/src/pages/leaderboard/StationsLeaderboard.tsx +++ b/packages/frontend/src/pages/leaderboard/StationsLeaderboard.tsx @@ -3,24 +3,30 @@ import { ChangeEvent, useEffect, useState } from "react"; import { StationTable } from "../../components/pages/leaderboard/StationTable.tsx"; import { useDebounce } from "use-debounce"; import { Search } from "../../components/mini/util/Search.tsx"; +import { useSearchParams } from "react-router-dom"; export const StationLeaderboard = () => { const [ data, setData ] = useState([]); + const [ searchParams, setSearchParams ] = useSearchParams(); + const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? ""); useEffect(() => { fetch(`${ import.meta.env.VITE_API_URL }leaderboard/station/`).then(x => x.json()).then(x => { + setData(x.data.records); }); }, []); - const [ searchItem, setSearchItem ] = useState(""); const [ searchValue ] = useDebounce(searchItem, 500); const [ error, setError ] = useState<0 | 1 | 2>(0); useEffect(() => { + searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue); + setSearchParams(searchParams); + setData([]); setError(0); fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/station/?q=${ searchValue }`).then(x => x.json()).then(x => @@ -30,17 +36,22 @@ export const StationLeaderboard = () => }); }, [ searchValue ]); + useEffect(() => + { + setSearchItem(searchParams.get("q") ?? ""); + }, [ searchParams ]); + const handleInputChange = (e: ChangeEvent) => { setSearchItem(e.target.value); }; return ( - <> -
    - - -
    - + <> +
    + + +
    + ); }; diff --git a/packages/frontend/src/pages/leaderboard/TrainLeaderboard.tsx b/packages/frontend/src/pages/leaderboard/TrainLeaderboard.tsx index cb651be..785d078 100644 --- a/packages/frontend/src/pages/leaderboard/TrainLeaderboard.tsx +++ b/packages/frontend/src/pages/leaderboard/TrainLeaderboard.tsx @@ -3,10 +3,14 @@ import { ChangeEvent, useEffect, useState } from "react"; import { TrainTable } from "../../components/pages/leaderboard/TrainTable.tsx"; import { useDebounce } from "use-debounce"; import { Search } from "../../components/mini/util/Search.tsx"; +import { useSearchParams } from "react-router-dom"; export const TrainLeaderboard = () => { const [ data, setData ] = useState([]); + const [ searchParams, setSearchParams ] = useSearchParams(); + const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? ""); + useEffect(() => { fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/train/`).then(x => x.json()).then(x => @@ -15,12 +19,14 @@ export const TrainLeaderboard = () => }); }, []); - const [ searchItem, setSearchItem ] = useState(""); const [ searchValue ] = useDebounce(searchItem, 500); const [ error, setError ] = useState<0 | 1 | 2>(0); useEffect(() => { + searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue); + setSearchParams(searchParams); + setData([]); setError(0); fetch(`${ import.meta.env.VITE_API_URL }/leaderboard/train/?q=${ searchValue }`).then(x => x.json()).then(x => @@ -30,18 +36,23 @@ export const TrainLeaderboard = () => }); }, [ searchValue ]); + useEffect(() => + { + setSearchItem(searchParams.get("q") ?? ""); + }, [ searchParams ]); + const handleInputChange = (e: ChangeEvent) => { setSearchItem(e.target.value); }; return ( - <> -
    - - + <> +
    + + -
    - +
    + ); }; diff --git a/packages/frontend/src/pages/log/Log.tsx b/packages/frontend/src/pages/log/Log.tsx index a39c4a0..654dcfc 100644 --- a/packages/frontend/src/pages/log/Log.tsx +++ b/packages/frontend/src/pages/log/Log.tsx @@ -1,23 +1,27 @@ -import { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; -import { ContentLoader } from '../../components/mini/loaders/ContentLoader.tsx'; -import { WarningAlert } from '../../components/mini/alerts/Warning.tsx'; -import { useTranslation } from 'react-i18next'; -import { TLogResponse, TLogStationData, TLogTrainData } from '../../types/log.ts'; -import { StationLog } from '../../components/pages/log/StationLog.tsx'; -import { TrainLog } from '../../components/pages/log/TrainLog.tsx'; -import { PageMeta } from '../../components/mini/util/PageMeta.tsx'; +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { ContentLoader } from "../../components/mini/loaders/ContentLoader.tsx"; +import { WarningAlert } from "../../components/mini/alerts/Warning.tsx"; +import { useTranslation } from "react-i18next"; +import { TLogResponse, TLogStationData, TLogTrainData } from "../../types/log.ts"; +import { StationLog } from "../../components/pages/log/StationLog.tsx"; +import { TrainLog } from "../../components/pages/log/TrainLog.tsx"; +import { PageMeta } from "../../components/mini/util/PageMeta.tsx"; -export const Log = () => { +export const Log = () => +{ const { id } = useParams(); - const [error, setError] = useState<0 | 1 | 2 | 3>(0); - const [trainData, setTrainData] = useState(undefined!); - const [stationData, setStationData] = useState(undefined!); + const [ error, setError ] = useState<0 | 1 | 2 | 3>(0); + const [ trainData, setTrainData ] = useState(undefined!); + const [ stationData, setStationData ] = useState(undefined!); - useEffect(() => { - fetch(`${import.meta.env.VITE_API_URL}/log/${id}`).then(x => x.json()).then((data: TLogResponse) => { - switch (data.code) { + useEffect(() => + { + fetch(`${ import.meta.env.VITE_API_URL }/log/${ id }`).then(x => x.json()).then((data: TLogResponse) => + { + switch (data.code) + { case 404: setError(2); break; @@ -28,7 +32,7 @@ export const Log = () => { case 200: setError(1); - 'trainNumber' in data.data ? setTrainData(data.data) : setStationData(data.data); + "trainNumber" in data.data ? setTrainData(data.data) : setStationData(data.data); break; } }); @@ -38,30 +42,30 @@ export const Log = () => { return ( <> - {/* LOADING */} - {error === 0 && } - {/* NOT FOUND */} - {error === 2 && } - {error === 2 && } - {/* BLACKLISTED LOG */} - {error === 3 && } - {error === 3 && } - {/* SUCCESS */} - {error === 1 && stationData && } - {error === 1 && stationData && < StationLog data={stationData} />} + {/* LOADING */ } + { error === 0 && } + {/* NOT FOUND */ } + { error === 2 && } + { error === 2 && } + {/* BLACKLISTED LOG */ } + { error === 3 && } + { error === 3 && } + {/* SUCCESS */ } + { error === 1 && stationData && } + { error === 1 && stationData && < StationLog data={ stationData }/> } - {error === 1 && trainData && } - {error === 1 && trainData && < TrainLog data={trainData} />} + { error === 1 && trainData && } + { error === 1 && trainData && < TrainLog data={ trainData }/> } ); }; diff --git a/packages/frontend/src/pages/logs/StationLogs.tsx b/packages/frontend/src/pages/logs/StationLogs.tsx index 85bb983..1705518 100644 --- a/packages/frontend/src/pages/logs/StationLogs.tsx +++ b/packages/frontend/src/pages/logs/StationLogs.tsx @@ -3,10 +3,13 @@ import { StationTable } from "../../components/pages/logs/StationTable.tsx"; import { useDebounce } from "use-debounce"; import { TStationRecord } from "../../types/station.ts"; import { Search } from "../../components/mini/util/Search.tsx"; +import { useSearchParams } from "react-router-dom"; export const StationLogs = () => { const [ data, setData ] = useState([]); + const [ searchParams, setSearchParams ] = useSearchParams(); + const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? ""); useEffect(() => { fetch(`${ import.meta.env.VITE_API_URL }/stations/`).then(x => x.json()).then(x => @@ -15,11 +18,13 @@ export const StationLogs = () => }); }, []); const [ error, setError ] = useState<0 | 1 | 2>(0); - const [ searchItem, setSearchItem ] = useState(""); const [ searchValue ] = useDebounce(searchItem, 500); useEffect(() => { + searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue); + setSearchParams(searchParams); + setData([]); setError(0); fetch(`${ import.meta.env.VITE_API_URL }/stations/?q=${ searchValue }`).then(x => x.json()).then(x => @@ -29,17 +34,22 @@ export const StationLogs = () => }); }, [ searchValue ]); + useEffect(() => + { + setSearchItem(searchParams.get("q") ?? ""); + }, [ searchParams ]); + const handleInputChange = (e: ChangeEvent) => { setSearchItem(e.target.value); }; return ( - <> -
    - - -
    - + <> +
    + + +
    + ); }; diff --git a/packages/frontend/src/pages/logs/TrainLogs.tsx b/packages/frontend/src/pages/logs/TrainLogs.tsx index 07c9206..45daaeb 100644 --- a/packages/frontend/src/pages/logs/TrainLogs.tsx +++ b/packages/frontend/src/pages/logs/TrainLogs.tsx @@ -3,10 +3,14 @@ import { TTrainRecord } from "../../types/train.ts"; import { TrainTable } from "../../components/pages/logs/TrainTable.tsx"; import { useDebounce } from "use-debounce"; import { Search } from "../../components/mini/util/Search.tsx"; +import { useSearchParams } from "react-router-dom"; export const TrainLogs = () => { const [ data, setData ] = useState([]); + const [ searchParams, setSearchParams ] = useSearchParams(); + const [ searchItem, setSearchItem ] = useState(searchParams.get("q") ?? ""); + useEffect(() => { fetch(`${ import.meta.env.VITE_API_URL }/trains/`).then(x => x.json()).then(x => @@ -14,12 +18,15 @@ export const TrainLogs = () => setData(x.data.records); }); }, []); + const [ error, setError ] = useState<0 | 1 | 2>(0); - const [ searchItem, setSearchItem ] = useState(""); const [ searchValue ] = useDebounce(searchItem, 500); useEffect(() => { + searchValue === "" ? searchParams.delete("q") : searchParams.set("q", searchValue); + setSearchParams(searchParams); + setData([]); setError(0); fetch(`${ import.meta.env.VITE_API_URL }/trains/?q=${ searchValue }`).then(x => x.json()).then(x => @@ -29,17 +36,22 @@ export const TrainLogs = () => }); }, [ searchValue ]); + useEffect(() => + { + setSearchItem(searchParams.get("q") ?? ""); + }, [ searchParams ]); + const handleInputChange = (e: ChangeEvent) => { setSearchItem(e.target.value); }; return ( - <> -
    - - -
    - + <> +
    + + +
    + ); }; diff --git a/packages/frontend/src/pages/profile/Profile.tsx b/packages/frontend/src/pages/profile/Profile.tsx index a2ee699..e9f63f7 100644 --- a/packages/frontend/src/pages/profile/Profile.tsx +++ b/packages/frontend/src/pages/profile/Profile.tsx @@ -1,22 +1,25 @@ -import { useEffect, useState } from 'react'; -import { useParams } from 'react-router-dom'; -import { TProfileData, TProfileResponse } from '../../types/profile.ts'; -import { ContentLoader } from '../../components/mini/loaders/ContentLoader.tsx'; -import { WarningAlert } from '../../components/mini/alerts/Warning.tsx'; -import { ProfileCard } from '../../components/pages/profile/Profile.tsx'; -import { useTranslation } from 'react-i18next'; -import { PageMeta } from '../../components/mini/util/PageMeta.tsx'; +import { useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { TProfileData, TProfileResponse } from "../../types/profile.ts"; +import { ContentLoader } from "../../components/mini/loaders/ContentLoader.tsx"; +import { WarningAlert } from "../../components/mini/alerts/Warning.tsx"; +import { ProfileCard } from "../../components/pages/profile/Profile.tsx"; +import { useTranslation } from "react-i18next"; +import { PageMeta } from "../../components/mini/util/PageMeta.tsx"; +import { formatTime } from "../../util/time.ts"; -export const Profile = () => { +export const Profile = () => +{ const { id } = useParams(); + const [ error, setError ] = useState<0 | 1 | 2 | 3>(0); + const [ data, setData ] = useState(undefined!); - - const [error, setError] = useState<0 | 1 | 2 | 3>(0); - const [data, setData] = useState(undefined!); - - useEffect(() => { - fetch(`${import.meta.env.VITE_API_URL}/profiles/${id}`).then(x => x.json()).then((data: TProfileResponse) => { - switch (data.code) { + useEffect(() => + { + fetch(`${ import.meta.env.VITE_API_URL }/profiles/${ id }`).then(x => x.json()).then((data: TProfileResponse) => + { + switch (data.code) + { case 404: setError(2); break; @@ -35,24 +38,24 @@ export const Profile = () => { return ( <> - {/* LOADING */} - {error === 0 && } - {/* NOT FOUND */} - {error === 2 && } - {error === 2 && } - {/* BLACKLISTED PROFILE */} - {error === 3 && } - {error === 3 && } - {/* SUCCESS */} - {error === 1 && } - {error === 1 && } + {/* LOADING */ } + { error === 0 && } + {/* NOT FOUND */ } + { error === 2 && } + { error === 2 && } + {/* BLACKLISTED PROFILE */ } + { error === 3 && } + { error === 3 && } + {/* SUCCESS */ } + { error === 1 && } + { error === 1 && } ); }; diff --git a/packages/frontend/src/types/leaderboard.ts b/packages/frontend/src/types/leaderboard.ts index c69e3ec..19229e5 100644 --- a/packages/frontend/src/types/leaderboard.ts +++ b/packages/frontend/src/types/leaderboard.ts @@ -21,6 +21,7 @@ export interface TLeaderboardRecord dispatcherTime: number; dispatcherStats?: { [ key: string ]: TLeaderboardDispatcherStat }; trainStats?: { [ key: string ]: TLeaderboardTrainStat }; + verified: boolean; } export interface TLeaderboardDispatcherStat diff --git a/packages/frontend/src/types/log.ts b/packages/frontend/src/types/log.ts index 34d8bab..1213b51 100644 --- a/packages/frontend/src/types/log.ts +++ b/packages/frontend/src/types/log.ts @@ -18,7 +18,7 @@ export interface TLogTrainData server: string; trainName: string; joinedDate?: number; - + verified: boolean; } export interface TLogStationData @@ -32,4 +32,6 @@ export interface TLogStationData stationShort: string; server: string; joinedDate?: number; + verified: boolean; + } diff --git a/packages/frontend/src/types/profile.ts b/packages/frontend/src/types/profile.ts index d6a8030..c8e37ad 100644 --- a/packages/frontend/src/types/profile.ts +++ b/packages/frontend/src/types/profile.ts @@ -32,6 +32,7 @@ export interface TProfilePlayer trainStats: Record; trainDistance: number; trainPoints: number; + verified: boolean; } export interface TProfileDispatcherStatsRecord diff --git a/packages/frontend/src/types/station.ts b/packages/frontend/src/types/station.ts index 6dc5183..12f3d08 100644 --- a/packages/frontend/src/types/station.ts +++ b/packages/frontend/src/types/station.ts @@ -21,4 +21,5 @@ export interface TStationRecord stationShort: string; server: string; joinedDate?: number; + verified: boolean; } \ No newline at end of file diff --git a/packages/frontend/src/types/train.ts b/packages/frontend/src/types/train.ts index 758d6cb..54bc3e8 100644 --- a/packages/frontend/src/types/train.ts +++ b/packages/frontend/src/types/train.ts @@ -23,5 +23,6 @@ export interface TTrainRecord points: number; server: string; trainName: string; + verified: boolean; } diff --git a/packages/frontend/src/util/time.ts b/packages/frontend/src/util/time.ts new file mode 100644 index 0000000..9837837 --- /dev/null +++ b/packages/frontend/src/util/time.ts @@ -0,0 +1,14 @@ +export const formatTime = (time: number) => +{ + if (Math.floor(time / 3600000) > 0) + { + return `${ Math.floor(time / 3600000) }h`; + } + + if (Math.floor(time / 60000) > 0) + { + return `${ Math.floor(time / 60000) }m` + } + + return '0h'; +} \ No newline at end of file From d86f477434929052c4e2beb55a7104a027396ecc Mon Sep 17 00:00:00 2001 From: alekswilc Date: Sat, 9 Nov 2024 19:55:23 +0100 Subject: [PATCH 2/2] feat(backend): Add verification mark on user profile --- packages/backend/src/http/routes/log.ts | 9 ++++++++- packages/backend/src/http/routes/profile.ts | 15 ++++++--------- packages/backend/src/http/routes/stations.ts | 12 ++++++++++-- packages/backend/src/http/routes/trains.ts | 12 +++++++++++- packages/backend/src/mongo/profile.ts | 6 ++++++ 5 files changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/backend/src/http/routes/log.ts b/packages/backend/src/http/routes/log.ts index aed9c01..ecc2ada 100644 --- a/packages/backend/src/http/routes/log.ts +++ b/packages/backend/src/http/routes/log.ts @@ -4,6 +4,7 @@ import { MBlacklist } from "../../mongo/blacklist.js"; import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js"; import { removeProperties } from "../../util/functions.js"; import { ILog, MLog } from "../../mongo/logs.js"; +import { MProfile } from "../../mongo/profile.js"; export class LogRoute @@ -24,6 +25,7 @@ export class LogRoute } const log = await MLog.findOne({ id }) || await MTrainLog.findOne({ id }); + if (!log) { res.status(404).json(new ErrorResponseBuilder() @@ -31,8 +33,13 @@ export class LogRoute .setData("Invalid Id parameter").toJSON()); return; } + const profile = await MProfile.findOne({ steam: log.userSteamId }); - res.status(200).json(new SuccessResponseBuilder().setCode(200).setData(removeProperties>(log.toJSON(), [ "_id", "__v" ]))); + + res.status(200).json(new SuccessResponseBuilder().setCode(200).setData({ + verified: profile?.verified, + ...removeProperties>(log.toJSON(), [ "_id", "__v" ]) + })); }); return app; diff --git a/packages/backend/src/http/routes/profile.ts b/packages/backend/src/http/routes/profile.ts index 5528e18..ddb72ad 100644 --- a/packages/backend/src/http/routes/profile.ts +++ b/packages/backend/src/http/routes/profile.ts @@ -4,6 +4,7 @@ import { MProfile } from "../../mongo/profile.js"; import { MBlacklist } from "../../mongo/blacklist.js"; import { SteamUtil } from "../../util/SteamUtil.js"; import { ErrorResponseBuilder, SuccessResponseBuilder } from "../responseBuilder.js"; +import { removeProperties } from "../../util/functions.js"; export class ProfilesRoute { @@ -11,7 +12,6 @@ export class ProfilesRoute { const app = Router(); - app.get("/:id", async (req, res) => { if (!req.params.id) @@ -19,13 +19,15 @@ export class ProfilesRoute res.redirect("/"); return; } + const player = await MProfile.findOne({ steam: req.params.id }); if (!player) { res.status(404).json(new ErrorResponseBuilder() - .setCode(404).setData("Profile not found! (propably private)")); + .setCode(404).setData("Profile not found! (probably private)")); return; } + const blacklist = await MBlacklist.findOne({ steam: req.params.id! }); if (blacklist && blacklist.status) { @@ -33,23 +35,18 @@ export class ProfilesRoute .setCode(403).setData("Profile blacklisted!")); return; } + const steam = await SteamUtil.getPlayer(player?.steam!); const steamStats = await SteamUtil.getPlayerStats(player?.steam!); - res.json( new SuccessResponseBuilder() .setCode(200) .setData({ - player, steam, steamStats, + player: removeProperties(player, ['_id', '__v']), steam, steamStats, }) .toJSON(), ); - - res.render("profiles/index.ejs", { - player, steam, steamStats: steamStats, - msToTime, - }); }); return app; diff --git a/packages/backend/src/http/routes/stations.ts b/packages/backend/src/http/routes/stations.ts index 53f1a37..8070792 100644 --- a/packages/backend/src/http/routes/stations.ts +++ b/packages/backend/src/http/routes/stations.ts @@ -8,6 +8,7 @@ import { SteamUtil } from "../../util/SteamUtil.js"; import { GitUtil } from "../../util/git.js"; import { removeProperties } from "../../util/functions.js"; import { SuccessResponseBuilder } from "../responseBuilder.js"; +import { MProfile } from "../../mongo/profile.js"; const generateSearch = (regex: RegExp) => [ { @@ -36,7 +37,7 @@ export class StationsRoute app.get("/", async (req, res) => { const s = req.query.q?.toString().split(",").map(x => new RegExp(x, "i")); - + const profiles = await MProfile.find({ verified: true }); const filter: PipelineStage[] = []; @@ -48,13 +49,20 @@ export class StationsRoute }, }); + const records = await MLog.aggregate(filter) .sort({ leftDate: -1 }) .limit(30); + res.json( new SuccessResponseBuilder<{ records: Omit[] }>() .setCode(200) - .setData({ records: records.map(x => removeProperties>(x, [ "_id", "__v" ])) }) + .setData({ records: records.map(x => { + return { + ...removeProperties>(x, [ "_id", "__v" ]), + verified: profiles.find(xx => xx.steam === x.userSteamId) + } + }) }) .toJSON(), ); }); diff --git a/packages/backend/src/http/routes/trains.ts b/packages/backend/src/http/routes/trains.ts index d9accc1..8ace493 100644 --- a/packages/backend/src/http/routes/trains.ts +++ b/packages/backend/src/http/routes/trains.ts @@ -8,6 +8,7 @@ import { SteamUtil } from "../../util/SteamUtil.js"; import { GitUtil } from "../../util/git.js"; import { SuccessResponseBuilder } from "../responseBuilder.js"; import { removeProperties } from "../../util/functions.js"; +import { MProfile } from "../../mongo/profile.js"; const generateSearch = (regex: RegExp) => [ { @@ -33,6 +34,7 @@ export class TrainsRoute app.get("/", async (req, res) => { const s = req.query.q?.toString().split(",").map(x => new RegExp(x, "i")); + const profiles = await MProfile.find({ verified: true }); const filter: PipelineStage[] = []; @@ -53,7 +55,15 @@ export class TrainsRoute res.json( new SuccessResponseBuilder<{ records: Omit[] }>() .setCode(200) - .setData({ records: records.map(x => removeProperties>(x, [ "_id", "__v" ])) }) + .setData({ + records: records.map(x => + { + return { + ...removeProperties>(x, [ "_id", "__v" ]), + verified: profiles.find(xx => xx.steam === x.userSteamId) + }; + }), + }) .toJSON(), ); }); diff --git a/packages/backend/src/mongo/profile.ts b/packages/backend/src/mongo/profile.ts index 194684f..c646c41 100644 --- a/packages/backend/src/mongo/profile.ts +++ b/packages/backend/src/mongo/profile.ts @@ -45,6 +45,11 @@ export const raw_schema = { required: false, default: 0, }, + verified: { + type: Boolean, + required: true, + default: false + } }; const schema = new Schema(raw_schema); @@ -76,4 +81,5 @@ export interface IProfile trainPoints: number; steamName: string; trainDistance: number; + verified: boolean; } \ No newline at end of file