Add export/import
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vuestic-ui": "^1.10.3",
|
"vuestic-ui": "^1.10.3",
|
||||||
|
"zod": "^4.1.13",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node22": "^22.0.2",
|
"@tsconfig/node22": "^22.0.2",
|
||||||
@@ -1627,6 +1628,8 @@
|
|||||||
|
|
||||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||||
|
|
||||||
|
"zod": ["zod@4.1.13", "", {}, "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig=="],
|
||||||
|
|
||||||
"@apideck/better-ajv-errors/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
"@apideck/better-ajv-errors/ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
|
||||||
|
|
||||||
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||||
|
|||||||
+2
-1
@@ -27,7 +27,8 @@
|
|||||||
"tailwind": "^4.0.0",
|
"tailwind": "^4.0.0",
|
||||||
"vue": "^3.5.18",
|
"vue": "^3.5.18",
|
||||||
"vue-router": "^4.5.1",
|
"vue-router": "^4.5.1",
|
||||||
"vuestic-ui": "^1.10.3"
|
"vuestic-ui": "^1.10.3",
|
||||||
|
"zod": "^4.1.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tsconfig/node22": "^22.0.2",
|
"@tsconfig/node22": "^22.0.2",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
export type SpecialCar = [string, string, number] | [string, string, number, string];
|
export type SpecialCar = [string, string, number] | [string, string, number, string];
|
||||||
export const specialCars = {
|
export const specialCars = {
|
||||||
crash: [
|
crash: [
|
||||||
@@ -30,3 +32,15 @@ export const specialCars = {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const allSpecials = [...specialCars.crash, ...specialCars.work, ...specialCars.help];
|
export const allSpecials = [...specialCars.crash, ...specialCars.work, ...specialCars.help];
|
||||||
|
|
||||||
|
export const specialsSchema = z.object({
|
||||||
|
bsag: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.refine((id) =>
|
||||||
|
allSpecials.some((a) => a[0] === id, {
|
||||||
|
error: (v: z.core.$ZodIssue) => `Unknown id '${v.input}'!`
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.array()
|
||||||
|
});
|
||||||
|
|||||||
+93
-1
@@ -5,9 +5,82 @@ import { specialCars } from '@/data/specials.ts';
|
|||||||
import { type, useTramStore } from '@/stores/tram.ts';
|
import { type, useTramStore } from '@/stores/tram.ts';
|
||||||
import { groupBy } from '@/utils/array.ts';
|
import { groupBy } from '@/utils/array.ts';
|
||||||
import { idsOf } from '@/utils/typeGroup.ts';
|
import { idsOf } from '@/utils/typeGroup.ts';
|
||||||
import { VaCard, VaCardContent, VaCardTitle } from 'vuestic-ui';
|
import {
|
||||||
|
useModal,
|
||||||
|
useToast,
|
||||||
|
VaButton,
|
||||||
|
VaCard,
|
||||||
|
VaCardContent,
|
||||||
|
VaCardTitle,
|
||||||
|
type VaFile,
|
||||||
|
VaFileUpload
|
||||||
|
} from 'vuestic-ui';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const { init } = useToast();
|
||||||
|
const { confirm } = useModal();
|
||||||
const tramStore = useTramStore();
|
const tramStore = useTramStore();
|
||||||
|
|
||||||
|
function download() {
|
||||||
|
const fileType = 'application/json';
|
||||||
|
const blob = new Blob([tramStore.exportData()], { type: fileType });
|
||||||
|
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.download = `tramtrack-export-${Date.now()}.json`;
|
||||||
|
a.href = URL.createObjectURL(blob);
|
||||||
|
a.dataset.downloadurl = [fileType, a.download, a.href].join(':');
|
||||||
|
a.style.display = 'none';
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
setTimeout(() => URL.revokeObjectURL(a.href), 1500);
|
||||||
|
|
||||||
|
init({
|
||||||
|
title: 'Export erfolgreich',
|
||||||
|
message: 'Datenbestand wurde erfolgreich exportiert!',
|
||||||
|
color: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function upload(file: VaFile) {
|
||||||
|
const data = (await file?.text?.()?.catch(() => '')) || '';
|
||||||
|
|
||||||
|
const error = tramStore.importData(data);
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
console.warn(z.prettifyError(error));
|
||||||
|
init({
|
||||||
|
title: 'Import fehlgeschlagen',
|
||||||
|
message: `Die hochgeladene Datei ist im falschen Format!`,
|
||||||
|
color: 'danger'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
init({
|
||||||
|
title: 'Import erfolgreich',
|
||||||
|
message: 'Datenbestand wurde erfolgreich importiert!',
|
||||||
|
color: 'success'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function reset() {
|
||||||
|
const yes = await confirm({
|
||||||
|
title: 'Bist du dir sicher?',
|
||||||
|
message:
|
||||||
|
'Willst du wirklich deinen Fortschritt zurücksetzen? Dies ist nicht rückgängig zu machen!',
|
||||||
|
okText: 'Ja zurücksetzen!',
|
||||||
|
cancelText: 'Abbrechen',
|
||||||
|
'child:okButton': {
|
||||||
|
color: 'danger'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!yes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tramStore.$reset();
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -79,6 +152,25 @@ const tramStore = useTramStore();
|
|||||||
<QuickStats />
|
<QuickStats />
|
||||||
</VaCardContent>
|
</VaCardContent>
|
||||||
</VaCard>
|
</VaCard>
|
||||||
|
<VaCard stripe-color="warning" stripe>
|
||||||
|
<VaCardTitle>Export/Import</VaCardTitle>
|
||||||
|
<VaCardContent>
|
||||||
|
<div class="flex flex-row items-center justify-center gap-2 md:justify-start">
|
||||||
|
<VaButton color="success" icon="file_upload" @click="download">Export</VaButton>
|
||||||
|
<VaFileUpload
|
||||||
|
color="warning"
|
||||||
|
fileTypes="json"
|
||||||
|
type="single"
|
||||||
|
file-incorrect-message="Falscher Datentyp, .json erforderlich!"
|
||||||
|
upload-button-text="Import"
|
||||||
|
hideFileList
|
||||||
|
@update:model-value="upload"
|
||||||
|
><VaButton color="warning" icon="file_download">Import</VaButton></VaFileUpload
|
||||||
|
>
|
||||||
|
<VaButton color="danger" icon="delete_forever" @click="reset">Reset</VaButton>
|
||||||
|
</div>
|
||||||
|
</VaCardContent>
|
||||||
|
</VaCard>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+42
-8
@@ -1,6 +1,8 @@
|
|||||||
import { allSpecials } from '@/data/specials.ts';
|
import { allSpecials, specialsSchema } from '@/data/specials.ts';
|
||||||
|
import { stringToJsonSchema } from '@/utils/zod.ts';
|
||||||
import { defineStore } from 'pinia';
|
import { defineStore } from 'pinia';
|
||||||
import { computed, reactive } from 'vue';
|
import { computed, reactive } from 'vue';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
export type CarType = [number, number, boolean, string];
|
export type CarType = [number, number, boolean, string];
|
||||||
export const type: CarType[] = [
|
export const type: CarType[] = [
|
||||||
@@ -49,18 +51,23 @@ export function getType(id: number | string): null | { id: number; isTram: boole
|
|||||||
type: foundType[3]
|
type: foundType[3]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
export const tramsSchema = z.object({
|
||||||
|
bsag: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.refine((id) => getType(id) !== null, {
|
||||||
|
error: (v) => `Unknown id '${v.input}'!`
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
});
|
||||||
|
|
||||||
export const useTramStore = defineStore(
|
export const useTramStore = defineStore(
|
||||||
'tram',
|
'tram',
|
||||||
() => {
|
() => {
|
||||||
const trams = reactive<{
|
const trams = reactive<z.infer<typeof tramsSchema>>({
|
||||||
bsag: number[];
|
|
||||||
}>({
|
|
||||||
bsag: []
|
bsag: []
|
||||||
});
|
});
|
||||||
const specials = reactive<{
|
const specials = reactive<z.infer<typeof specialsSchema>>({
|
||||||
bsag: string[];
|
|
||||||
}>({
|
|
||||||
bsag: []
|
bsag: []
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,9 +100,36 @@ export const useTramStore = defineStore(
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
return { trams, addNumber, shortStats, specials };
|
function exportData(): string {
|
||||||
|
return JSON.stringify({ trams, specials });
|
||||||
|
}
|
||||||
|
|
||||||
|
function importData(data: string | null | undefined) {
|
||||||
|
const result = importSchema.safeParse(data);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return result.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
trams.bsag = result.data.trams.bsag;
|
||||||
|
specials.bsag = result.data.specials.bsag;
|
||||||
|
}
|
||||||
|
|
||||||
|
function $reset() {
|
||||||
|
trams.bsag = [];
|
||||||
|
specials.bsag = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { trams, addNumber, shortStats, specials, exportData, importData, $reset };
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
persist: true
|
persist: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const importSchema = stringToJsonSchema.pipe(
|
||||||
|
z.object({
|
||||||
|
trams: tramsSchema,
|
||||||
|
specials: specialsSchema
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const stringToJsonSchema = z
|
||||||
|
.string()
|
||||||
|
.transform((str, ctx) => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(str);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch (_) {
|
||||||
|
ctx.addIssue({ code: 'invalid_type', message: 'Invalid JSON', expected: 'string' });
|
||||||
|
return z.NEVER;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.pipe(z.json());
|
||||||
Reference in New Issue
Block a user