Compare commits

...

8 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
55 changed files with 8932 additions and 4838 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",
},
};
+3
View File
@@ -26,3 +26,6 @@ node_modules
# Dev files
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="manifest" href="/site.webmanifest" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#e4c428" />
</head>
+7141 -218
View File
File diff suppressed because it is too large Load Diff
+12 -2
View File
@@ -1,20 +1,30 @@
{
"name": "pojazdownik",
"version": "1.6.0",
"version": "1.7.0",
"private": true,
"scripts": {
"dev": "vite",
"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": {
"axios": "^1.4.0",
"pinia": "^2.0.17",
"prettier": "^3.0.3",
"vue": "^3.2.37",
"vue-i18n": "9"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.3.3",
"@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",
"typescript": "^5.0.2",
"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.
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

-12
View File
@@ -18,11 +18,6 @@ export default defineComponent({
};
},
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.handleRouting();
},
@@ -59,11 +54,4 @@ h2 {
color: #d1d1d1;
}
@media screen and (max-width: $breakpointMd) {
#app {
font-size: calc(0.7rem + 0.75vw);
}
}
</style>
+3 -3
View File
@@ -7,9 +7,9 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store';
import RealStockCard from '../cards/RealStockCard.vue';
import { defineComponent } from "vue";
import { useStore } from "../../store";
import RealStockCard from "../cards/RealStockCard.vue";
export default defineComponent({
components: { RealStockCard },
+11 -8
View File
@@ -3,32 +3,35 @@
<i18n-t keypath="footer.disclaimer" tag="div" class="text--grayed">
<template #tos>
<a style="color: #ccc" :href="$t('footer.tos-href')" target="_blank">
{{ $t('footer.tos') }}
{{ $t("footer.tos") }}
</a>
</template>
</i18n-t>
<div class="text--grayed" v-if="store.stockData">
{{ $t('footer.version-check', { version: store.stockData.version }) }}
{{ $t("footer.version-check", { version: store.stockData.version }) }}
</div>
<div>
&copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} | v{{ VERSION }}{{ !isOnProductionHost ? 'dev' : '' }}
<a href="https://td2.info.pl/profile/?u=20777" target="_blank"
>Spythere</a
>
{{ new Date().getUTCFullYear() }} | v{{ VERSION
}}{{ !isOnProductionHost ? "dev" : "" }}
</div>
</footer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import packageInfo from '../../../package.json';
import { useStore } from '../../store';
import { defineComponent } from "vue";
import packageInfo from "../../../package.json";
import { useStore } from "../../store";
export default defineComponent({
data() {
return {
isOnProductionHost: location.hostname == 'pojazdownik-td2.web.app',
isOnProductionHost: location.hostname == "pojazdownik-td2.web.app",
VERSION: packageInfo.version,
store: useStore(),
};
+7 -7
View File
@@ -8,11 +8,11 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import LogoSection from '../sections/LogoSection.vue';
import InputsSection from '../sections/InputsSection.vue';
import TrainImageSection from '../sections/TrainImageSection.vue';
import StockSection from '../sections/StockSection.vue';
import { defineComponent } from "vue";
import LogoSection from "../sections/LogoSection.vue";
import InputsSection from "../sections/InputsSection.vue";
import TrainImageSection from "../sections/TrainImageSection.vue";
import StockSection from "../sections/StockSection.vue";
export default defineComponent({
components: { LogoSection, InputsSection, TrainImageSection, StockSection },
@@ -20,14 +20,14 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '../../styles/global.scss';
@import "../../styles/global.scss";
main {
display: grid;
gap: 1em;
width: 100%;
max-width: 1500px;
max-width: 1300px;
min-height: 75vh;
grid-template-columns: 1fr 2fr;
+90 -46
View File
@@ -1,14 +1,25 @@
<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="card_content">
<div class="card_nav">
<div class="top-pane">
<h1>
{{ $t('realstock.title') }} <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>
<button class="btn exit-btn" @click="store.isRealStockListCardOpen = false">&Cross;</button>
<button
class="btn exit-btn"
@click="store.isRealStockListCardOpen = false"
>
&Cross;
</button>
</div>
<div class="filters" ref="focus" tabindex="0">
@@ -19,7 +30,11 @@
/>
<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 }}
</option>
</datalist>
@@ -31,12 +46,18 @@
/>
<datalist id="readyStockStringList">
<option v-for="stock in computedAvailableStockTypes" :value="stock">
{{ stock }}
<option
v-for="stockType in computedAvailableStockTypes"
:value="stockType"
:key="stockType"
>
{{ stockType }}
</option>
</datalist>
<button class="btn" @click="resetStockFilters">{{ $t('realstock.action-reset') }}</button>
<button class="btn" @click="resetStockFilters">
{{ $t("realstock.action-reset") }}
</button>
</div>
</div>
@@ -46,14 +67,29 @@
:key="rStock.stockId"
:data-last-selected="store.chosenRealStockName === rStock.stockId"
>
<div class="stock-title" 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
class="stock-title"
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>
<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>{{ stockType }}</div>
<img
@@ -61,7 +97,7 @@
:title="stockType"
style="opacity: 0"
@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>
@@ -75,23 +111,24 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent } from "vue";
import { useStore } from '../../store';
import imageMixin from '../../mixins/imageMixin';
import stockMixin from '../../mixins/stockMixin';
import { useStore } from "../../store";
import imageMixin from "../../mixins/imageMixin";
import stockMixin from "../../mixins/stockMixin";
import { IReadyStockItem } from '../../types';
import { IReadyStockItem } from "../../types";
import http from "../../http";
interface ResponseJSONData {
[key: string]: string;
}
function getVehicleType(stockType: string) {
if (/^E/.test(stockType)) return 'loco-e';
if (/^S/.test(stockType)) return 'loco-s';
if (/^E/.test(stockType)) return "loco-e";
if (/^S/.test(stockType)) return "loco-s";
return 'car-passenger';
return "car-passenger";
}
export default defineComponent({
@@ -99,11 +136,15 @@ export default defineComponent({
data: () => ({
store: useStore(),
responseStatus: 'loading',
isMobile: 'ontouchstart' in document.documentElement && navigator.userAgent.match(/Mobi/) ? true : false,
responseStatus: "loading",
isMobile:
"ontouchstart" in document.documentElement &&
navigator.userAgent.match(/Mobi/)
? true
: false,
observer: null as IntersectionObserver | null,
searchedReadyStockName: '',
searchedReadyStockString: '',
searchedReadyStockName: "",
searchedReadyStockString: "",
visibleIndexesTo: 0,
lastSelectedStockId: null as string | null,
scrollTop: 0,
@@ -115,11 +156,11 @@ export default defineComponent({
},
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,
behavior: 'auto',
behavior: "auto",
});
},
@@ -130,8 +171,12 @@ export default defineComponent({
return this.store.readyStockList
.filter(
(rs) =>
rs.stockId.toLocaleLowerCase().includes(this.searchedReadyStockName.toLocaleLowerCase()) &&
rs.stockString.toLocaleLowerCase().includes(this.searchedReadyStockString.toLocaleLowerCase())
rs.stockId
.toLocaleLowerCase()
.includes(this.searchedReadyStockName.toLocaleLowerCase()) &&
rs.stockString
.toLocaleLowerCase()
.includes(this.searchedReadyStockString.toLocaleLowerCase()),
)
.filter((_, i) => i <= this.visibleIndexesTo);
},
@@ -139,7 +184,7 @@ export default defineComponent({
computedAvailableStockTypes() {
return this.store.readyStockList
.reduce((acc, rs) => {
rs.stockString.split(';').forEach((s) => {
rs.stockString.split(";").forEach((s) => {
if (!acc.includes(s)) acc.push(s);
});
@@ -153,7 +198,7 @@ export default defineComponent({
computedReadyStockList(curr, prev) {
if (curr.length < prev.length) {
this.visibleIndexesTo = 20;
(this.$refs['list'] as HTMLElement).scrollTo({
(this.$refs["list"] as HTMLElement).scrollTo({
top: 0,
});
}
@@ -162,21 +207,21 @@ export default defineComponent({
methods: {
async fetchStockListData() {
const readyStockJSONData: ResponseJSONData = await (
await fetch(`https://spythere.github.io/api/td2/data/readyStock.json?t=${Math.floor(Date.now() / 60000)}`)
).json();
const readyStockJSONData = (
await http.get<ResponseJSONData>("td2/data/readyStock.json")
).data;
if (!readyStockJSONData) {
this.responseStatus = 'error';
this.responseStatus = "error";
return;
}
for (let stockKey in readyStockJSONData) {
const [type, number, ...name] = stockKey.split(' ');
const [type, number, ...name] = stockKey.split(" ");
const obj = {
number: number.replace(/_/g, '/'),
name: name.join(' '),
number: number.replace(/_/g, "/"),
name: name.join(" "),
stockString: readyStockJSONData[stockKey],
type,
};
@@ -187,7 +232,7 @@ export default defineComponent({
});
}
this.responseStatus = 'loaded';
this.responseStatus = "loaded";
},
mountObserver() {
@@ -195,7 +240,7 @@ export default defineComponent({
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) {
@@ -203,8 +248,8 @@ export default defineComponent({
},
resetStockFilters() {
this.searchedReadyStockName = '';
this.searchedReadyStockString = '';
this.searchedReadyStockName = "";
this.searchedReadyStockString = "";
},
chooseStock(stockItem: IReadyStockItem) {
@@ -216,7 +261,7 @@ export default defineComponent({
onStockItemError(e: Event, stockType: string) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = `images/${getVehicleType(stockType)}-unknown.png`;
imageEl.style.opacity = '1';
imageEl.style.opacity = "1";
},
onListScroll(e: Event) {
@@ -230,7 +275,7 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '../../styles/global.scss';
@import "../../styles/global.scss";
.exit-btn {
font-size: 1.2em;
@@ -316,7 +361,7 @@ ul {
gap: 1rem;
padding: 0.1em;
&[data-last-selected='true'] .stock-title {
&[data-last-selected="true"] .stock-title {
border: 1px solid $accentColor;
}
@@ -378,4 +423,3 @@ ul {
padding: 1em;
}
</style>
+57 -22
View File
@@ -7,6 +7,7 @@
<div class="vehicle-types locos">
<button
v-for="locoType in locomotiveTypeList"
:key="locoType.id"
class="btn btn--choice"
:data-selected="locoType.id == store.chosenLocoPower"
@click="selectLocoType(locoType.id)"
@@ -23,10 +24,10 @@
@keydown.enter.prevent="addOrSwitchVehicle"
@keydown.backspace="removeVehicle"
>
<option :value="null" disabled>{{ $t('inputs.input-vehicle') }}</option>
<option v-for="loco in locoOptions" :value="loco" :key="loco.type">
{{ loco.type }}<b v-if="loco.supportersOnly">*</b>
<option :value="null" disabled>
{{ $t('inputs.input-vehicle') }}
</option>
<option v-for="loco in locoOptions" :value="loco" :key="loco.type">{{ loco.type }}<b v-if="loco.isSponsorsOnly">*</b></option>
</select>
</div>
@@ -34,6 +35,7 @@
<div class="vehicle-types carwagons">
<button
v-for="carType in carTypeList"
:key="carType.id"
class="btn btn--choice"
:data-selected="carType.id == store.chosenCarUseType"
@click="selectCarWagonType(carType.id)"
@@ -50,11 +52,11 @@
@keydown.enter.prevent="addOrSwitchVehicle"
@keydown.backspace="removeVehicle"
>
<option :value="null" disabled>{{ $t('inputs.input-carwagon') }}</option>
<option v-for="car in carOptions" :value="car" :key="car.type">
{{ car.type }}<b v-if="car.supportersOnly">*</b>
<option :value="null" disabled>
{{ $t('inputs.input-carwagon') }}
</option>
<option v-for="car in carOptions" :value="car" :key="car.type">{{ car.type }}<b v-if="car.isSponsorsOnly">*</b></option>
</select>
</div>
@@ -63,9 +65,7 @@
<select
id="cargo-select"
:disabled="
(store.chosenCar && !store.chosenCar.loadable) ||
(store.chosenCar && store.chosenCar.useType == 'car-passenger') ||
!store.chosenCar
(store.chosenCar && !store.chosenCar.loadable) || (store.chosenCar && store.chosenCar.useType == 'car-passenger') || !store.chosenCar
"
data-select="cargo"
data-ignore-outside="1"
@@ -90,12 +90,7 @@
<button class="btn" @click="addVehicle(store.chosenVehicle, store.chosenCargo)">
{{ $t('inputs.action-add') }}
</button>
<button
class="btn"
@click="switchVehicles"
:disabled="store.chosenStockListIndex == -1"
:data-disabled="store.chosenStockListIndex == -1"
>
<button class="btn" @click="switchVehicles" :disabled="store.chosenStockListIndex == -1" :data-disabled="store.chosenStockListIndex == -1">
{{ $t('inputs.action-swap') }}
<b class="text--accent">
{{ store.chosenStockListIndex == -1 ? '' : `${store.chosenStockListIndex + 1}.` }}
@@ -122,6 +117,7 @@ export default defineComponent({
mixins: [imageMixin, stockPreviewMixin, stockMixin],
data: () => ({
store: useStore(),
locomotiveTypeList: [
{
id: 'loco-e',
@@ -153,12 +149,20 @@ export default defineComponent({
],
}),
setup() {
const store = useStore();
computed: {
locoOptions() {
return this.store.locoDataList
.slice()
.sort((a, b) => (a.type > b.type ? 1 : -1))
.filter((loco) => loco.power == this.store.chosenLocoPower);
},
return {
store,
};
carOptions() {
return this.store.carDataList
.slice()
.sort((a, b) => (a.type > b.type ? 1 : -1))
.filter((car) => car.useType == this.store.chosenCarUseType);
},
},
methods: {
@@ -192,6 +196,29 @@ export default defineComponent({
const stockObject = this.getStockObject(vehicle, this.store.chosenCargo);
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>
@@ -207,6 +234,11 @@ export default defineComponent({
grid-column: 1;
}
.input_container {
width: 100%;
max-width: 380px;
}
.input_header {
margin-bottom: 1em;
}
@@ -226,6 +258,10 @@ button.btn--choice {
.input_list {
margin: 0.5em 0;
select {
width: 100%;
}
label {
display: block;
@@ -267,4 +303,3 @@ button.btn--choice {
}
}
</style>
+15 -11
View File
@@ -1,11 +1,16 @@
<template>
<section class="logo-section">
<img :src="`/logo-${$i18n.locale}.svg`" alt="logo pojazdownik" @click="navigate" />
<img
:src="`/logo-${$i18n.locale}.svg`"
alt="logo pojazdownik"
@click="navigate"
/>
<div class="actions">
<button
class="btn btn--text"
v-for="action in localeActions"
:key="action.name"
class="btn btn--text"
:data-selected="$i18n.locale == action.locale"
@click="chooseLocale(action.locale)"
>
@@ -21,31 +26,31 @@ export default {
return {
localeActions: [
{
name: 'POLSKI',
locale: 'pl',
name: "POLSKI",
locale: "pl",
},
{
name: 'ENGLISH',
locale: 'en',
name: "ENGLISH",
locale: "en",
},
],
};
},
methods: {
navigate() {
window.location.pathname = '';
window.location.pathname = "";
},
chooseLocale(locale: string) {
this.$i18n.locale = locale;
window.localStorage.setItem('locale', locale);
window.localStorage.setItem("locale", locale);
},
},
};
</script>
<style lang="scss" scoped>
@import '../../styles/global.scss';
@import "../../styles/global.scss";
.logo-section {
grid-row: 1;
@@ -64,7 +69,7 @@ export default {
display: flex;
gap: 0.5em;
button[data-selected='true'] {
button[data-selected="true"] {
font-weight: bold;
color: $accentColor;
text-decoration: underline;
@@ -76,4 +81,3 @@ img {
width: 100%;
}
</style>
+26 -18
View File
@@ -2,9 +2,10 @@
<section class="stock-section">
<div class="section_modes">
<button
v-for="(id, i) in sectionModes"
:key="id"
class="btn"
ref="sectionButtonRefs"
v-for="(id, i) in sectionModes"
@click="chooseSection(id)"
:data-selected="store.stockSectionMode == id"
>
@@ -15,29 +16,37 @@
<transition name="tab-change" mode="out-in">
<keep-alive>
<component :is="chosenSectionComponent" :key="chosenSectionComponent"></component>
<component
:is="chosenSectionComponent"
:key="chosenSectionComponent"
></component>
</keep-alive>
</transition>
</section>
</template>
<script lang="ts" setup>
import { computed, KeepAlive, onMounted, ref } from 'vue';
import { useStore } from '../../store';
import StockListTab from '../tabs/StockListTab.vue';
import StockGeneratorTab from '../tabs/StockGeneratorTab.vue';
import NumberGeneratorTab from '../tabs/NumberGeneratorTab.vue';
import WikiListTab from '../tabs/WikiListTab.vue';
import { computed, onMounted, ref } from "vue";
import { useStore } from "../../store";
import StockListTab from "../tabs/StockListTab.vue";
import StockGeneratorTab from "../tabs/StockGeneratorTab.vue";
import NumberGeneratorTab from "../tabs/NumberGeneratorTab.vue";
import WikiListTab from "../tabs/WikiListTab.vue";
const sectionButtonRefs = ref([]);
const store = useStore();
type SectionMode = typeof store.stockSectionMode;
const sectionModes: SectionMode[] = ['stock-list', 'wiki-list', 'number-generator', 'stock-generator'];
const sectionModes: SectionMode[] = [
"stock-list",
"wiki-list",
"number-generator",
"stock-generator",
];
onMounted(() => {
window.addEventListener('keydown', (e) => {
window.addEventListener("keydown", (e) => {
if (e.target instanceof HTMLInputElement) return;
if (/[1234]/.test(e.key)) {
@@ -50,16 +59,16 @@ onMounted(() => {
const chosenSectionComponent = computed(() => {
switch (store.stockSectionMode) {
case 'stock-list':
case "stock-list":
return StockListTab;
case 'wiki-list':
case "wiki-list":
return WikiListTab;
case 'stock-generator':
case "stock-generator":
return StockGeneratorTab;
case 'number-generator':
case "number-generator":
return NumberGeneratorTab;
default:
@@ -73,7 +82,7 @@ function chooseSection(sectionId: SectionMode) {
</script>
<style lang="scss">
@import '../../styles/global.scss';
@import "../../styles/global.scss";
// Tab change animation
.tab-change {
@@ -115,14 +124,14 @@ function chooseSection(sectionId: SectionMode) {
left: 50%;
transform: translateX(-50%);
content: '';
content: "";
width: 0;
height: 2px;
transition: all 100ms;
background-color: $accentColor;
}
&[data-selected='true']::after {
&[data-selected="true"]::after {
width: 100%;
}
}
@@ -134,4 +143,3 @@ function chooseSection(sectionId: SectionMode) {
}
}
</style>
+76 -90
View File
@@ -1,58 +1,47 @@
<template>
<section class="train-image-section">
<div class="train-image__wrapper">
<div class="train-image__content" :class="{ supporter: store.chosenVehicle?.supportersOnly }">
<transition name="img-message-anim">
<div class="empty-message" v-if="store.imageLoading && store.chosenVehicle?.imageSrc">
{{ $t('preview.loading') }}
</div>
</transition>
<div class="train-image__content" :class="{ sponsor: store.chosenVehicle?.isSponsorsOnly }">
<img
v-if="store.chosenVehicle"
tabindex="0"
:src="getThumbnailURL(store.chosenVehicle.type, 'small')"
@click="onImageClick"
@keydown.enter="onImageClick"
@error="onImageError"
type="image/jpeg"
/>
<div class="no-img" v-if="!store.chosenVehicle">{{ $t('preview.title') }}</div>
<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">
{{
$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' // ? store.stockData?.usage[store.chosenVehicle.constructionType]
? $t(`usage.${store.chosenVehicle.constructionType}`)
: `${$t('preview.construction')} ${store.chosenVehicle.constructionType}`
}}
</div>
<b style="color: salmon" v-if="store.chosenVehicle.supportersOnly">{{ $t('preview.sponsor-only') }}</b>
</div>
</div>
<div class="train-image__info" v-else>{{ $t('preview.desc') }}</div>
<img v-else src="/images/placeholder.jpg" alt="placeholder" />
</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>
</template>
@@ -61,8 +50,17 @@ import { computed, defineComponent } from 'vue';
import { useStore } from '../../store';
import { isLocomotive } from '../../utils/vehicleUtils';
import { ILocomotive, Vehicle } from '../../types';
import imageMixin from '../../mixins/imageMixin';
export default defineComponent({
mixins: [imageMixin],
data() {
return {
noImageAvailable: false,
};
},
setup() {
const store = useStore();
@@ -85,16 +83,26 @@ export default defineComponent({
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 {
return isLocomotive(vehicle);
},
onImageClick() {
onImageClick(e: Event) {
const target = e.target as HTMLElement;
const chosenVehicle = this.store.chosenVehicle;
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');
},
},
});
@@ -104,66 +112,45 @@ export default defineComponent({
@import '../../styles/global.scss';
.train-image-section {
display: flex;
flex-direction: column;
text-align: center;
grid-row: 3;
grid-column: 1;
margin-top: 2em;
margin-top: 1em;
height: 22em;
}
.train-image {
&__wrapper {
text-align: center;
}
&__content {
border: 1px solid white;
position: relative;
overflow: hidden;
max-width: 22em;
height: 13em;
margin: 0 auto;
&.supporter {
&.sponsor img {
border: 1px solid salmon;
}
img {
max-width: 380px;
width: 100%;
height: 100%;
border: 1px solid white;
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 {
margin: 1em 0;
font-size: 1.1em;
padding: 0 1em;
padding: 0.5em;
margin: 0.5em auto;
line-height: 1.35;
b {
font-size: 1.1em;
}
width: 100%;
max-width: 380px;
div {
margin: 0.25em 0;
}
background-color: $secondaryColor;
font-weight: bold;
}
// Transition animations
@@ -185,4 +172,3 @@ export default defineComponent({
}
}
</style>
+68 -32
View File
@@ -1,34 +1,55 @@
<template>
<div class="number-generator tab">
<div class="tab_header">
<h2>{{ $t('numgen.title') }}</h2>
<h2>{{ $t("numgen.title") }}</h2>
</div>
<div class="tab_content">
<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" :value="category">
<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()">
<option :value="null" disabled>{{ $t('numgen.start-region') }}</option>
<option v-for="(_, name) in genData.regionNumbers" :value="name">{{ name }}</option>
<option :value="null" disabled>
{{ $t("numgen.start-region") }}
</option>
<option
v-for="(_, name) in genData.regionNumbers"
:key="name"
:value="name"
>
{{ name }}
</option>
</select>
<select v-model="endRegionName" @change="randomizeTrainNumber()">
<option :value="null" disabled>{{ $t('numgen.end-region') }}</option>
<option v-for="(_, name) in genData.regionNumbers" :value="name">{{ name }}</option>
<option :value="null" disabled>{{ $t("numgen.end-region") }}</option>
<option
v-for="(_, name) in genData.regionNumbers"
:key="name"
:value="name"
>
{{ name }}
</option>
</select>
</div>
<div class="generated-number" @click="copyNumber">
<span v-if="trainNumber">
{{ $t('numgen.number-info') }} <b class="text--accent">{{ trainNumber }}</b>
{{ $t("numgen.number-info") }}
<b class="text--accent">{{ trainNumber }}</b>
</span>
<span v-else>{{ $t('numgen.warning') }}</span>
<span v-else>{{ $t("numgen.warning") }}</span>
</div>
<!-- <div v-if="chosenCategory">
@@ -50,25 +71,29 @@
<div class="tab_links">
<a :href="$t('numgen.td2-wiki-link')" target="_blank">
{{ $t('numgen.td2-wiki') }}
{{ $t("numgen.td2-wiki") }}
</a>
</div>
<hr />
<div class="tab_actions">
<button class="btn" @click="randomizeTrainNumber(true)">{{ $t('numgen.action-random-region') }}</button>
<button class="btn" @click="randomizeTrainNumber(false)">{{ $t('numgen.action-random-number') }}</button>
<button class="btn" @click="randomizeTrainNumber(true)">
{{ $t("numgen.action-random-region") }}
</button>
<button class="btn" @click="randomizeTrainNumber(false)">
{{ $t("numgen.action-random-number") }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n';
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;
@@ -83,7 +108,7 @@ const trainNumber = ref(null) as Ref<string | null>;
const copyNumber = () => {
if (trainNumber.value) {
navigator.clipboard.writeText(trainNumber.value);
alert(i18n.t('numgen.alert'));
alert(i18n.t("numgen.alert"));
}
};
@@ -93,16 +118,21 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
const regionKeys = Object.keys(genData.regionNumbers);
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)
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) {
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();
} else {
const beginRegionNumber = genData.regionNumbers[beginRegionName.value!];
@@ -117,23 +147,30 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
return;
}
if (chosenCategory.value == null) chosenCategory.value = 'EI';
if (chosenCategory.value == null) chosenCategory.value = "EI";
const rulesArray = genData.categories[chosenCategory.value].split(';').map((r) => ({
index: r.split(':')[0],
rule: r.split(':')[1],
nums: Number(r.split(':')[2] || '1'),
}));
const rulesArray = genData.categories[chosenCategory.value]
.split(";")
.map((r) => ({
index: r.split(":")[0],
rule: r.split(":")[1],
nums: Number(r.split(":")[2] || "1"),
}));
rulesArray.forEach((r) => {
const range = r.rule.split('-');
const range = r.rule.split("-");
if (range.length == 1) number += r.rule;
else {
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;
}
});
@@ -142,8 +179,8 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
</script>
<style lang="scss" scoped>
@import '../../styles/tab.scss';
@import '../../styles/global.scss';
@import "../../styles/tab.scss";
@import "../../styles/global.scss";
.options {
display: grid;
@@ -189,4 +226,3 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
}
}
</style>
+36 -35
View File
@@ -37,9 +37,10 @@
<div class="generator_cargo">
<button
v-for="(cargoArray, cargoName) in store.stockData?.generator.cargo"
:key="cargoName"
class="btn"
:data-chosen="chosenCargoTypes.includes(cargoName.toString())"
v-for="(cargoArray, cargoName) in store.stockData?.generator.cargo"
@click="toggleCargoChosen(cargoName.toString(), cargoArray)"
>
{{ $t(`cargo.${cargoName}`) }}
@@ -126,7 +127,7 @@ export default defineComponent({
computed: {
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) {
const generatedChosenStockList = this.chosenCargoTypes.reduce((acc, type) => {
this.store.stockData?.generator.cargo[type]
.filter((c) => !this.excludedCarTypes.includes(c.split(':')[0]))
.forEach((c) => {
const [type, cargoType] = c.split(':');
const generatedChosenStockList = this.chosenCargoTypes.reduce(
(acc, type) => {
this.store.stockData?.generator.cargo[type]
.filter((c) => !this.excludedCarTypes.includes(c.split(':')[0]))
.forEach((c) => {
const [type, cargoType] = c.split(':');
const carWagonObjs = this.store.carDataList.filter((cw) => cw.type.startsWith(type));
const cargoObjs = [] as (ICargo | undefined)[];
const carWagonObjs = this.store.carDataList.filter((cw) => cw.type.startsWith(type));
const cargoObjs = [] as (ICargo | undefined)[];
if (!cargoType || empty) cargoObjs.push(undefined);
else if (cargoType == 'all') cargoObjs.push(...carWagonObjs[0]?.cargoList);
else cargoObjs.push(carWagonObjs[0]?.cargoList.find((cargo) => cargo.id == cargoType));
if (!cargoType || empty) cargoObjs.push(undefined);
else if (cargoType == 'all') cargoObjs.push(...carWagonObjs[0]!.cargoList);
else cargoObjs.push(carWagonObjs[0]?.cargoList.find((cargo) => cargo.id == cargoType));
carWagonObjs.forEach((cw) => {
cargoObjs.forEach((cargoObj) => {
const chosenStock = acc.find((a) => a.constructionType.includes(cw.constructionType));
carWagonObjs.forEach((cw) => {
cargoObjs.forEach((cargoObj) => {
const chosenStock = acc.find((a) => a.constructionType.includes(cw.constructionType));
if (!chosenStock)
acc.push({
constructionType: cw.constructionType,
carPool: [{ carWagon: cw, cargo: cargoObj }],
});
else chosenStock.carPool.push({ carWagon: cw, cargo: cargoObj });
if (!chosenStock)
acc.push({
constructionType: cw.constructionType,
carPool: [{ carWagon: cw, cargo: cargoObj }],
});
else chosenStock.carPool.push({ carWagon: cw, cargo: cargoObj });
});
});
});
});
return acc;
}, [] as { constructionType: string; carPool: { carWagon: ICarWagon; cargo?: ICargo }[] }[]);
return acc;
},
[] 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++) {
const headingLoco = this.store.stockList[0]?.isLoco ? this.store.stockList[0] : undefined;
this.store.stockList.length = headingLoco ? 1 : 0;
const maxMass =
this.store.acceptableMass > 0 ? Math.min(this.store.acceptableMass, this.maxMass) : this.maxMass;
const maxMass = this.store.acceptableMass > 0 ? Math.min(this.store.acceptableMass, this.maxMass) : this.maxMass;
let exceeded = false;
@@ -277,13 +286,6 @@ export default defineComponent({
background-color: $secondaryColor;
&[data-chosen='true'] {
background-color: $accentColor;
color: black;
box-shadow: 0 0 5px 1px $accentColor;
}
&[data-excluded='true'] {
background-color: gray;
box-shadow: none;
@@ -316,4 +318,3 @@ export default defineComponent({
color: black;
}
</style>
+40 -66
View File
@@ -5,14 +5,11 @@
</div>
<div class="stock_actions">
<label class="file-label">
<div class="btn btn--image">
<img src="/images/icon-upload.svg" alt="" />
{{ $t('stocklist.action-upload') }}
</div>
<button class="btn btn--image" @click="clickFileInput">
<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>
<button class="btn btn--image" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="downloadStock">
<img src="/images/icon-download.svg" alt="download icon" />
@@ -37,36 +34,26 @@
<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;
{{ $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)"
>
<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)"
>
<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)"
>
<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>
@@ -82,22 +69,19 @@
</b>
<span>
{{ $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
{{ $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
>) - {{ $t('stocklist.length') }}:
<span class="text--accent">{{ store.totalLength }}m</span>
- {{ $t('stocklist.vmax') }}: <span class="text--accent">{{ store.maxStockSpeed }} km/h</span>
- {{ $t('stocklist.vmax') }}:
<span class="text--accent">{{ store.maxStockSpeed }} km/h</span>
</span>
</div>
<div class="stock_cold-start">
<label>
<input
type="checkbox"
v-model="store.isColdStart"
:disabled="!locoSupportsColdStart(store.stockList[0]?.constructionType || '')"
/>
<input type="checkbox" v-model="store.isColdStart" :disabled="!locoSupportsColdStart(store.stockList[0]?.constructionType || '')" />
{{ $t('stocklist.coldstart-info') }}
</label>
</div>
@@ -105,22 +89,15 @@
<div class="stock_warnings" v-if="stockHasWarnings">
<div class="warning" v-if="locoNotSuitable">(!) {{ $t('stocklist.warning-not-suitable') }}</div>
<div class="warning" v-if="trainTooLong && store.isTrainPassenger">
(!) {{ $t('stocklist.warning-passenger-too-long') }}
</div>
<div class="warning" v-if="trainTooLong && store.isTrainPassenger">(!) {{ $t('stocklist.warning-passenger-too-long') }}</div>
<div class="warning" v-if="trainTooLong && !store.isTrainPassenger">
(!) {{ $t('stocklist.warning-freight-too-long') }}
</div>
<div class="warning" v-if="trainTooLong && !store.isTrainPassenger">(!) {{ $t('stocklist.warning-freight-too-long') }}</div>
<div class="warning" v-if="trainTooHeavy">
(!)
<i18n-t keypath="stocklist.warning-too-heavy">
<template #href>
<a
target="_blank"
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') }}
</a>
</template>
@@ -153,23 +130,19 @@
@keydown.backspace="removeStock(i)"
ref="itemRefs"
>
<div
class="stock-info"
@dragstart="onDragStart(i)"
@drop="onDrop($event, i)"
@dragover="allowDrop"
draggable="true"
>
<div 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 v-if="i == store.chosenStockListIndex">&bull;&nbsp;</span>
{{ i + 1 }}.
</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) }}
</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__mass">{{ stock.cargo ? stock.cargo.totalMass : stock.mass }}t </span>
<span class="stock-info__speed"> {{ stock.maxSpeed }}km/h </span>
@@ -182,7 +155,6 @@
<script lang="ts">
import { defineComponent } from 'vue';
import TrainImage from '../sections/TrainImageSection.vue';
import { useStore } from '../../store';
@@ -195,7 +167,7 @@ import stockMixin from '../../mixins/stockMixin';
export default defineComponent({
name: 'stock-list',
components: { TrainImage, StockThumbnails },
components: { StockThumbnails },
mixins: [warningsMixin, imageMixin, stockMixin, stockPreviewMixin],
@@ -219,8 +191,7 @@ export default defineComponent({
return this.store.stockList
.map((stock, i) => {
let stockTypeStr = stock.isLoco || !stock.cargo ? stock.type : `${stock.type}:${stock.cargo.id}`;
let coldStart =
i == 0 && this.store.isColdStart && locoSupportsColdStart(stock.constructionType || '') ? ',c' : '';
let coldStart = i == 0 && this.store.isColdStart && locoSupportsColdStart(stock.constructionType || '') ? ',c' : '';
return stockTypeStr + coldStart;
})
@@ -251,11 +222,14 @@ export default defineComponent({
}, 20);
},
clickFileInput() {
(this.$refs['conFile'] as HTMLInputElement).click();
},
onListItemClick(stockID: number) {
const stock = this.store.stockList[stockID];
this.store.chosenStockListIndex =
this.store.chosenStockListIndex == stockID && this.store.chosenVehicle?.type == stock.type ? -1 : stockID;
this.store.chosenStockListIndex = this.store.chosenStockListIndex == stockID && this.store.chosenVehicle?.type == stock.type ? -1 : stockID;
if (this.store.chosenStockListIndex == -1) {
this.store.chosenVehicle = null;
@@ -352,9 +326,9 @@ export default defineComponent({
downloadStock() {
if (this.store.stockList.length == 0) return alert(this.$t('stocklist.alert-empty'));
const defaultName = `${this.store.chosenRealStockName || this.store.stockList[0].type} ${
this.store.totalMass
}t; ${this.store.totalLength}m; vmax ${this.store.maxStockSpeed}`;
const defaultName = `${this.store.chosenRealStockName || this.store.stockList[0].type} ${this.store.totalMass}t; ${
this.store.totalLength
}m; vmax ${this.store.maxStockSpeed}`;
const fileName = prompt(this.$t('stocklist.prompt-file'), defaultName);
@@ -487,12 +461,13 @@ export default defineComponent({
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
label.file-label {
text-align: center;
cursor: pointer;
button {
width: 100%;
input {
display: none;
opacity: 0;
width: 0;
height: 0;
}
}
}
@@ -549,7 +524,7 @@ li > .stock-info {
}
}
.supporter {
.sponsor {
color: salmon;
}
@@ -610,4 +585,3 @@ li > .stock-info {
}
}
</style>
+112 -140
View File
@@ -7,10 +7,10 @@
<div class="tab_content">
<div class="actions-panel">
<div class="actions-panel_vehicles">
<button class="btn btn--choice" @click="changeWikiMode('locomotives')">
<button class="btn" :data-chosen="currentFilterMode == 'tractions'" @click="toggleFilter('tractions')">
{{ $t('wiki.action-vehicles') }}
</button>
<button class="btn btn--choice" @click="changeWikiMode('carWagons')">
<button class="btn" :data-chosen="currentFilterMode == 'carriages'" @click="toggleFilter('carriages')">
{{ $t('wiki.action-carriages') }}
</button>
</div>
@@ -20,70 +20,58 @@
</div>
</div>
<div class="table-wrapper" @scroll="scrollEvent" ref="table-wrapper">
<div class="table-wrapper" ref="table-wrapper">
<table>
<thead>
<tr>
<th v-for="header in wikiMode == 'locomotives' ? locoHeaders : carHeaders" @click="toggleSorter(header)">
<th v-for="header in visibleHeaders" @click="toggleSorter(header)" :key="header.id">
{{ $t(`wiki.header.${header.id}`) }}
<span v-if="currentModeSorter.id == header.id">
{{ currentModeSorter.direction == 1 ? `&uArr;` : `&dArr;` }}
<span v-if="currentSorter.id == header.id">
{{ currentSorter.direction == 1 ? `&uArr;` : `&dArr;` }}
</span>
</th>
</tr>
</thead>
<tbody v-if="wikiMode == 'locomotives'">
<tbody>
<tr
v-for="loco in computedLocoList"
@click="previewLocomotive(loco)"
@keydown.enter="previewLocomotive(loco)"
@dblclick="addLocomotive(loco)"
v-for="{ vehicle, show } in computedTableData"
tabindex="0"
v-show="show"
:key="vehicle.type"
@click="previewVehicle(vehicle)"
@keydown.enter="previewVehicle(vehicle)"
@dblclick="addVehicle(vehicle)"
>
<td>
<td style="width: 120px">
<img
:src="`https://spythere.github.io/api/td2/images/${loco.type}--300px.jpg`"
width="120"
:src="getThumbnailURL(vehicle.type, 'small')"
:alt="`${vehicle.type}`"
loading="lazy"
:alt="`Lokomotywa ${loco.type}`"
@error="(e) => ((e.target as HTMLElement).style.display = 'none')"
/>
</td>
<td>{{ loco.type }}</td>
<td>{{ $t(`wiki.${loco.power}`) }}</td>
<td>{{ loco.constructionType }}</td>
<td>{{ locoSupportsColdStart(loco.constructionType) ? `&check;` : '&cross;' }}</td>
<td>{{ loco.length }}m</td>
<td>{{ loco.mass }}t</td>
<td>{{ loco.maxSpeed }}km/h</td>
</tr>
</tbody>
<td :data-sponsoronly="vehicle.isSponsorsOnly">{{ vehicle.type }}</td>
<tbody v-else>
<tr
v-for="car in computedCarList"
@keydow.enter="previewCarWagon(car)"
@click="previewCarWagon(car)"
@dblclick="addCarWagon(car)"
tabindex="0"
>
<td>
<img
:src="`https://spythere.github.io/api/td2/images/${car.type}--300px.jpg`"
loading="lazy"
:alt="`Lokomotywa ${car.type}`"
/>
<td v-if="isLocomotive(vehicle)">{{ $t(`wiki.${vehicle.power}`) }}</td>
<td v-else>{{ $t(`wiki.${vehicle.useType}`) }}</td>
<td>{{ vehicle.constructionType }}</td>
<td>{{ vehicle.length }}m</td>
<td>{{ vehicle.mass }}t</td>
<td>{{ vehicle.maxSpeed }}km/h</td>
<td v-if="currentFilterMode == 'carriages'">{{ !isLocomotive(vehicle) ? vehicle.cargoList.length : '---' }}</td>
<td v-if="currentFilterMode == 'tractions'">
{{ isLocomotive(vehicle) ? (locoSupportsColdStart(vehicle.constructionType) ? `&check;` : '&cross;') : '---' }}
</td>
<td>{{ car.type }}</td>
<td>{{ car.constructionType }}</td>
<td>{{ car.length }}m</td>
<td>{{ car.mass }}t</td>
<td>{{ car.maxSpeed }}km/h</td>
<td>{{ car.cargoList.length == 0 ? '-' : car.cargoList.length }}</td>
</tr>
</tbody>
<span ref="table-bottom"></span>
</table>
</div>
</div>
@@ -97,152 +85,139 @@ import stockPreviewMixin from '../../mixins/stockPreviewMixin';
import { Vehicle } from '../../types';
import { isLocomotive } from '../../utils/vehicleUtils';
import stockMixin from '../../mixins/stockMixin';
import imageMixin from '../../mixins/imageMixin';
import { locoSupportsColdStart } from '../../utils/locoUtils';
type WikiMode = 'locomotives' | 'carWagons';
type SorterID =
| 'type'
| 'constructionType'
| 'image'
| 'length'
| 'mass'
| 'maxSpeed'
| 'cargoCount'
| 'power'
| 'coldStart';
type SorterID = 'type' | 'constructionType' | 'image' | 'length' | 'mass' | 'maxSpeed' | 'cargoCount' | 'group' | 'coldStart';
interface WikiHeader {
interface IWikiHeader {
id: SorterID;
sortable: boolean;
for: 'all' | 'carriages' | 'tractions';
}
const locoHeaders: WikiHeader[] = [
{ id: 'image', sortable: false },
{ id: 'type', sortable: true },
{ id: 'power', sortable: true },
{ id: 'constructionType', sortable: true },
{ id: 'coldStart', sortable: true },
{ id: 'length', sortable: true },
{ id: 'mass', sortable: true },
{ id: 'maxSpeed', sortable: true },
];
interface IWikiRow {
vehicle: Vehicle;
show: boolean;
}
const carHeaders: WikiHeader[] = [
{ id: 'image', sortable: false },
{ id: 'type', sortable: true },
{ id: 'constructionType', sortable: true },
{ id: 'length', sortable: true },
{ id: 'mass', sortable: true },
{ id: 'maxSpeed', sortable: true },
{ id: 'cargoCount', sortable: true },
const headers: IWikiHeader[] = [
{ id: 'image', sortable: false, for: 'all' },
{ id: 'type', sortable: true, for: 'all' },
{ id: 'group', sortable: true, for: 'all' },
{ id: 'constructionType', sortable: true, for: 'all' },
{ id: 'length', sortable: true, for: 'all' },
{ id: 'mass', sortable: true, for: 'all' },
{ id: 'maxSpeed', sortable: true, for: 'all' },
{ id: 'coldStart', sortable: true, for: 'tractions' },
{ id: 'cargoCount', sortable: true, for: 'carriages' },
];
export default defineComponent({
mixins: [stockPreviewMixin, stockMixin],
mixins: [stockPreviewMixin, stockMixin, imageMixin],
data() {
return {
store: useStore(),
locoHeaders,
carHeaders,
headers,
locosScrollTop: 0,
carsScrollTop: 0,
scrollTop: 0,
wikiMode: 'locomotives' as WikiMode,
searchedVehicleTypeName: '',
currentLocoSorter: {
currentSorter: {
id: 'type' as SorterID,
direction: 1,
},
currentCarSorter: {
id: 'type' as SorterID,
direction: 1,
},
currentFilterMode: 'all' as 'all' | 'tractions' | 'carriages',
};
},
activated() {
const tableWrapperRef = this.$refs['table-wrapper'] as HTMLElement;
tableWrapperRef.scrollTo({ top: this.wikiMode == 'locomotives' ? this.locosScrollTop : this.carsScrollTop });
tableWrapperRef.scrollTo({
top: this.scrollTop,
});
},
methods: {
locoSupportsColdStart,
isLocomotive,
scrollEvent(e: Event) {
const tableScrollTop = (e.target as HTMLElement).scrollTop;
if (this.wikiMode == 'locomotives') this.locosScrollTop = tableScrollTop;
else this.carsScrollTop = tableScrollTop;
toggleFilter(name: typeof this.currentFilterMode) {
this.currentFilterMode = this.currentFilterMode == name ? 'all' : name;
},
changeWikiMode(wikiMode: WikiMode) {
this.searchedVehicleTypeName = '';
this.wikiMode = wikiMode;
},
toggleSorter(header: WikiHeader) {
toggleSorter(header: IWikiHeader) {
if (!header.sortable) return;
if (header.id == this.currentModeSorter.id) this.currentModeSorter.direction *= -1;
this.currentModeSorter.id = header.id;
if (header.id == this.currentSorter.id) this.currentSorter.direction *= -1;
this.currentSorter.id = header.id;
},
sortVehicles(vA: Vehicle, vB: Vehicle) {
const { id, direction } = this.currentModeSorter;
const vehiclesAreLocos = isLocomotive(vA) && isLocomotive(vB);
const vehiclesAreCars = !isLocomotive(vA) && !isLocomotive(vB);
sortTableRows(row1: IWikiRow, row2: IWikiRow) {
if (!row1.show) return 0;
const { id, direction } = this.currentSorter;
switch (id) {
case 'type':
case 'constructionType':
return direction == 1 ? vA[id].localeCompare(vB[id]) : vB[id].localeCompare(vA[id]);
case 'group':
return direction == 1 ? row1.vehicle[id].localeCompare(row2.vehicle[id]) : row2.vehicle[id].localeCompare(row1.vehicle[id]);
case 'mass':
case 'length':
case 'maxSpeed':
return Math.sign(vA[id] - vB[id]) * direction;
return Math.sign(row1.vehicle[id] - row2.vehicle[id]) * direction;
case 'cargoCount':
if (vehiclesAreCars) return Math.sign((vA.cargoList.length || -1) - (vB.cargoList.length || -1)) * direction;
return (
(!isLocomotive(row1.vehicle) ? Math.sign(row1.vehicle.cargoList.length || -1) : -1) -
(!isLocomotive(row2.vehicle) ? (row2.vehicle.cargoList.length || -1) * direction : -1)
);
case 'coldStart':
if (vehiclesAreLocos)
return (
(locoSupportsColdStart(vA.constructionType) > locoSupportsColdStart(vB.constructionType) ? 1 : -1) *
direction
);
return (locoSupportsColdStart(row1.vehicle.constructionType) > locoSupportsColdStart(row2.vehicle.constructionType) ? 1 : -1) * direction;
default:
break;
}
return direction == 1 ? vA.type.localeCompare(vB.type) : vB.type.localeCompare(vA.type);
return direction == 1 ? row1.vehicle.type.localeCompare(row2.vehicle.type) : row2.vehicle.type.localeCompare(row1.vehicle.type);
},
},
computed: {
currentModeSorter() {
return this.wikiMode == 'carWagons' ? this.currentCarSorter : this.currentLocoSorter;
computedTableData(): IWikiRow[] {
return this.store.vehicleDataList
.map((vehicle) => ({
vehicle,
show:
new RegExp(`${this.searchedVehicleTypeName.trim()}`, 'i').test(vehicle.type) &&
(this.currentFilterMode == 'all' ||
(this.currentFilterMode == 'tractions' && isLocomotive(vehicle)) ||
(this.currentFilterMode == 'carriages' && !isLocomotive(vehicle))),
// ((this.filters.tractions && isLocomotive(vehicle)) || (this.filters.carriages && !isLocomotive(vehicle))),
}))
.sort((a, b) => this.sortTableRows(a, b));
},
computedLocoList() {
const trimmedSearchValue = this.searchedVehicleTypeName.trim();
visibleHeaders() {
const filtersActive = this.currentFilterMode;
return this.store.locoDataList
.filter((loco) => new RegExp(`${trimmedSearchValue}`, 'i').test(loco.type))
.sort(this.sortVehicles);
return this.headers.filter((header) => header.for == 'all' || header.for == filtersActive);
},
computedCarList() {
const trimmedSearchValue = this.searchedVehicleTypeName.trim();
areTractionVehiclesShown() {
return this.currentFilterMode == 'all' || this.currentFilterMode == 'tractions';
},
return this.store.carDataList
.filter((car) => new RegExp(`${trimmedSearchValue}`, 'i').test(car.type))
.sort(this.sortVehicles);
areCarriagesShown() {
return this.currentFilterMode == 'all' || this.currentFilterMode == 'carriages';
},
},
});
@@ -300,6 +275,10 @@ export default defineComponent({
cursor: pointer;
background-color: #333;
&:first-child {
min-width: 120px;
}
&:nth-child(odd) {
background-color: #444;
}
@@ -311,29 +290,22 @@ export default defineComponent({
td {
text-align: center;
padding: 0.25em;
height: 85px;
}
height: 70px;
td:first-child {
width: 120px;
}
td img {
display: block;
width: 120px;
&[data-sponsoronly='true'] {
color: salmon;
}
}
}
@media screen and (max-width: $breakpointMd) {
.wiki-list table {
td {
width: 100px;
height: auto;
th {
min-width: 100px;
}
img {
width: 6em;
}
img {
max-width: 100px;
}
}
}
@@ -10,8 +10,8 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store';
import { defineComponent } from "vue";
import { useStore } from "../../store";
export default defineComponent({
data() {
+8 -6
View File
@@ -2,6 +2,7 @@
<div class="stock_thumbnails" ref="thumbnailsRef">
<div
v-for="(stock, stockIndex) in store.stockList"
:key="stockIndex"
:data-selected="store.chosenStockListIndex == stockIndex"
draggable="true"
@dragstart="onDragStart(stockIndex)"
@@ -9,7 +10,7 @@
@dragover="allowDrop"
>
<span @click="onListItemClick(stockIndex)" :key="stock.id">
<b :class="{ supporter: stock.supportersOnly }">
<b :class="{ sponsor: stock.isSponsorsOnly }">
{{ stock.type }}
</b>
@@ -52,9 +53,11 @@ watch(
if (index < 0) return;
nextTick(() => {
(thumbnailsRef.value as HTMLElement)
.querySelector(`div:nth-child(${index + 1})`)
?.scrollIntoView({ block: 'nearest', inline: 'start', behavior: 'smooth' });
(thumbnailsRef.value as HTMLElement).querySelector(`div:nth-child(${index + 1})`)?.scrollIntoView({
block: 'nearest',
inline: 'start',
behavior: 'smooth',
});
});
}
);
@@ -118,8 +121,7 @@ const allowDrop = (e: DragEvent) => {
}
}
.supporter {
.sponsor {
color: salmon;
}
</style>
+2 -3
View File
@@ -12,8 +12,8 @@
},
"sameRegions": {
"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,
79, 88, 80, 89, 92, 98
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, 79, 88, 80, 89, 92, 98
],
"Warszawa": [10, 11, 19, 91, 93, 97, 99],
"Lublin": [20, 22, 29],
@@ -35,4 +35,3 @@
"LT": "2:5;3:0-899:3"
}
}
+6 -2
View File
@@ -27,6 +27,12 @@
},
"cargo": null
},
"EP09": {
"passenger": {
"650": 160
},
"cargo": null
},
"ET41": {
"passenger": {
"700": 125
@@ -58,5 +64,3 @@
}
}
}
+6 -6
View File
@@ -1,9 +1,9 @@
export const enum EVehicleUseType {
LOCO_ELECTRICAL = 'loco-e',
LOCO_DIESEL = "loco-s",
EMU = "loco-ezt",
DMU = "loco-szt",
LOCO_ELECTRICAL = "loco-e",
LOCO_DIESEL = "loco-s",
EMU = "loco-ezt",
DMU = "loco-szt",
CAR_PASSENGER = "car-passenger",
CAR_CARGO = "car-cargo"
CAR_PASSENGER = "car-passenger",
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;
+13 -11
View File
@@ -1,31 +1,33 @@
import localePL from './locales/pl.json';
import localeEN from './locales/en.json';
import { createI18n } from 'vue-i18n';
import axios from 'axios';
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';
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 locale =
window.localStorage.getItem("locale") ||
(/^pl\b/.test(navigator.language) ? "pl" : "en");
const i18n = createI18n<[LocaleMessageSchema], 'en' | 'pl'>({
const i18n = createI18n<[LocaleMessageSchema], "en" | "pl">({
locale,
fallbackLocale: 'pl',
fallbackLocale: "pl",
legacy: false,
globalInjection: true,
messages: locales,
});
async function fetchBackendTranslations() {
const localeData = (await axios.get(`https://spythere.github.io/api/td2/data/locales.json`)).data;
const localeData = (await http.get(`td2/data/locales.json`)).data;
i18n.global.mergeLocaleMessage('pl', localeData.pl);
i18n.global.mergeLocaleMessage('en', localeData.en);
i18n.global.mergeLocaleMessage("pl", localeData.pl);
i18n.global.mergeLocaleMessage("en", localeData.en);
}
fetchBackendTranslations();
+6 -4
View File
@@ -29,13 +29,13 @@
"title": "RAILWAY VEHICLE PREVIEW",
"loading": "IMAGE LOADING...",
"desc": "Choose a railway vehicle above to see its preview",
"sponsor-only": "* SPONSORS ONLY",
"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 CAR",
"car-cargo": "FREIGHT CARRIAGE",
"cabin": "Cabin type:",
"construction": "Construction type:"
},
@@ -128,7 +128,7 @@
"header": {
"image": "Image",
"type": "Name",
"power": "Type",
"group": "Type group",
"constructionType": "Construction",
"coldStart": "Cold start",
"length": "Length",
@@ -139,7 +139,9 @@
"loco-ezt": "EMU",
"loco-szt": "DMU",
"loco-s": "Diesel locomotive",
"loco-e": "Electric locomotive"
"loco-e": "Electric locomotive",
"car-passenger": "Passenger carriage",
"car-cargo": "Frieght carriage"
},
"realstock": {
"title": "POLISH TRAIN COMPOSITIONS by",
+5 -3
View File
@@ -29,7 +29,7 @@
"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",
"sponsor-only": "* TYLKO DLA SPONSORÓW DO {0}",
"loco-e": "ELEKTROWÓZ",
"loco-s": "SPALINOWÓZ",
"loco-ezt": "EZT",
@@ -128,7 +128,7 @@
"header": {
"image": "Zdjęcie",
"type": "Nazwa",
"power": "Rodzaj",
"group": "Rodzaj",
"constructionType": "Konstrukcja",
"coldStart": "Zimny start",
"length": "Długość",
@@ -139,7 +139,9 @@
"loco-ezt": "EZT",
"loco-szt": "SZT",
"loco-s": "Spalinowóz",
"loco-e": "Elektrowóz"
"loco-e": "Elektrowóz",
"car-passenger": "Wagon pasażerski",
"car-cargo": "Wagon towarowy"
},
"realstock": {
"title": "ZESTAWIENIA REALNE by",
+6 -7
View File
@@ -1,14 +1,13 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { registerSW } from 'virtual:pwa-register';
import { createApp } from "vue";
import { createPinia } from "pinia";
import { registerSW } from "virtual:pwa-register";
import App from './App.vue';
import i18n from './i18n-setup';
import App from "./App.vue";
import i18n from "./i18n-setup";
const pinia = createPinia();
registerSW({
immediate: true,
});
createApp(App).use(pinia).use(i18n).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({
methods: {
getIconURL(name: string, ext = 'svg'): string {
getIconURL(name: string, ext = "svg"): string {
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,
imgSrc: vehicle.imageSrc,
useType: isLoco ? vehicle.power : vehicle.useType,
supportersOnly: vehicle.supportersOnly,
isSponsorsOnly: vehicle.isSponsorsOnly,
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 { useStore } from '../store';
import { ICarWagon, ILocomotive, IStock, Vehicle } from '../types';
import { isLocomotive } from '../utils/vehicleUtils';
export default defineComponent({
setup() {
@@ -9,45 +10,9 @@ export default defineComponent({
};
},
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);
},
},
computed: {},
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) {
if (this.store.chosenVehicle?.imageSrc != stock.imgSrc) this.store.imageLoading = true;
@@ -81,6 +46,11 @@ export default defineComponent({
this.store.chosenCargo = null;
},
previewVehicle(vehicle: Vehicle) {
if (isLocomotive(vehicle)) this.previewLocomotive(vehicle);
else this.previewCarWagon(vehicle);
},
resetPreview() {
this.store.chosenVehicle = 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 { useStore } from '../store';
import { defineComponent } from "vue";
import { useStore } from "../store";
export default defineComponent({
setup() {
@@ -18,7 +18,10 @@ export default defineComponent({
},
trainTooHeavy() {
return this.store.acceptableMass && this.store.totalMass > this.store.acceptableMass;
return (
this.store.acceptableMass &&
this.store.totalMass > this.store.acceptableMass
);
},
locoNotSuitable() {
@@ -26,15 +29,19 @@ export default defineComponent({
!this.store.isTrainPassenger &&
this.store.stockList.length > 1 &&
!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() {
return this.store.stockList.reduce((acc, stock) => {
if (stock.isLoco) acc += stock.count;
return acc;
}, 0) > 2;
return (
this.store.stockList.reduce((acc, stock) => {
if (stock.isLoco) acc += stock.count;
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 {
acceptableMass,
@@ -10,6 +10,7 @@ import {
totalLength,
totalMass,
} from './utils/vehicleUtils';
import http from './http';
export const useStore = defineStore({
id: 'store',
@@ -46,11 +47,14 @@ export const useStore = defineStore({
isRealStockListCardOpen: false,
stockData: undefined,
} as IStore),
lastFocusedElement: null,
}) as IStore,
getters: {
locoDataList: (state) => locoDataList(state),
carDataList: (state) => carDataList(state),
vehicleDataList: (state) => [...locoDataList(state), ...carDataList(state)],
totalMass: (state) => totalMass(state),
totalLength: (state) => totalLength(state),
maxStockSpeed: (state) => maxStockSpeed(state),
@@ -61,7 +65,7 @@ export const useStore = defineStore({
actions: {
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;
},
@@ -82,5 +86,3 @@ export const useStore = defineStore({
},
},
});
+43 -6
View File
@@ -1,5 +1,3 @@
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700;900&display=swap');
$breakpointMd: 960px;
$breakpointSm: 550px;
@@ -8,6 +6,36 @@ $textColor: #fff;
$secondaryColor: #222;
$accentColor: #e4c428;
@font-face {
font-family: 'Lato';
src:
url('$fonts/Lato-Light.woff2') format('woff2'),
url('$fonts/Lato-Light.woff') format('woff');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Lato';
src:
url('$fonts/Lato-Bold.woff2') format('woff2'),
url('$fonts/Lato-Bold.woff') format('woff');
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Lato';
src:
url('$fonts/Lato-Regular.woff2') format('woff2'),
url('$fonts/Lato-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
::-webkit-scrollbar {
width: 7px;
height: 7px;
@@ -32,7 +60,7 @@ html {
margin: 0;
padding: 0;
font-family: 'Lato', sans-serif;
font-family: Lato, sans-serif;
background-color: $bgColor;
overflow-x: hidden;
@@ -64,7 +92,7 @@ select,
option,
input,
button {
font-family: 'Lato', sans-serif;
font-family: Lato, sans-serif;
font-size: 1em;
}
@@ -94,7 +122,9 @@ button {
border-radius: 8px;
font-weight: bold;
transition: all 250ms;
transition:
color 150ms,
background-color 150ms;
&:hover {
color: $accentColor;
@@ -111,6 +141,13 @@ button {
outline: 1px solid white;
}
&[data-chosen='true'] {
background-color: $accentColor;
color: black;
box-shadow: 0 0 5px 1px $accentColor;
}
&[data-disabled='true'] {
user-select: none;
pointer-events: none;
@@ -148,7 +185,7 @@ button {
select,
input[type='text'],
input[type='number'] {
background: none;
background: $bgColor;
border: 2px solid #aaa;
outline: none;
+1 -2
View File
@@ -1,4 +1,4 @@
@import './global.scss';
@import "./global.scss";
.tab {
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';
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 {
type: string;
@@ -52,12 +55,12 @@ export interface IStockData {
};
info: {
'car-cargo': [string, string, boolean, boolean, string][];
'car-passenger': [string, string, boolean, boolean, string][];
'loco-e': [string, string, string, string, boolean][];
'loco-s': [string, string, string, string, boolean][];
'loco-szt': [string, string, string, string, boolean][];
'loco-ezt': [string, string, string, string, boolean][];
'car-cargo': [string, string, boolean, number | null, string][];
'car-passenger': [string, string, boolean, number | null, string][];
'loco-e': [string, string, string, string, number | null][];
'loco-s': [string, string, string, string, number | null][];
'loco-szt': [string, string, string, string, number | null][];
'loco-ezt': [string, string, string, string, number | null][];
};
props: IStockProps[];
@@ -67,11 +70,13 @@ export interface IStockData {
export interface ILocomotive {
type: string;
power: string;
power: TLocoGroup;
group: TLocoGroup;
constructionType: string;
cabinType: string;
maxSpeed: number;
supportersOnly: boolean;
isSponsorsOnly: boolean;
sponsorsOnlyTimestamp: number;
imageSrc: string;
mass: number;
@@ -79,12 +84,13 @@ export interface ILocomotive {
}
export interface ICarWagon {
//"203V_PKPC_Fll_01","203V",true,false,"100",img
type: string;
useType: 'car-passenger' | 'car-cargo';
useType: TCarWagonGroup;
group: TCarWagonGroup;
constructionType: string;
loadable: boolean;
supportersOnly: boolean;
isSponsorsOnly: boolean;
sponsorsOnlyTimestamp: number;
maxSpeed: number;
imageSrc: string;
@@ -108,7 +114,8 @@ export interface IStock {
maxSpeed: number;
cargo?: { id: string; totalMass: number };
isLoco: boolean;
supportersOnly: boolean;
isSponsorsOnly: boolean;
sponsorsOnlyTimestamp: number;
count: number;
imgSrc?: string;
}
@@ -120,4 +127,3 @@ export interface IReadyStockItem {
number: string;
name: string;
}
+4 -2
View File
@@ -1,5 +1,7 @@
const supportedConstructions = ['303e', '203e'];
const supportedConstructions = ["303e", "203e"];
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 const calculateSpeedLimit = (locoType: LocoType, stockMass: number, isTrainPassenger: boolean) => {
const speedTable = speedLimitTable[locoType][isTrainPassenger ? 'passenger' : 'cargo'];
export const calculateSpeedLimit = (
locoType: LocoType,
stockMass: number,
isTrainPassenger: boolean,
) => {
const speedTable =
speedLimitTable[locoType][isTrainPassenger ? "passenger" : "cargo"];
if (!speedTable) return undefined;
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;
};
+23 -26
View File
@@ -1,5 +1,5 @@
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';
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) => {
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) => {
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);
acc.push({
power: vehiclePower,
power: vehiclePower as TLocoGroup,
group: vehiclePower as TLocoGroup,
type,
constructionType,
cabinType,
maxSpeed: Number(maxSpeed),
supportersOnly,
isSponsorsOnly: Number(sponsorsTimestamp) > Date.now(),
sponsorsOnlyTimestamp: Number(sponsorsTimestamp),
imageSrc: '',
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) => {
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) => {
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({
useType: vehicleUseType as 'car-passenger' | 'car-cargo',
type: car[0],
constructionType: car[1],
loadable: car[2],
supportersOnly: car[3],
maxSpeed: Number(car[4]),
useType: vehicleUseType as TCarWagonGroup,
group: vehicleUseType as TCarWagonGroup,
type,
constructionType,
loadable,
isSponsorsOnly: Number(sponsorsOnlyTimestamp) > Date.now(),
sponsorsOnlyTimestamp: Number(sponsorsOnlyTimestamp),
maxSpeed: Number(maxSpeed),
imageSrc: '',
cargoList:
!carPropsData || carPropsData.cargo === null
@@ -81,10 +87,7 @@ export function carDataList(state: IStore) {
}
export function totalMass(state: IStore) {
return ~~state.stockList.reduce(
(acc, stock) => acc + (stock.cargo ? stock.cargo.totalMass : stock.mass) * stock.count,
0
);
return ~~state.stockList.reduce((acc, stock) => acc + (stock.cargo ? stock.cargo.totalMass : stock.mass) * stock.count, 0);
}
export function totalLength(state: IStore) {
@@ -92,10 +95,7 @@ export function totalLength(state: IStore) {
}
export function maxStockSpeed(state: IStore) {
const stockSpeedLimit = state.stockList.reduce(
(acc, stock) => (stock.maxSpeed < acc || acc == 0 ? stock.maxSpeed : acc),
0
);
const stockSpeedLimit = state.stockList.reduce((acc, stock) => (stock.maxSpeed < acc || acc == 0 ? stock.maxSpeed : acc), 0);
const headingLoco = state.stockList[0]?.isLoco ? state.stockList[0] : undefined;
if (!headingLoco) return stockSpeedLimit;
@@ -139,9 +139,7 @@ export function isTrainPassenger(state: IStore) {
if (state.stockList.length == 0) return false;
if (state.stockList.every((stock) => stock.isLoco)) return false;
return state.stockList
.filter((stock) => !stock.isLoco)
.every((stock) => stock.useType === EVehicleUseType.CAR_PASSENGER);
return state.stockList.filter((stock) => !stock.isLoco).every((stock) => stock.useType === EVehicleUseType.CAR_PASSENGER);
}
export function chosenRealStock(state: IStore) {
@@ -158,4 +156,3 @@ export function chosenRealStock(state: IStore) {
return realStockObj;
}
+6 -6
View File
@@ -1,21 +1,21 @@
<template>
<div class="app-container">
<MainContainer />
<Footer />
<FooterVue />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../store';
import { defineComponent } from "vue";
import { useStore } from "../store";
import MainContainer from '../components/app/MainContainer.vue';
import Footer from '../components/app/Footer.vue';
import MainContainer from "../components/app/MainContainer.vue";
import FooterVue from "../components/app/Footer.vue";
export default defineComponent({
components: {
MainContainer,
Footer,
FooterVue,
},
data: () => ({
+4 -4
View File
@@ -1,7 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
+23 -5
View File
@@ -2,19 +2,26 @@ import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { VitePWA } from 'vite-plugin-pwa';
import { resolve } from 'path';
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 2137,
},
resolve: {
alias: {
$fonts: resolve('/fonts'),
$images: resolve('/images'),
},
},
plugins: [
vue(),
VitePWA({
registerType: 'autoUpdate',
workbox: {
// globPatterns: ['**/*.{js,css,html,png,svg,img}'],
globPatterns: ['**/*.{js,css,html,png,svg,img,woff,woff2}'],
runtimeCaching: [
{
@@ -24,10 +31,23 @@ export default defineConfig({
cacheName: 'swdr-images-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 60 * 60 * 24 * 7, // <== 7 days
maxAgeSeconds: 60 * 60 * 24, // <== 1 day
},
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({
}),
],
});
-2959
View File
File diff suppressed because it is too large Load Diff