From 1b021c0a1f975830843665b40ecbb7e6c26b3957 Mon Sep 17 00:00:00 2001 From: alekswilc Date: Wed, 7 Aug 2024 19:51:25 +0200 Subject: [PATCH] v1.1.0 --- .gitea/assets/image-2.png | Bin 0 -> 30037 bytes package.json | 13 +- readme.md | 19 +- src/http/api.ts | 103 ---------- src/http/routes/stations.ts | 129 +++++++++++++ src/http/server.ts | 26 +++ src/http/views/_modules/footer.ejs | 7 + src/http/views/_modules/header.ejs | 21 +++ src/http/views/home.ejs | 113 +++++++++++ src/http/views/{ => stations}/details.ejs | 173 +++++++++-------- src/http/views/{ => stations}/index.ejs | 174 ++++++++--------- .../{search.ejs => stations/leaderboard.ejs} | 177 +++++++++--------- src/index.ts | 2 +- src/mongo/logs.ts | 82 ++++---- src/util/time.ts | 4 + 15 files changed, 618 insertions(+), 425 deletions(-) create mode 100644 .gitea/assets/image-2.png delete mode 100644 src/http/api.ts create mode 100644 src/http/routes/stations.ts create mode 100644 src/http/server.ts create mode 100644 src/http/views/_modules/footer.ejs create mode 100644 src/http/views/_modules/header.ejs create mode 100644 src/http/views/home.ejs rename src/http/views/{ => stations}/details.ejs (83%) rename src/http/views/{ => stations}/index.ejs (77%) rename src/http/views/{search.ejs => stations/leaderboard.ejs} (76%) diff --git a/.gitea/assets/image-2.png b/.gitea/assets/image-2.png new file mode 100644 index 0000000000000000000000000000000000000000..eaf1c5858ba4e3f3fb962fc65f7511470a70a6fb GIT binary patch literal 30037 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 z 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 Wilczyński", + "license": "AGPL-3.0-only", "devDependencies": { "@types/express": "^4.17.21", "@types/node": "^22.0.0", diff --git a/readme.md b/readme.md index c07e919..79f86d9 100644 --- a/readme.md +++ b/readme.md @@ -3,26 +3,17 @@ Prosty projekt, logujący wyjścia z posterunków. ### Hej! Podoba Ci się projekt? Polajkuj post na [forum](https://forum.simrail.eu/topic/9142-logowanie-wyj%C5%9B%C4%87-z-posterunk%C3%B3w/), a będzie mi miło! Dla Ciebie to jedno kliknięcie, a dla mnie motywacja do rozwijania projektu +## Aplikacja +https://simrail.alekswilc.dev/ + ## Cele - Ułatwienie zgłaszania graczy, którzy robią "Hit and Run" (psuje i wychodze z posterunku) +- Statystyki ## Dalszy rozwój -- Obsługa pociagów, a nie tylko posterunków - -# Jak korzystać? - -- Otwórz https://simrail.alekswilc.dev/ -- Znajdź interesujący Cie rekord. -- Naciśnij przycisk więcej -- Naciśnij na okienko z angielskim zapisem informacji (lub przycisk kopiuj link) -- Wklej na kanał #multiplayer-help-request z opisem sytuacji. -- Gotowe, kolejny troll jest zgłoszony. - -![alt text](.gitea/assets/image.png) +- Obsługa pociagów, a nie tylko posterunków (#8) # Zgłaszanie błedów -Aplikacja jest w wersji testowej, i obsługuje tylko serwer PL2 - Jak zgłosić błąd? - Otwórz https://git.alekswilc.dev/alekswilc/simrail-logs/issues - Załóż konto (to tylko kilka kliknięć, a twoje zgłoszenie bardzo mi pomoże) diff --git a/src/http/api.ts b/src/http/api.ts deleted file mode 100644 index 494c6f0..0000000 --- a/src/http/api.ts +++ /dev/null @@ -1,103 +0,0 @@ -import express from 'express'; -import { MLog } from '../mongo/logs.js'; -import { fileURLToPath } from 'node:url'; -import path from 'node:path'; -import dayjs from 'dayjs'; -import { PlayerUtil } from '../util/PlayerUtil.js'; -import { msToTime } from '../util/time.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - - -export class ApiModule { - public static load() { - const app = express(); - - app.set('view engine', 'ejs'); - app.set('views', __dirname + '/views') - - app.get('/', async (req, res) => { - const records = await MLog.find() - .sort({ leftDate: -1 }) - .limit(30) - res.render('index.ejs', { - records, - dayjs, - msToTime - }); - }) - - const generateSearch = (regex: RegExp) => [ - { - stationName: { $regex: regex }, - }, - { - userUsername: { $regex: regex }, - }, - { - stationShort: { $regex: regex }, - }, - { - userSteamId: { $regex: regex }, - }, - { - server: { $regex: regex }, - } - ] - - app.get('/search', async (req, res) => { - if (!req.query.q) return res.redirect('/'); - const s = req.query.q.toString().split(',').map(x => new RegExp(x, "i")); - - const records = await MLog.aggregate([ - { - $match: { - $and: [ - ...s.map(x => ({ $or: generateSearch(x) })) - ] - } - } - ]) - .sort({ leftDate: -1 }) - .limit(30) - res.render('search.ejs', { - records, - dayjs, - q: req.query.q, - msToTime - }); - }) - - app.get('/details/:id', async (req, res) => { - if (!req.params.id) return res.redirect('/'); - const record = await MLog.findOne({ id: req.params.id }); - const player = await PlayerUtil.getPlayer(record?.userSteamId!); - - res.render('details.ejs', { - record, - dayjs, - player, - msToTime - }); - }) - - app.get('/api/last', async (req, res) => { - const records = await MLog.find() - .sort({ leftDate: -1 }) - .limit(30) - res.json({ code: 200, records }); - }) - - app.get('/api/search', async (req, res) => { - if (!req.query.q) return res.send('invalid'); - const records = await MLog.find({ $text: { $search: req.query.q as string } }) - .sort({ leftDate: -1 }) - .limit(30) - - res.json({ code: 200, records }); - }) - - app.listen(2005); - } -} \ No newline at end of file diff --git a/src/http/routes/stations.ts b/src/http/routes/stations.ts new file mode 100644 index 0000000..6c98ae1 --- /dev/null +++ b/src/http/routes/stations.ts @@ -0,0 +1,129 @@ +import { Router } from 'express'; +import { MLog, raw_schema } from '../../mongo/logs.js'; +import dayjs from 'dayjs'; +import { PlayerUtil } from '../../util/PlayerUtil.js'; +import { msToTime } from '../../util/time.js'; +import { PipelineStage } from 'mongoose'; + +const generateSearch = (regex: RegExp) => [ + { + stationName: { $regex: regex }, + }, + { + userUsername: { $regex: regex }, + }, + { + stationShort: { $regex: regex }, + }, + { + userSteamId: { $regex: regex }, + }, + { + server: { $regex: regex }, + } +] + +export class StationsRoute { + static load() { + const app = Router(); + + app.get('/', async (req, res) => { + const s = req.query.q?.toString().split(',').map(x => new RegExp(x, "i")); + + const filter: PipelineStage[] = []; + + + s && filter.push({ + $match: { + $and: [ + ...s.map(x => ({ $or: generateSearch(x) })) + ] + } + }) + + const records = await MLog.aggregate(filter) + .sort({ leftDate: -1 }) + .limit(30) + res.render('stations/index.ejs', { + records, + dayjs, + q: req.query.q, + msToTime + }); + }) + + app.get('/details/:id', async (req, res) => { + if (!req.params.id) return res.redirect('/stations/'); + const record = await MLog.findOne({ id: req.params.id }); + const player = await PlayerUtil.getPlayer(record?.userSteamId!); + + res.render('stations/details.ejs', { + record, + dayjs, + player, + msToTime + }); + }) + + app.get('/leaderboard/', async (req, res) => { + const s = req.query.q?.toString().split(',').map(x => new RegExp(x, "i")); + + const data = Object.keys(raw_schema) + .reduce((o, key) => ({ ...o, [key]: `$${key}` }), {}); + + const filter: PipelineStage[] = [ + { + $project: { + // record.leftDate - record.joinedDate + result: { $subtract: ['$leftDate', '$joinedDate'] }, + ...data + } + }, + ]; + + s && filter.unshift( + { + $match: { + $and: [ + ...s.map(x => ({ $or: generateSearch(x) })) + ] + } + } + ) + + + const records = await MLog.aggregate(filter) + .sort({ result: -1 }) + .limit(30) + res.render('stations/leaderboard.ejs', { + records, + dayjs, + q: req.query.q, + msToTime + }); + }) + + + // API ENDPOINTS + // CREATE AN ISSUE IF YOU NEED API ACCESS: https://git.alekswilc.dev/alekswilc/simrail-logs/issues + /* + app.get('/api/last', async (req, res) => { + const records = await MLog.find() + .sort({ leftDate: -1 }) + .limit(30) + res.json({ code: 200, records }); + }) + + app.get('/api/search', async (req, res) => { + if (!req.query.q) return res.send('invalid'); + const records = await MLog.find({ $text: { $search: req.query.q as string } }) + .sort({ leftDate: -1 }) + .limit(30) + + res.json({ code: 200, records }); + })*/ + + + return app; + } +} \ No newline at end of file diff --git a/src/http/server.ts b/src/http/server.ts new file mode 100644 index 0000000..33ace50 --- /dev/null +++ b/src/http/server.ts @@ -0,0 +1,26 @@ +import express from 'express'; +import { fileURLToPath } from 'node:url'; +import path from 'node:path'; +import { StationsRoute } from './routes/stations.js'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); + + +export class ApiModule { + public static load() { + const app = express(); + + app.set('view engine', 'ejs'); + app.set('views', __dirname + '/views') + + app.get('/', (_, res) => res.render('home')); + + // backward compatible + app.get('/details/:id', (req, res) => res.redirect('/stations/details/'+req.params.id)); + + app.use('/stations/', StationsRoute.load()); + + app.listen(2005); + } +} \ No newline at end of file diff --git a/src/http/views/_modules/footer.ejs b/src/http/views/_modules/footer.ejs new file mode 100644 index 0000000..bd506b7 --- /dev/null +++ b/src/http/views/_modules/footer.ejs @@ -0,0 +1,7 @@ +
+

