diff --git a/package.json b/package.json index 742eea7..f5bb05f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "stacjownik", - "version": "1.31.1", + "version": "1.32.0", "private": true, "type": "module", "scripts": { diff --git a/public/images/default-avatar.jpg b/public/images/default-avatar.jpg new file mode 100644 index 0000000..9b35e48 Binary files /dev/null and b/public/images/default-avatar.jpg differ diff --git a/src/App.vue b/src/App.vue index cf6996e..42e818d 100644 --- a/src/App.vue +++ b/src/App.vue @@ -7,11 +7,6 @@ - - @@ -52,7 +47,6 @@ import UpdateCard from './components/App/UpdateCard.vue'; import StorageManager from './managers/storageManager'; import AppFooter from './components/App/AppFooter.vue'; import AppWelcomeCard from './components/App/AppWelcomeCard.vue'; -import MigrateInfoCard from './components/App/MigrateInfoCard.vue'; const STORAGE_VERSION_KEY = 'app_version'; const WELCOME_CARD_SEEN_KEY = 'welcome_card_seen'; @@ -66,7 +60,6 @@ export default defineComponent({ AppFooter, UpdateCard, AppWelcomeCard, - MigrateInfoCard, Tooltip }, @@ -212,6 +205,7 @@ export default defineComponent({ \ No newline at end of file + diff --git a/src/components/Global/RegionDropdown.vue b/src/components/Global/RegionDropdown.vue index b024e65..081917f 100644 --- a/src/components/Global/RegionDropdown.vue +++ b/src/components/Global/RegionDropdown.vue @@ -160,7 +160,7 @@ ul.options { height: auto; - z-index: 100; + z-index: 150; width: 100%; font-size: 0.9em; diff --git a/src/components/JournalView/JournalDailyStats.vue b/src/components/JournalView/JournalDailyStats.vue index 131bd68..71c9117 100644 --- a/src/components/JournalView/JournalDailyStats.vue +++ b/src/components/JournalView/JournalDailyStats.vue @@ -1,6 +1,6 @@ - @@ -265,7 +226,7 @@ ul.stats-list { gap: 0.5em; } -@include responsive.smallScreen{ +@include responsive.smallScreen { h3 { text-align: center; } diff --git a/src/components/JournalView/JournalDispatchers/JournalDispatcherStats.vue b/src/components/JournalView/JournalDispatchers/JournalDispatcherStats.vue deleted file mode 100644 index 4923d4b..0000000 --- a/src/components/JournalView/JournalDispatchers/JournalDispatcherStats.vue +++ /dev/null @@ -1,85 +0,0 @@ - - - - - diff --git a/src/components/JournalView/JournalOptions.vue b/src/components/JournalView/JournalOptions.vue index 3a77abb..d2631c8 100644 --- a/src/components/JournalView/JournalOptions.vue +++ b/src/components/JournalView/JournalOptions.vue @@ -1,5 +1,5 @@ - diff --git a/src/components/JournalView/JournalTimetables/JournalTimetableEntry.vue b/src/components/JournalView/JournalTimetables/JournalTimetableEntry.vue index 584db44..8540c35 100644 --- a/src/components/JournalView/JournalTimetables/JournalTimetableEntry.vue +++ b/src/components/JournalView/JournalTimetables/JournalTimetableEntry.vue @@ -10,14 +10,14 @@
-
+
@@ -28,7 +28,6 @@ import { defineComponent, PropType } from 'vue'; import { API } from '../../../typings/api'; import { useApiStore } from '../../../store/apiStore'; -import { Journal } from '../typings'; import trainCategoryMixin from '../../../mixins/trainCategoryMixin'; import dateMixin from '../../../mixins/dateMixin'; @@ -41,7 +40,7 @@ import EntryDetails from './EntryDetails.vue'; export default defineComponent({ props: { timetableEntry: { - type: Object as PropType, + type: Object as PropType, required: true }, showExtraInfo: { @@ -60,74 +59,9 @@ export default defineComponent({ }; }, - computed: { - timetablePathDetails() { - if (!this.timetableEntry.path || this.timetableEntry.path == '') return null; - - return this.timetableEntry.path.split(';').map((pathEl, i) => { - const [arrival, name, departure] = pathEl.split(','); - const sceneryName = name.split(' ').slice(0, -1).join(' '); - const sceneryHash = name.split(' ').pop()?.replace('.sc', '') ?? ''; - - return { - arrival, - sceneryName, - sceneryHash, - departure, - isVisited: this.timetableEntry.visitedSceneries?.includes(sceneryHash) ?? false - }; - }); - }, - - timetableStops(): Journal.TimetableStopDetails[] { - const timetableEntry = this.timetableEntry; - - const stopNames = timetableEntry.sceneriesString.split('%'); - - return stopNames.reduce((acc, stopName, i, arr) => { - const arrivalDate = - i == arr.length - 1 - ? (timetableEntry.checkpointArrivals.at(i) ?? timetableEntry.endDate) - : timetableEntry.checkpointArrivals.at(i); - - const scheduledArrivalDate = - i == arr.length - 1 - ? (timetableEntry.checkpointArrivalsScheduled.at(i) ?? timetableEntry.scheduledEndDate) - : timetableEntry.checkpointArrivalsScheduled.at(i); - - const departureDate = - i == 0 - ? (timetableEntry.checkpointDepartures.at(i) ?? timetableEntry.beginDate) - : timetableEntry.checkpointDepartures.at(i); - - const scheduledDepartureDate = - i == 0 - ? (timetableEntry.checkpointDeparturesScheduled.at(i) ?? - timetableEntry.scheduledBeginDate) - : timetableEntry.checkpointDeparturesScheduled.at(i); - - const stopTime = Number(timetableEntry.checkpointStopTypes.at(i)?.split(',')[0]) || 0; - const stopType = timetableEntry.checkpointStopTypes.at(i)?.split(',')[1] || ''; - - acc.push({ - stopName, - arrivalTimestamp: this.dateStringToTimestamp(arrivalDate), - scheduledArrivalTimestamp: this.dateStringToTimestamp(scheduledArrivalDate), - departureTimestamp: this.dateStringToTimestamp(departureDate), - scheduledDepartureTimestamp: this.dateStringToTimestamp(scheduledDepartureDate), - stopTime, - stopType, - isConfirmed: i < timetableEntry.confirmedStopsCount - }); - - return acc; - }, []); - } - }, - methods: { - toggleExtraInfo() { - this.$emit('toggleShowExtraInfo'); + toggleExtraInfo(data: API.TimetableHistory.Data | null) { + this.$emit('toggleShowExtraInfo', data); } } }); @@ -145,7 +79,7 @@ export default defineComponent({ display: flex; } -@include responsive.smallScreen{ +@include responsive.smallScreen { .entry-route { justify-content: center; text-align: center; diff --git a/src/components/JournalView/JournalTimetables/JournalTimetablesList.vue b/src/components/JournalView/JournalTimetables/JournalTimetablesList.vue index 6cd0662..2bd36f6 100644 --- a/src/components/JournalView/JournalTimetables/JournalTimetablesList.vue +++ b/src/components/JournalView/JournalTimetables/JournalTimetablesList.vue @@ -20,7 +20,7 @@ v-for="(timetableEntry, i) in timetableHistory" :key="timetableEntry.id" :timetableEntry="timetableEntry" - :onToggleShowExtraInfo="() => toggleExtraInfo(timetableEntry.id)" + :onToggleShowExtraInfo="toggleExtraInfo" :showExtraInfo="extraInfoIndexes.includes(timetableEntry.id)" /> @@ -59,9 +59,11 @@ export default defineComponent({ JournalTimetableEntry }, + emits: ['toggleExtraInfo'], + props: { timetableHistory: { - type: Array as PropType, + type: Array as PropType, required: true }, scrollNoMoreData: { @@ -75,32 +77,23 @@ export default defineComponent({ }, dataStatus: { type: Number as PropType + }, + extraInfoIndexes: { + type: Object as PropType, + required: true } }, data() { return { Status, - store: useMainStore(), - extraInfoIndexes: [] as number[] + store: useMainStore() }; }, - watch: { - '$route.query': { - deep: true, - handler() { - this.extraInfoIndexes.length = 0; - } - } - }, - methods: { - toggleExtraInfo(id: number) { - const existingIdx = this.extraInfoIndexes.indexOf(id); - - if (existingIdx != -1) this.extraInfoIndexes.splice(existingIdx, 1); - else this.extraInfoIndexes.push(id); + toggleExtraInfo(data: API.TimetableHistory.Data | null) { + this.$emit('toggleExtraInfo', data); } } }); @@ -111,7 +104,7 @@ export default defineComponent({ @use '../../../styles/journal-section'; @use '../../../styles/responsive'; -@include responsive.smallScreen{ +@include responsive.smallScreen { .journal_item-info { text-align: center; } diff --git a/src/components/JournalView/typings.ts b/src/components/JournalView/typings.ts index b7931a1..a2605cf 100644 --- a/src/components/JournalView/typings.ts +++ b/src/components/JournalView/typings.ts @@ -1,5 +1,6 @@ export namespace Journal { export type DispatcherSearchKey = + | 'search-duty-id' | 'search-dispatcher' | 'search-station' | 'search-date-from' @@ -62,19 +63,6 @@ export namespace Journal { default: boolean; } - export enum StatsTab { - DRIVER_STATS = 'journal-driver-stats', - DISPATCHER_STATS = 'journal-dispatcher-stats', - DAILY_STATS = 'journal-daily-stats' - } - - export interface StatsButton { - tab: StatsTab; - localeKey: string; - iconName: string; - disabled: boolean; - } - export interface TimetableStopDetails { stopName: string; arrivalTimestamp: number; diff --git a/src/components/PlayerProfileView/ProfileHistoryList.vue b/src/components/PlayerProfileView/ProfileHistoryList.vue new file mode 100644 index 0000000..24bb03e --- /dev/null +++ b/src/components/PlayerProfileView/ProfileHistoryList.vue @@ -0,0 +1,298 @@ + + + + + diff --git a/src/components/PlayerProfileView/ProfilePlayerAvatar.vue b/src/components/PlayerProfileView/ProfilePlayerAvatar.vue new file mode 100644 index 0000000..577305f --- /dev/null +++ b/src/components/PlayerProfileView/ProfilePlayerAvatar.vue @@ -0,0 +1,80 @@ + + + + + diff --git a/src/components/PlayerProfileView/ProfileRecentStats.vue b/src/components/PlayerProfileView/ProfileRecentStats.vue new file mode 100644 index 0000000..21610f5 --- /dev/null +++ b/src/components/PlayerProfileView/ProfileRecentStats.vue @@ -0,0 +1,107 @@ + + + + + diff --git a/src/components/PlayerProfileView/ProfileSummary.vue b/src/components/PlayerProfileView/ProfileSummary.vue new file mode 100644 index 0000000..0f9b629 --- /dev/null +++ b/src/components/PlayerProfileView/ProfileSummary.vue @@ -0,0 +1,392 @@ + + + + + diff --git a/src/components/SceneryView/SceneryInfo/SceneryInfoDispatcher.vue b/src/components/SceneryView/SceneryInfo/SceneryInfoDispatcher.vue index 57b20f4..4e1f221 100644 --- a/src/components/SceneryView/SceneryInfo/SceneryInfoDispatcher.vue +++ b/src/components/SceneryView/SceneryInfo/SceneryInfoDispatcher.vue @@ -8,10 +8,7 @@ {{ onlineScenery.dispatcherExp > 1 ? onlineScenery.dispatcherExp : 'L' }} - + timetable.beginDate ? new Date(timetable.beginDate) diff --git a/src/components/StationsView/StationTable.vue b/src/components/StationsView/StationTable.vue index 6f20a2f..a400a1c 100644 --- a/src/components/StationsView/StationTable.vue +++ b/src/components/StationsView/StationTable.vue @@ -132,7 +132,6 @@ @@ -446,7 +445,7 @@ export default defineComponent({ $rowCol: #424242; .station_table { - height: calc(100vh - 11em); + height: calc(100vh - 17em); max-height: 2000px; min-height: 500px; overflow: auto; diff --git a/src/components/TrainsView/TrainTable.vue b/src/components/TrainsView/TrainTable.vue index 6413b3c..28f3cfb 100644 --- a/src/components/TrainsView/TrainTable.vue +++ b/src/components/TrainsView/TrainTable.vue @@ -97,7 +97,7 @@ export default defineComponent({ @use '../../styles/animations'; .train-table { - height: calc(100vh - 11em); + height: calc(100vh - 17em); min-height: 500px; position: relative; diff --git a/src/composables/badge.ts b/src/composables/badge.ts new file mode 100644 index 0000000..43a7e27 --- /dev/null +++ b/src/composables/badge.ts @@ -0,0 +1,8 @@ +export function calculateExpStyles(exp: number, isSupporter = false) { + const bgColor = exp > -1 ? (exp < 2 ? '#26B0D9' : `hsl(${-exp * 5 + 100}, 85%, 50%)`) : '#666'; + + const fontColor = exp > 14 || exp == -1 ? 'white' : 'black'; + const boxShadow = isSupporter ? `0 0 6px 2px ${bgColor};` : ''; + + return { 'background-color': bgColor, color: fontColor, 'box-shadow': boxShadow }; +} diff --git a/src/composables/time.ts b/src/composables/time.ts new file mode 100644 index 0000000..edb1eba --- /dev/null +++ b/src/composables/time.ts @@ -0,0 +1,37 @@ +import { useI18n } from 'vue-i18n'; + +export function calculateDuration(timestampMs: number) { + const secondsTotal = Math.floor(timestampMs / 1000); + const minsTotal = Math.round(timestampMs / 60000); + const hoursTotal = Math.floor(minsTotal / 60); + const minsInHour = minsTotal % 60; + + return { + secondsTotal, + minsTotal, + hoursTotal, + minsInHour + }; +} + +export function humanizeDuration(timestampMs: number, showSeconds = false) { + const { t } = useI18n(); + + const duration = calculateDuration(timestampMs); + + return duration.minsTotal >= 60 + ? `${t('journal.hours', { value: duration.hoursTotal }, duration.hoursTotal)} ${t( + 'journal.minutes', + { value: duration.minsInHour }, + duration.minsInHour + )}` + : showSeconds && duration.secondsTotal <= 60 + ? t('journal.seconds', { value: duration.secondsTotal }, duration.secondsTotal) + : t('journal.minutes', { value: duration.minsTotal }, duration.minsTotal); +} + +export function dateToLocaleString(date: Date, dateOptions: Intl.DateTimeFormatOptions) { + const { locale } = useI18n(); + + return date.toLocaleString(locale.value == 'pl' ? 'pl-PL' : 'en-GB', dateOptions); +} diff --git a/src/locales/en.json b/src/locales/en.json index 735f1c5..65e8495 100644 --- a/src/locales/en.json +++ b/src/locales/en.json @@ -87,10 +87,8 @@ "tooltip-scenery-offline": "Scenery is offline", "pojazdownik-link-content": "POJAZDOWNIK", "language-tooltip-content": "JĘZYK / LANGUAGE", - "gnr-link-content": "TRAIN ORDERS
GENERATOR" - }, - "footer": { - "discord": "Stacjownik Discord server" + "gnr-link-content": "TRAIN ORDERS
GENERATOR", + "discord-link-content": "STACJOWNIK
DISCORD SERVER" }, "categories": { "EI": "domestic express", @@ -197,6 +195,7 @@ "search-train": "Train no. / #", "select-driver": "Choose a driver...", "search-driver": "Driver name", + "search-duty-id": "Duty ID", "search-dispatcher": "Dispatcher name", "search-station": "Scenery name / #", "search-author": "Timetable author name", @@ -428,7 +427,7 @@ "last-seen-ago": "since {minutes} minutes", "scenery-offline": "Offline ride", "timeout": "An error occured while trying to refresh SWDR timetable data!", - "driver-journal-link": "DRIVER JOURNAL", + "driver-profile-link": "PLAYER'S PROFILE", "driver-srjp-link": "SRJP", "driver-return-link": "RETURN", "driver-not-found-header": "Train not found! :/", @@ -619,9 +618,54 @@ "desc-end": "The train terminates here", "desc-terminated": "The train has been terminated" }, - "history": { - "title": "TIMETABLE JOURNAL", - "search-train": "Train no.", - "search-driver": "Driver name" + "profile": { + "journal-button": "PLAYER'S PROFILE", + "no-player-found": "Player not found! :/", + "return-to-main": "Return to the main site", + + "filters": { + "Timetable": "TIMETABLES", + "Dispatcher": "DISPATCHER DUTIES", + "IssuedTimetable": "ISSUED TIMETABLES" + }, + + "stats": { + "timetables-journal": "TIMETABLE JOURNAL", + "dispatchers-journal": "DISPATCHER JOURNAL", + "forum-profile": "FORUM PROFILE", + + "driver": "DRIVER", + "dispatcher": "DISPATCHER", + + "header-driver": "DRIVER'S STATS", + "fulfilled-timetables": "fulfilled timetables", + "route-distance": "confirmed timetables distance", + "confirmed-stops": "confirmed stations in timetables", + "longest-timetable": "longest timetable", + "avg-timetable-length": "average distance of all timetables", + "no-timetable-stats": "This player does not have any registered timetables in Stacjownik!", + + "header-dispatcher": "DISPATCHER'S STATS", + "duties-count": "duties as dispatcher", + "longest-duty": "longest duty", + "created-timetables-count": "issued timetables as dispatcher", + "longest-created-timetable": "longest issued timetable", + "created-timetables-length-sum": "distance sum of issued timetables", + "no-dispatcher-stats": "No registered dispatcher duties in Stacjownik!" + }, + + "recent-stats": { + "header": "ACTIVITY STATISTICS (30 LAST DAYS)", + "timetables": "TIMETABLES", + "distance": "MADE KILOMETERS", + "duties": "DISPATCHER DUTIES", + "created-timetables": "ISSUED TIMETABLES" + }, + + "list": { + "for": "for", + "online-since": "online since", + "no-recent-history": "No recent activity in the simulator :(" + } } } diff --git a/src/locales/pl.json b/src/locales/pl.json index 5d0f90e..adce89b 100644 --- a/src/locales/pl.json +++ b/src/locales/pl.json @@ -83,10 +83,8 @@ "tooltip-scenery-offline": "Sceneria offline", "pojazdownik-link-content": "POJAZDOWNIK", "language-tooltip-content": "JĘZYK / LANGUAGE", - "gnr-link-content": "GENERATOR
ROZKAZÓW PISEMNYCH" - }, - "footer": { - "discord": "Serwer Discord Stacjownika" + "gnr-link-content": "GENERATOR
ROZKAZÓW PISEMNYCH", + "discord-link-content": "SERWER DISCORD
STACJOWNIKA" }, "categories": { "EI": "ekspres krajowy", @@ -193,6 +191,7 @@ "search-train": "Nr pociągu / #", "search-driver": "Nick maszynisty", "select-driver": "Wybierz maszynistę...", + "search-duty-id": "ID służby", "search-dispatcher": "Nick dyżurnego", "search-station": "Nazwa scenerii / #", "search-author": "Nick autora rozkładu jazdy", @@ -414,7 +413,7 @@ "last-seen-ago": "od {minutes} minut", "scenery-offline": "Przejazd offline", "timeout": "Wystąpił problem z aktualizacją rozkładów jazdy z SWDR", - "driver-journal-link": "DZIENNIK MASZYNISTY", + "driver-profile-link": "PROFIL GRACZA", "driver-srjp-link": "SRJP", "driver-return-link": "POWRÓT", "driver-not-found-header": "Nie znaleziono pociągu! :/", @@ -604,7 +603,54 @@ "desc-end": "Pociąg kończy bieg", "desc-terminated": "Pociąg zakończył bieg" }, - "history": { - "title": "DZIENNIK ROZKŁADÓW JAZDY" + "profile": { + "journal-button": "PROFIL GRACZA", + "no-player-found": "Nie znaleziono gracza! :/", + "return-to-main": "Powrót do strony głównej", + + "filters": { + "Timetable": "ROZKŁADY JAZDY", + "Dispatcher": "SŁUŻBY DYŻURNEGO", + "IssuedTimetable": "WYSTAWIONE RJ" + }, + + "stats": { + "timetables-journal": "DZIENNIK RJ", + "dispatchers-journal": "DZIENNIK DR", + "forum-profile": "PROFIL FORUM", + + "driver": "MASZYNISTA", + "dispatcher": "DYŻURNY RUCHU", + + "header-driver": "STATYSTYKI MASZYNISTY", + "fulfilled-timetables": "wypełnione rozkłady jazdy", + "route-distance": "zatwierdzony kilometraż w RJ", + "confirmed-stops": "potwierdzonych stacji w RJ", + "longest-timetable": "najdłuższy rozkład jazdy", + "avg-timetable-length": "średnia długość wszystkich rozkładów", + "no-timetable-stats": "Ten użytkownik nie posiada statystyk maszynisty zarejestrowanych przez Stacjownik!", + + "header-dispatcher": "STATYSTYKI DYŻURNEGO RUCHU", + "duties-count": "służby jako dyżurny ruchu", + "longest-duty": "najdłuższa służba", + "created-timetables-count": "wystawione RJ jako dyżurny ruchu", + "longest-created-timetable": "najdłuższy wystawiony RJ", + "created-timetables-length-sum": "suma długości wystawionych RJ", + "no-dispatcher-stats": "Ten użytkownik nie posiada statystyk dyżurnego zarejestrowanych przez Stacjownik!" + }, + + "recent-stats": { + "header": "STATYSTYKI AKTYWNOŚCI (30 DNI)", + "timetables": "ROZKŁADÓW JAZDY", + "distance": "POKONANYCH KILOMETRÓW", + "duties": "SŁUŻB DYŻURNEGO", + "created-timetables": "WYSTAWIONYCH ROZKŁADÓW" + }, + + "list": { + "for": "dla", + "online-since": "online od", + "no-recent-history": "Brak ostatniej aktywności w symulatorze :(" + } } } diff --git a/src/router/index.ts b/src/router/index.ts index 5d628e8..03fff48 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -61,6 +61,11 @@ const routes: Array = [ region: route.query.region }) }, + { + path: '/profile', + name: 'PlayerProfileView', + component: () => import('../views/PlayerProfileView.vue') + }, { path: '/:catchAll(.*)', redirect: '/' @@ -70,12 +75,12 @@ const routes: Array = [ const router = createRouter({ scrollBehavior(to, from, savedPosition) { if ( - (to.name == 'SceneryView' || to.name == 'DriverView') && + (to.name == 'SceneryView' || to.name == 'DriverView' || to.name == 'PlayerProfileView') && from.name !== to.name && from.query['view'] === undefined && !savedPosition ) - return { el: `.app_main`, behavior: 'instant', top: -13 }; + return { el: `.app_main`, behavior: 'smooth', top: 0 }; if (savedPosition) return savedPosition; }, diff --git a/src/store/apiStore.ts b/src/store/apiStore.ts index 3977102..0404bdd 100644 --- a/src/store/apiStore.ts +++ b/src/store/apiStore.ts @@ -9,7 +9,8 @@ export const useApiStore = defineStore('apiStore', { dataStatuses: { connection: Status.Data.Loading, sceneries: Status.Data.Loading, - vehicles: Status.Data.Loading + vehicles: Status.Data.Loading, + dailyStatsData: Status.Data.Loading }, activeData: undefined as API.ActiveData.Response | undefined, @@ -18,6 +19,8 @@ export const useApiStore = defineStore('apiStore', { donatorsData: [] as API.Donators.Response, sceneryData: [] as StationJSONData[], + dailyStatsData: null as API.DailyStats.Response | null, + nextUpdateTime: 0, nextDataCheckTime: 0, @@ -65,7 +68,7 @@ export const useApiStore = defineStore('apiStore', { // Active data fefresh if (t >= this.nextUpdateTime) { this.fetchActiveData(); - this.nextUpdateTime = t + 20000; + this.nextUpdateTime = t + 31000; } window.requestAnimationFrame(this.updateTick); @@ -119,6 +122,21 @@ export const useApiStore = defineStore('apiStore', { this.dataStatuses.vehicles = Status.Data.Error; console.error('Ups! Wystąpił błąd podczas pobierania informacji o pojazdach:', error); } + }, + + async fetchDailyStats() { + try { + const res: API.DailyStats.Response = await ( + await this.client!.get('api/getDailyStats') + ).data; + + this.dailyStatsData = res; + + this.dataStatuses.dailyStatsData = Status.Data.Loaded; + } catch (error) { + console.error('Ups! Wystąpił błąd podczas pobierania statystyk rozkładów jazdy...'); + this.dataStatuses.dailyStatsData = Status.Data.Error; + } } } }); diff --git a/src/store/mainStore.ts b/src/store/mainStore.ts index 357b671..d863e9c 100644 --- a/src/store/mainStore.ts +++ b/src/store/mainStore.ts @@ -26,20 +26,12 @@ export const useMainStore = defineStore('mainStore', { isOffline: false, appUpdate: null, - dispatcherStatsName: '', - dispatcherStatsStatus: Status.Data.Initialized, - - driverStatsName: '', - driverStatsData: undefined, - driverStatsStatus: Status.Data.Initialized, - chosenModalTrainId: undefined, modalLastClickedTarget: null, currentLocale: 'pl', - isMigrateInfoCardOpen: false, - pinnedStationNames: [] + isMigrateInfoCardOpen: false }) as MainStoreState, actions: { diff --git a/src/store/typings.ts b/src/store/typings.ts index 2b6f9a2..e7e9305 100644 --- a/src/store/typings.ts +++ b/src/store/typings.ts @@ -5,11 +5,6 @@ export interface MainStoreState { region: { id: string; value: string; name: string }; isOffline: boolean; appUpdate: { version: string; changelog: string; releaseURL: string } | null; - dispatcherStatsName: string; - dispatcherStatsData?: API.DispatcherStats.Response; - driverStatsName: string; - driverStatsData?: API.DriverStats.Response; - driverStatsStatus: Status.Data; chosenModalTrainId?: string; modalLastClickedTarget: EventTarget | null; currentLocale: string; diff --git a/src/styles/_badge.scss b/src/styles/_badge.scss index 1aeb6eb..f919df3 100644 --- a/src/styles/_badge.scss +++ b/src/styles/_badge.scss @@ -135,3 +135,20 @@ color: black; } } + +.timetable-status-badge { + padding: 0.05em 0.35em; + color: black; + + &.terminated { + background-color: salmon; + } + + &.fulfilled { + background-color: lightgreen; + } + + &.active { + background-color: lightblue; + } +} \ No newline at end of file diff --git a/src/styles/_global.scss b/src/styles/_global.scss index b24ed4d..9103bec 100644 --- a/src/styles/_global.scss +++ b/src/styles/_global.scss @@ -9,6 +9,9 @@ --clr-bg2: #1b1b1b; --clr-bg3: #1d1d1d; --clr-view-bg: #1a1a1a; + --clr-bg-light: #2b2b2b; + + --clr-tile: #181818; --clr-accent: #1085b3; --clr-accent2: #ff3d5d; @@ -23,6 +26,8 @@ --clr-donator: #f7a4ff; + --clr-success: springgreen; + --no-scroll-padding: 17px; --max-container-width: 1700px; @@ -30,9 +35,8 @@ } ::-webkit-scrollbar { - width: var(--no-scroll-padding); - height: var(--no-scroll-padding); - background-color: transparent; + // width: var(--no-scroll-padding); + // height: var(--no-scroll-padding); &-track { background-color: #333; @@ -49,6 +53,7 @@ body { background: var(--clr-bg); + color-scheme: dark; margin: 0; padding: 0; @@ -331,19 +336,6 @@ a.a-button { } @include responsive.smallScreen { - ::-webkit-scrollbar { - width: 0.5em; - height: 0.5em; - - &-track { - background-color: #222; - } - - &-thumb { - background-color: #777; - } - } - [data-tooltip]:hover::after, [data-tooltip]:focus::after { transform: translate(-50%, 2em); diff --git a/src/styles/_journal-section.scss b/src/styles/_journal-section.scss index cffb516..f809b29 100644 --- a/src/styles/_journal-section.scss +++ b/src/styles/_journal-section.scss @@ -11,8 +11,8 @@ .list_wrapper { overflow-y: auto; - height: calc(100vh - 12.5em); - min-height: 700px; + height: calc(100vh - 21em); + min-height: 500px; margin-top: 0.5em; position: relative; diff --git a/src/typings/api.ts b/src/typings/api.ts index 60ea024..606f1b8 100644 --- a/src/typings/api.ts +++ b/src/typings/api.ts @@ -1,3 +1,4 @@ +import { Journal } from '../components/JournalView/typings'; import { Status, Vehicle, VehicleGroup } from './common'; export enum APIDataStatus { @@ -27,11 +28,22 @@ export namespace API { } } + export namespace PlayerActivity { + export interface Data { + dispatcher: API.ActiveSceneries.Data[]; + driver: API.ActiveTrains.Data | null; + } + + export type Response = Data; + } + export namespace DispatcherHistory { export type Response = Data[]; export interface Data { id: number; + createdAt: string; + updatedAt: string; currentDuration: number; dispatcherId: number; dispatcherName: string; @@ -52,61 +64,64 @@ export namespace API { } export namespace DispatcherStats { - export interface DistanceStat { - routeDistance: number | null; + export interface Services { + count: number; + durationMax: number; + durationAvg: number; } - export interface DurationStat { - currentDuration: number | null; + export interface IssuedTimetables { + count: number; + distanceMax: number; + distanceAvg: number; + distanceSum: number; } - export interface Count { - _all: number; + export interface Data { + dispatcherId: number | null; + dispatcherName: string | null; + dispatcherLevel: number | null; + services: Services | null; + issuedTimetables: IssuedTimetables | null; } - export interface Response { - services: { - count: number; - durationMax: number; - durationAvg: number; - } | null; - - issuedTimetables: { - count: number; - distanceMax: number; - distanceAvg: number; - distanceSum: number; - } | null; - } + export type Response = Data; } export namespace DriverStats { - export interface SumStats { - routeDistance: number; - confirmedStopsCount: number; - allStopsCount: number; - currentDistance: number; + export interface Data { + driverName: string | null; + driverId: number | null; + driverLevel: number | null; + countAll: number; + countTerminated: number; + countFulfilled: number; + routeDistanceTotal: number | null; + routeDistanceAvg: number | null; + routeDistanceMax: number | null; + currentDistanceTotal: number | null; + confirmedStopsTotal: number | null; + allStopsTotal: number | null; } - export interface CountStats { - fulfilled: number; - terminated: number; - _all: number; - } + export type Response = Data; + } - export interface MaxStats { - routeDistance: number; + export namespace PlayerInfo { + export interface Data { + currentActivity: PlayerActivity.Data; + dispatcherStats: DispatcherStats.Data; + dispatcherStatsLastMonth: DispatcherStats.Data; + driverStats: DriverStats.Data; + driverStatsLastMonth: DriverStats.Data; } + } - export interface AvdStats { - routeDistance: number; - } - - export interface Response { - _sum: SumStats; - _count: CountStats; - _max: MaxStats; - _avg: AvdStats; + export namespace PlayerJournal { + export interface Data { + timetables: TimetableHistory.DataShort[]; + issuedTimetables: TimetableHistory.DataShort[]; + duties: DispatcherHistory.Data[]; } } @@ -211,14 +226,48 @@ export namespace API { } export namespace TimetableHistory { - export interface Data { + export interface QueryParams { + driverName?: string; + trainNo?: string; + timetableId?: string; + categoryCode?: string; + + authorName?: string; + + dateFrom?: string; + dateTo?: string; + + issuedFrom?: string; + terminatingAt?: string; + via?: string; + includesScenery?: string; + + countFrom?: number; + countLimit?: number; + + fulfilled?: number; + terminated?: number; + + twr?: number; + skr?: number; + pn?: number; + tn?: number; + + returnType?: 'all' | 'short' | 'detailed'; + + sortBy?: Journal.TimetableSorter['id']; + } + + export interface Data extends DataShort, DataDetailsOnly { + updatedAt: string; + } + + export interface DataShort { id: number; createdAt: string; - updatedAt: string; - - timetableId: number; trainNo: number; trainCategoryCode: string; + timetableId: number; driverId: number; driverName: string; @@ -229,7 +278,6 @@ export namespace API { route: string; twr: number; skr: number; - sceneriesString: string; currentLocation: string[]; routeDistance: number; @@ -240,7 +288,6 @@ export namespace API { beginDate: string; endDate: string; - scheduledBeginDate: string; scheduledEndDate: string; @@ -250,15 +297,25 @@ export namespace API { authorName?: string; authorId?: number; + currentSceneryName?: string; + currentSceneryHash?: string; + hasDangerousCargo: boolean; + hasExtraDeliveries: boolean; + } + + export interface DataDetailsOnly { + id: number; + timetableId: number; + + sceneriesString: string; stockString?: string; stockHistory: string[]; stockMass?: number; stockLength?: number; maxSpeed?: number; + trainMaxSpeed?: number; - currentSceneryName?: string; - currentSceneryHash?: string; routeSceneries: string; checkpointArrivals: string[]; checkpointDepartures: string[]; @@ -268,14 +325,20 @@ export namespace API { checkpointComments: string[]; visitedSceneries: string[]; sceneryNames: string[]; + path: string; warningNotes: string | null; - hasDangerousCargo: boolean; - hasExtraDeliveries: boolean; - trainMaxSpeed?: number; + + authorId?: number; + authorName?: string; + driverId: number; + driverName: string; + driverLanguageId: number | null; } export type Response = Data[]; + export type ResponseShort = DataShort[]; + export type ResponseDetailsOnly = DataDetailsOnly[]; } export namespace DailyStats { @@ -427,6 +490,62 @@ export namespace GithubAPI { } } +export namespace Td2API { + export namespace UsersInfoByName { + export interface UserStat { + variable: string; + value: number; + position: number; + server_total: number; + server_max: number; + server_min: number; + server_avg: number; + } + + export interface Levels { + driver: number; + dispatcher: number; + } + + export interface UserGroup { + id_group: number; + group_name: string; + description: string; + online_color: string; + min_posts: number; + max_messages: number; + stars: string; + group_type: number; + hidden: number; + id_parent: number; + } + + export interface UserInfo { + id_member: number; + id_group: number; + additional_groups: string; + member_name: string; + karma_bad: number; + karma_good: number; + date_registered: number; + last_login: number; + avatar: number; + lngfile: string; + user_stats: UserStat[]; + levels: Levels; + user_groups: UserGroup[]; + } + + export type Message = UserInfo[]; + + export interface Response { + success: boolean; + respCode: number; + message: Message; + } + } +} + export namespace Websocket { export interface Payload { activeSceneries: API.ActiveSceneries.Response; diff --git a/src/typings/common.ts b/src/typings/common.ts index 79af5d1..684c030 100644 --- a/src/typings/common.ts +++ b/src/typings/common.ts @@ -220,6 +220,7 @@ export interface CheckpointTrain { export type Vehicle = API.VehiclesData.VehicleObject; export type VehicleGroup = API.VehiclesData.VehicleGroupObject; +// Train Tooltip Info export interface TooltipUserTrain { driverName: string; trainNo: number; @@ -240,4 +241,4 @@ export interface TooltipTrainInfo { headVehicleName: string; stockCount: number; trainTimetableCategory?: string; -} +} \ No newline at end of file diff --git a/src/utils/calcUtils.ts b/src/utils/calcUtils.ts new file mode 100644 index 0000000..7541073 --- /dev/null +++ b/src/utils/calcUtils.ts @@ -0,0 +1,5 @@ +export function getCountPercentage(partCount: number, allCount: number, fixedDigits: number) { + if (allCount == 0) return 0; + + return ((partCount / allCount) * 100).toFixed(fixedDigits); +} diff --git a/src/utils/regionUtils.ts b/src/utils/regionUtils.ts new file mode 100644 index 0000000..2807b58 --- /dev/null +++ b/src/utils/regionUtils.ts @@ -0,0 +1,19 @@ +export enum ServerRegion { + 'eu' = 'PL1', + 'cae' = 'PL2', + 'usw' = 'DE', + 'us' = 'CZE', + 'ru' = 'ENG' +} + +export const regions: Record = { + eu: 'PL1', + cae: 'PL2', + usw: 'DE', + us: 'CZE', + ru: 'ENG' +}; + +export function getRegionNameById(id: string) { + return regions[id] ?? 'PL1'; +} diff --git a/src/views/DriverView.vue b/src/views/DriverView.vue index 5739d8f..80bf7e9 100644 --- a/src/views/DriverView.vue +++ b/src/views/DriverView.vue @@ -47,6 +47,6 @@ const chosenTrain = computed(() => margin: 0 auto; padding: 1em 0; max-width: var(--max-container-width); - min-height: calc(100vh - 7em); + min-height: 100vh; } diff --git a/src/views/JournalDispatchers.vue b/src/views/JournalDispatchers.vue index 8123de6..47e4d04 100644 --- a/src/views/JournalDispatchers.vue +++ b/src/views/JournalDispatchers.vue @@ -14,7 +14,7 @@ optionsType="dispatchers" /> - +
@@ -50,16 +50,8 @@ import JournalHeader from '../components/JournalView/JournalHeader.vue'; import JournalStats from '../components/JournalView/JournalStats.vue'; import { useApiStore } from '../store/apiStore'; -const statsButtons: Journal.StatsButton[] = [ - { - tab: Journal.StatsTab.DISPATCHER_STATS, - localeKey: 'journal.dispatcher-stats.button', - iconName: 'user', - disabled: true - } -]; - interface DispatchersQueryParams { + dutyId?: number; dispatcherName?: string; stationName?: string; stationHash?: string; @@ -105,18 +97,15 @@ export default defineComponent({ }, data: () => ({ - statsButtons, - dataRefreshedAt: null as Date | null, currentQueryParams: {} as DispatchersQueryParams, scrollDataLoaded: true, scrollNoMoreData: false, - showReturnButton: false, - statsCardOpen: false, - currentOptionsActive: false, + chosenPlayerId: -1, + currentOptionsActive: false, dataStatus: Status.Data.Loading, historyList: [] as API.DispatcherHistory.Response @@ -126,12 +115,13 @@ export default defineComponent({ const sorterActive: Journal.DispatcherSorter = reactive({ id: 'timestampFrom', dir: -1 }); const journalFilterActive = ref({}); - const searchersValues = reactive({ + const searchersValues = reactive>({ + 'search-duty-id': '', 'search-dispatcher': '', 'search-station': '', 'search-date-from': '', 'search-date-to': '' - } as Journal.DispatcherSearchType); + }); provide('sorterActive', sorterActive); provide('journalFilterActive', journalFilterActive); @@ -158,15 +148,6 @@ export default defineComponent({ queryParams[k as keyof DispatchersQueryParams] != defaultQueryParams[k as keyof DispatchersQueryParams] ); - }, - - 'mainStore.dispatcherStatsData'(stats) { - this.statsButtons.find((sb) => sb.tab == Journal.StatsTab.DISPATCHER_STATS)!.disabled = - stats === undefined; - }, - - async 'mainStore.dispatcherStatsName'() { - this.fetchDispatcherStats(); } }, @@ -192,6 +173,7 @@ export default defineComponent({ handleRouteParams() { this.$router.push({ query: { + 'search-duty-id': this.searchersValues['search-duty-id'] || undefined, 'search-date-from': this.searchersValues['search-date-from'] || undefined, 'search-date-to': this.searchersValues['search-date-to'] || undefined, 'search-station': this.searchersValues['search-station'] || undefined, @@ -215,30 +197,8 @@ export default defineComponent({ this.setOptions(query as any); }, - async fetchDispatcherStats() { - if (!this.mainStore.dispatcherStatsName) { - this.mainStore.dispatcherStatsData = undefined; - return; - } - - try { - const statsData: API.DispatcherStats.Response = await ( - await this.apiStore.client!.get('api/getDispatcherStats', { - params: { - name: this.mainStore.dispatcherStatsName - } - }) - ).data; - - this.mainStore.dispatcherStatsData = statsData; - } catch (error) { - this.mainStore.dispatcherStatsData = undefined; - - console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk dyżurnego! :/'); - } - }, - - setOptions(options: { [key: string]: string }) { + setOptions(options: Record) { + this.searchersValues['search-duty-id'] = options['search-duty-id'] ?? ''; this.searchersValues['search-date-from'] = options['search-date-from'] ?? ''; this.searchersValues['search-date-to'] = options['search-date-to'] ?? ''; this.searchersValues['search-station'] = options['search-station'] ?? ''; @@ -275,6 +235,7 @@ export default defineComponent({ async fetchHistoryData() { const queryParams: DispatchersQueryParams = {}; + const dutyId = this.searchersValues['search-duty-id'].trim() || undefined; const dispatcherName = this.searchersValues['search-dispatcher'].trim() || undefined; const stationName = this.searchersValues['search-station'].trim() || undefined; const dateFromString = this.searchersValues['search-date-from'].trim() || undefined; @@ -295,6 +256,7 @@ export default defineComponent({ dateToISO = dateTo.toISOString(); } + queryParams['dutyId'] = Number(dutyId) || undefined; queryParams['dispatcherName'] = dispatcherName; queryParams['dateFrom'] = dateFromISO; @@ -320,24 +282,24 @@ export default defineComponent({ if (!responseData) { this.dataStatus = Status.Data.Error; + this.chosenPlayerId = -1; + return; } - if (!responseData) return; - // Response data exists this.historyList = responseData; - // Stats display - this.mainStore.dispatcherStatsName = - this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim() - ? this.historyList[0].dispatcherName - : ''; + this.chosenPlayerId = + this.historyList.length > 0 && this.searchersValues['search-dispatcher'].trim() != '' + ? this.historyList[0].dispatcherId + : -1; this.dataRefreshedAt = new Date(); this.dataStatus = Status.Data.Loaded; } catch (error) { this.dataStatus = Status.Data.Error; + this.chosenPlayerId = -1; } this.scrollNoMoreData = false; diff --git a/src/views/JournalTimetables.vue b/src/views/JournalTimetables.vue index 978d0bd..1e7986d 100644 --- a/src/views/JournalTimetables.vue +++ b/src/views/JournalTimetables.vue @@ -14,7 +14,7 @@ optionsType="timetables" /> - +
@@ -29,6 +29,8 @@ :dataStatus="dataStatus" :scrollDataLoaded="scrollDataLoaded" :scrollNoMoreData="scrollNoMoreData" + :extraInfoIndexes="extraInfoIndexes" + @toggleExtraInfo="toggleExtraInfo" />
@@ -118,36 +120,6 @@ export const journalTimetableFilters: Journal.TimetableFilter[] = [ } ]; -interface TimetablesQueryParams { - driverName?: string; - trainNo?: string; - timetableId?: string; - categoryCode?: string; - - authorName?: string; - - dateFrom?: string; - dateTo?: string; - - issuedFrom?: string; - terminatingAt?: string; - via?: string; - includesScenery?: string; - - countFrom?: number; - countLimit?: number; - - fulfilled?: number; - terminated?: number; - - twr?: number; - skr?: number; - pn?: number; - tn?: number; - - sortBy?: Journal.TimetableSorter['id']; -} - export default defineComponent({ components: { JournalOptions, @@ -170,35 +142,18 @@ export default defineComponent({ mainStore: useMainStore(), apiStore: useApiStore(), - statsButtons: [ - { - tab: Journal.StatsTab.DAILY_STATS, - localeKey: 'journal.daily-stats.button', - iconName: 'stats', - disabled: false - }, - { - tab: Journal.StatsTab.DRIVER_STATS, - localeKey: 'journal.driver-stats.button', - iconName: 'train', - disabled: true - } - ], - - currentQueryParams: {} as TimetablesQueryParams, + currentQueryParams: {} as API.TimetableHistory.QueryParams, dataRefreshedAt: null as Date | null, scrollDataLoaded: true, scrollNoMoreData: false, + extraInfoIndexes: [] as number[], - showReturnButton: false, - statsCardOpen: false, - currentOptionsActive: false, + chosenPlayerId: -1, - timetableHistory: [] as API.TimetableHistory.Response, + timetableHistory: [] as API.TimetableHistory.ResponseShort, - dataStatus: Status.Data.Loading, - dataErrorMessage: '' + dataStatus: Status.Data.Loading }), setup() { @@ -245,18 +200,11 @@ export default defineComponent({ }; }, - watch: { - currentQueryParams(q: TimetablesQueryParams) { - this.currentOptionsActive = Object.values(q).some((v) => v !== undefined); - }, - - 'mainStore.driverStatsData'(driverStats) { - this.statsButtons.find((sb) => sb.tab == Journal.StatsTab.DRIVER_STATS)!.disabled = - driverStats === undefined; - }, - - async 'mainStore.driverStatsName'() { - this.fetchDriverStats(); + computed: { + currentOptionsActive() { + return Object.keys(this.currentQueryParams) + .filter((k) => k != 'countFrom' && k != 'returnType') + .some((k) => (this.currentQueryParams as any)[k] !== undefined); } }, @@ -287,28 +235,21 @@ export default defineComponent({ this.setOptions(query as any); }, - async fetchDriverStats() { - if (!this.mainStore.driverStatsName) { - this.mainStore.driverStatsData = undefined; - this.mainStore.driverStatsStatus = Status.Data.Initialized; - return; - } + async toggleExtraInfo(timetableDetails: API.TimetableHistory.Data | null) { + if (!timetableDetails) return; - try { - this.mainStore.driverStatsStatus = Status.Data.Loading; + const existingIdx = this.extraInfoIndexes.indexOf(timetableDetails.id); - const statsData: API.DriverStats.Response = await ( - await this.apiStore.client!.get( - `api/getDriverInfo?name=${this.mainStore.driverStatsName}` - ) - ).data; + if (existingIdx == -1) { + this.extraInfoIndexes.push(timetableDetails.id); - this.mainStore.driverStatsData = statsData; - this.mainStore.driverStatsStatus = Status.Data.Loaded; - } catch (error) { - this.mainStore.driverStatsData = undefined; - this.mainStore.driverStatsStatus = Status.Data.Error; - console.error('Ups! Wystąpił błąd przy próbie pobrania statystyk maszynisty! :/'); + const synchedTimetable = this.timetableHistory.find((t) => t.id == timetableDetails.id); + + if (synchedTimetable) { + Object.assign(synchedTimetable, timetableDetails); + } + } else { + this.extraInfoIndexes.splice(existingIdx, 1); } }, @@ -354,6 +295,8 @@ export default defineComponent({ }, async fetchHistoryData() { + this.extraInfoIndexes.length = 0; + const driverName = this.searchersValues['search-driver'].trim() || undefined; const trainNo = this.searchersValues['search-train'].trim() || undefined; const authorName = this.searchersValues['search-dispatcher'].trim() || undefined; @@ -378,7 +321,7 @@ export default defineComponent({ dateToISO = dateTo.toISOString(); } - const queryParams: TimetablesQueryParams = {}; + const queryParams: API.TimetableHistory.QueryParams = {}; this.filterList .filter((f) => f.isActive) @@ -445,6 +388,7 @@ export default defineComponent({ queryParams['terminatingAt'] = terminatingAt; queryParams['via'] = via; queryParams['categoryCode'] = categoryCode; + queryParams['returnType'] = 'short'; queryParams['issuedFrom'] = issuedFrom; queryParams['sortBy'] = @@ -456,7 +400,7 @@ export default defineComponent({ this.currentQueryParams = queryParams; try { - const responseData: API.TimetableHistory.Response = await ( + const responseData: API.TimetableHistory.ResponseShort = await ( await this.apiStore.client!.get('api/getTimetables', { params: this.currentQueryParams }) @@ -464,26 +408,23 @@ export default defineComponent({ if (!responseData) { this.dataStatus = Status.Data.Error; - this.dataErrorMessage = 'Brak danych!'; + this.chosenPlayerId = -1; return; } - if (!responseData) return; - // Response data exists this.timetableHistory = responseData; - // Stats display - this.mainStore.driverStatsName = - this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim() - ? this.timetableHistory[0].driverName - : ''; + this.chosenPlayerId = + this.timetableHistory.length > 0 && this.searchersValues['search-driver'].trim() != '' + ? this.timetableHistory[0].driverId + : -1; this.dataStatus = Status.Data.Loaded; this.dataRefreshedAt = new Date(); } catch (error) { this.dataStatus = Status.Data.Error; - this.dataErrorMessage = 'Ups! Coś poszło nie tak!'; + this.chosenPlayerId = -1; } this.scrollNoMoreData = false; diff --git a/src/views/PlayerProfileView.vue b/src/views/PlayerProfileView.vue new file mode 100644 index 0000000..5266a22 --- /dev/null +++ b/src/views/PlayerProfileView.vue @@ -0,0 +1,221 @@ + + + + + diff --git a/src/views/SceneryView.vue b/src/views/SceneryView.vue index c4cef30..767e293 100644 --- a/src/views/SceneryView.vue +++ b/src/views/SceneryView.vue @@ -135,6 +135,10 @@ function setViewMode(componentName: string) { &-view { display: flex; justify-content: center; + + height: 100vh; + min-height: 500px; + max-height: 2000px; } &-offline { @@ -181,10 +185,6 @@ function setViewMode(componentName: string) { background-color: #181818; border-radius: 0.5em; padding: 1em 0.5em; - - height: calc(100vh - 0.5em); - min-height: 500px; - max-height: 2000px; } .scenery-left { @@ -236,6 +236,10 @@ function setViewMode(componentName: string) { } @include responsive.midScreen { + .scenery-view { + height: auto; + } + .scenery-wrapper { grid-template-columns: 1fr; gap: 0; diff --git a/src/views/StationsView.vue b/src/views/StationsView.vue index 834f049..0f8717f 100644 --- a/src/views/StationsView.vue +++ b/src/views/StationsView.vue @@ -32,6 +32,16 @@ + + discord logo icon + +