Compare commits

...

21 Commits

Author SHA1 Message Date
Spythere 37e4149a34 Merge pull request #15 from Spythere/development
Wersja 1.7.0
2023-10-26 21:43:46 +02:00
Spythere e515203557 stockObject & static handling hotfixes 2023-10-26 21:37:33 +02:00
Spythere 0d79c71eba 1.7.0 bump 2023-10-26 21:36:00 +02:00
Spythere 2bbf9a8ac3 section improvements; hotfixes 2023-10-26 21:35:42 +02:00
Spythere 45b2bd01a2 enhanced wiki list 2023-10-26 00:55:13 +02:00
Spythere 665ffb9dce lock: hotfix; bump 1.6.1 2023-10-24 23:31:45 +02:00
Spythere 1c2a93fbd5 format; linting; aktualizacja do 2023.2.1 2023-10-24 23:28:42 +02:00
Spythere 57ab6cc02d gitignore 2023-08-25 20:29:17 +02:00
Spythere aa62240d27 Merge do wersji 1.6.0 2023-08-24 01:29:45 +02:00
Spythere a699c9851f tłumaczenia; fix losowania generatora numeru 2023-08-24 00:01:19 +02:00
Spythere d7c1f8c5b4 fix: tłumaczenia alertów 2023-08-23 23:04:04 +02:00
Spythere c1c01c496a poprawki w widoku składu; tłumaczenia 2023-08-23 22:54:17 +02:00
Spythere a7f92d3ec5 feature: zapamiętywanie języka 2023-08-22 18:34:15 +02:00
Spythere ae7be6d6f8 tłumaczenie PL/EN (cz.3) 2023-08-22 18:30:04 +02:00
Spythere bf204d5e36 hotfix: pliki lock 2023-08-22 00:55:17 +02:00
Spythere 459876f5b4 bump: 1.6.0 2023-08-22 00:53:02 +02:00
Spythere 4a1a840c16 cleanup i18n; wykrywanie języka 2023-08-22 00:52:51 +02:00
Spythere c0552e890c hotfix: synchronizacja plików lock 2023-08-22 00:36:31 +02:00
Spythere 80d6f2b85f tłumaczenie PL/EN (wip, cz.2) 2023-08-22 00:33:51 +02:00
Spythere 17882e3e6e feature: tłumaczenie PL/EN (wip) 2023-08-21 03:19:37 +02:00
Spythere 4a03535b07 feature: ikony akcji 2023-08-19 13:13:00 +02:00
62 changed files with 14302 additions and 13277 deletions
+18
View File
@@ -0,0 +1,18 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier/skip-formatting'
],
rules: {
'vue/multi-word-component-names': 'off'
},
parserOptions: {
ecmaVersion: 'latest'
}
}
-20
View File
@@ -1,20 +0,0 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint",
],
parserOptions: {
ecmaVersion: 2020,
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
};
+4 -1
View File
@@ -25,4 +25,7 @@ pnpm-debug.log*
node_modules node_modules
# Dev files # Dev files
stockInfoDev.json stockInfoDev.json
# Lock files
yarn.lock
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};
+1
View File
@@ -13,6 +13,7 @@
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" /> <link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" /> <link rel="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" /> <link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" /> <meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#e4c428" /> <meta name="theme-color" content="#e4c428" />
</head> </head>
+12288 -8878
View File
File diff suppressed because it is too large Load Diff
+15 -3
View File
@@ -1,18 +1,30 @@
{ {
"name": "pojazdownik", "name": "pojazdownik",
"version": "1.5.0", "version": "1.7.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"preview": "yarn build && vite preview --port 4174" "preview": "yarn build && vite preview --port 4174",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
}, },
"dependencies": { "dependencies": {
"axios": "^1.4.0",
"pinia": "^2.0.17", "pinia": "^2.0.17",
"vue": "^3.2.37" "prettier": "^3.0.3",
"vue": "^3.2.37",
"vue-i18n": "9"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@vitejs/plugin-vue": "^4.1.0", "@vitejs/plugin-vue": "^4.1.0",
"@vue/eslint-config-prettier": "^8.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.4.0",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"sass": "^1.59.3", "sass": "^1.59.3",
"typescript": "^5.0.2", "typescript": "^5.0.2",
"vite": "^4.2.1", "vite": "^4.2.1",
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" fill="white" width="48"><path d="M314.696-195.478q-32.507 0-55.862-23.356-23.356-23.355-23.356-55.862v-549.826q0-32.74 23.356-56.262 23.355-23.522 55.862-23.522h429.826q32.74 0 56.262 23.522t23.522 56.262v549.826q0 32.507-23.522 55.862-23.522 23.356-56.262 23.356H314.696Zm0-79.218h429.826v-549.826H314.696v549.826ZM175.478-55.694q-32.74 0-56.262-23.522t-23.522-56.262v-629.044h79.784v629.044h509.044v79.784H175.478Zm139.218-219.002v-549.826 549.826Z"/></svg>

After

Width:  |  Height:  |  Size: 536 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" fill="white" width="48"><path d="M480-318.087 266.651-531.436l57.131-56.001 116.609 116.609v-343.868h79.218v343.868l116.609-116.609 57.131 56.001L480-318.087ZM225.087-145.869q-32.507 0-55.862-23.356-23.356-23.355-23.356-55.862v-143h79.218v143h509.826v-143h79.783v143q0 32.478-23.521 55.848-23.522 23.37-56.262 23.37H225.087Z"/></svg>

After

Width:  |  Height:  |  Size: 410 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" fill="white" width="48"><path d="M451-107.304q-128.652-10.565-216.892-105.522-88.239-94.956-88.239-225.609 0-78.13 35.5-147.543 35.5-69.413 99.5-114.674l56.566 56.565q-52.609 32.435-82.478 87.957-29.87 55.521-29.87 117.695 0 98.305 64.587 169.609T451-187.087v79.783Zm60 0v-79.783q98.304-11.435 161.609-82.239 63.304-70.804 63.304-169.109 0-103.913-70.978-177.434-70.978-73.522-174.891-76.913h-21.696l62.261 62.826-47.522 47.522-152.783-152.784 152.783-153.349 47.522 47.522-68.479 68.479h22.261q138.522 0 234.914 98.022 96.391 98.022 96.391 236.109 0 130.653-88.022 225.609Q639.652-117.869 511-107.304Z"/></svg>

After

Width:  |  Height:  |  Size: 688 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" fill="white" width="48"><path d="M575.913-143.782v-72.436h115.956l-173.26-172.825 51.043-51.479 175.261 172.695v-117.391h73v241.436h-242Zm-382.783 2.826L142.652-193l551.217-551.782H575.913v-72.436h242v241.436h-73v-116.391L193.13-140.956Zm198.479-378.522L142.652-768l51.478-52.044 250.088 248.522-52.609 52.044Z"/></svg>

After

Width:  |  Height:  |  Size: 396 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" fill="white" width="48"><path d="M440.391-318.087v-343.868L323.782-544.782l-57.131-56.566L480-814.696l213.349 213.348-57.131 56.566-116.609-117.173v343.868h-79.218ZM225.087-145.869q-32.507 0-55.862-23.356-23.356-23.355-23.356-55.862v-143h79.218v143h509.826v-143h79.783v143q0 32.478-23.521 55.848-23.522 23.37-56.262 23.37H225.087Z"/></svg>

After

Width:  |  Height:  |  Size: 416 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

-12
View File
@@ -18,11 +18,6 @@ export default defineComponent({
}; };
}, },
async created() { async created() {
/* dev info testing */
// if (import.meta.env['VITE_STOCK_DEV'] == '1') {
// const data = await import('../stockInfoDev.json');
// this.store.stockData = data.default as any;
// }
this.store.fetchStockInfoData(); this.store.fetchStockInfoData();
this.store.handleRouting(); this.store.handleRouting();
}, },
@@ -59,11 +54,4 @@ h2 {
color: #d1d1d1; color: #d1d1d1;
} }
@media screen and (max-width: $breakpointMd) {
#app {
font-size: calc(0.7rem + 0.75vw);
}
}
</style> </style>
+24 -24
View File
@@ -1,24 +1,24 @@
<template> <template>
<div> <div>
<keep-alive> <keep-alive>
<RealStockCard v-if="store.isRealStockListCardOpen" /> <RealStockCard v-if="store.isRealStockListCardOpen" />
</keep-alive> </keep-alive>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from "vue";
import { useStore } from '../../store'; import { useStore } from "../../store";
import RealStockCard from '../cards/RealStockCard.vue'; import RealStockCard from "../cards/RealStockCard.vue";
export default defineComponent({ export default defineComponent({
components: { RealStockCard }, components: { RealStockCard },
data() { data() {
return { return {
store: useStore(), store: useStore(),
}; };
}, },
}); });
</script> </script>
<style scoped></style> <style scoped></style>
+52 -52
View File
@@ -1,52 +1,52 @@
<template> <template>
<footer> <footer>
<div class="text--grayed"> <i18n-t keypath="footer.disclaimer" tag="div" class="text--grayed">
Ta strona ma charakter informacyjny. Autor nie ponosi odpowiedzialności za tworzenie pociągów niezgodnych z <template #tos>
<a <a style="color: #ccc" :href="$t('footer.tos-href')" target="_blank">
style="color: #ccc" {{ $t("footer.tos") }}
href="https://docs.google.com/document/d/1UAAPUtN0d_RoS4RgOzEzllJZJhA0VcizzCzKW4QylbY/edit" </a>
target="_blank" </template>
> </i18n-t>
regulaminem symulatora Train Driver 2</a
>! <div class="text--grayed" v-if="store.stockData">
</div> {{ $t("footer.version-check", { version: store.stockData.version }) }}
</div>
<div class="text--grayed" v-if="store.stockData">
Strona jest kompletna dla wersji {{ store.stockData.version }} symulatora TD2 <div>
</div> &copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank"
<div> >Spythere</a
&copy; >
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a> {{ new Date().getUTCFullYear() }} | v{{ VERSION
{{ new Date().getUTCFullYear() }} | v{{ VERSION }}{{ !isOnProductionHost ? 'dev' : '' }} }}{{ !isOnProductionHost ? "dev" : "" }}
</div> </div>
</footer> </footer>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from "vue";
import packageInfo from '../../../package.json'; import packageInfo from "../../../package.json";
import { useStore } from '../../store'; import { useStore } from "../../store";
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
isOnProductionHost: location.hostname == 'pojazdownik-td2.web.app', isOnProductionHost: location.hostname == "pojazdownik-td2.web.app",
VERSION: packageInfo.version, VERSION: packageInfo.version,
store: useStore(), store: useStore(),
}; };
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
footer { footer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.25em; gap: 0.25em;
text-align: center; text-align: center;
padding: 1em 1em 0 1em; padding: 1em 1em 0 1em;
margin-top: auto; margin-top: auto;
} }
</style> </style>
+45 -45
View File
@@ -1,45 +1,45 @@
<template> <template>
<main> <main>
<LogoSection /> <LogoSection />
<InputsSection /> <InputsSection />
<TrainImageSection /> <TrainImageSection />
<StockSection /> <StockSection />
</main> </main>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from "vue";
import LogoSection from '../sections/LogoSection.vue'; import LogoSection from "../sections/LogoSection.vue";
import InputsSection from '../sections/InputsSection.vue'; import InputsSection from "../sections/InputsSection.vue";
import TrainImageSection from '../sections/TrainImageSection.vue'; import TrainImageSection from "../sections/TrainImageSection.vue";
import StockSection from '../sections/StockSection.vue'; import StockSection from "../sections/StockSection.vue";
export default defineComponent({ export default defineComponent({
components: { LogoSection, InputsSection, TrainImageSection, StockSection }, components: { LogoSection, InputsSection, TrainImageSection, StockSection },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/global.scss'; @import "../../styles/global.scss";
main { main {
display: grid; display: grid;
gap: 1em; gap: 1em;
width: 100%; width: 100%;
max-width: 1500px; max-width: 1300px;
min-height: 75vh; min-height: 75vh;
grid-template-columns: 1fr 2fr; grid-template-columns: 1fr 2fr;
grid-template-rows: auto 360px minmax(400px, 1fr); grid-template-rows: auto 360px minmax(400px, 1fr);
} }
@media screen and (max-width: $breakpointMd) { @media screen and (max-width: $breakpointMd) {
main { main {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-rows: 1fr; grid-template-rows: 1fr;
} }
} }
</style> </style>
+100 -48
View File
@@ -1,34 +1,63 @@
<template> <template>
<div class="real-stock-card g-card" @keydown.esc="store.isRealStockListCardOpen = false"> <div
class="real-stock-card g-card"
@keydown.esc="store.isRealStockListCardOpen = false"
>
<div class="g-card_bg" @click="store.isRealStockListCardOpen = false"></div> <div class="g-card_bg" @click="store.isRealStockListCardOpen = false"></div>
<div class="card_content"> <div class="card_content">
<div class="card_nav"> <div class="card_nav">
<div class="top-pane"> <div class="top-pane">
<h1> <h1>
ZESTAWIENIA REALNE by <a href="https://td2.info.pl/profile/?u=17708" target="_blank">Railtrains997</a> {{ $t("realstock.title") }}
<a href="https://td2.info.pl/profile/?u=17708" target="_blank"
>Railtrains997</a
>
</h1> </h1>
<button class="btn exit-btn" @click="store.isRealStockListCardOpen = false">&Cross;</button> <button
class="btn exit-btn"
@click="store.isRealStockListCardOpen = false"
>
&Cross;
</button>
</div> </div>
<div class="filters" ref="focus" tabindex="0"> <div class="filters" ref="focus" tabindex="0">
<input list="readyStockDataList" v-model="searchedReadyStockName" placeholder="Szukaj po nazwie" /> <input
list="readyStockDataList"
v-model="searchedReadyStockName"
:placeholder="$t('realstock.search-name')"
/>
<datalist id="readyStockDataList"> <datalist id="readyStockDataList">
<option v-for="stock in store.readyStockList" :value="stock.stockId"> <option
v-for="stock in store.readyStockList"
:value="stock.stockId"
:key="stock.name"
>
{{ stock.stockId }} {{ stock.stockId }}
</option> </option>
</datalist> </datalist>
<input list="readyStockStringList" v-model="searchedReadyStockString" placeholder="Szukaj po pojazdach" /> <input
list="readyStockStringList"
v-model="searchedReadyStockString"
:placeholder="$t('realstock.search-stock')"
/>
<datalist id="readyStockStringList"> <datalist id="readyStockStringList">
<option v-for="stock in computedAvailableStockTypes" :value="stock"> <option
{{ stock }} v-for="stockType in computedAvailableStockTypes"
:value="stockType"
:key="stockType"
>
{{ stockType }}
</option> </option>
</datalist> </datalist>
<button class="btn" @click="resetStockFilters">RESETUJ</button> <button class="btn" @click="resetStockFilters">
{{ $t("realstock.action-reset") }}
</button>
</div> </div>
</div> </div>
@@ -38,14 +67,29 @@
:key="rStock.stockId" :key="rStock.stockId"
:data-last-selected="store.chosenRealStockName === rStock.stockId" :data-last-selected="store.chosenRealStockName === rStock.stockId"
> >
<div class="stock-title" tabindex="0" @click="chooseStock(rStock)" @keydown.enter="chooseStock(rStock)"> <div
<img class="stock-icon" :src="getIconURL(rStock.type)" :alt="rStock.type" /> class="stock-title"
<b class="text--accent" style="margin-left: 5px"> {{ rStock.name }}</b> tabindex="0"
@click="chooseStock(rStock)"
@keydown.enter="chooseStock(rStock)"
>
<img
class="stock-icon"
:src="getIconURL(rStock.type)"
:alt="rStock.type"
/>
<b class="text--accent" style="margin-left: 5px">
{{ rStock.name }}</b
>
<div>{{ rStock.number }}</div> <div>{{ rStock.number }}</div>
</div> </div>
<div class="stock-thumbnails" ref="thumbnailsRef"> <div class="stock-thumbnails" ref="thumbnailsRef">
<div class="thumbnail-item" v-for="stockType in rStock.stockString.split(';')"> <div
class="thumbnail-item"
v-for="stockType in rStock.stockString.split(';')"
:key="stockType"
>
<div class="thumbnail-container"> <div class="thumbnail-container">
<div>{{ stockType }}</div> <div>{{ stockType }}</div>
<img <img
@@ -53,7 +97,7 @@
:title="stockType" :title="stockType"
style="opacity: 0" style="opacity: 0"
@error="(e) => onStockItemError(e, stockType)" @error="(e) => onStockItemError(e, stockType)"
@load="e => (e.target as HTMLElement).style.opacity = '1'" @load="(e) => ((e.target as HTMLElement).style.opacity = '1')"
/> />
</div> </div>
</div> </div>
@@ -67,23 +111,24 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from "vue";
import { useStore } from '../../store'; import { useStore } from "../../store";
import imageMixin from '../../mixins/imageMixin'; import imageMixin from "../../mixins/imageMixin";
import stockMixin from '../../mixins/stockMixin'; import stockMixin from "../../mixins/stockMixin";
import { IReadyStockItem } from '../../types'; import { IReadyStockItem } from "../../types";
import http from "../../http";
interface ResponseJSONData { interface ResponseJSONData {
[key: string]: string; [key: string]: string;
} }
function getVehicleType(stockType: string) { function getVehicleType(stockType: string) {
if (/^E/.test(stockType)) return 'loco-e'; if (/^E/.test(stockType)) return "loco-e";
if (/^S/.test(stockType)) return 'loco-s'; if (/^S/.test(stockType)) return "loco-s";
return 'car-passenger'; return "car-passenger";
} }
export default defineComponent({ export default defineComponent({
@@ -91,11 +136,15 @@ export default defineComponent({
data: () => ({ data: () => ({
store: useStore(), store: useStore(),
responseStatus: 'loading', responseStatus: "loading",
isMobile: 'ontouchstart' in document.documentElement && navigator.userAgent.match(/Mobi/) ? true : false, isMobile:
"ontouchstart" in document.documentElement &&
navigator.userAgent.match(/Mobi/)
? true
: false,
observer: null as IntersectionObserver | null, observer: null as IntersectionObserver | null,
searchedReadyStockName: '', searchedReadyStockName: "",
searchedReadyStockString: '', searchedReadyStockString: "",
visibleIndexesTo: 0, visibleIndexesTo: 0,
lastSelectedStockId: null as string | null, lastSelectedStockId: null as string | null,
scrollTop: 0, scrollTop: 0,
@@ -107,11 +156,11 @@ export default defineComponent({
}, },
activated() { activated() {
(this.$refs['focus'] as HTMLElement).focus(); (this.$refs["focus"] as HTMLElement).focus();
(this.$refs['list'] as HTMLElement).scrollTo({ (this.$refs["list"] as HTMLElement).scrollTo({
top: this.scrollTop, top: this.scrollTop,
behavior: 'auto', behavior: "auto",
}); });
}, },
@@ -122,8 +171,12 @@ export default defineComponent({
return this.store.readyStockList return this.store.readyStockList
.filter( .filter(
(rs) => (rs) =>
rs.stockId.toLocaleLowerCase().includes(this.searchedReadyStockName.toLocaleLowerCase()) && rs.stockId
rs.stockString.toLocaleLowerCase().includes(this.searchedReadyStockString.toLocaleLowerCase()) .toLocaleLowerCase()
.includes(this.searchedReadyStockName.toLocaleLowerCase()) &&
rs.stockString
.toLocaleLowerCase()
.includes(this.searchedReadyStockString.toLocaleLowerCase()),
) )
.filter((_, i) => i <= this.visibleIndexesTo); .filter((_, i) => i <= this.visibleIndexesTo);
}, },
@@ -131,7 +184,7 @@ export default defineComponent({
computedAvailableStockTypes() { computedAvailableStockTypes() {
return this.store.readyStockList return this.store.readyStockList
.reduce((acc, rs) => { .reduce((acc, rs) => {
rs.stockString.split(';').forEach((s) => { rs.stockString.split(";").forEach((s) => {
if (!acc.includes(s)) acc.push(s); if (!acc.includes(s)) acc.push(s);
}); });
@@ -145,7 +198,7 @@ export default defineComponent({
computedReadyStockList(curr, prev) { computedReadyStockList(curr, prev) {
if (curr.length < prev.length) { if (curr.length < prev.length) {
this.visibleIndexesTo = 20; this.visibleIndexesTo = 20;
(this.$refs['list'] as HTMLElement).scrollTo({ (this.$refs["list"] as HTMLElement).scrollTo({
top: 0, top: 0,
}); });
} }
@@ -154,21 +207,21 @@ export default defineComponent({
methods: { methods: {
async fetchStockListData() { async fetchStockListData() {
const readyStockJSONData: ResponseJSONData = await ( const readyStockJSONData = (
await fetch(`https://spythere.github.io/api/td2/data/readyStock.json?t=${Math.floor(Date.now() / 60000)}`) await http.get<ResponseJSONData>("td2/data/readyStock.json")
).json(); ).data;
if (!readyStockJSONData) { if (!readyStockJSONData) {
this.responseStatus = 'error'; this.responseStatus = "error";
return; return;
} }
for (let stockKey in readyStockJSONData) { for (let stockKey in readyStockJSONData) {
const [type, number, ...name] = stockKey.split(' '); const [type, number, ...name] = stockKey.split(" ");
const obj = { const obj = {
number: number.replace(/_/g, '/'), number: number.replace(/_/g, "/"),
name: name.join(' '), name: name.join(" "),
stockString: readyStockJSONData[stockKey], stockString: readyStockJSONData[stockKey],
type, type,
}; };
@@ -179,7 +232,7 @@ export default defineComponent({
}); });
} }
this.responseStatus = 'loaded'; this.responseStatus = "loaded";
}, },
mountObserver() { mountObserver() {
@@ -187,7 +240,7 @@ export default defineComponent({
if (entries[0].intersectionRatio > 0) this.visibleIndexesTo += 20; if (entries[0].intersectionRatio > 0) this.visibleIndexesTo += 20;
}); });
this.observer.observe(this.$refs['bottom'] as HTMLElement); this.observer.observe(this.$refs["bottom"] as HTMLElement);
}, },
getImageUrl(name: string) { getImageUrl(name: string) {
@@ -195,8 +248,8 @@ export default defineComponent({
}, },
resetStockFilters() { resetStockFilters() {
this.searchedReadyStockName = ''; this.searchedReadyStockName = "";
this.searchedReadyStockString = ''; this.searchedReadyStockString = "";
}, },
chooseStock(stockItem: IReadyStockItem) { chooseStock(stockItem: IReadyStockItem) {
@@ -208,7 +261,7 @@ export default defineComponent({
onStockItemError(e: Event, stockType: string) { onStockItemError(e: Event, stockType: string) {
const imageEl = e.target as HTMLImageElement; const imageEl = e.target as HTMLImageElement;
imageEl.src = `images/${getVehicleType(stockType)}-unknown.png`; imageEl.src = `images/${getVehicleType(stockType)}-unknown.png`;
imageEl.style.opacity = '1'; imageEl.style.opacity = "1";
}, },
onListScroll(e: Event) { onListScroll(e: Event) {
@@ -222,7 +275,7 @@ export default defineComponent({
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/global.scss'; @import "../../styles/global.scss";
.exit-btn { .exit-btn {
font-size: 1.2em; font-size: 1.2em;
@@ -308,7 +361,7 @@ ul {
gap: 1rem; gap: 1rem;
padding: 0.1em; padding: 0.1em;
&[data-last-selected='true'] .stock-title { &[data-last-selected="true"] .stock-title {
border: 1px solid $accentColor; border: 1px solid $accentColor;
} }
@@ -370,4 +423,3 @@ ul {
padding: 1em; padding: 1em;
} }
</style> </style>
+73 -44
View File
@@ -1,17 +1,18 @@
<template> <template>
<section class="inputs-section"> <section class="inputs-section">
<div class="input_container"> <div class="input_container">
<h2 class="input_header">WYBIERZ POJAZDY / WAGONY</h2> <h2 class="input_header">{{ $t('inputs.title') }}</h2>
<div class="input_list type"> <div class="input_list type">
<div class="vehicle-types locos"> <div class="vehicle-types locos">
<button <button
v-for="locoType in locomotiveTypeList" v-for="locoType in locomotiveTypeList"
:key="locoType.id"
class="btn btn--choice" class="btn btn--choice"
:data-selected="locoType.id == store.chosenLocoPower" :data-selected="locoType.id == store.chosenLocoPower"
@click="selectLocoType(locoType.id)" @click="selectLocoType(locoType.id)"
> >
{{ locoType.value }} {{ $t(`inputs.${locoType.id}`) }}
</button> </button>
</div> </div>
@@ -23,10 +24,10 @@
@keydown.enter.prevent="addOrSwitchVehicle" @keydown.enter.prevent="addOrSwitchVehicle"
@keydown.backspace="removeVehicle" @keydown.backspace="removeVehicle"
> >
<option :value="null" disabled>Wybierz pojazd trakcyjny</option> <option :value="null" disabled>
<option v-for="loco in locoOptions" :value="loco" :key="loco.type"> {{ $t('inputs.input-vehicle') }}
{{ loco.type }}<b v-if="loco.supportersOnly">*</b>
</option> </option>
<option v-for="loco in locoOptions" :value="loco" :key="loco.type">{{ loco.type }}<b v-if="loco.isSponsorsOnly">*</b></option>
</select> </select>
</div> </div>
@@ -34,11 +35,12 @@
<div class="vehicle-types carwagons"> <div class="vehicle-types carwagons">
<button <button
v-for="carType in carTypeList" v-for="carType in carTypeList"
:key="carType.id"
class="btn btn--choice" class="btn btn--choice"
:data-selected="carType.id == store.chosenCarUseType" :data-selected="carType.id == store.chosenCarUseType"
@click="selectCarWagonType(carType.id)" @click="selectCarWagonType(carType.id)"
> >
{{ carType.value }} {{ $t(`inputs.${carType.id}`) }}
</button> </button>
</div> </div>
@@ -50,22 +52,20 @@
@keydown.enter.prevent="addOrSwitchVehicle" @keydown.enter.prevent="addOrSwitchVehicle"
@keydown.backspace="removeVehicle" @keydown.backspace="removeVehicle"
> >
<option :value="null" disabled>Wybierz wagon</option> <option :value="null" disabled>
{{ $t('inputs.input-carwagon') }}
<option v-for="car in carOptions" :value="car" :key="car.type">
{{ car.type }}<b v-if="car.supportersOnly">*</b>
</option> </option>
<option v-for="car in carOptions" :value="car" :key="car.type">{{ car.type }}<b v-if="car.isSponsorsOnly">*</b></option>
</select> </select>
</div> </div>
<div class="input_list cargo"> <div class="input_list cargo">
<label for="cargo-select">Ładunek (tylko wybrane towarowe)</label> <label for="cargo-select">{{ $t('inputs.cargo-title') }}</label>
<select <select
id="cargo-select" id="cargo-select"
:disabled=" :disabled="
(store.chosenCar && !store.chosenCar.loadable) || (store.chosenCar && !store.chosenCar.loadable) || (store.chosenCar && store.chosenCar.useType == 'car-passenger') || !store.chosenCar
(store.chosenCar && store.chosenCar.useType == 'car-passenger') ||
!store.chosenCar
" "
data-select="cargo" data-select="cargo"
data-ignore-outside="1" data-ignore-outside="1"
@@ -75,8 +75,10 @@
@keydown.enter.prevent="addOrSwitchVehicle" @keydown.enter.prevent="addOrSwitchVehicle"
@keydown.backspace="removeVehicle" @keydown.backspace="removeVehicle"
> >
<option :value="null" v-if="!store.chosenCar || !store.chosenCar.loadable">brak dostępnych ładunków</option> <option :value="null" v-if="!store.chosenCar || !store.chosenCar.loadable">
<option :value="null" v-else>próżny</option> {{ $t('inputs.no-cargo-available') }}
</option>
<option :value="null" v-else>{{ $t('inputs.cargo-empty') }}</option>
<option v-for="cargo in store.chosenCar?.cargoList" :value="cargo" :key="cargo.id"> <option v-for="cargo in store.chosenCar?.cargoList" :value="cargo" :key="cargo.id">
{{ cargo.id }} {{ cargo.id }}
@@ -85,20 +87,19 @@
</div> </div>
<div class="input_actions"> <div class="input_actions">
<button class="btn" @click="addVehicle(store.chosenVehicle, store.chosenCargo)">DODAJ NOWY</button> <button class="btn" @click="addVehicle(store.chosenVehicle, store.chosenCargo)">
<button {{ $t('inputs.action-add') }}
class="btn" </button>
@click="switchVehicles" <button class="btn" @click="switchVehicles" :disabled="store.chosenStockListIndex == -1" :data-disabled="store.chosenStockListIndex == -1">
:disabled="store.chosenStockListIndex == -1" {{ $t('inputs.action-swap') }}
:data-disabled="store.chosenStockListIndex == -1"
>
ZAMIEŃ ZA
<b class="text--accent"> <b class="text--accent">
{{ store.chosenStockListIndex == -1 ? '' : `${store.chosenStockListIndex + 1}.` }} {{ store.chosenStockListIndex == -1 ? '' : `${store.chosenStockListIndex + 1}.` }}
</b> </b>
</button> </button>
<button class="btn" @click="store.isRealStockListCardOpen = true"><b>REALNE ZESTAWIENIA</b></button> <button class="btn" @click="store.isRealStockListCardOpen = true">
<b>{{ $t('inputs.real-stock') }}</b>
</button>
</div> </div>
</div> </div>
</section> </section>
@@ -112,59 +113,56 @@ import { useStore } from '../../store';
import stockPreviewMixin from '../../mixins/stockPreviewMixin'; import stockPreviewMixin from '../../mixins/stockPreviewMixin';
import stockMixin from '../../mixins/stockMixin'; import stockMixin from '../../mixins/stockMixin';
interface ILocoType {
id: string;
value: string;
desc: string;
}
export default defineComponent({ export default defineComponent({
mixins: [imageMixin, stockPreviewMixin, stockMixin], mixins: [imageMixin, stockPreviewMixin, stockMixin],
data: () => ({ data: () => ({
store: useStore(),
locomotiveTypeList: [ locomotiveTypeList: [
{ {
id: 'loco-e', id: 'loco-e',
value: 'ELEKTR',
desc: 'ELEKTRYCZNE', desc: 'ELEKTRYCZNE',
}, },
{ {
id: 'loco-s', id: 'loco-s',
value: 'SPAL',
desc: 'SPALINOWE', desc: 'SPALINOWE',
}, },
{ {
id: 'loco-ezt', id: 'loco-ezt',
value: 'EZT',
desc: 'ELEKTR. ZESPOŁY TRAKCYJNE', desc: 'ELEKTR. ZESPOŁY TRAKCYJNE',
}, },
{ {
id: 'loco-szt', id: 'loco-szt',
value: 'SZT',
desc: 'SPAL. ZESPOŁY TRAKCYJNE', desc: 'SPAL. ZESPOŁY TRAKCYJNE',
}, },
] as ILocoType[], ],
carTypeList: [ carTypeList: [
{ {
id: 'car-passenger', id: 'car-passenger',
value: 'PAS',
desc: 'PASAŻERSKIE', desc: 'PASAŻERSKIE',
}, },
{ {
id: 'car-cargo', id: 'car-cargo',
value: 'TOW',
desc: 'TOWAROWE', desc: 'TOWAROWE',
}, },
], ],
}), }),
setup() { computed: {
const store = useStore(); locoOptions() {
return this.store.locoDataList
.slice()
.sort((a, b) => (a.type > b.type ? 1 : -1))
.filter((loco) => loco.power == this.store.chosenLocoPower);
},
return { carOptions() {
store, return this.store.carDataList
}; .slice()
.sort((a, b) => (a.type > b.type ? 1 : -1))
.filter((car) => car.useType == this.store.chosenCarUseType);
},
}, },
methods: { methods: {
@@ -198,6 +196,29 @@ export default defineComponent({
const stockObject = this.getStockObject(vehicle, this.store.chosenCargo); const stockObject = this.getStockObject(vehicle, this.store.chosenCargo);
this.store.stockList[this.store.chosenStockListIndex] = stockObject; this.store.stockList[this.store.chosenStockListIndex] = stockObject;
}, },
selectLocoType(locoTypeId: string) {
this.store.chosenLocoPower = locoTypeId;
this.store.chosenVehicle = this.locoOptions[0];
this.store.chosenLoco = this.locoOptions[0];
},
selectCarWagonType(carWagonTypeId: string) {
this.store.chosenCarUseType = carWagonTypeId;
this.store.chosenVehicle = this.carOptions[0];
this.store.chosenCar = this.carOptions[0];
this.store.chosenCargo = null;
},
previewVehicleByType(type: 'loco' | 'car' | 'cargo') {
this.$nextTick(() => {
if (!this.store.chosenLoco && !this.store.chosenCar) return;
this.store.chosenVehicle = type == 'loco' ? this.store.chosenLoco : this.store.chosenCar;
this.store.chosenCargo = this.store.chosenCar?.cargoList.find((cargo) => cargo.id == this.store.chosenCargo?.id) || null;
});
},
}, },
}); });
</script> </script>
@@ -213,6 +234,11 @@ export default defineComponent({
grid-column: 1; grid-column: 1;
} }
.input_container {
width: 100%;
max-width: 380px;
}
.input_header { .input_header {
margin-bottom: 1em; margin-bottom: 1em;
} }
@@ -232,6 +258,10 @@ button.btn--choice {
.input_list { .input_list {
margin: 0.5em 0; margin: 0.5em 0;
select {
width: 100%;
}
label { label {
display: block; display: block;
@@ -273,4 +303,3 @@ button.btn--choice {
} }
} }
</style> </style>
+52 -8
View File
@@ -1,39 +1,83 @@
<template> <template>
<section class="logo-section" @click="navigate"> <section class="logo-section">
<img src="/images/logo.svg" alt="logo pojazdownik" /> <img
:src="`/logo-${$i18n.locale}.svg`"
alt="logo pojazdownik"
@click="navigate"
/>
<div class="actions">
<button
v-for="action in localeActions"
:key="action.name"
class="btn btn--text"
:data-selected="$i18n.locale == action.locale"
@click="chooseLocale(action.locale)"
>
{{ action.name }}
</button>
</div>
</section> </section>
</template> </template>
<script lang="ts"> <script lang="ts">
export default { export default {
setup() { data() {
return {}; return {
localeActions: [
{
name: "POLSKI",
locale: "pl",
},
{
name: "ENGLISH",
locale: "en",
},
],
};
}, },
methods: { methods: {
navigate() { navigate() {
window.location.pathname = ''; window.location.pathname = "";
},
chooseLocale(locale: string) {
this.$i18n.locale = locale;
window.localStorage.setItem("locale", locale);
}, },
}, },
}; };
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "../../styles/global.scss";
.logo-section { .logo-section {
grid-row: 1; grid-row: 1;
grid-column: 1; grid-column: 1;
margin-bottom: 1.5em; margin-bottom: 1.5em;
display: flex; display: flex;
justify-content: center;
align-items: center; align-items: center;
flex-direction: column;
gap: 0.5em;
cursor: pointer; cursor: pointer;
} }
.actions {
display: flex;
gap: 0.5em;
button[data-selected="true"] {
font-weight: bold;
color: $accentColor;
text-decoration: underline;
}
}
img { img {
max-width: 25em; max-width: 25em;
width: 100%; width: 100%;
} }
</style> </style>
+29 -33
View File
@@ -2,59 +2,56 @@
<section class="stock-section"> <section class="stock-section">
<div class="section_modes"> <div class="section_modes">
<button <button
v-for="(id, i) in sectionModes"
:key="id"
class="btn" class="btn"
ref="sectionButtonRefs" ref="sectionButtonRefs"
v-for="(id, name, i) in sectionModes"
@click="chooseSection(id)" @click="chooseSection(id)"
:data-selected="store.stockSectionMode == id" :data-selected="store.stockSectionMode == id"
> >
<span class="text--accent">{{ i + 1 }}.</span> {{ name }} <span class="text--accent">{{ i + 1 }}.</span> {{ $t(`topbar.${id}`) }}
<span v-if="id == 'stock-list'">({{ store.stockList.length }})</span> <span v-if="id == 'stock-list'">({{ store.stockList.length }})</span>
</button> </button>
</div> </div>
<transition name="tab-change" mode="out-in"> <transition name="tab-change" mode="out-in">
<keep-alive> <keep-alive>
<component :is="chosenSectionComponent" :key="chosenSectionComponent"></component> <component
:is="chosenSectionComponent"
:key="chosenSectionComponent"
></component>
</keep-alive> </keep-alive>
</transition> </transition>
</section> </section>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, KeepAlive, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from "vue";
import { useStore } from '../../store'; import { useStore } from "../../store";
import StockListTab from '../tabs/StockListTab.vue'; import StockListTab from "../tabs/StockListTab.vue";
import StockGeneratorTab from '../tabs/StockGeneratorTab.vue'; import StockGeneratorTab from "../tabs/StockGeneratorTab.vue";
import NumberGeneratorTab from '../tabs/NumberGeneratorTab.vue'; import NumberGeneratorTab from "../tabs/NumberGeneratorTab.vue";
import WikiListTab from '../tabs/WikiListTab.vue'; import WikiListTab from "../tabs/WikiListTab.vue";
const sectionButtonRefs = ref([]); const sectionButtonRefs = ref([]);
const store = useStore(); const store = useStore();
type SectionMode = typeof store.stockSectionMode; type SectionMode = typeof store.stockSectionMode;
const sectionModes: { [key: string]: SectionMode } = { const sectionModes: SectionMode[] = [
SKŁAD: 'stock-list', "stock-list",
POJAZDY: 'wiki-list', "wiki-list",
'GNR NUMERU': 'number-generator', "number-generator",
'GNR SKŁADU': 'stock-generator', "stock-generator",
}; ];
const sectionKeyIndexes: { [key: number]: SectionMode } = {
1: 'stock-list',
2: 'wiki-list',
3: 'number-generator',
4: 'stock-generator',
};
onMounted(() => { onMounted(() => {
window.addEventListener('keydown', (e) => { window.addEventListener("keydown", (e) => {
if (e.target instanceof HTMLInputElement) return; if (e.target instanceof HTMLInputElement) return;
if (/[1234]/.test(e.key)) { if (/[1234]/.test(e.key)) {
const keyNum = Number(e.key); const keyNum = Number(e.key);
store.stockSectionMode = sectionKeyIndexes[keyNum]; store.stockSectionMode = sectionModes[keyNum - 1];
(sectionButtonRefs.value[keyNum - 1] as HTMLButtonElement).focus(); (sectionButtonRefs.value[keyNum - 1] as HTMLButtonElement).focus();
} }
}); });
@@ -62,16 +59,16 @@ onMounted(() => {
const chosenSectionComponent = computed(() => { const chosenSectionComponent = computed(() => {
switch (store.stockSectionMode) { switch (store.stockSectionMode) {
case 'stock-list': case "stock-list":
return StockListTab; return StockListTab;
case 'wiki-list': case "wiki-list":
return WikiListTab; return WikiListTab;
case 'stock-generator': case "stock-generator":
return StockGeneratorTab; return StockGeneratorTab;
case 'number-generator': case "number-generator":
return NumberGeneratorTab; return NumberGeneratorTab;
default: default:
@@ -85,7 +82,7 @@ function chooseSection(sectionId: SectionMode) {
</script> </script>
<style lang="scss"> <style lang="scss">
@import '../../styles/global.scss'; @import "../../styles/global.scss";
// Tab change animation // Tab change animation
.tab-change { .tab-change {
@@ -115,7 +112,7 @@ function chooseSection(sectionId: SectionMode) {
gap: 0.5em; gap: 0.5em;
margin-bottom: 0.5em; margin-bottom: 1em;
button { button {
position: relative; position: relative;
@@ -127,14 +124,14 @@ function chooseSection(sectionId: SectionMode) {
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
content: ''; content: "";
width: 0; width: 0;
height: 2px; height: 2px;
transition: all 100ms; transition: all 100ms;
background-color: $accentColor; background-color: $accentColor;
} }
&[data-selected='true']::after { &[data-selected="true"]::after {
width: 100%; width: 100%;
} }
} }
@@ -146,4 +143,3 @@ function chooseSection(sectionId: SectionMode) {
} }
} }
</style> </style>
+76 -101
View File
@@ -1,56 +1,47 @@
<template> <template>
<section class="train-image-section"> <section class="train-image-section">
<div class="train-image__wrapper"> <div class="train-image__content" :class="{ sponsor: store.chosenVehicle?.isSponsorsOnly }">
<div class="train-image__content" :class="{'supporter': store.chosenVehicle?.supportersOnly}"> <img
<transition name="img-message-anim"> v-if="store.chosenVehicle"
<div class="empty-message" v-if="store.imageLoading && store.chosenVehicle?.imageSrc"> tabindex="0"
ŁADOWANIE OBRAZU... :src="getThumbnailURL(store.chosenVehicle.type, 'small')"
</div> @click="onImageClick"
</transition> @keydown.enter="onImageClick"
@error="onImageError"
type="image/jpeg"
/>
<div class="no-img" v-if="!store.chosenVehicle">PODGLĄD WYBRANEGO POJAZDU</div> <img v-else src="/images/placeholder.jpg" alt="placeholder" />
<img
v-if="store.chosenVehicle"
:src="`https://spythere.github.io/api/td2/images/${store.chosenVehicle.type}--300px.jpg`"
:alt="store.chosenVehicle.type"
@load="onImageLoad"
@click="onImageClick"
/>
<!-- <div class="empty-message" v-if="store.chosenVehicle && !store.chosenVehicle.imageSrc">Ten pojazd nie ma jeszcze podglądu!</div> -->
</div>
<div class="train-image__info" v-if="store.chosenVehicle">
<b class="text--accent">{{ store.chosenVehicle.type }}</b> &bull;
<b style="color: #ccc">{{
vehicleTypes[
isLocomotive(store.chosenVehicle) ? store.chosenVehicle.power : store.chosenVehicle.useType || 'loco-e'
]
}}</b>
<div style="color: #ccc">
<div>
{{ store.chosenVehicle.length }}m | {{ store.chosenVehicle.mass }}t |
{{ store.chosenVehicle.maxSpeed }} km/h
</div>
<div v-if="isLocomotive(store.chosenVehicle)">Typ kabiny: {{ store.chosenVehicle.cabinType }}</div>
<div v-else>
{{
store.chosenVehicle.useType == 'car-cargo'
? store.stockData?.usage[store.chosenVehicle.constructionType]
: 'Typ konstrukcji: ' + store.chosenVehicle.constructionType
}}
</div>
<b style="color: salmon;" v-if="store.chosenVehicle.supportersOnly">* TYLKO DLA SPONSORÓW</b>
</div>
</div>
<div class="train-image__info" v-else>Wybierz pojazd lub wagon, aby zobaczyć jego podgląd powyżej</div>
</div> </div>
<div class="train-image__info" v-if="store.chosenVehicle">
<b class="text--accent">{{ store.chosenVehicle.type }}</b> &bull;
<b style="color: #ccc">
{{ $t(`preview.${isLocomotive(store.chosenVehicle) ? store.chosenVehicle.power : store.chosenVehicle.useType}`) }}
</b>
<div style="color: #ccc">
<div>{{ store.chosenVehicle.length }}m | {{ store.chosenVehicle.mass }}t | {{ store.chosenVehicle.maxSpeed }} km/h</div>
<div v-if="isLocomotive(store.chosenVehicle)">{{ $t('preview.cabin') }} {{ store.chosenVehicle.cabinType }}</div>
<div v-else>
{{
store.chosenVehicle.useType == 'car-cargo'
? $t(`usage.${store.chosenVehicle.constructionType}`)
: `${$t('preview.construction')} ${store.chosenVehicle.constructionType}`
}}
</div>
<b style="color: salmon" v-if="store.chosenVehicle.isSponsorsOnly">{{
$t('preview.sponsor-only', [
new Date(store.chosenVehicle.sponsorsOnlyTimestamp).toLocaleDateString($i18n.locale == 'pl' ? 'pl-PL' : 'en-GB'),
])
}}</b>
</div>
</div>
<div class="train-image__info" v-else>{{ $t('preview.desc') }}</div>
</section> </section>
</template> </template>
@@ -59,8 +50,17 @@ import { computed, defineComponent } from 'vue';
import { useStore } from '../../store'; import { useStore } from '../../store';
import { isLocomotive } from '../../utils/vehicleUtils'; import { isLocomotive } from '../../utils/vehicleUtils';
import { ILocomotive, Vehicle } from '../../types'; import { ILocomotive, Vehicle } from '../../types';
import imageMixin from '../../mixins/imageMixin';
export default defineComponent({ export default defineComponent({
mixins: [imageMixin],
data() {
return {
noImageAvailable: false,
};
},
setup() { setup() {
const store = useStore(); const store = useStore();
@@ -70,19 +70,6 @@ export default defineComponent({
}; };
}, },
data() {
return {
vehicleTypes: {
'loco-e': 'ELEKTROWÓZ',
'loco-s': 'SPALINOWÓZ',
'loco-ezt': 'EZT',
'loco-szt': 'SZT',
'car-passenger': 'WAGON PASAŻERSKI',
'car-cargo': 'WAGON TOWAROWY',
} as { [key: string]: string },
};
},
watch: { watch: {
chosenVehicle(vehicle: Vehicle, prevVehicle: Vehicle) { chosenVehicle(vehicle: Vehicle, prevVehicle: Vehicle) {
if (vehicle && vehicle.type != prevVehicle?.type) { if (vehicle && vehicle.type != prevVehicle?.type) {
@@ -96,16 +83,26 @@ export default defineComponent({
this.store.imageLoading = false; this.store.imageLoading = false;
}, },
onImageError(e: Event) {
const el = e.target as HTMLImageElement;
if (el.src == '/images/placeholder.jpg') return;
el.src = '/images/placeholder.jpg';
},
isLocomotive(vehicle: Vehicle): vehicle is ILocomotive { isLocomotive(vehicle: Vehicle): vehicle is ILocomotive {
return isLocomotive(vehicle); return isLocomotive(vehicle);
}, },
onImageClick() { onImageClick(e: Event) {
const target = e.target as HTMLElement;
const chosenVehicle = this.store.chosenVehicle; const chosenVehicle = this.store.chosenVehicle;
if (!chosenVehicle) return; if (!chosenVehicle) return;
this.store.vehiclePreviewSrc = `https://spythere.github.io/api/td2/images/${chosenVehicle.type}--800px.jpg`; this.store.lastFocusedElement = target;
this.store.vehiclePreviewSrc = this.getThumbnailURL(chosenVehicle.type, 'large');
}, },
}, },
}); });
@@ -115,66 +112,45 @@ export default defineComponent({
@import '../../styles/global.scss'; @import '../../styles/global.scss';
.train-image-section { .train-image-section {
display: flex;
flex-direction: column;
text-align: center;
grid-row: 3; grid-row: 3;
grid-column: 1; grid-column: 1;
margin-top: 2em; margin-top: 1em;
height: 22em; height: 22em;
} }
.train-image { .train-image {
&__wrapper {
text-align: center;
}
&__content { &__content {
border: 1px solid white; &.sponsor img {
position: relative;
overflow: hidden;
max-width: 22em;
height: 13em;
margin: 0 auto;
&.supporter {
border: 1px solid salmon; border: 1px solid salmon;
} }
img { img {
max-width: 380px;
width: 100%; width: 100%;
height: 100%; height: 100%;
border: 1px solid white;
cursor: zoom-in; cursor: zoom-in;
} }
.empty-message,
.no-img {
position: absolute;
left: 0;
bottom: 0;
padding: 0.3em 0;
width: 100%;
}
.empty-message {
background: rgba(#000, 0.75);
}
} }
} }
.train-image__info { .train-image__info {
margin: 1em 0;
font-size: 1.1em; font-size: 1.1em;
padding: 0 1em; padding: 0.5em;
margin: 0.5em auto;
line-height: 1.35;
b { width: 100%;
font-size: 1.1em; max-width: 380px;
}
div { background-color: $secondaryColor;
margin: 0.25em 0; font-weight: bold;
}
} }
// Transition animations // Transition animations
@@ -196,4 +172,3 @@ export default defineComponent({
} }
} }
</style> </style>
+101 -41
View File
@@ -1,86 +1,138 @@
<template> <template>
<div class="number-generator tab"> <div class="number-generator tab">
<div class="tab_header"> <div class="tab_header">
<h2>GENERATOR NUMERU POCIĄGU</h2> <h2>{{ $t("numgen.title") }}</h2>
</div> </div>
<div class="tab_content"> <div class="tab_content">
<div class="options"> <div class="options">
<select v-model="chosenCategory" @change="randomizeTrainNumber()">
<option :value="null" disabled>
{{ $t("numgen.train-category") }}
</option>
<option
v-for="(_, category) in genData.categories"
:key="category"
:value="category"
>
{{ $t(`numgen.categories.${category}`) }}
</option>
</select>
<select v-model="beginRegionName" @change="randomizeTrainNumber()"> <select v-model="beginRegionName" @change="randomizeTrainNumber()">
<option :value="null" disabled>Początkowy obszar konstrukcyjny</option> <option :value="null" disabled>
<option v-for="(_, name) in genData.regionNumbers" :value="name">{{ name }}</option> {{ $t("numgen.start-region") }}
</option>
<option
v-for="(_, name) in genData.regionNumbers"
:key="name"
:value="name"
>
{{ name }}
</option>
</select> </select>
<select v-model="endRegionName" @change="randomizeTrainNumber()"> <select v-model="endRegionName" @change="randomizeTrainNumber()">
<option :value="null" disabled>Końcowy obszar konstrukcyjny</option> <option :value="null" disabled>{{ $t("numgen.end-region") }}</option>
<option v-for="(_, name) in genData.regionNumbers" :value="name">{{ name }}</option> <option
</select> v-for="(_, name) in genData.regionNumbers"
:key="name"
<select v-model="categoryRules" @change="randomizeTrainNumber()"> :value="name"
<option :value="null" disabled>Kategoria pociągu</option> >
<option v-for="(rules, category) in genData.categories" :value="rules">{{ category }}</option> {{ name }}
</option>
</select> </select>
</div> </div>
<div class="generated-number" @click="copyNumber"> <div class="generated-number" @click="copyNumber">
<span v-if="trainNumber"> <span v-if="trainNumber">
Wygenerowany numer pociągu: <b class="text--accent">{{ trainNumber }}</b> {{ $t("numgen.number-info") }}
<b class="text--accent">{{ trainNumber }}</b>
</span> </span>
<span v-else>Wybierz kategorię oraz obszary konstrukcyjne (opcjonalnie)</span> <span v-else>{{ $t("numgen.warning") }}</span>
</div> </div>
<!-- <div v-if="chosenCategory">
Current numbering rules: {{ $t(`numgen.rules.${chosenCategory}`) }};
<span v-if="beginRegionName && endRegionName">
<span v-if="beginRegionName == endRegionName">
pierwsze dwie cyfry:
{{ genData.sameRegions[beginRegionName].join(', ') }}
(numer w obrębie obszaru {{ beginRegionName }})
</span>
<span v-else>
pierwsza cyfra: {{ genData.regionNumbers[beginRegionName] }}; druga cyfra:
{{ genData.regionNumbers[endRegionName] }}
</span>
</span>
</div> -->
<div class="tab_links"> <div class="tab_links">
<a href="https://wiki.td2.info.pl/index.php?title=Zasady_numeracji_poci%C4%85g%C3%B3w" target="_blank"> <a :href="$t('numgen.td2-wiki-link')" target="_blank">
> Szczegółowe zasady numeracji (wikipedia TD2) {{ $t("numgen.td2-wiki") }}
</a> </a>
</div> </div>
<hr /> <hr />
<div class="tab_actions"> <div class="tab_actions">
<button class="btn" @click="randomizeTrainNumber(true)">LOSUJ OBSZARY</button> <button class="btn" @click="randomizeTrainNumber(true)">
<button class="btn" @click="randomizeTrainNumber(false)">LOSUJ NUMER</button> {{ $t("numgen.action-random-region") }}
</button>
<button class="btn" @click="randomizeTrainNumber(false)">
{{ $t("numgen.action-random-number") }}
</button>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { Ref, ref } from 'vue'; import { Ref, ref } from "vue";
import { useI18n } from "vue-i18n";
import genData from '../../constants/numberGeneratorData.json'; import genData from "../../constants/numberGeneratorData.json";
const i18n = useI18n();
type RegionName = keyof typeof genData.regionNumbers; type RegionName = keyof typeof genData.regionNumbers;
type Category = keyof typeof genData.categories;
const beginRegionName = ref(null) as Ref<RegionName | null>; const beginRegionName = ref(null) as Ref<RegionName | null>;
const endRegionName = ref(null) as Ref<RegionName | null>; const endRegionName = ref(null) as Ref<RegionName | null>;
const categoryRules = ref(null) as Ref<string | null>; const chosenCategory = ref(null) as Ref<Category | null>;
const trainNumber = ref(null) as Ref<string | null>; const trainNumber = ref(null) as Ref<string | null>;
const copyNumber = () => { const copyNumber = () => {
if (trainNumber.value) { if (trainNumber.value) {
navigator.clipboard.writeText(trainNumber.value); navigator.clipboard.writeText(trainNumber.value);
alert('Skopiowano numer do schowka!'); alert(i18n.t("numgen.alert"));
} }
}; };
const randomizeTrainNumber = (randomizeRegions = false) => { const randomizeTrainNumber = (randomizeRegions = false) => {
if (categoryRules.value == null) return; // if (categoryRules.value == null) return;
const regionKeys = Object.keys(genData.regionNumbers); const regionKeys = Object.keys(genData.regionNumbers);
if (beginRegionName.value == null || randomizeRegions) if (beginRegionName.value == null || randomizeRegions)
beginRegionName.value = regionKeys[(regionKeys.length * Math.random()) << 0] as RegionName; beginRegionName.value = regionKeys[
(regionKeys.length * Math.random()) << 0
] as RegionName;
if (endRegionName.value == null || randomizeRegions) if (endRegionName.value == null || randomizeRegions)
endRegionName.value = regionKeys[(regionKeys.length * Math.random()) << 0] as RegionName; endRegionName.value = regionKeys[
(regionKeys.length * Math.random()) << 0
] as RegionName;
let number = ''; let number = "";
if (beginRegionName.value == endRegionName.value) { if (beginRegionName.value == endRegionName.value) {
const sameRegionsNumbers = genData.sameRegions[beginRegionName.value!]; const sameRegionsNumbers = genData.sameRegions[beginRegionName.value!];
const randRegionNumber = sameRegionsNumbers[Math.floor(Math.random() * sameRegionsNumbers.length)]; const randRegionNumber =
sameRegionsNumbers[Math.floor(Math.random() * sameRegionsNumbers.length)];
number += randRegionNumber.toString(); number += randRegionNumber.toString();
} else { } else {
const beginRegionNumber = genData.regionNumbers[beginRegionName.value!]; const beginRegionNumber = genData.regionNumbers[beginRegionName.value!];
@@ -89,27 +141,36 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
number += `${beginRegionNumber}${endRegionNumber}`; number += `${beginRegionNumber}${endRegionNumber}`;
} }
// Do not roll the rest of number again if only randomize regions // Do not roll the rest of number again if only randomize regions and category rules are already selected
if (randomizeRegions) { if (randomizeRegions && chosenCategory.value != null) {
trainNumber.value = number + trainNumber.value?.substring(2); trainNumber.value = number + trainNumber.value?.substring(2);
return; return;
} }
const rulesArray = categoryRules.value.split(';').map((r) => ({ if (chosenCategory.value == null) chosenCategory.value = "EI";
index: r.split(':')[0],
rule: r.split(':')[1], const rulesArray = genData.categories[chosenCategory.value]
nums: Number(r.split(':')[2] || '1'), .split(";")
})); .map((r) => ({
index: r.split(":")[0],
rule: r.split(":")[1],
nums: Number(r.split(":")[2] || "1"),
}));
rulesArray.forEach((r) => { rulesArray.forEach((r) => {
const range = r.rule.split('-'); const range = r.rule.split("-");
if (range.length == 1) number += r.rule; if (range.length == 1) number += r.rule;
else { else {
const [minRange, maxRange] = range; const [minRange, maxRange] = range;
const randRange = Math.floor(Math.random() * (Number(maxRange) - Number(minRange)) + Number(minRange)).toString(); const randRange = Math.floor(
Math.random() * (Number(maxRange) - Number(minRange)) +
Number(minRange),
).toString();
number += new Array(Math.abs(randRange.length - r.nums)).fill('0').join('') + randRange; number +=
new Array(Math.abs(randRange.length - r.nums)).fill("0").join("") +
randRange;
} }
}); });
@@ -118,16 +179,16 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/tab.scss'; @import "../../styles/tab.scss";
@import '../../styles/global.scss'; @import "../../styles/global.scss";
.options { .options {
display: flex; display: grid;
flex-wrap: wrap; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 0.5em; gap: 0.5em;
select { select {
width: calc(50% - 0.5em); width: 100%;
} }
} }
@@ -165,4 +226,3 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
} }
} }
</style> </style>
+54 -53
View File
@@ -1,62 +1,62 @@
<template> <template>
<div class="stock-generator tab"> <div class="stock-generator tab">
<div class="tab_header"> <div class="tab_header">
<h2>GENERATOR SKŁADU TOWAROWEGO</h2> <h2>{{ $t('stockgen.title') }}</h2>
</div> </div>
<div class="tab_content"> <div class="tab_content">
<div> <div>
<h2>WŁAŚCIWOŚCI SKŁADU</h2> <h2>{{ $t('stockgen.properties-title') }}</h2>
<b class="text--accent"> <b class="text--accent">
&lArr; Dodaj lokomotywę na pierwsze miejsce listy, aby uwzględnić przy losowaniu składu! {{ $t('stockgen.properties-desc') }}
</b> </b>
<div class="tab_attributes"> <div class="tab_attributes">
<label> <label>
Maksymalna masa (t) {{ $t('stockgen.input-mass') }}
<input type="number" v-model="maxMass" step="100" max="4000" min="0" /> <input type="number" v-model="maxMass" step="100" max="4000" min="0" />
</label> </label>
<label> <label>
Maks. długość (m) {{ $t('stockgen.input-length') }}
<input type="number" v-model="maxLength" step="25" max="650" min="0" /> <input type="number" v-model="maxLength" step="25" max="650" min="0" />
</label> </label>
<label> <label>
Maks. liczba wagonów {{ $t('stockgen.input-carcount') }}
<input type="number" v-model="maxCarCount" step="1" max="60" min="1" /> <input type="number" v-model="maxCarCount" step="1" max="60" min="1" />
</label> </label>
</div> </div>
</div> </div>
<div> <div>
<h2>ŁADUNEK</h2> <h2>{{ $t('stockgen.cargo-title') }}</h2>
<b>Wybierz ładunki, którymi chcesz wypełnić dostępne wagony:</b> <b>{{ $t('stockgen.cargo-desc') }}</b>
</div> </div>
<div class="generator_cargo"> <div class="generator_cargo">
<button <button
v-for="(cargoArray, cargoName) in store.stockData?.generator.cargo"
:key="cargoName"
class="btn" class="btn"
:data-chosen="chosenCargoTypes.includes(k.toString())" :data-chosen="chosenCargoTypes.includes(cargoName.toString())"
v-for="(v, k) in store.stockData?.generator.cargo" @click="toggleCargoChosen(cargoName.toString(), cargoArray)"
@click="toggleCargoChosen(k.toString(), v)"
> >
{{ k }} {{ $t(`cargo.${cargoName}`) }}
</button> </button>
</div> </div>
<div> <div>
<h2>WAGONY Z WYBRANYMI ŁADUNKAMI</h2> <h2>{{ $t('stockgen.chosen-title') }}</h2>
<div class="generator_warning"> <div class="generator_warning">
<span v-if="computedChosenCarTypes.size == 0"> <span v-if="computedChosenCarTypes.size == 0">
Wybierz co najmniej jeden ładunek, aby zobaczyć wagony, które go posiadają! {{ $t('stockgen.chosen-empty-warning') }}
</span> </span>
<span v-else> <span v-else>
Wagony posiadające wybrane ładunki. Najedź na nazwę, aby zobaczyć podgląd wagonu. Kliknij, aby wyłączyć z {{ $t('stockgen.chosen-warning') }}
losowania (tylko podświetlone nazwy będą uwzględnione).
</span> </span>
</div> </div>
</div> </div>
@@ -80,14 +80,15 @@
<div class="tab_actions"> <div class="tab_actions">
<button class="btn" :data-disabled="computedChosenCarTypes.size == 0" @click="generateStock()"> <button class="btn" :data-disabled="computedChosenCarTypes.size == 0" @click="generateStock()">
WYGENERUJ {{ $t('stockgen.action-generate') }}
</button> </button>
<button class="btn" :data-disabled="computedChosenCarTypes.size == 0" @click="generateStock(true)"> <button class="btn" :data-disabled="computedChosenCarTypes.size == 0" @click="generateStock(true)">
WYGENERUJ PRÓŻNE WAGONY {{ $t('stockgen.action-generate-empty') }}
</button> </button>
<button class="btn" :data-disabled="computedChosenCarTypes.size == 0" @click="resetChosenCargo"> <button class="btn" :data-disabled="computedChosenCarTypes.size == 0" @click="resetChosenCargo">
ZRESETUJ ŁADUNKI {{ $t('stockgen.action-reset') }}
</button> </button>
</div> </div>
</div> </div>
@@ -126,7 +127,7 @@ export default defineComponent({
computed: { computed: {
computedChosenCarTypes() { computedChosenCarTypes() {
return new Set<string>(this.chosenCarTypes.sort((c1, c2) => (c1 > c2 ? 1 : -1))); return new Set<string>(this.chosenCarTypes.slice().sort((c1, c2) => (c1 > c2 ? 1 : -1)));
}, },
}, },
@@ -150,44 +151,52 @@ export default defineComponent({
}, },
generateStock(empty = false) { generateStock(empty = false) {
const generatedChosenStockList = this.chosenCargoTypes.reduce((acc, type) => { const generatedChosenStockList = this.chosenCargoTypes.reduce(
this.store.stockData?.generator.cargo[type] (acc, type) => {
.filter((c) => !this.excludedCarTypes.includes(c.split(':')[0])) this.store.stockData?.generator.cargo[type]
.forEach((c) => { .filter((c) => !this.excludedCarTypes.includes(c.split(':')[0]))
const [type, cargoType] = c.split(':'); .forEach((c) => {
const [type, cargoType] = c.split(':');
const carWagonObjs = this.store.carDataList.filter((cw) => cw.type.startsWith(type)); const carWagonObjs = this.store.carDataList.filter((cw) => cw.type.startsWith(type));
const cargoObjs = [] as (ICargo | undefined)[]; const cargoObjs = [] as (ICargo | undefined)[];
if (!cargoType || empty) cargoObjs.push(undefined); if (!cargoType || empty) cargoObjs.push(undefined);
else if (cargoType == 'all') cargoObjs.push(...carWagonObjs[0]?.cargoList); else if (cargoType == 'all') cargoObjs.push(...carWagonObjs[0]!.cargoList);
else cargoObjs.push(carWagonObjs[0]?.cargoList.find((cargo) => cargo.id == cargoType)); else cargoObjs.push(carWagonObjs[0]?.cargoList.find((cargo) => cargo.id == cargoType));
carWagonObjs.forEach((cw) => { carWagonObjs.forEach((cw) => {
cargoObjs.forEach((cargoObj) => { cargoObjs.forEach((cargoObj) => {
const chosenStock = acc.find((a) => a.constructionType.includes(cw.constructionType)); const chosenStock = acc.find((a) => a.constructionType.includes(cw.constructionType));
if (!chosenStock) if (!chosenStock)
acc.push({ acc.push({
constructionType: cw.constructionType, constructionType: cw.constructionType,
carPool: [{ carWagon: cw, cargo: cargoObj }], carPool: [{ carWagon: cw, cargo: cargoObj }],
}); });
else chosenStock.carPool.push({ carWagon: cw, cargo: cargoObj }); else chosenStock.carPool.push({ carWagon: cw, cargo: cargoObj });
});
}); });
}); });
});
return acc; return acc;
}, [] as { constructionType: string; carPool: { carWagon: ICarWagon; cargo?: ICargo }[] }[]); },
[] as {
constructionType: string;
carPool: { carWagon: ICarWagon; cargo?: ICargo }[];
}[]
);
let bestGeneration: { stockList: IStock[]; value: number } = { stockList: [], value: 0 }; let bestGeneration: { stockList: IStock[]; value: number } = {
stockList: [],
value: 0,
};
for (let i = 0; i < 10; i++) { for (let i = 0; i < 10; i++) {
const headingLoco = this.store.stockList[0]?.isLoco ? this.store.stockList[0] : undefined; const headingLoco = this.store.stockList[0]?.isLoco ? this.store.stockList[0] : undefined;
this.store.stockList.length = headingLoco ? 1 : 0; this.store.stockList.length = headingLoco ? 1 : 0;
const maxMass = const maxMass = this.store.acceptableMass > 0 ? Math.min(this.store.acceptableMass, this.maxMass) : this.maxMass;
this.store.acceptableMass > 0 ? Math.min(this.store.acceptableMass, this.maxMass) : this.maxMass;
let exceeded = false; let exceeded = false;
@@ -277,13 +286,6 @@ export default defineComponent({
background-color: $secondaryColor; background-color: $secondaryColor;
&[data-chosen='true'] {
background-color: $accentColor;
color: black;
box-shadow: 0 0 5px 1px $accentColor;
}
&[data-excluded='true'] { &[data-excluded='true'] {
background-color: gray; background-color: gray;
box-shadow: none; box-shadow: none;
@@ -316,4 +318,3 @@ export default defineComponent({
color: black; color: black;
} }
</style> </style>
+100 -100
View File
@@ -1,53 +1,62 @@
<template> <template>
<section class="stock-list-tab"> <section class="stock-list-tab">
<div class="stock_controls" :data-disabled="store.chosenStockListIndex == -1"> <div class="tab_header">
<b class="no"> <h2>{{ $t('stocklist.title') }}</h2>
POJAZD NR <span class="text--accent">{{ store.chosenStockListIndex + 1 }}</span> &nbsp;
</b>
<button
class="btn"
:tabindex="store.chosenStockListIndex == -1 ? -1 : 0"
@click="moveUpStock(store.chosenStockListIndex)"
>
<img :src="getIconURL('higher')" alt="move up vehicle" />
PRZENIEŚ WYŻEJ
</button>
<button
class="btn"
:tabindex="store.chosenStockListIndex == -1 ? -1 : 0"
@click="moveDownStock(store.chosenStockListIndex)"
>
<img :src="getIconURL('lower')" alt="move down vehicle" />
PRZENIEŚ NIŻEJ
</button>
<button
class="btn"
:tabindex="store.chosenStockListIndex == -1 ? -1 : 0"
@click="removeStock(store.chosenStockListIndex)"
>
<img :src="getIconURL('remove')" alt="remove vehicle" />
USUŃ
</button>
</div> </div>
<div class="stock_actions"> <div class="stock_actions">
<label class="file-label"> <button class="btn btn--image" @click="clickFileInput">
<div class="btn">WCZYTAJ</div>
<input type="file" @change="uploadStock" ref="conFile" accept=".con,.txt" /> <input type="file" @change="uploadStock" ref="conFile" accept=".con,.txt" />
</label> <img src="/images/icon-upload.svg" alt="upload icon" />
{{ $t('stocklist.action-upload') }}
<button class="btn" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="downloadStock">POBIERZ</button>
<button class="btn" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="copyToClipboard">
SKOPIUJ
</button> </button>
<button class="btn" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="resetStock">ZRESETUJ</button> <button class="btn btn--image" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="downloadStock">
<img src="/images/icon-download.svg" alt="download icon" />
{{ $t('stocklist.action-download') }}
</button>
<button class="btn" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="shuffleCars">PRZETASUJ</button> <button class="btn btn--image" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="copyToClipboard">
<img src="/images/icon-copy.svg" alt="copy icon" />
{{ $t('stocklist.action-copy') }}
</button>
<button class="btn btn--image" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="resetStock">
<img src="/images/icon-reset.svg" alt="reset icon" />
{{ $t('stocklist.action-reset') }}
</button>
<button class="btn btn--image" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="shuffleCars">
<img src="/images/icon-shuffle.svg" alt="shuffle icon" />
{{ $t('stocklist.action-shuffle') }}
</button>
</div>
<div class="stock_controls" :data-disabled="store.chosenStockListIndex == -1">
<b v-if="store.chosenStockListIndex >= 0">
{{ $t('stocklist.vehicle-no') }}
<span class="text--accent">{{ store.chosenStockListIndex + 1 }}</span>
&nbsp;
</b>
<b v-else>
{{ $t('stocklist.no-vehicle-chosen') }}
</b>
<button class="btn" :tabindex="store.chosenStockListIndex == -1 ? -1 : 0" @click="moveUpStock(store.chosenStockListIndex)">
<img :src="getIconURL('higher')" alt="move up vehicle" />
{{ $t('stocklist.action-move-up') }}
</button>
<button class="btn" :tabindex="store.chosenStockListIndex == -1 ? -1 : 0" @click="moveDownStock(store.chosenStockListIndex)">
<img :src="getIconURL('lower')" alt="move down vehicle" />
{{ $t('stocklist.action-move-down') }}
</button>
<button class="btn" :tabindex="store.chosenStockListIndex == -1 ? -1 : 0" @click="removeStock(store.chosenStockListIndex)">
<img :src="getIconURL('remove')" alt="remove vehicle" />
{{ $t('stocklist.action-remove') }}
</button>
</div> </div>
<div class="stock_specs"> <div class="stock_specs">
@@ -60,49 +69,44 @@
</b> </b>
<span> <span>
Masa: <span class="text--accent">{{ store.totalMass }}t</span> (dopuszczalna: {{ $t('stocklist.mass') }}
<span class="text--accent">{{ store.totalMass }}t</span> ({{ $t('stocklist.mass-accepted') }}:
<span class="text--accent">{{ store.acceptableMass ? store.acceptableMass + 't' : '-' }}</span <span class="text--accent">{{ store.acceptableMass ? store.acceptableMass + 't' : '-' }}</span
>) - Długość: >) - {{ $t('stocklist.length') }}:
<span class="text--accent">{{ store.totalLength }}m</span> <span class="text--accent">{{ store.totalLength }}m</span>
- vMax: <span class="text--accent">{{ store.maxStockSpeed }} km/h</span> - {{ $t('stocklist.vmax') }}:
<span class="text--accent">{{ store.maxStockSpeed }} km/h</span>
</span> </span>
</div> </div>
<div class="stock_cold-start"> <div class="stock_cold-start">
<label> <label>
<input <input type="checkbox" v-model="store.isColdStart" :disabled="!locoSupportsColdStart(store.stockList[0]?.constructionType || '')" />
type="checkbox" {{ $t('stocklist.coldstart-info') }}
v-model="store.isColdStart"
:disabled="!locoSupportsColdStart(store.stockList[0]?.constructionType || '')"
/>
Zimny start lokomotywy czołowej (tylko elektrowozy typów 303E i 203E)
</label> </label>
</div> </div>
<div class="stock_warnings" v-if="stockHasWarnings"> <div class="stock_warnings" v-if="stockHasWarnings">
<div class="warning" v-if="locoNotSuitable"> <div class="warning" v-if="locoNotSuitable">(!) {{ $t('stocklist.warning-not-suitable') }}</div>
Lokomotywy EP07 i EP08 przeznaczone jedynie do ruchu pasażerskiego!
</div>
<div class="warning" v-if="trainTooLong && store.isTrainPassenger"> <div class="warning" v-if="trainTooLong && store.isTrainPassenger">(!) {{ $t('stocklist.warning-passenger-too-long') }}</div>
Maksymalna długość składów pasażerskich nie może przekraczać 350m!
</div>
<div class="warning" v-if="trainTooLong && !store.isTrainPassenger"> <div class="warning" v-if="trainTooLong && !store.isTrainPassenger">(!) {{ $t('stocklist.warning-freight-too-long') }}</div>
Maksymalna długość składów innych niż pasażerskie nie może przekraczać 650m!
</div>
<div class="warning" v-if="trainTooHeavy"> <div class="warning" v-if="trainTooHeavy">
Ten skład jest za ciężki! Sprawdź (!)
<a <i18n-t keypath="stocklist.warning-too-heavy">
target="_blank" <template #href>
href="https://docs.google.com/spreadsheets/d/1bFXUsHsAu4youmNz-46Q1HslZaaoklvfoBDS553TnNk/edit" <a target="_blank" href="https://docs.google.com/spreadsheets/d/1bFXUsHsAu4youmNz-46Q1HslZaaoklvfoBDS553TnNk/edit">
> {{ $t('stocklist.acceptable-mass-docs') }}
dopuszczalne masy składów </a>
</a> </template>
</i18n-t>
</div> </div>
<div class="warning" v-if="tooManyLocomotives">Ten skład posiada za dużo pojazdów trakcyjnych!</div> <div class="warning" v-if="tooManyLocomotives">
{{ $t('stocklist.warning-too-many-locos') }}
</div>
</div> </div>
<StockThumbnails :onListItemClick="onListItemClick" /> <StockThumbnails :onListItemClick="onListItemClick" />
@@ -110,7 +114,7 @@
<!-- Stock list --> <!-- Stock list -->
<ul ref="stock_list"> <ul ref="stock_list">
<li v-if="stockIsEmpty" class="list-empty"> <li v-if="stockIsEmpty" class="list-empty">
<div class="stock-info">Lista pojazdów jest pusta!</div> <div class="stock-info">{{ $t('stocklist.list-empty') }}</div>
</li> </li>
<TransitionGroup name="stock-list-anim"> <TransitionGroup name="stock-list-anim">
@@ -126,23 +130,19 @@
@keydown.backspace="removeStock(i)" @keydown.backspace="removeStock(i)"
ref="itemRefs" ref="itemRefs"
> >
<div <div class="stock-info" @dragstart="onDragStart(i)" @drop="onDrop($event, i)" @dragover="allowDrop" draggable="true">
class="stock-info"
@dragstart="onDragStart(i)"
@drop="onDrop($event, i)"
@dragover="allowDrop"
draggable="true"
>
<span class="stock-info__no" :data-selected="i == store.chosenStockListIndex"> <span class="stock-info__no" :data-selected="i == store.chosenStockListIndex">
<span v-if="i == store.chosenStockListIndex">&bull;&nbsp;</span> <span v-if="i == store.chosenStockListIndex">&bull;&nbsp;</span>
{{ i + 1 }}. {{ i + 1 }}.
</span> </span>
<span class="stock-info__type" :class="{ supporter: stock.supportersOnly }"> <span class="stock-info__type" :class="{ sponsor: stock.isSponsorsOnly }">
{{ stock.isLoco ? stock.type : getCarSpecFromType(stock.type) }} {{ stock.isLoco ? stock.type : getCarSpecFromType(stock.type) }}
</span> </span>
<span class="stock-info__cargo" v-if="stock.cargo"> {{ stock.cargo.id }} </span> <span class="stock-info__cargo" v-if="stock.cargo">
{{ stock.cargo.id }}
</span>
<span class="stock-info__length"> {{ stock.length }}m </span> <span class="stock-info__length"> {{ stock.length }}m </span>
<span class="stock-info__mass">{{ stock.cargo ? stock.cargo.totalMass : stock.mass }}t </span> <span class="stock-info__mass">{{ stock.cargo ? stock.cargo.totalMass : stock.mass }}t </span>
<span class="stock-info__speed"> {{ stock.maxSpeed }}km/h </span> <span class="stock-info__speed"> {{ stock.maxSpeed }}km/h </span>
@@ -155,7 +155,6 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import TrainImage from '../sections/TrainImageSection.vue';
import { useStore } from '../../store'; import { useStore } from '../../store';
@@ -168,7 +167,7 @@ import stockMixin from '../../mixins/stockMixin';
export default defineComponent({ export default defineComponent({
name: 'stock-list', name: 'stock-list',
components: { TrainImage, StockThumbnails }, components: { StockThumbnails },
mixins: [warningsMixin, imageMixin, stockMixin, stockPreviewMixin], mixins: [warningsMixin, imageMixin, stockMixin, stockPreviewMixin],
@@ -182,8 +181,9 @@ export default defineComponent({
data: () => ({ data: () => ({
imageOffsetY: 0, imageOffsetY: 0,
draggedVehicleID: -1, draggedVehicleID: -1,
stockActions: [{}],
}), }),
computed: { computed: {
@@ -191,8 +191,7 @@ export default defineComponent({
return this.store.stockList return this.store.stockList
.map((stock, i) => { .map((stock, i) => {
let stockTypeStr = stock.isLoco || !stock.cargo ? stock.type : `${stock.type}:${stock.cargo.id}`; let stockTypeStr = stock.isLoco || !stock.cargo ? stock.type : `${stock.type}:${stock.cargo.id}`;
let coldStart = let coldStart = i == 0 && this.store.isColdStart && locoSupportsColdStart(stock.constructionType || '') ? ',c' : '';
i == 0 && this.store.isColdStart && locoSupportsColdStart(stock.constructionType || '') ? ',c' : '';
return stockTypeStr + coldStart; return stockTypeStr + coldStart;
}) })
@@ -219,15 +218,18 @@ export default defineComponent({
navigator.clipboard.writeText(this.stockString); navigator.clipboard.writeText(this.stockString);
setTimeout(() => { setTimeout(() => {
alert('Pociąg został skopiowany do schowka!'); alert(this.$t('stocklist.alert-copied'));
}, 20); }, 20);
}, },
clickFileInput() {
(this.$refs['conFile'] as HTMLInputElement).click();
},
onListItemClick(stockID: number) { onListItemClick(stockID: number) {
const stock = this.store.stockList[stockID]; const stock = this.store.stockList[stockID];
this.store.chosenStockListIndex = this.store.chosenStockListIndex = this.store.chosenStockListIndex == stockID && this.store.chosenVehicle?.type == stock.type ? -1 : stockID;
this.store.chosenStockListIndex == stockID && this.store.chosenVehicle?.type == stock.type ? -1 : stockID;
if (this.store.chosenStockListIndex == -1) { if (this.store.chosenStockListIndex == -1) {
this.store.chosenVehicle = null; this.store.chosenVehicle = null;
@@ -322,16 +324,13 @@ export default defineComponent({
}, },
downloadStock() { downloadStock() {
if (this.store.stockList.length == 0) return alert('Lista pojazdów jest pusta!'); if (this.store.stockList.length == 0) return alert(this.$t('stocklist.alert-empty'));
const defaultName = `${this.store.chosenRealStockName || this.store.stockList[0].type} ${ const defaultName = `${this.store.chosenRealStockName || this.store.stockList[0].type} ${this.store.totalMass}t; ${
this.store.totalMass this.store.totalLength
}t; ${this.store.totalLength}m; vmax ${this.store.maxStockSpeed}`; }m; vmax ${this.store.maxStockSpeed}`;
const fileName = prompt( const fileName = prompt(this.$t('stocklist.prompt-file'), defaultName);
'Nazwij plik, a następnie pobierz do folderu Presets (Dokumenty/TTSK/TrainDriver2):',
defaultName
);
if (!fileName) return; if (!fileName) return;
@@ -398,6 +397,7 @@ export default defineComponent({
<style lang="scss" scoped> <style lang="scss" scoped>
@import '../../styles/global'; @import '../../styles/global';
@import '../../styles/tab.scss';
.stock-list-tab { .stock-list-tab {
display: grid; display: grid;
@@ -459,14 +459,15 @@ export default defineComponent({
display: grid; display: grid;
gap: 0.5em; gap: 0.5em;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
label.file-label { button {
text-align: center; width: 100%;
cursor: pointer;
input { input {
display: none; opacity: 0;
width: 0;
height: 0;
} }
} }
} }
@@ -523,7 +524,7 @@ li > .stock-info {
} }
} }
.supporter { .sponsor {
color: salmon; color: salmon;
} }
@@ -584,4 +585,3 @@ li > .stock-info {
} }
} }
</style> </style>
+328 -361
View File
@@ -1,361 +1,328 @@
<template> <template>
<section class="wiki-list tab"> <section class="wiki-list tab">
<div class="tab_header"> <div class="tab_header">
<h2>LISTA DOSTĘPNYCH POJAZDÓW</h2> <h2>{{ $t('wiki.title') }}</h2>
</div> </div>
<div class="tab_content"> <div class="tab_content">
<div class="actions-panel"> <div class="actions-panel">
<div class="actions-panel_vehicles"> <div class="actions-panel_vehicles">
<button class="btn btn--choice" @click="changeWikiMode('locomotives')">POJ. TRAKCYJNE</button> <button class="btn" :data-chosen="currentFilterMode == 'tractions'" @click="toggleFilter('tractions')">
<button class="btn btn--choice" @click="changeWikiMode('carWagons')">WAGONY</button> {{ $t('wiki.action-vehicles') }}
</div> </button>
<button class="btn" :data-chosen="currentFilterMode == 'carriages'" @click="toggleFilter('carriages')">
<div class="actions-panel_search"> {{ $t('wiki.action-carriages') }}
<input type="text" placeholder="Wyszukaj pojazd..." v-model="searchedVehicleTypeName" /> </button>
</div> </div>
</div>
<div class="actions-panel_search">
<div class="table-wrapper" @scroll="scrollEvent" ref="table-wrapper"> <input type="text" :placeholder="$t('wiki.search')" v-model="searchedVehicleTypeName" />
<table> </div>
<thead> </div>
<tr>
<th v-for="header in wikiMode == 'locomotives' ? locoHeaders : carHeaders" @click="toggleSorter(header)"> <div class="table-wrapper" ref="table-wrapper">
{{ header.name }} <table>
<thead>
<span v-if="currentModeSorter.id == header.id"> <tr>
{{ currentModeSorter.direction == 1 ? `&uArr;` : `&dArr;` }} <th v-for="header in visibleHeaders" @click="toggleSorter(header)" :key="header.id">
</span> {{ $t(`wiki.header.${header.id}`) }}
</th>
</tr> <span v-if="currentSorter.id == header.id">
</thead> {{ currentSorter.direction == 1 ? `&uArr;` : `&dArr;` }}
</span>
<tbody v-if="wikiMode == 'locomotives'"> </th>
<tr </tr>
v-for="loco in computedLocoList" </thead>
@click="previewLocomotive(loco)"
@keydown.enter="previewLocomotive(loco)" <tbody>
@dblclick="addLocomotive(loco)" <tr
tabindex="0" v-for="{ vehicle, show } in computedTableData"
> tabindex="0"
<td> v-show="show"
<img :key="vehicle.type"
:src="`https://spythere.github.io/api/td2/images/${loco.type}--300px.jpg`" @click="previewVehicle(vehicle)"
loading="lazy" @keydown.enter="previewVehicle(vehicle)"
:alt="`Lokomotywa ${loco.type}`" @dblclick="addVehicle(vehicle)"
/> >
</td> <td style="width: 120px">
<img
<td>{{ loco.type }}</td> width="120"
<td>{{ vehicleTypes[loco.power] }}</td> :src="getThumbnailURL(vehicle.type, 'small')"
<td>{{ loco.constructionType }}</td> :alt="`${vehicle.type}`"
<td>{{ locoSupportsColdStart(loco.constructionType) ? `&check;` : '&cross;' }}</td> loading="lazy"
<td>{{ loco.length }}m</td> @error="(e) => ((e.target as HTMLElement).style.display = 'none')"
<td>{{ loco.mass }}t</td> />
<td>{{ loco.maxSpeed }}km/h</td> </td>
</tr>
</tbody> <td :data-sponsoronly="vehicle.isSponsorsOnly">{{ vehicle.type }}</td>
<tbody v-else> <td v-if="isLocomotive(vehicle)">{{ $t(`wiki.${vehicle.power}`) }}</td>
<tr <td v-else>{{ $t(`wiki.${vehicle.useType}`) }}</td>
v-for="car in computedCarList"
@keydow.enter="previewCarWagon(car)" <td>{{ vehicle.constructionType }}</td>
@click="previewCarWagon(car)" <td>{{ vehicle.length }}m</td>
@dblclick="addCarWagon(car)" <td>{{ vehicle.mass }}t</td>
tabindex="0" <td>{{ vehicle.maxSpeed }}km/h</td>
>
<td> <td v-if="currentFilterMode == 'carriages'">{{ !isLocomotive(vehicle) ? vehicle.cargoList.length : '---' }}</td>
<img <td v-if="currentFilterMode == 'tractions'">
:src="`https://spythere.github.io/api/td2/images/${car.type}--300px.jpg`" {{ isLocomotive(vehicle) ? (locoSupportsColdStart(vehicle.constructionType) ? `&check;` : '&cross;') : '---' }}
loading="lazy" </td>
:alt="`Lokomotywa ${car.type}`" </tr>
/> </tbody>
</td>
<span ref="table-bottom"></span>
<td>{{ car.type }}</td> </table>
<td>{{ car.constructionType }}</td> </div>
<td>{{ car.length }}m</td> </div>
<td>{{ car.mass }}t</td> </section>
<td>{{ car.maxSpeed }}km/h</td> </template>
<td>{{ car.cargoList.length == 0 ? '-' : car.cargoList.length }}</td>
</tr> <script lang="ts">
</tbody> import { defineComponent } from 'vue';
</table> import { useStore } from '../../store';
</div> import stockPreviewMixin from '../../mixins/stockPreviewMixin';
</div> import { Vehicle } from '../../types';
</section> import { isLocomotive } from '../../utils/vehicleUtils';
</template> import stockMixin from '../../mixins/stockMixin';
import imageMixin from '../../mixins/imageMixin';
<script lang="ts"> import { locoSupportsColdStart } from '../../utils/locoUtils';
import { defineComponent } from 'vue';
import { useStore } from '../../store'; type SorterID = 'type' | 'constructionType' | 'image' | 'length' | 'mass' | 'maxSpeed' | 'cargoCount' | 'group' | 'coldStart';
import stockPreviewMixin from '../../mixins/stockPreviewMixin';
import { Vehicle } from '../../types'; interface IWikiHeader {
import { isLocomotive } from '../../utils/vehicleUtils'; id: SorterID;
import stockMixin from '../../mixins/stockMixin'; sortable: boolean;
import { locoSupportsColdStart } from '../../utils/locoUtils'; for: 'all' | 'carriages' | 'tractions';
}
type WikiMode = 'locomotives' | 'carWagons';
type SorterID = interface IWikiRow {
| 'type' vehicle: Vehicle;
| 'constructionType' show: boolean;
| 'image' }
| 'length'
| 'mass' const headers: IWikiHeader[] = [
| 'maxSpeed' { id: 'image', sortable: false, for: 'all' },
| 'cargoCount' { id: 'type', sortable: true, for: 'all' },
| 'power' { id: 'group', sortable: true, for: 'all' },
| 'coldStart'; { id: 'constructionType', sortable: true, for: 'all' },
{ id: 'length', sortable: true, for: 'all' },
interface WikiHeader { { id: 'mass', sortable: true, for: 'all' },
name: string; { id: 'maxSpeed', sortable: true, for: 'all' },
id: SorterID; { id: 'coldStart', sortable: true, for: 'tractions' },
sortable: boolean; { id: 'cargoCount', sortable: true, for: 'carriages' },
} ];
const locoHeaders: WikiHeader[] = [ export default defineComponent({
{ name: 'Zdjęcie', id: 'image', sortable: false }, mixins: [stockPreviewMixin, stockMixin, imageMixin],
{ name: 'Nazwa', id: 'type', sortable: true },
{ name: 'Rodzaj', id: 'power', sortable: true }, data() {
{ name: 'Konstrukcja', id: 'constructionType', sortable: true }, return {
{ name: 'Zimny start', id: 'coldStart', sortable: true }, store: useStore(),
{ name: 'Długość', id: 'length', sortable: true }, headers,
{ name: 'Masa', id: 'mass', sortable: true },
{ name: 'Prędkość', id: 'maxSpeed', sortable: true }, scrollTop: 0,
];
searchedVehicleTypeName: '',
const carHeaders: WikiHeader[] = [
{ name: 'Zdjęcie', id: 'image', sortable: false }, currentSorter: {
{ name: 'Nazwa', id: 'type', sortable: true }, id: 'type' as SorterID,
{ name: 'Konstrukcja', id: 'constructionType', sortable: true }, direction: 1,
{ name: 'Długość', id: 'length', sortable: true }, },
{ name: 'Masa', id: 'mass', sortable: true },
{ name: 'Prędkość', id: 'maxSpeed', sortable: true }, currentFilterMode: 'all' as 'all' | 'tractions' | 'carriages',
{ name: 'Ładunki', id: 'cargoCount', sortable: true }, };
]; },
const vehicleTypes: { [key: string]: string } = { activated() {
'loco-ezt': 'EZT', const tableWrapperRef = this.$refs['table-wrapper'] as HTMLElement;
'loco-szt': 'SZT',
'loco-s': 'Spalinowóz', tableWrapperRef.scrollTo({
'loco-e': 'Elektrowóz', top: this.scrollTop,
}; });
},
export default defineComponent({
mixins: [stockPreviewMixin, stockMixin], methods: {
locoSupportsColdStart,
data() { isLocomotive,
return {
store: useStore(), toggleFilter(name: typeof this.currentFilterMode) {
locoHeaders, this.currentFilterMode = this.currentFilterMode == name ? 'all' : name;
carHeaders, },
vehicleTypes,
toggleSorter(header: IWikiHeader) {
locosScrollTop: 0, if (!header.sortable) return;
carsScrollTop: 0,
if (header.id == this.currentSorter.id) this.currentSorter.direction *= -1;
wikiMode: 'locomotives' as WikiMode, this.currentSorter.id = header.id;
searchedVehicleTypeName: '', },
currentLocoSorter: { sortTableRows(row1: IWikiRow, row2: IWikiRow) {
id: 'type' as SorterID, if (!row1.show) return 0;
direction: 1,
}, const { id, direction } = this.currentSorter;
currentCarSorter: { switch (id) {
id: 'type' as SorterID, case 'type':
direction: 1, case 'constructionType':
}, case 'group':
}; return direction == 1 ? row1.vehicle[id].localeCompare(row2.vehicle[id]) : row2.vehicle[id].localeCompare(row1.vehicle[id]);
},
case 'mass':
activated() { case 'length':
const tableWrapperRef = this.$refs['table-wrapper'] as HTMLElement; case 'maxSpeed':
tableWrapperRef.scrollTo({ top: this.wikiMode == 'locomotives' ? this.locosScrollTop : this.carsScrollTop }); return Math.sign(row1.vehicle[id] - row2.vehicle[id]) * direction;
},
case 'cargoCount':
methods: { return (
locoSupportsColdStart, (!isLocomotive(row1.vehicle) ? Math.sign(row1.vehicle.cargoList.length || -1) : -1) -
(!isLocomotive(row2.vehicle) ? (row2.vehicle.cargoList.length || -1) * direction : -1)
scrollEvent(e: Event) { );
const tableScrollTop = (e.target as HTMLElement).scrollTop;
case 'coldStart':
if (this.wikiMode == 'locomotives') this.locosScrollTop = tableScrollTop; return (locoSupportsColdStart(row1.vehicle.constructionType) > locoSupportsColdStart(row2.vehicle.constructionType) ? 1 : -1) * direction;
else this.carsScrollTop = tableScrollTop;
}, default:
break;
changeWikiMode(wikiMode: WikiMode) { }
this.searchedVehicleTypeName = '';
this.wikiMode = wikiMode; return direction == 1 ? row1.vehicle.type.localeCompare(row2.vehicle.type) : row2.vehicle.type.localeCompare(row1.vehicle.type);
}, },
},
toggleSorter(header: WikiHeader) {
if (!header.sortable) return; computed: {
computedTableData(): IWikiRow[] {
if (header.id == this.currentModeSorter.id) this.currentModeSorter.direction *= -1; return this.store.vehicleDataList
this.currentModeSorter.id = header.id; .map((vehicle) => ({
}, vehicle,
show:
sortVehicles(vA: Vehicle, vB: Vehicle) { new RegExp(`${this.searchedVehicleTypeName.trim()}`, 'i').test(vehicle.type) &&
const { id, direction } = this.currentModeSorter; (this.currentFilterMode == 'all' ||
const vehiclesAreLocos = isLocomotive(vA) && isLocomotive(vB); (this.currentFilterMode == 'tractions' && isLocomotive(vehicle)) ||
const vehiclesAreCars = !isLocomotive(vA) && !isLocomotive(vB); (this.currentFilterMode == 'carriages' && !isLocomotive(vehicle))),
switch (id) { // ((this.filters.tractions && isLocomotive(vehicle)) || (this.filters.carriages && !isLocomotive(vehicle))),
case 'type': }))
case 'constructionType': .sort((a, b) => this.sortTableRows(a, b));
return direction == 1 ? vA[id].localeCompare(vB[id]) : vB[id].localeCompare(vA[id]); },
case 'mass': visibleHeaders() {
case 'length': const filtersActive = this.currentFilterMode;
case 'maxSpeed':
return Math.sign(vA[id] - vB[id]) * direction; return this.headers.filter((header) => header.for == 'all' || header.for == filtersActive);
},
case 'cargoCount':
if (vehiclesAreCars) return Math.sign((vA.cargoList.length || -1) - (vB.cargoList.length || -1)) * direction; areTractionVehiclesShown() {
return this.currentFilterMode == 'all' || this.currentFilterMode == 'tractions';
case 'coldStart': },
if (vehiclesAreLocos)
return ( areCarriagesShown() {
(locoSupportsColdStart(vA.constructionType) > locoSupportsColdStart(vB.constructionType) ? 1 : -1) * return this.currentFilterMode == 'all' || this.currentFilterMode == 'carriages';
direction },
); },
});
default: </script>
break;
} <style lang="scss" scoped>
@import '../../styles/tab.scss';
return direction == 1 ? vA.type.localeCompare(vB.type) : vB.type.localeCompare(vA.type);
}, .actions-panel {
}, display: flex;
justify-content: space-between;
computed: { align-items: center;
currentModeSorter() {
return this.wikiMode == 'carWagons' ? this.currentCarSorter : this.currentLocoSorter; flex-wrap: wrap;
}, gap: 0.5em;
computedLocoList() { margin: 0.5em 0;
const trimmedSearchValue = this.searchedVehicleTypeName.trim(); }
return this.store.locoDataList .actions-panel_vehicles {
.filter((loco) => new RegExp(`${trimmedSearchValue}`, 'i').test(loco.type)) display: flex;
.sort(this.sortVehicles); gap: 0.5em;
}, }
computedCarList() { .actions-panel_search {
const trimmedSearchValue = this.searchedVehicleTypeName.trim(); input {
width: auto;
return this.store.carDataList }
.filter((car) => new RegExp(`${trimmedSearchValue}`, 'i').test(car.type)) }
.sort(this.sortVehicles);
}, .table-wrapper {
}, overflow: auto;
}); height: 750px;
</script> max-height: 95vh;
}
<style lang="scss" scoped>
@import '../../styles/tab.scss'; .wiki-list table {
border-collapse: collapse;
.actions-panel { width: 100%;
display: flex;
justify-content: space-between; thead {
align-items: center; position: sticky;
top: 0;
flex-wrap: wrap; }
gap: 0.5em;
th {
margin: 0.5em 0; background-color: #111;
} padding: 0.5em;
user-select: none;
.actions-panel_vehicles { -moz-user-select: none;
display: flex; -webkit-user-select: none;
gap: 0.5em; }
}
tr {
.actions-panel_search { cursor: pointer;
input { background-color: #333;
width: auto;
} &:first-child {
} min-width: 120px;
}
.table-wrapper {
overflow: auto; &:nth-child(odd) {
height: 750px; background-color: #444;
max-height: 95vh; }
}
&:hover {
.wiki-list table { background-color: #666;
border-collapse: collapse; }
width: 100%; }
thead { td {
position: sticky; text-align: center;
top: 0; height: 70px;
}
&[data-sponsoronly='true'] {
th { color: salmon;
background-color: #111; }
padding: 0.5em; }
user-select: none; }
-moz-user-select: none;
-webkit-user-select: none; @media screen and (max-width: $breakpointMd) {
} .wiki-list table {
th {
tr { min-width: 100px;
cursor: pointer; }
background-color: #333;
img {
&:nth-child(odd) { max-width: 100px;
background-color: #444; }
} }
}
&:hover {
background-color: #666; @media screen and (max-width: $breakpointSm) {
} .actions-panel {
} align-items: stretch;
flex-direction: column;
td { }
text-align: center;
padding: 0.25em; .actions-panel_vehicles {
height: 85px; display: grid;
} grid-template-columns: 1fr 1fr;
}
td:first-child {
width: 120px; .actions-panel_search {
} display: grid;
}
td img { }
display: block; </style>
width: 120px;
}
}
@media screen and (max-width: $breakpointMd) {
.wiki-list table {
td {
width: 100px;
height: auto;
img {
width: 6em;
}
}
}
}
@media screen and (max-width: $breakpointSm) {
.actions-panel {
align-items: stretch;
flex-direction: column;
}
.actions-panel_vehicles {
display: grid;
grid-template-columns: 1fr 1fr;
}
.actions-panel_search {
display: grid;
}
}
</style>
+52 -52
View File
@@ -1,52 +1,52 @@
<template> <template>
<div <div
class="image-preview" class="image-preview"
@click="store.vehiclePreviewSrc = ''" @click="store.vehiclePreviewSrc = ''"
@keydown.esc="store.vehiclePreviewSrc = ''" @keydown.esc="store.vehiclePreviewSrc = ''"
tabindex="0" tabindex="0"
> >
<img :src="store.vehiclePreviewSrc" alt="preview" /> <img :src="store.vehiclePreviewSrc" alt="preview" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from "vue";
import { useStore } from '../../store'; import { useStore } from "../../store";
export default defineComponent({ export default defineComponent({
data() { data() {
return { return {
store: useStore(), store: useStore(),
}; };
}, },
mounted() { mounted() {
this.$el.focus(); this.$el.focus();
}, },
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.image-preview { .image-preview {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
z-index: 99; z-index: 99;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: rgba(black, 0.85); background: rgba(black, 0.85);
cursor: zoom-out; cursor: zoom-out;
img { img {
max-width: 100%; max-width: 100%;
height: auto; height: auto;
max-height: 100%; max-height: 100%;
} }
} }
</style> </style>
+8 -6
View File
@@ -2,6 +2,7 @@
<div class="stock_thumbnails" ref="thumbnailsRef"> <div class="stock_thumbnails" ref="thumbnailsRef">
<div <div
v-for="(stock, stockIndex) in store.stockList" v-for="(stock, stockIndex) in store.stockList"
:key="stockIndex"
:data-selected="store.chosenStockListIndex == stockIndex" :data-selected="store.chosenStockListIndex == stockIndex"
draggable="true" draggable="true"
@dragstart="onDragStart(stockIndex)" @dragstart="onDragStart(stockIndex)"
@@ -9,7 +10,7 @@
@dragover="allowDrop" @dragover="allowDrop"
> >
<span @click="onListItemClick(stockIndex)" :key="stock.id"> <span @click="onListItemClick(stockIndex)" :key="stock.id">
<b :class="{ supporter: stock.supportersOnly }"> <b :class="{ sponsor: stock.isSponsorsOnly }">
{{ stock.type }} {{ stock.type }}
</b> </b>
@@ -52,9 +53,11 @@ watch(
if (index < 0) return; if (index < 0) return;
nextTick(() => { nextTick(() => {
(thumbnailsRef.value as HTMLElement) (thumbnailsRef.value as HTMLElement).querySelector(`div:nth-child(${index + 1})`)?.scrollIntoView({
.querySelector(`div:nth-child(${index + 1})`) block: 'nearest',
?.scrollIntoView({ block: 'nearest', inline: 'start', behavior: 'smooth' }); inline: 'start',
behavior: 'smooth',
});
}); });
} }
); );
@@ -118,8 +121,7 @@ const allowDrop = (e: DragEvent) => {
} }
} }
.supporter { .sponsor {
color: salmon; color: salmon;
} }
</style> </style>
+9 -10
View File
@@ -12,8 +12,8 @@
}, },
"sameRegions": { "sameRegions": {
"Losowy": [ "Losowy": [
10, 11, 19, 91, 93, 97, 99, 20, 22, 29, 30, 33, 39, 40, 44, 49, 94, 50, 55, 59, 90, 95, 96, 66, 60, 69, 77, 70, 10, 11, 19, 91, 93, 97, 99, 20, 22, 29, 30, 33, 39, 40, 44, 49, 94, 50,
79, 88, 80, 89, 92, 98 55, 59, 90, 95, 96, 66, 60, 69, 77, 70, 79, 88, 80, 89, 92, 98
], ],
"Warszawa": [10, 11, 19, 91, 93, 97, 99], "Warszawa": [10, 11, 19, 91, 93, 97, 99],
"Lublin": [20, 22, 29], "Lublin": [20, 22, 29],
@@ -26,13 +26,12 @@
"Rezerwa": [89, 92, 98] "Rezerwa": [89, 92, 98]
}, },
"categories": { "categories": {
"ekspres krajowy (EI)": "2:00-99:2", "EI": "2:00-99:2",
"(między)wojewódzki pośpieszny (MP/RP)": "2:050-169:3", "MP/RP": "2:050-169:3",
"wojewódzki osobowy (RO)": "2:200-999:3", "RO": "2:200-999:3",
"próżny \"służbowy\" (PW)": "2:6;3:0-899:3", "PW": "2:6;3:0-899:3",
"towarowy do przewozów masowych (TM)": "2:4;3:0-899:3", "TM": "2:4;3:0-899:3",
"towarowy do obsługi stacji (TK)": "2:3;3:0-899:3", "TK": "2:3;3:0-899:3",
"lokomotywa luzem (LT)": "2:5;3:0-899:3" "LT": "2:5;3:0-899:3"
} }
} }
+6 -2
View File
@@ -27,6 +27,12 @@
}, },
"cargo": null "cargo": null
}, },
"EP09": {
"passenger": {
"650": 160
},
"cargo": null
},
"ET41": { "ET41": {
"passenger": { "passenger": {
"700": 125 "700": 125
@@ -58,5 +64,3 @@
} }
} }
} }
+7 -7
View File
@@ -1,9 +1,9 @@
export const enum EVehicleUseType { export const enum EVehicleUseType {
LOCO_ELECTRICAL = 'loco-e', LOCO_ELECTRICAL = "loco-e",
LOCO_DIESEL = "loco-s", LOCO_DIESEL = "loco-s",
EMU = "loco-ezt", EMU = "loco-ezt",
DMU = "loco-szt", DMU = "loco-szt",
CAR_PASSENGER = "car-passenger", CAR_PASSENGER = "car-passenger",
CAR_CARGO = "car-cargo" CAR_CARGO = "car-cargo",
} }
+10
View File
@@ -0,0 +1,10 @@
import axios from "axios";
const http = axios.create({
baseURL:
import.meta.env.VITE_API_DEV === "1" && import.meta.env.DEV
? "http://localhost:5500"
: "https://spythere.github.io/api",
});
export default http;
+35
View File
@@ -0,0 +1,35 @@
import localePL from "./locales/pl.json";
import localeEN from "./locales/en.json";
import { createI18n } from "vue-i18n";
import http from "./http";
type LocaleMessageSchema = typeof localePL;
type LocaleKey = "en" | "pl";
const locales: { [key in LocaleKey]: LocaleMessageSchema } = {
en: localeEN,
pl: localePL,
};
const locale =
window.localStorage.getItem("locale") ||
(/^pl\b/.test(navigator.language) ? "pl" : "en");
const i18n = createI18n<[LocaleMessageSchema], "en" | "pl">({
locale,
fallbackLocale: "pl",
legacy: false,
globalInjection: true,
messages: locales,
});
async function fetchBackendTranslations() {
const localeData = (await http.get(`td2/data/locales.json`)).data;
i18n.global.mergeLocaleMessage("pl", localeData.pl);
i18n.global.mergeLocaleMessage("en", localeData.en);
}
fetchBackendTranslations();
export default i18n;
+152
View File
@@ -0,0 +1,152 @@
{
"app": {
"title": "ROLLING STOCK EDITOR"
},
"footer": {
"disclaimer": "This site has only an informational intent. The author does not carry any responsibility for creating trains against {tos}!",
"tos": "Train Driver 2 simulator rules",
"tos-href": "https://docs.google.com/document/d/1UAAPUtN0d_RoS4RgOzEzllJZJhA0VcizzCzKW4QylbY/edit#heading=h.1ldcvhomwjp9",
"version-check": "Site is complete for version {version} of Train Driver 2 simulator"
},
"inputs": {
"title": "CHOOSE A VEHICLE",
"input-vehicle": "Choose a traction unit",
"input-carwagon": "Choose a carriage",
"cargo-title": "Cargo (only selected freight cars)",
"no-cargo-available": "no cargo available",
"cargo-empty": "empty",
"loco-e": "ELECTR.",
"loco-s": "DIESEL",
"loco-ezt": "EMU",
"loco-szt": "DMU",
"car-passenger": "PASSENGER",
"car-cargo": "FREIGHT",
"action-add": "ADD NEW",
"action-swap": "SWAP WITH",
"real-stock": "POLISH TRAIN COMPOSITIONS"
},
"preview": {
"title": "RAILWAY VEHICLE PREVIEW",
"loading": "IMAGE LOADING...",
"desc": "Choose a railway vehicle above to see its preview",
"sponsor-only": "* SPONSORS ONLY UNTIL {0}",
"loco-e": "ELECTRIC LOCO",
"loco-s": "DIESEL LOCO",
"loco-ezt": "ELECTRIC M.U.",
"loco-szt": "DIESEL M.U.",
"car-passenger": "PASSENGER CARRIAGE",
"car-cargo": "FREIGHT CARRIAGE",
"cabin": "Cabin type:",
"construction": "Construction type:"
},
"topbar": {
"stock-list": "STOCK",
"wiki-list": "VEHICLES",
"number-generator": "NUMBER GEN.",
"stock-generator": "STOCK GEN."
},
"stocklist": {
"title": "STOCK EDITOR",
"alert-copied": "The rolling stock has been copied to your clipboard!",
"alert-empty": "Lista pojazdów jest pusta!",
"prompt-file": "Name a file and download it to the Presets folder (Documents/TTSK/TrainDriver2):",
"vehicle-no": "VEHICLE NO.",
"no-vehicle-chosen": "NO VEHICLE CHOSEN",
"action-move-up": "MOVE UP",
"action-move-down": "MOVE DOWN",
"action-remove": "REMOVE",
"action-upload": "LOAD",
"action-download": "DOWNLOAD",
"action-copy": "COPY",
"action-reset": "RESET",
"action-shuffle": "SHUFFLE",
"mass": "Mass",
"mass-accepted": "accepted",
"length": "Length",
"vmax": "vMax",
"coldstart-info": "Cold start heading locomotive (only locos 303E & 203E type)",
"list-empty": "Stock list is empty!",
"warning-not-suitable": "EP07 & EP08 type locomotives are designed for passenger traffic only!",
"warning-passenger-too-long": "Maximum length of a passenger train may not be greater than 350m!",
"warning-freight-too-long": "Maximum length of a freight train may not be greater than 650m!",
"warning-too-many-locos": "This train has too many traction units!",
"warning-too-heavy": "This train is too heavy! Check {href}",
"acceptable-mass-docs": "acceptable rolling stock masses (PL)"
},
"stockgen": {
"title": "FREIGHT TRAIN GENERATOR",
"properties-title": "ROLLING STOCK PROPERTIES",
"properties-desc": "⇐ Add a locomotive in the first place of the stock list to include it in a drawing!",
"input-mass": "Max. mass (t)",
"input-length": "Max. length (m)",
"input-carcount": "Max. car count",
"cargo-title": "CARGO",
"cargo-desc": "Choose cargo you want to fill available cars with:",
"chosen-title": "CARS WITH CHOSEN CARGO",
"chosen-empty-warning": "Choose at least one cargo type to see available cars!",
"chosen-warning": "Cars containing chosen cargo are shown below. Hover over a type to see a preview of the car. Click it to include/exclude it from a drawing (only highlighted types will be included).",
"action-generate": "GENERATE",
"action-generate-empty": "GENERATE EMPTY",
"action-reset": "RESET CARGO"
},
"numgen": {
"title": "TRAIN NUMBER GENERATOR",
"alert": "The number has been copied to your clipboard!",
"start-region": "Beginning construction region",
"end-region": "Terminating construction region",
"train-category": "Train category",
"number-info": "Generated train number:",
"warning": "Choose category and (optionally) construction regions",
"td2-wiki": "> Polish rules of train numbering (TD2 wiki)",
"td2-wiki-link": "https://wiki.td2.info.pl/index.php?title=Zasady_numeracji_poci%C4%85g%C3%B3w/en",
"action-random-region": "DRAW REGIONS",
"action-random-number": "DRAW A NUMBER",
"categories": {
"EI": "domestic express (EI)",
"MP/RP": "(inter)voivodeship bullet (MP/RP)",
"RO": "regional passenger (RO)",
"PW": "empty passenger (PW)",
"TM": "mass transport freight (TM)",
"TK": "non-mass transport freight (TK)",
"LT": "locomotive alone (LT)"
},
"rules": {
"EI": "4 digits - ends within the range of 00-99",
"MP/RP": "5 digits - ends within the range of 050-169",
"RO": "5 digits - ends within the range of 200-999",
"PW": "6 digits - '6' on the 3rd place; ends within the range of 000-899",
"TM": "6 digits - '4' on the 3rd place; ends within the range of 000-899",
"TK": "6 digits - '3' on the 3rd place; ends within the range of 000-899",
"LT": "6 digits - '5' on the 3rd place; ends within the range of 000-899"
}
},
"wiki": {
"title": "LIST OF AVAILABLE VEHICLES",
"action-vehicles": "TRACTION UNITS",
"action-carriages": "CARRIAGES",
"search": "Search for a vehicle...",
"header": {
"image": "Image",
"type": "Name",
"group": "Type group",
"constructionType": "Construction",
"coldStart": "Cold start",
"length": "Length",
"mass": "Mass",
"maxSpeed": "Speed",
"cargoCount": "Cargo count"
},
"loco-ezt": "EMU",
"loco-szt": "DMU",
"loco-s": "Diesel locomotive",
"loco-e": "Electric locomotive",
"car-passenger": "Passenger carriage",
"car-cargo": "Frieght carriage"
},
"realstock": {
"title": "POLISH TRAIN COMPOSITIONS by",
"search-name": "Search by name",
"search-stock": "Search by vehicles",
"action-reset": "RESET"
}
}
+152
View File
@@ -0,0 +1,152 @@
{
"app": {
"title": "EDYTOR SKŁADÓW ONLINE"
},
"footer": {
"disclaimer": "Ta strona ma charakter informacyjny. Autor nie ponosi odpowiedzialności za tworzenie pociągów niezgodnych z {tos}!",
"tos": "regulaminem symulatora Train Driver 2",
"tos-href": "https://docs.google.com/document/d/1UAAPUtN0d_RoS4RgOzEzllJZJhA0VcizzCzKW4QylbY/edit",
"version-check": "Strona jest kompletna dla wersji {version} symulatora TD2"
},
"inputs": {
"title": "WYBIERZ POJAZD SZYNOWY",
"input-vehicle": "Wybierz pojazd trakcyjny",
"input-carwagon": "Wybierz wagon",
"cargo-title": "Ładunek (tylko wybrane towarowe)",
"no-cargo-available": "brak dostępnych ładunków",
"cargo-empty": "próżny",
"loco-e": "ELEKTR.",
"loco-s": "SPAL.",
"loco-ezt": "EZT",
"loco-szt": "SZT",
"car-passenger": "PASAŻERSKIE",
"car-cargo": "TOWAROWE",
"action-add": "DODAJ NOWY",
"action-swap": "ZAMIEŃ ZA",
"real-stock": "REALNE ZESTAWIENIA"
},
"preview": {
"title": "PODGLĄD WYBRANEGO POJAZDU",
"loading": "ŁADOWANIE OBRAZU...",
"desc": "Wybierz pojazd lub wagon, aby zobaczyć jego podgląd powyżej",
"sponsor-only": "* TYLKO DLA SPONSORÓW DO {0}",
"loco-e": "ELEKTROWÓZ",
"loco-s": "SPALINOWÓZ",
"loco-ezt": "EZT",
"loco-szt": "SZT",
"car-passenger": "WAGON PASAŻERSKI",
"car-cargo": "WAGON TOWAROWY",
"cabin": "Typ kabiny:",
"construction": "Typ konstrukcji:"
},
"topbar": {
"stock-list": "SKŁAD",
"wiki-list": "POJAZDY",
"number-generator": "GNR NUMERU",
"stock-generator": "GNR SKŁADU"
},
"stocklist": {
"title": "EDYTOR SKŁADU",
"alert-copied": "Skład został skopiowany do twojego schowka!",
"alert-empty": "Lista pojazdów jest pusta!",
"prompt-file": "Nazwij plik, a następnie pobierz do folderu Presets (Dokumenty/TTSK/TrainDriver2):",
"vehicle-no": "POJAZD NR",
"no-vehicle-chosen": "NIE WYBRANO POJAZDU",
"action-move-up": "PRZENIEŚ WYŻEJ",
"action-move-down": "PRZENIEŚ NIŻEJ",
"action-remove": "USUŃ",
"action-upload": "WCZYTAJ",
"action-download": "POBIERZ",
"action-copy": "SKOPIUJ",
"action-reset": "ZRESETUJ",
"action-shuffle": "PRZETASUJ",
"mass": "Masa",
"mass-accepted": "dopuszczalna",
"length": "Długość",
"vmax": "vMax",
"coldstart-info": "Zimny start lokomotywy czołowej (tylko elektrowozy typów 303E i 203E)",
"list-empty": "Lista pojazdów jest pusta!",
"warning-not-suitable": "Lokomotywy EP07 i EP08 są przeznaczone jedynie do ruchu pasażerskiego!",
"warning-passenger-too-long": "Maksymalna długość składów pasażerskich nie może przekraczać 350m!",
"warning-freight-too-long": "Maksymalna długość składów innych niż pasażerskie nie może przekraczać 650m!",
"warning-too-many-locos": "Ten skład posiada za dużo pojazdów trakcyjnych!",
"warning-too-heavy": "Ten skład jest za ciężki! Sprawdź {href}",
"acceptable-mass-docs": "dopuszczalne masy składów"
},
"stockgen": {
"title": "GENERATOR SKŁADU TOWAROWEGO",
"properties-title": "WŁAŚCIWOŚCI SKŁADU",
"properties-desc": "⇐ Dodaj lokomotywę na pierwsze miejsce listy, aby uwzględnić ją przy losowaniu składu!",
"input-mass": "Maksymalna masa (t)",
"input-length": "Maks. długość (m)",
"input-carcount": "Maks. liczba wagonów",
"cargo-title": "ŁADUNEK",
"cargo-desc": "Wybierz ładunki, którymi chcesz wypełnić dostępne wagony:",
"chosen-title": "WAGONY Z WYBRANYMI ŁADUNKAMI",
"chosen-empty-warning": "Wybierz co najmniej jeden ładunek, aby zobaczyć wagony, które go posiadają!",
"chosen-warning": "Wagony posiadające wybrane ładunki. Najedź na nazwę, aby zobaczyć podgląd wagonu. Kliknij, aby wyłączyć z losowania (tylko podświetlone nazwy będą uwzględnione).",
"action-generate": "WYGENERUJ",
"action-generate-empty": "WYGENERUJ PRÓŻNE WAGONY",
"action-reset": "ZRESETUJ ŁADUNKI"
},
"numgen": {
"title": "GENERATOR NUMERU POCIĄGU",
"alert": "Numer został skopiowany do twojego schowka!",
"start-region": "Początkowy obszar konstrukcyjny",
"end-region": "Końcowy obszar konstrukcyjny",
"train-category": "Kategoria pociągu",
"number-info": "Wygenerowany numer pociągu:",
"warning": "Wybierz kategorię oraz (opcjonalnie) obszary konstrukcyjne",
"td2-wiki": "> Szczegółowe zasady numeracji (wikipedia TD2)",
"td2-wiki-link": "https://wiki.td2.info.pl/index.php?title=Zasady_numeracji_poci%C4%85g%C3%B3w",
"action-random-region": "LOSUJ OBSZARY",
"action-random-number": "LOSUJ NUMER",
"categories": {
"EI": "ekspres krajowy (EI)",
"MP/RP": "(między)wojewódzki pośpieszny (MP/RP)",
"RO": "wojewódzki osobowy (RO)",
"PW": "próżny \"służbowy\" (PW)",
"TM": "towarowy do przewozów masowych (TM)",
"TK": "towarowy do obsługi stacji (TK)",
"LT": "lokomotywa luzem (LT)"
},
"rules": {
"EI": "4 cyfry - końcówka z przedziału 00-99",
"MP/RP": "5 cyfr - końcówka z przedziału 050-169",
"RO": "5 cyfr - końcówka z przedziału 200-999",
"PW": "6 cyfr - '6' na 3. miejscu; końcówka z przedziału 000-899",
"TM": "6 cyfr - '4' na 3. miejscu; końcówka z przedziału 000-899",
"TK": "6 cyfr - '3' na 3. miejscu; końcówka z przedziału 000-899",
"LT": "6 cyfr - '5' na 3. miejscu; końcówka z przedziału 000-899"
}
},
"wiki": {
"title": "LISTA DOSTĘPNYCH POJAZDÓW",
"action-vehicles": "POJ. TRAKCYJNE",
"action-carriages": "WAGONY",
"search": "Wyszukaj pojazd...",
"header": {
"image": "Zdjęcie",
"type": "Nazwa",
"group": "Rodzaj",
"constructionType": "Konstrukcja",
"coldStart": "Zimny start",
"length": "Długość",
"mass": "Masa",
"maxSpeed": "Prędkość",
"cargoCount": "Ładunki"
},
"loco-ezt": "EZT",
"loco-szt": "SZT",
"loco-s": "Spalinowóz",
"loco-e": "Elektrowóz",
"car-passenger": "Wagon pasażerski",
"car-cargo": "Wagon towarowy"
},
"realstock": {
"title": "ZESTAWIENIA REALNE by",
"search-name": "Szukaj po nazwie",
"search-stock": "Szukaj po pojazdach",
"action-reset": "RESETUJ"
}
}
+7 -8
View File
@@ -1,14 +1,13 @@
import { createApp } from 'vue'; import { createApp } from "vue";
import { createPinia } from 'pinia'; import { createPinia } from "pinia";
import { registerSW } from 'virtual:pwa-register'; import { registerSW } from "virtual:pwa-register";
import App from './App.vue';
import App from "./App.vue";
import i18n from "./i18n-setup";
const pinia = createPinia(); const pinia = createPinia();
const updateSW = registerSW({ registerSW({
immediate: true, immediate: true,
}); });
createApp(App).use(pinia).mount('#app'); createApp(App).use(pinia).use(i18n).mount("#app");
+10 -2
View File
@@ -1,9 +1,17 @@
import { defineComponent } from 'vue'; import { defineComponent } from "vue";
export default defineComponent({ export default defineComponent({
methods: { methods: {
getIconURL(name: string, ext = 'svg'): string { getIconURL(name: string, ext = "svg"): string {
return `/images/icon-${name}.${ext}`; return `/images/icon-${name}.${ext}`;
}, },
getThumbnailURL(vehicleType: string, size: "small" | "large") {
return `${
import.meta.env.VITE_API_DEV === "1"
? "http://localhost:5500"
: "https://spythere.github.io/api"
}/td2/images/${vehicleType}--${size == "small" ? 300 : 800}px.jpg`;
},
}, },
}); });
+2 -2
View File
@@ -29,8 +29,9 @@ export default defineComponent({
count, count,
imgSrc: vehicle.imageSrc, imgSrc: vehicle.imageSrc,
useType: isLoco ? vehicle.power : vehicle.useType, useType: isLoco ? vehicle.power : vehicle.useType,
supportersOnly: vehicle.supportersOnly, isSponsorsOnly: vehicle.isSponsorsOnly,
constructionType: vehicle.constructionType, constructionType: vehicle.constructionType,
sponsorsOnlyTimestamp: vehicle.sponsorsOnlyTimestamp,
}; };
}, },
@@ -91,4 +92,3 @@ export default defineComponent({
}, },
}, },
}); });
+7 -38
View File
@@ -1,6 +1,7 @@
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { useStore } from '../store'; import { useStore } from '../store';
import { ICarWagon, ILocomotive, IStock, Vehicle } from '../types'; import { ICarWagon, ILocomotive, IStock, Vehicle } from '../types';
import { isLocomotive } from '../utils/vehicleUtils';
export default defineComponent({ export default defineComponent({
setup() { setup() {
@@ -9,45 +10,9 @@ export default defineComponent({
}; };
}, },
computed: { computed: {},
locoOptions() {
return this.store.locoDataList
.sort((a, b) => (a.type > b.type ? 1 : -1))
.filter((loco) => loco.power == this.store.chosenLocoPower);
},
carOptions() {
return this.store.carDataList
.sort((a, b) => (a.type > b.type ? 1 : -1))
.filter((car) => car.useType == this.store.chosenCarUseType);
},
},
methods: { methods: {
selectLocoType(locoTypeId: string) {
this.store.chosenLocoPower = locoTypeId;
this.store.chosenVehicle = this.locoOptions[0];
this.store.chosenLoco = this.locoOptions[0];
},
selectCarWagonType(carWagonTypeId: string) {
this.store.chosenCarUseType = carWagonTypeId;
this.store.chosenVehicle = this.carOptions[0];
this.store.chosenCar = this.carOptions[0];
this.store.chosenCargo = null;
},
previewVehicleByType(type: 'loco' | 'car' | 'cargo') {
this.$nextTick(() => {
if (!this.store.chosenLoco && !this.store.chosenCar) return;
this.store.chosenVehicle = type == 'loco' ? this.store.chosenLoco : this.store.chosenCar;
this.store.chosenCargo =
this.store.chosenCar?.cargoList.find((cargo) => cargo.id == this.store.chosenCargo?.id) || null;
});
},
previewStock(stock: IStock) { previewStock(stock: IStock) {
if (this.store.chosenVehicle?.imageSrc != stock.imgSrc) this.store.imageLoading = true; if (this.store.chosenVehicle?.imageSrc != stock.imgSrc) this.store.imageLoading = true;
@@ -81,6 +46,11 @@ export default defineComponent({
this.store.chosenCargo = null; this.store.chosenCargo = null;
}, },
previewVehicle(vehicle: Vehicle) {
if (isLocomotive(vehicle)) this.previewLocomotive(vehicle);
else this.previewCarWagon(vehicle);
},
resetPreview() { resetPreview() {
this.store.chosenVehicle = null; this.store.chosenVehicle = null;
this.store.chosenCar = null; this.store.chosenCar = null;
@@ -89,4 +59,3 @@ export default defineComponent({
}, },
}, },
}); });
+15 -8
View File
@@ -1,5 +1,5 @@
import { defineComponent } from 'vue'; import { defineComponent } from "vue";
import { useStore } from '../store'; import { useStore } from "../store";
export default defineComponent({ export default defineComponent({
setup() { setup() {
@@ -18,7 +18,10 @@ export default defineComponent({
}, },
trainTooHeavy() { trainTooHeavy() {
return this.store.acceptableMass && this.store.totalMass > this.store.acceptableMass; return (
this.store.acceptableMass &&
this.store.totalMass > this.store.acceptableMass
);
}, },
locoNotSuitable() { locoNotSuitable() {
@@ -26,15 +29,19 @@ export default defineComponent({
!this.store.isTrainPassenger && !this.store.isTrainPassenger &&
this.store.stockList.length > 1 && this.store.stockList.length > 1 &&
!this.store.stockList.every((stock) => stock.isLoco) && !this.store.stockList.every((stock) => stock.isLoco) &&
this.store.stockList.some((stock) => stock.isLoco && stock.type.startsWith('EP')) this.store.stockList.some(
(stock) => stock.isLoco && stock.type.startsWith("EP"),
)
); );
}, },
tooManyLocomotives() { tooManyLocomotives() {
return this.store.stockList.reduce((acc, stock) => { return (
if (stock.isLoco) acc += stock.count; this.store.stockList.reduce((acc, stock) => {
return acc; if (stock.isLoco) acc += stock.count;
}, 0) > 2; return acc;
}, 0) > 2
);
}, },
}, },
}); });
+7 -5
View File
@@ -1,4 +1,4 @@
import { IStore } from './types'; import { IStockData, IStore } from './types';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { import {
acceptableMass, acceptableMass,
@@ -10,6 +10,7 @@ import {
totalLength, totalLength,
totalMass, totalMass,
} from './utils/vehicleUtils'; } from './utils/vehicleUtils';
import http from './http';
export const useStore = defineStore({ export const useStore = defineStore({
id: 'store', id: 'store',
@@ -46,11 +47,14 @@ export const useStore = defineStore({
isRealStockListCardOpen: false, isRealStockListCardOpen: false,
stockData: undefined, stockData: undefined,
} as IStore),
lastFocusedElement: null,
}) as IStore,
getters: { getters: {
locoDataList: (state) => locoDataList(state), locoDataList: (state) => locoDataList(state),
carDataList: (state) => carDataList(state), carDataList: (state) => carDataList(state),
vehicleDataList: (state) => [...locoDataList(state), ...carDataList(state)],
totalMass: (state) => totalMass(state), totalMass: (state) => totalMass(state),
totalLength: (state) => totalLength(state), totalLength: (state) => totalLength(state),
maxStockSpeed: (state) => maxStockSpeed(state), maxStockSpeed: (state) => maxStockSpeed(state),
@@ -61,7 +65,7 @@ export const useStore = defineStore({
actions: { actions: {
async fetchStockInfoData() { async fetchStockInfoData() {
const stockData = await (await fetch(`https://spythere.github.io/api/td2/data/stockInfo.json`)).json(); const stockData = (await http.get<IStockData>('td2/data/stockInfo.json')).data;
this.stockData = stockData; this.stockData = stockData;
}, },
@@ -82,5 +86,3 @@ export const useStore = defineStore({
}, },
}, },
}); });
+305 -256
View File
@@ -1,256 +1,305 @@
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700;900&display=swap'); $breakpointMd: 960px;
$breakpointSm: 550px;
$breakpointMd: 960px;
$breakpointSm: 550px; $bgColor: #2b3552;
$textColor: #fff;
$bgColor: #2b3552; $secondaryColor: #222;
$textColor: #fff; $accentColor: #e4c428;
$secondaryColor: #222;
$accentColor: #e4c428; @font-face {
font-family: 'Lato';
::-webkit-scrollbar { src:
width: 7px; url('$fonts/Lato-Light.woff2') format('woff2'),
height: 7px; url('$fonts/Lato-Light.woff') format('woff');
font-weight: 300;
&-track { font-style: normal;
background: #222; font-display: swap;
border-radius: 0.5rem; }
}
@font-face {
&-thumb { font-family: 'Lato';
border-radius: 1rem; src:
background: #777; url('$fonts/Lato-Bold.woff2') format('woff2'),
} url('$fonts/Lato-Bold.woff') format('woff');
font-weight: bold;
&-corner { font-style: normal;
background: #222; font-display: swap;
} }
}
@font-face {
body, font-family: 'Lato';
html { src:
margin: 0; url('$fonts/Lato-Regular.woff2') format('woff2'),
padding: 0; url('$fonts/Lato-Regular.woff') format('woff');
font-weight: normal;
font-family: 'Lato', sans-serif; font-style: normal;
font-display: swap;
background-color: $bgColor; }
overflow-x: hidden;
} ::-webkit-scrollbar {
width: 7px;
*, height: 7px;
*::before,
*::after { &-track {
box-sizing: border-box; background: #222;
} border-radius: 0.5rem;
}
a {
color: white; &-thumb {
text-decoration: none; border-radius: 1rem;
background: #777;
transition: color 250ms; }
&:visited { &-corner {
color: white; background: #222;
} }
}
&:hover,
&:focus { body,
color: $accentColor; html {
} margin: 0;
} padding: 0;
select, font-family: Lato, sans-serif;
option,
input, background-color: $bgColor;
button { overflow-x: hidden;
font-family: 'Lato', sans-serif; }
font-size: 1em;
} *,
*::before,
button { *::after {
border: none; box-sizing: border-box;
outline: none; }
background: none;
a {
padding: 0; color: white;
margin: 0; text-decoration: none;
cursor: pointer; transition: color 250ms;
font-size: 1em; &:visited {
color: white; color: white;
}
&:hover {
color: $accentColor; &:hover,
} &:focus {
} color: $accentColor;
}
.btn { }
padding: 0.4em 0.75em;
select,
outline: none; option,
background-color: #222; input,
border-radius: 8px; button {
font-weight: bold; font-family: Lato, sans-serif;
font-size: 1em;
transition: all 250ms; }
&:hover { button {
color: $accentColor; border: none;
} outline: none;
background: none;
&.btn--outline {
background: none; padding: 0;
font-weight: bold; margin: 0;
outline: 1px solid $accentColor;
} cursor: pointer;
&:focus-visible { font-size: 1em;
color: $accentColor; color: white;
outline: 1px solid white;
} &:hover {
color: $accentColor;
&[data-disabled='true'] { }
user-select: none; }
pointer-events: none;
-moz-user-select: none; .btn {
-webkit-user-select: none; padding: 0.4em 0.75em;
opacity: 0.75; outline: none;
background-color: #2b2b2b; background-color: #222;
} border-radius: 8px;
font-weight: bold;
&--text {
font-weight: bold; transition:
transition: all 250ms; color 150ms,
background: none; background-color 150ms;
padding: 0;
&:hover {
&:focus-visible { color: $accentColor;
outline: 1px solid white; }
}
} &.btn--outline {
} background: none;
font-weight: bold;
select, outline: 1px solid $accentColor;
input[type="text"], }
input[type="number"] {
background: none; &:focus-visible {
border: 2px solid #aaa; color: $accentColor;
outline: none; outline: 1px solid white;
}
padding: 0.25em 0.35em;
&[data-chosen='true'] {
color: white; background-color: $accentColor;
font-size: 1em; color: black;
width: 18em; box-shadow: 0 0 5px 1px $accentColor;
}
&:focus-visible {
border-color: $accentColor; &[data-disabled='true'] {
} user-select: none;
pointer-events: none;
&::placeholder { -moz-user-select: none;
color: #aaa; -webkit-user-select: none;
}
} opacity: 0.75;
background-color: #2b2b2b;
option { }
color: white;
border: none; &--image {
background-color: $bgColor; display: flex;
} justify-content: center;
align-items: center;
ul { gap: 0.5em;
list-style: none;
margin: 0; img {
padding: 0; width: 1.3em;
} vertical-align: middle;
}
.text { }
&--accent {
color: $accentColor; &--text {
} font-weight: bold;
transition: all 250ms;
&--grayed { background: none;
color: #aaa; padding: 0;
}
} &:focus-visible {
outline: 1px solid white;
.g-card { }
position: fixed; }
top: 1em; }
left: 0;
select,
width: 100vw; input[type='text'],
height: 100vh; input[type='number'] {
background: $bgColor;
display: flex; border: 2px solid #aaa;
justify-content: center; outline: none;
z-index: 200; padding: 0.25em 0.35em;
&_bg { color: white;
position: fixed; font-size: 1em;
top: 0;
left: 0; width: 18em;
width: 100vw; &:focus-visible {
height: 100vh; border-color: $accentColor;
}
background-color: #000000aa;
z-index: 10; &::placeholder {
} color: #aaa;
} }
}
.g-choice {
input { option {
display: none; color: white;
} border: none;
background-color: $bgColor;
span { }
padding: 0.25em 1em;
border-radius: 0.25em; ul {
border: 2px solid white; list-style: none;
margin: 0.25em; margin: 0;
cursor: pointer; padding: 0;
}
transition: all 100ms ease;
} .text {
&--accent {
span:focus { color: $accentColor;
color: $accentColor; }
outline: none;
} &--grayed {
color: #aaa;
label > input:checked + span { }
color: $accentColor; }
border-color: $accentColor;
} .g-card {
} position: fixed;
top: 1em;
// Vue Transition anims left: 0;
.slide-top {
&-enter-from, width: 100vw;
&-leave-to { height: 100vh;
transform: translateY(-100%);
} display: flex;
justify-content: center;
&-enter-active,
&-leave-active { z-index: 200;
transition: transform 100ms ease-in-out;
} &_bg {
} position: fixed;
top: 0;
.card-appear { left: 0;
&-enter-from,
&-leave-to { width: 100vw;
opacity: 0; height: 100vh;
}
background-color: #000000aa;
&-enter-active, z-index: 10;
&-leave-active { }
transition: all 100ms ease-in-out; }
}
} .g-choice {
input {
display: none;
}
span {
padding: 0.25em 1em;
border-radius: 0.25em;
border: 2px solid white;
margin: 0.25em;
cursor: pointer;
transition: all 100ms ease;
}
span:focus {
color: $accentColor;
outline: none;
}
label > input:checked + span {
color: $accentColor;
border-color: $accentColor;
}
}
// Vue Transition anims
.slide-top {
&-enter-from,
&-leave-to {
transform: translateY(-100%);
}
&-enter-active,
&-leave-active {
transition: transform 100ms ease-in-out;
}
}
.card-appear {
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-active,
&-leave-active {
transition: all 100ms ease-in-out;
}
}
+1 -2
View File
@@ -1,4 +1,4 @@
@import './global.scss'; @import "./global.scss";
.tab { .tab {
height: 100%; height: 100%;
@@ -77,4 +77,3 @@ hr {
} }
} }
} }
+20 -14
View File
@@ -30,9 +30,12 @@ export interface IStore {
stockSectionMode: 'stock-list' | 'stock-generator' | 'number-generator' | 'wiki-list'; stockSectionMode: 'stock-list' | 'stock-generator' | 'number-generator' | 'wiki-list';
stockData?: IStockData; stockData?: IStockData;
lastFocusedElement: HTMLElement | null;
} }
export type TStockInfoKey = 'loco-e' | 'loco-s' | 'loco-ezt' | 'loco-szt' | 'car-passenger' | 'car-cargo'; export type TLocoGroup = 'loco-e' | 'loco-s' | 'loco-ezt' | 'loco-szt';
export type TCarWagonGroup = 'car-passenger' | 'car-cargo';
export interface IStockProps { export interface IStockProps {
type: string; type: string;
@@ -52,12 +55,12 @@ export interface IStockData {
}; };
info: { info: {
'car-cargo': [string, string, boolean, boolean, string][]; 'car-cargo': [string, string, boolean, number | null, string][];
'car-passenger': [string, string, boolean, boolean, string][]; 'car-passenger': [string, string, boolean, number | null, string][];
'loco-e': [string, string, string, string, boolean][]; 'loco-e': [string, string, string, string, number | null][];
'loco-s': [string, string, string, string, boolean][]; 'loco-s': [string, string, string, string, number | null][];
'loco-szt': [string, string, string, string, boolean][]; 'loco-szt': [string, string, string, string, number | null][];
'loco-ezt': [string, string, string, string, boolean][]; 'loco-ezt': [string, string, string, string, number | null][];
}; };
props: IStockProps[]; props: IStockProps[];
@@ -67,11 +70,13 @@ export interface IStockData {
export interface ILocomotive { export interface ILocomotive {
type: string; type: string;
power: string; power: TLocoGroup;
group: TLocoGroup;
constructionType: string; constructionType: string;
cabinType: string; cabinType: string;
maxSpeed: number; maxSpeed: number;
supportersOnly: boolean; isSponsorsOnly: boolean;
sponsorsOnlyTimestamp: number;
imageSrc: string; imageSrc: string;
mass: number; mass: number;
@@ -79,12 +84,13 @@ export interface ILocomotive {
} }
export interface ICarWagon { export interface ICarWagon {
//"203V_PKPC_Fll_01","203V",true,false,"100",img
type: string; type: string;
useType: 'car-passenger' | 'car-cargo'; useType: TCarWagonGroup;
group: TCarWagonGroup;
constructionType: string; constructionType: string;
loadable: boolean; loadable: boolean;
supportersOnly: boolean; isSponsorsOnly: boolean;
sponsorsOnlyTimestamp: number;
maxSpeed: number; maxSpeed: number;
imageSrc: string; imageSrc: string;
@@ -108,7 +114,8 @@ export interface IStock {
maxSpeed: number; maxSpeed: number;
cargo?: { id: string; totalMass: number }; cargo?: { id: string; totalMass: number };
isLoco: boolean; isLoco: boolean;
supportersOnly: boolean; isSponsorsOnly: boolean;
sponsorsOnlyTimestamp: number;
count: number; count: number;
imgSrc?: string; imgSrc?: string;
} }
@@ -120,4 +127,3 @@ export interface IReadyStockItem {
number: string; number: string;
name: string; name: string;
} }
+7 -5
View File
@@ -1,5 +1,7 @@
const supportedConstructions = ['303e', '203e']; const supportedConstructions = ["303e", "203e"];
export function locoSupportsColdStart(constructionType: string) { export function locoSupportsColdStart(constructionType: string) {
return new RegExp(`(${supportedConstructions.join('|')})`).test(constructionType); return new RegExp(`(${supportedConstructions.join("|")})`).test(
} constructionType,
);
}
+10 -5
View File
@@ -1,14 +1,19 @@
import speedLimitTable from '../constants/speedLimits.json'; import speedLimitTable from "../constants/speedLimits.json";
export type LocoType = keyof typeof speedLimitTable; export type LocoType = keyof typeof speedLimitTable;
export const calculateSpeedLimit = (locoType: LocoType, stockMass: number, isTrainPassenger: boolean) => { export const calculateSpeedLimit = (
const speedTable = speedLimitTable[locoType][isTrainPassenger ? 'passenger' : 'cargo']; locoType: LocoType,
stockMass: number,
isTrainPassenger: boolean,
) => {
const speedTable =
speedLimitTable[locoType][isTrainPassenger ? "passenger" : "cargo"];
if (!speedTable) return undefined; if (!speedTable) return undefined;
let speedLimit = 0; let speedLimit = 0;
for (let mass in speedTable) if (stockMass > Number(mass)) speedLimit = (speedTable as any)[mass]; for (const mass in speedTable)
if (stockMass > Number(mass)) speedLimit = (speedTable as any)[mass];
return speedLimit; return speedLimit;
}; };
+23 -26
View File
@@ -1,5 +1,5 @@
import { EVehicleUseType } from '../enums/EVehicleUseType'; import { EVehicleUseType } from '../enums/EVehicleUseType';
import { ICarWagon, ILocomotive, IStore, TStockInfoKey } from '../types'; import { ICarWagon, ILocomotive, IStore, TCarWagonGroup, TLocoGroup } from '../types';
import { LocoType, calculateSpeedLimit } from './speedLimitUtils'; import { LocoType, calculateSpeedLimit } from './speedLimitUtils';
export function isLocomotive(vehicle: ILocomotive | ICarWagon): vehicle is ILocomotive { export function isLocomotive(vehicle: ILocomotive | ICarWagon): vehicle is ILocomotive {
@@ -14,21 +14,23 @@ export function locoDataList(state: IStore) {
return Object.keys(stockData.info).reduce((acc, vehiclePower) => { return Object.keys(stockData.info).reduce((acc, vehiclePower) => {
if (!vehiclePower.startsWith('loco')) return acc; if (!vehiclePower.startsWith('loco')) return acc;
const locoVehiclesData = stockData.info[vehiclePower as 'loco-e' | 'loco-s' | 'loco-ezt' | 'loco-szt']; const locoVehiclesData = stockData.info[vehiclePower as TLocoGroup];
locoVehiclesData.forEach((loco) => { locoVehiclesData.forEach((loco) => {
if (state.showSupporter && !loco[4]) return; if (state.showSupporter && !loco[4]) return;
const [type, constructionType, cabinType, maxSpeed, supportersOnly] = loco; const [type, constructionType, cabinType, maxSpeed, sponsorsTimestamp] = loco;
const locoProps = stockData.props.find((prop) => constructionType == prop.type); const locoProps = stockData.props.find((prop) => constructionType == prop.type);
acc.push({ acc.push({
power: vehiclePower, power: vehiclePower as TLocoGroup,
group: vehiclePower as TLocoGroup,
type, type,
constructionType, constructionType,
cabinType, cabinType,
maxSpeed: Number(maxSpeed), maxSpeed: Number(maxSpeed),
supportersOnly, isSponsorsOnly: Number(sponsorsTimestamp) > Date.now(),
sponsorsOnlyTimestamp: Number(sponsorsTimestamp),
imageSrc: '', imageSrc: '',
length: locoProps?.length && type.startsWith('2EN') ? locoProps.length * 2 : locoProps?.length || 0, length: locoProps?.length && type.startsWith('2EN') ? locoProps.length * 2 : locoProps?.length || 0,
@@ -48,20 +50,24 @@ export function carDataList(state: IStore) {
return Object.keys(stockData.info).reduce((acc, vehicleUseType) => { return Object.keys(stockData.info).reduce((acc, vehicleUseType) => {
if (!vehicleUseType.startsWith('car')) return acc; if (!vehicleUseType.startsWith('car')) return acc;
const carVehiclesData = stockData.info[vehicleUseType as 'car-passenger' | 'car-cargo']; const carVehiclesData = stockData.info[vehicleUseType as TCarWagonGroup];
carVehiclesData.forEach((car) => { carVehiclesData.forEach((car) => {
if (state.showSupporter && !car[3]) return; const [type, constructionType, loadable, sponsorsOnlyTimestamp, maxSpeed] = car;
const carPropsData = stockData.props.find((v) => car[0].toString().startsWith(v.type)); if (state.showSupporter && Number(sponsorsOnlyTimestamp) <= Date.now()) return;
const carPropsData = stockData.props.find((v) => type.toString().startsWith(v.type));
acc.push({ acc.push({
useType: vehicleUseType as 'car-passenger' | 'car-cargo', useType: vehicleUseType as TCarWagonGroup,
type: car[0], group: vehicleUseType as TCarWagonGroup,
constructionType: car[1], type,
loadable: car[2], constructionType,
supportersOnly: car[3], loadable,
maxSpeed: Number(car[4]), isSponsorsOnly: Number(sponsorsOnlyTimestamp) > Date.now(),
sponsorsOnlyTimestamp: Number(sponsorsOnlyTimestamp),
maxSpeed: Number(maxSpeed),
imageSrc: '', imageSrc: '',
cargoList: cargoList:
!carPropsData || carPropsData.cargo === null !carPropsData || carPropsData.cargo === null
@@ -81,10 +87,7 @@ export function carDataList(state: IStore) {
} }
export function totalMass(state: IStore) { export function totalMass(state: IStore) {
return ~~state.stockList.reduce( return ~~state.stockList.reduce((acc, stock) => acc + (stock.cargo ? stock.cargo.totalMass : stock.mass) * stock.count, 0);
(acc, stock) => acc + (stock.cargo ? stock.cargo.totalMass : stock.mass) * stock.count,
0
);
} }
export function totalLength(state: IStore) { export function totalLength(state: IStore) {
@@ -92,10 +95,7 @@ export function totalLength(state: IStore) {
} }
export function maxStockSpeed(state: IStore) { export function maxStockSpeed(state: IStore) {
const stockSpeedLimit = state.stockList.reduce( const stockSpeedLimit = state.stockList.reduce((acc, stock) => (stock.maxSpeed < acc || acc == 0 ? stock.maxSpeed : acc), 0);
(acc, stock) => (stock.maxSpeed < acc || acc == 0 ? stock.maxSpeed : acc),
0
);
const headingLoco = state.stockList[0]?.isLoco ? state.stockList[0] : undefined; const headingLoco = state.stockList[0]?.isLoco ? state.stockList[0] : undefined;
if (!headingLoco) return stockSpeedLimit; if (!headingLoco) return stockSpeedLimit;
@@ -139,9 +139,7 @@ export function isTrainPassenger(state: IStore) {
if (state.stockList.length == 0) return false; if (state.stockList.length == 0) return false;
if (state.stockList.every((stock) => stock.isLoco)) return false; if (state.stockList.every((stock) => stock.isLoco)) return false;
return state.stockList return state.stockList.filter((stock) => !stock.isLoco).every((stock) => stock.useType === EVehicleUseType.CAR_PASSENGER);
.filter((stock) => !stock.isLoco)
.every((stock) => stock.useType === EVehicleUseType.CAR_PASSENGER);
} }
export function chosenRealStock(state: IStore) { export function chosenRealStock(state: IStore) {
@@ -158,4 +156,3 @@ export function chosenRealStock(state: IStore) {
return realStockObj; return realStockObj;
} }
+35 -35
View File
@@ -1,35 +1,35 @@
<template> <template>
<div class="app-container"> <div class="app-container">
<MainContainer /> <MainContainer />
<Footer /> <FooterVue />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from "vue";
import { useStore } from '../store'; import { useStore } from "../store";
import MainContainer from '../components/app/MainContainer.vue'; import MainContainer from "../components/app/MainContainer.vue";
import Footer from '../components/app/Footer.vue'; import FooterVue from "../components/app/Footer.vue";
export default defineComponent({ export default defineComponent({
components: { components: {
MainContainer, MainContainer,
Footer, FooterVue,
}, },
data: () => ({ data: () => ({
store: useStore(), store: useStore(),
}), }),
}); });
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.app-container { .app-container {
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
} }
</style> </style>
+4 -4
View File
@@ -1,7 +1,7 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
declare module '*.vue' { declare module "*.vue" {
import type { DefineComponent } from 'vue' import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any> const component: DefineComponent<{}, {}, any>;
export default component export default component;
} }
+23 -5
View File
@@ -2,19 +2,26 @@ import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa'; import { VitePWA } from 'vite-plugin-pwa';
import { resolve } from 'path';
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
server: { server: {
port: 2137, port: 2137,
}, },
resolve: {
alias: {
$fonts: resolve('/fonts'),
$images: resolve('/images'),
},
},
plugins: [ plugins: [
vue(), vue(),
VitePWA({ VitePWA({
registerType: 'autoUpdate', registerType: 'autoUpdate',
workbox: { workbox: {
// globPatterns: ['**/*.{js,css,html,png,svg,img}'], globPatterns: ['**/*.{js,css,html,png,svg,img,woff,woff2}'],
runtimeCaching: [ runtimeCaching: [
{ {
@@ -24,10 +31,23 @@ export default defineConfig({
cacheName: 'swdr-images-cache', cacheName: 'swdr-images-cache',
expiration: { expiration: {
maxEntries: 50, maxEntries: 50,
maxAgeSeconds: 60 * 60 * 24 * 7, // <== 7 days maxAgeSeconds: 60 * 60 * 24, // <== 1 day
}, },
cacheableResponse: { cacheableResponse: {
statuses: [404], statuses: [0, 200, 404],
},
},
},
{
urlPattern: /^https:\/\/spythere.github.io\/api\/td2\/.*/i,
handler: 'CacheFirst',
options: {
cacheName: 'spythere-api-cache',
expiration: {
maxAgeSeconds: 60 * 60 * 24, // <== 1 day
},
cacheableResponse: {
statuses: [0, 200],
}, },
}, },
}, },
@@ -36,5 +56,3 @@ export default defineConfig({
}), }),
], ],
}); });
-2958
View File
File diff suppressed because it is too large Load Diff