Made with ❤️ by alekswilc for SimRail Community

+

Open Source

+ <% if (thanks) { %> +

Podziękowania dla AQUALYTH, osób lajkujących posta, osób użytkujących strone i całego community Simrail ❤️

+ <% } %> +
\ No newline at end of file diff --git a/src/http/views/_modules/header.ejs b/src/http/views/_modules/header.ejs new file mode 100644 index 0000000..78ed9f9 --- /dev/null +++ b/src/http/views/_modules/header.ejs @@ -0,0 +1,21 @@ +
+

SimRail Logs

+ + <% if (section==='stations' ) { %> + + + <% } else {%> + Logi posterunków / + Logi pociągów + <% } %> +
+

Hej! Podoba Ci się projekt? Polajkuj post na forum, a będzie + mi miło! Dla Ciebie to jedno kliknięcie, a dla mnie motywacja do rozwijania projektu.

+
\ No newline at end of file diff --git a/src/http/views/home.ejs b/src/http/views/home.ejs new file mode 100644 index 0000000..b9511f6 --- /dev/null +++ b/src/http/views/home.ejs @@ -0,0 +1,113 @@ + + + + + + + simrail.alekswilc.dev + + + + + + + + + + + + + <%- include('_modules/header.ejs', { section:'home' }) %> + +

