Add export/import

This commit is contained in:
2025-11-24 17:37:04 +01:00
parent c87634a1c2
commit 2bbe9b7774
6 changed files with 168 additions and 10 deletions
+14
View File
@@ -1,3 +1,5 @@
import { z } from 'zod';
export type SpecialCar = [string, string, number] | [string, string, number, string];
export const specialCars = {
crash: [
@@ -30,3 +32,15 @@ export const specialCars = {
} as const;
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
View File
@@ -5,9 +5,82 @@ import { specialCars } from '@/data/specials.ts';
import { type, useTramStore } from '@/stores/tram.ts';
import { groupBy } from '@/utils/array.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();
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>
<template>
@@ -79,6 +152,25 @@ const tramStore = useTramStore();
<QuickStats />
</VaCardContent>
</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>
</article>
</template>
+42 -8
View File
@@ -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 { computed, reactive } from 'vue';
import { z } from 'zod';
export type CarType = [number, number, boolean, string];
export const type: CarType[] = [
@@ -49,18 +51,23 @@ export function getType(id: number | string): null | { id: number; isTram: boole
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(
'tram',
() => {
const trams = reactive<{
bsag: number[];
}>({
const trams = reactive<z.infer<typeof tramsSchema>>({
bsag: []
});
const specials = reactive<{
bsag: string[];
}>({
const specials = reactive<z.infer<typeof specialsSchema>>({
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
}
);
export const importSchema = stringToJsonSchema.pipe(
z.object({
trams: tramsSchema,
specials: specialsSchema
})
);
+14
View File
@@ -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());