Prosty projekt, logujący wyjścia z posterunków.

+

Cele projektu

+

Główne założenia

+
    +
  • +

    Ułatwienie zgłaszania graczy, którzy robią "Hit and Run" (psuje i wychodze z posterunku)

    +
  • +
  • +

    Statystyki

    +
  • +
+ +

Rozwój aplikacji

+

Planowane funkcjonalności

+
    +
  • +

    Obsługa pociągów (#8)

    +
  • +
+

Informacje dodatkowe

+
+ + Zgłaszanie błędów i propozycji + + +
Zgłaszanie błedów
+
    +
  • Otwórz https://git.alekswilc.dev/alekswilc/simrail-logs/issues +
  • +
  • Zaloguj się (np poprzez Discorda)
  • +
  • Naciśnij przycisk "New issue" ("Nowe zgłoszenie")
  • +
  • W tytule napisz krótki opis (np. Problem z wyświetleniem strony)
  • +
  • Opisz problem (kiedy występuje, podaj linki, postaraj sie napisać jak moge odtworzyć ten błąd, jak + powinna zachowywać sie aplikacja bez tego błędu)
  • +
  • Poczekaj na informacje z mojej strony
  • +
  • Dziekuje :)
  • +
+ + + + +
Zgłaszanie propozycji
+
    +
  • Otwórz https://git.alekswilc.dev/alekswilc/simrail-logs/issues +
  • +
  • Zaloguj się (np poprzez Discorda)
  • +
  • Naciśnij przycisk "New issue" ("Nowe zgłoszenie")
  • +
  • W tytule napisz krótki opis (np. Logi wejścia i wyjścia z pociągów)
  • +
  • Opisz propozycje (dokładnie co z dyżurkami ale dla pociągów)
  • +
  • Poczekaj na informacje z mojej strony
  • +
  • Dziekuje :)
  • +
+ + +
+ + + +

Licencja

+
+ + Copyright (C) 2024 Aleksander Wilczyński + + + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see https://www.gnu.org/licenses/. +
+ +
+ <%- include('_modules/footer.ejs', { thanks: true }) %> + + + + + \ No newline at end of file diff --git a/src/http/views/details.ejs b/src/http/views/stations/details.ejs similarity index 83% rename from src/http/views/details.ejs rename to src/http/views/stations/details.ejs index 383b973..727fe67 100644 --- a/src/http/views/details.ejs +++ b/src/http/views/stations/details.ejs @@ -1,89 +1,86 @@ - - - - - - - simrail.alekswilc.dev - - - - - - - - - - - - - - - - -
-

SimRail Logs

-

Dokumentacja

-
-

Hej! Podoba Ci się projekt? Polajkuj post na forum, a będzie mi miło! Dla Ciebie to jedno kliknięcie, a dla mnie motywacja do rozwijania projektu

-
- -
- -

Użytkownik: - <%= record.userUsername %> -

-

Stacja: <%= record.stationName %> -

-

Serwer: <%= record.server.toUpperCase() %> -

-

Data wejścia: <%= record.joinedDate ? dayjs(record.joinedDate).format('HH:mm DD/MM/YYYY') : '--:-- --/--/--' - %> (<%= record.joinedDate ? dayjs(record.joinedDate).fromNow() : '--' %>)

-

Data wyjścia: <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> (<%= dayjs(record.leftDate).fromNow() - %>)

-

Spędzony czas: <%= record.joinedDate ? msToTime(record.leftDate - record.joinedDate) : '--' %> -

- -
- ;station: <%= record.stationName %> - ;steam: <%= record.userSteamId %> - ;server: <%= record.server %> - ;name: <%= record.userUsername %> - ;joined: <%=record.joinedDate ? dayjs(record.joinedDate).format() : 'no-data'%> - ;left: <%=dayjs(record.leftDate).format()%> - ;url: https://simrail.alekswilc.dev/details/<%= record.id %>/ - -
-

- - - -
- - - + + + + + + + simrail.alekswilc.dev + + + + + + + + + + + + + + + + + <%- include('../_modules/header.ejs', { section: 'stations' }) %> + +
+ +

Użytkownik: + <%= record.userUsername %> +

+

Stacja: <%= record.stationName %> +

+

Serwer: <%= record.server.toUpperCase() %> +

+

Data wejścia: <%= record.joinedDate ? dayjs(record.joinedDate).format('HH:mm DD/MM/YYYY') : '--:-- --/--/--' + %> (<%= record.joinedDate ? dayjs(record.joinedDate).fromNow() : '--' %>)

+

Data wyjścia: <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> (<%= dayjs(record.leftDate).fromNow() + %>)

+

Spędzony czas: <%= record.joinedDate ? msToTime(record.leftDate - record.joinedDate) : '--' %> +

+ +
+ ;station: <%= record.stationName %> + ;steam: <%= record.userSteamId %> + ;server: <%= record.server %> + ;name: <%= record.userUsername %> + ;joined: <%=record.joinedDate ? dayjs(record.joinedDate).format() : 'no-data'%> + ;left: <%=dayjs(record.leftDate).format()%> + ;url: https://simrail.alekswilc.dev/details/<%= record.id %>/ + +
+

+ + + +
+
+ <%- include('../_modules/footer.ejs', { thanks: false }) %> + + + \ No newline at end of file diff --git a/src/http/views/index.ejs b/src/http/views/stations/index.ejs similarity index 77% rename from src/http/views/index.ejs rename to src/http/views/stations/index.ejs index 46d35b2..cae0e10 100644 --- a/src/http/views/index.ejs +++ b/src/http/views/stations/index.ejs @@ -1,85 +1,91 @@ - - - - - - - simrail.alekswilc.dev - - - - - - - - - - - - -
-

SimRail Logs

-

Dokumentacja

-
-

Hej! Podoba Ci się projekt? Polajkuj post na forum, a będzie mi miło! Dla Ciebie to jedno kliknięcie, a dla mnie motywacja do rozwijania projektu

-
- -

Wyszukaj posterunek, osobe lub serwer

- -
- - -

Użyj przecinka, aby wyszukać wiele wartości: pl2,Łazy

-
- -

Ostatnie opuszczenia posterunków

- -
    - <% records.forEach(record=> { %> -
  • -
    - [ - <%= record.server.toUpperCase() %> - ] - <%= record.stationName %> - - - <%= record.userUsername %> - -

    - <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> -

    -
    -

    Data dołączenia: <%= record.joinedDate ? dayjs(record.joinedDate).format('HH:mm DD/MM/YYYY') - : '--:-- --/--/--' %> (<%= record.joinedDate ? dayjs(record.joinedDate).fromNow() : '--' %>) -

    -

    Data wyjścia: <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> (<%= - dayjs(record.leftDate).fromNow() %>)

    -

    Spędzony czas: <%= record.joinedDate ? msToTime(record.leftDate - record.joinedDate) : '--' %> -

    - - - - -
    -
  • - <% }) %> -
- - - - - + + + + + + + simrail.alekswilc.dev + + + + + + + + + + + + + <%- include('../_modules/header.ejs', { section: 'stations' }) %> + +

Wyszukaj posterunek, osobe lub serwer

+ +
+ + + + +

Użyj przecinka, aby wyszukać wiele wartości: pl2,Łazy

+
+ +
    + <% records.forEach(record=> { %> +
  • +
    + [ + <%= record.server.toUpperCase() %> + ] + <%= record.stationName %> + - + <%= record.userUsername %> + +

    + <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> +

    +
    +

    Data dołączenia: <%= record.joinedDate ? dayjs(record.joinedDate).format('HH:mm DD/MM/YYYY') + : '--:-- --/--/--' %> (<%= record.joinedDate ? dayjs(record.joinedDate).fromNow() : '--' %>) +

    +

    Data wyjścia: <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> (<%= + dayjs(record.leftDate).fromNow() %>)

    +

    Spędzony czas: <%= record.joinedDate ? msToTime(record.leftDate - record.joinedDate) : '--' %> +

    + + + + +
    +
  • + <% }) %> +
+ + <% if (!records.length) { %> +

Nie znaleziono wyników dla twojego zapytania.

+ <% } %> + +
+ <%- include('../_modules/footer.ejs', { thanks: false }) %> + + + + + \ No newline at end of file diff --git a/src/http/views/search.ejs b/src/http/views/stations/leaderboard.ejs similarity index 76% rename from src/http/views/search.ejs rename to src/http/views/stations/leaderboard.ejs index 614273b..0487012 100644 --- a/src/http/views/search.ejs +++ b/src/http/views/stations/leaderboard.ejs @@ -1,87 +1,92 @@ - - - - - - - simrail.alekswilc.dev - - - - - - - - - - - - - -
-

SimRail Logs

-

Dokumentacja

-
-

Hej! Podoba Ci się projekt? Polajkuj post na forum, a będzie mi miło! Dla Ciebie to jedno kliknięcie, a dla mnie motywacja do rozwijania projektu

-
- -

Wyszukaj posterunek, osobe lub serwer

- - -
- - -

Użyj przecinka, aby wyszukać wiele wartości: pl2,Łazy

-
- -

Wyniki wyszukiwania

- -
    - <% records.forEach(record=> { %> -
  • -
    - [ - <%= record.server.toUpperCase() %> - ] - <%= record.stationName %> - - - <%= record.userUsername %> - -

    - <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> -

    -
    -

    Data dołączenia: <%= record.joinedDate ? dayjs(record.joinedDate).format('HH:mm DD/MM/YYYY') - : '--:-- --/--/--' %> (<%= record.joinedDate ? dayjs(record.joinedDate).fromNow() : '--' %>) -

    -

    Data wyjścia: <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> (<%= - dayjs(record.leftDate).fromNow() %>)

    -

    Spędzony czas: <%= record.joinedDate ? msToTime(record.leftDate - record.joinedDate) : '--' %> -

    - - - - -
    -
  • - <% }) %> -
- - - - - + + + + + + + simrail.alekswilc.dev + + + + + + + + + + + + + + <%- include('../_modules/header.ejs', { section: 'stations' }) %> + +

Tablica godzin

+
+ + + + +

Użyj przecinka, aby wyszukać wiele wartości: pl2,Łazy

+
+
    + <% records.forEach(record=> { %> +
  • +
    + [ + <%= record.server.toUpperCase() %> + ] + <%= record.stationName %> + - + <%= record.userUsername %> + - + <%= record.joinedDate ? msToTime(record.leftDate - record.joinedDate) : '--' %> + +

    + <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> +

    +
    +

    Data dołączenia: <%= record.joinedDate ? dayjs(record.joinedDate).format('HH:mm DD/MM/YYYY') + : '--:-- --/--/--' %> (<%= record.joinedDate ? dayjs(record.joinedDate).fromNow() : '--' %>) +

    +

    Data wyjścia: <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> (<%= + dayjs(record.leftDate).fromNow() %>)

    +

    Spędzony czas: <%= record.joinedDate ? msToTime(record.leftDate - record.joinedDate) : '--' %> +

    + + + + +
    +
  • + <% }) %> +
+ + <% if (!records.length) { %> +

Nie znaleziono wyników dla twojego zapytania.

+ <% } %> + + + +
+ <%- include('../_modules/footer.ejs', { thanks: false }) %> + + \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index af4fc7e..ac8593a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import './util/time.js'; import { SimrailClient, SimrailClientEvents } from './util/SimrailClient.js'; import dayjs from 'dayjs'; import { StationsModule } from './modules/stations.js'; -import { ApiModule } from './http/api.js'; +import { ApiModule } from './http/server.js'; import mongoose from 'mongoose'; import { IPlayer } from './types/player.js'; import { Station, Server } from '@simrail/types'; diff --git a/src/mongo/logs.ts b/src/mongo/logs.ts index 9555162..9467cee 100644 --- a/src/mongo/logs.ts +++ b/src/mongo/logs.ts @@ -1,47 +1,47 @@ import { Model, model, Schema } from 'mongoose'; -const schema = new Schema( - { - id: { - type: String, - required: true - }, - userSteamId: { - type: String, - required: true - }, - userUsername: { - type: String, - required: true - }, - userAvatar: { - type: String, - required: true - }, - joinedDate: { - type: Number, - required: false, - default: undefined - }, - leftDate: { - type: Number, - required: true - }, - stationName: { - type: String, - required: true - }, - stationShort: { - type: String, - required: true - }, - server: { - type: String, - required: true - }, - } -); +export const raw_schema = { + id: { + type: String, + required: true + }, + userSteamId: { + type: String, + required: true + }, + userUsername: { + type: String, + required: true + }, + userAvatar: { + type: String, + required: true + }, + joinedDate: { + type: Number, + required: false, + default: undefined + }, + leftDate: { + type: Number, + required: true + }, + stationName: { + type: String, + required: true + }, + stationShort: { + type: String, + required: true + }, + server: { + type: String, + required: true + }, +} + +const schema = new Schema(raw_schema); schema.index({ stationName: 'text', userUsername: 'text', stationShort: 'text', userSteamId: 'text', server: 'text' }) export type TMLog = Model diff --git a/src/util/time.ts b/src/util/time.ts index da32243..2d77399 100644 --- a/src/util/time.ts +++ b/src/util/time.ts @@ -10,5 +10,9 @@ export const msToTime = (duration: number) => { const minutes = Math.floor((duration / (1000 * 60)) % 60); const hours = Math.floor((duration / (1000 * 60 * 60)) % 24); + if (minutes === 0 && hours === 0 && duration > 0) + return "1m"; + + return `${hours ? `${hours}h ` : ''}${minutes ? `${minutes}m` : ''}`; } \ No newline at end of file