diff --git a/web/src/account/cards.test.ts b/web/src/account/cards.test.ts
index f943a65..dc08f9b 100644
--- a/web/src/account/cards.test.ts
+++ b/web/src/account/cards.test.ts
@@ -350,7 +350,7 @@ describe('save: bestehende Karte', () => {
it('ohne Bildwechsel: nur neue Vorschau, Update, dann alte Vorschau löschen', async () => {
const fake = fakeApi([existing()]);
- const c = config({ network: 'jcb' });
+ const c = config({ logo: 'abc123' });
const saved = await service(fake).save(
newInput({ name: 'Neu', config: c, imageChanged: false, previous: previous() }),
);
@@ -370,7 +370,7 @@ describe('save: bestehende Karte', () => {
expect(saved.id).toBe('row1');
expect(saved.imageFileId).toBe('oldImage');
expect(saved.previewFileId).toBe('file1');
- expect(saved.config.network).toBe('jcb');
+ expect(saved.config.logo).toBe('abc123');
});
it('veralteter Schnappschuss (Bild in anderem Tab ersetzt): das Update schickt kein altes imageFileId, die Zeile behält das neue Bild', async () => {
diff --git a/web/src/account/cards.ts b/web/src/account/cards.ts
index 6e32312..89b24a9 100644
--- a/web/src/account/cards.ts
+++ b/web/src/account/cards.ts
@@ -1,7 +1,7 @@
// Gespeicherte Karten: Dienst (Regeln fürs Speichern, Aufräumen, Laden) und Anschluss an Appwrite.
// Der Dienst kennt nur das schmale `CardsApi`-Interface und lässt sich dadurch ohne Netzwerk testen.
import { AppwriteException, ID, Permission, Query, Role, type Models } from 'appwrite';
-import { parseConfig, serializeConfig, type CardConfig } from '../card/config';
+import { ID_PATTERN, parseConfig, serializeConfig, type CardConfig } from '../card/config';
import type { SavedCardRef } from '../editor/state';
import { APPWRITE_ENDPOINT, IDS, client, storage, tables } from './appwrite';
import { currentUserId } from './auth';
@@ -46,8 +46,8 @@ export interface SaveInput {
export const DEFAULT_CARD_NAME = 'Meine Karte';
const NAME_MAX = 80;
-/** Zeilen- und Datei-IDs von Appwrite: höchstens 36 Zeichen, kein Sonderzeichen am Anfang. */
-export const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/;
+/** Zeilen- und Datei-IDs von Appwrite (definiert in card/config.ts, dort braucht sie auch die Logo-ID der Konfiguration). */
+export { ID_PATTERN };
/** Trimmt, fasst Leerraum zusammen und kürzt auf 80 Zeichen (Codepoints); leer ergibt „Meine Karte“. */
export function normalizeName(name: string): string {
diff --git a/web/src/assets/marks/amex-blue.svg b/web/src/assets/marks/amex-blue.svg
deleted file mode 100644
index ccb37e6..0000000
--- a/web/src/assets/marks/amex-blue.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/amex-outline.svg b/web/src/assets/marks/amex-outline.svg
deleted file mode 100644
index a569042..0000000
--- a/web/src/assets/marks/amex-outline.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/amex-select.svg b/web/src/assets/marks/amex-select.svg
deleted file mode 100644
index ccb37e6..0000000
--- a/web/src/assets/marks/amex-select.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/apple.svg b/web/src/assets/marks/apple.svg
deleted file mode 100644
index 31b057a..0000000
--- a/web/src/assets/marks/apple.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/discover.svg b/web/src/assets/marks/discover.svg
deleted file mode 100644
index d55863a..0000000
--- a/web/src/assets/marks/discover.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/jcb.svg b/web/src/assets/marks/jcb.svg
deleted file mode 100644
index 133310f..0000000
--- a/web/src/assets/marks/jcb.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/mc-classic.svg b/web/src/assets/marks/mc-classic.svg
deleted file mode 100644
index 37a8330..0000000
--- a/web/src/assets/marks/mc-classic.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/mc-color.svg b/web/src/assets/marks/mc-color.svg
deleted file mode 100644
index 1aef221..0000000
--- a/web/src/assets/marks/mc-color.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/mc-silver-text.svg b/web/src/assets/marks/mc-silver-text.svg
deleted file mode 100644
index 4aa4250..0000000
--- a/web/src/assets/marks/mc-silver-text.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/mc-silver.svg b/web/src/assets/marks/mc-silver.svg
deleted file mode 100644
index fb19c12..0000000
--- a/web/src/assets/marks/mc-silver.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/mc-wordmark.svg b/web/src/assets/marks/mc-wordmark.svg
deleted file mode 100644
index c24ddcc..0000000
--- a/web/src/assets/marks/mc-wordmark.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/up-color.svg b/web/src/assets/marks/up-color.svg
deleted file mode 100644
index 8d51b91..0000000
--- a/web/src/assets/marks/up-color.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/up-english.svg b/web/src/assets/marks/up-english.svg
deleted file mode 100644
index 8c92134..0000000
--- a/web/src/assets/marks/up-english.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/up-silver.svg b/web/src/assets/marks/up-silver.svg
deleted file mode 100644
index 9d9dd36..0000000
--- a/web/src/assets/marks/up-silver.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/up-white-frame.svg b/web/src/assets/marks/up-white-frame.svg
deleted file mode 100644
index 4660669..0000000
--- a/web/src/assets/marks/up-white-frame.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/visa-auto.svg b/web/src/assets/marks/visa-auto.svg
deleted file mode 100644
index 1f64047..0000000
--- a/web/src/assets/marks/visa-auto.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/visa-blue.svg b/web/src/assets/marks/visa-blue.svg
deleted file mode 100644
index 9fe1811..0000000
--- a/web/src/assets/marks/visa-blue.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/visa-navy.svg b/web/src/assets/marks/visa-navy.svg
deleted file mode 100644
index 739c9bb..0000000
--- a/web/src/assets/marks/visa-navy.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/assets/marks/visa-silver.svg b/web/src/assets/marks/visa-silver.svg
deleted file mode 100644
index 673abb0..0000000
--- a/web/src/assets/marks/visa-silver.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/web/src/card/config.test.ts b/web/src/card/config.test.ts
index 07ebd07..e807ae4 100644
--- a/web/src/card/config.test.ts
+++ b/web/src/card/config.test.ts
@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import {
DEFAULT_CONFIG,
DEFAULT_CROP,
- NETWORK_IDS,
+ ID_PATTERN,
TONE_COLOR,
parseConfig,
serializeConfig,
@@ -12,9 +12,9 @@ import {
const custom: CardConfig = {
v: 1,
template: 'classic',
- network: 'jcb',
+ logo: 'abc123',
details: 'dark',
- brand: 'apple',
+ brand: 'aircards-text',
crop: { x: -120.5, y: 33, zoom: 2.5 },
};
@@ -23,7 +23,7 @@ describe('Standardwerte', () => {
expect(DEFAULT_CONFIG).toEqual({
v: 1,
template: 'signature',
- network: 'visa-auto',
+ logo: null,
details: 'light',
brand: 'aircards-symbol',
crop: { x: 0, y: 0, zoom: 1 },
@@ -36,16 +36,13 @@ describe('Standardwerte', () => {
expect(DEFAULT_CONFIG.crop).not.toBe(DEFAULT_CROP);
});
- it('NETWORK_IDS: 18 Logos in der vereinbarten Reihenfolge', () => {
- expect(NETWORK_IDS.length).toBe(18);
- expect([...NETWORK_IDS]).toEqual([
- 'visa-auto', 'visa-blue', 'visa-navy', 'visa-silver',
- 'mc-color', 'mc-silver', 'mc-wordmark', 'mc-silver-text', 'mc-classic',
- 'up-color', 'up-silver', 'up-white-frame', 'up-english',
- 'amex-blue', 'amex-outline', 'amex-select',
- 'jcb', 'discover',
- ]);
- expect(new Set(NETWORK_IDS).size).toBe(18);
+ it('eine neue Karte hat kein Logo unten rechts', () => {
+ expect(DEFAULT_CONFIG.logo).toBeNull();
+ });
+
+ it('ID_PATTERN: Appwrite-IDs mit höchstens 36 Zeichen und ohne Sonderzeichen am Anfang', () => {
+ for (const ok of ['abc123', 'A', '0', 'a.b_c-d', 'x'.repeat(36)]) expect(ID_PATTERN.test(ok), ok).toBe(true);
+ for (const bad of ['', 'kaputt id', '-abc', '_abc', '.abc', 'ä', 'a/b', 'x'.repeat(37)]) expect(ID_PATTERN.test(bad), bad).toBe(false);
});
it('TONE_COLOR: Hell weiß, Dunkel fast schwarz', () => {
@@ -72,9 +69,31 @@ describe('parseConfig / serializeConfig', () => {
}
});
- it('unbekanntes Netzwerk setzt nur dieses Feld auf den Standard', () => {
- const raw = JSON.stringify({ ...custom, network: 'paypal' });
- expect(parseConfig(raw)).toEqual({ ...custom, network: DEFAULT_CONFIG.network });
+ it('eine alte Konfiguration mit network und Apple-Logo: network entfällt, Logo oben links fällt auf den Standard', () => {
+ const raw = '{"v":1,"template":"classic","network":"mc-color","brand":"apple","details":"dark"}';
+ expect(parseConfig(raw)).toEqual({
+ v: 1,
+ template: 'classic',
+ logo: null,
+ details: 'dark',
+ brand: 'aircards-symbol',
+ crop: { x: 0, y: 0, zoom: 1 },
+ });
+ // andere unbekannte Marken ebenso
+ expect(parseConfig('{"brand":"visa"}').brand).toBe('aircards-symbol');
+ });
+
+ it('logo: eine gültige ID bleibt, alles andere ergibt null', () => {
+ expect(parseConfig('{"logo":"abc123"}').logo).toBe('abc123');
+ expect(parseConfig(JSON.stringify({ logo: 'x'.repeat(36) })).logo).toBe('x'.repeat(36));
+ for (const bad of ['kaputt id', '', '-abc', 'x'.repeat(37), 42, true, null, ['abc'], { id: 'abc' }]) {
+ expect(parseConfig(JSON.stringify({ logo: bad })).logo, JSON.stringify(bad)).toBeNull();
+ }
+ expect(parseConfig('{}').logo).toBeNull();
+ });
+
+ it('das gewählte Logo bleibt nur dem Feld logo zugeordnet: ein altes network wird nie zum Logo', () => {
+ expect(parseConfig('{"network":"abc123"}').logo).toBeNull();
});
it('jedes Feld wird einzeln validiert', () => {
@@ -88,11 +107,11 @@ describe('parseConfig / serializeConfig', () => {
});
it('Namen aus Object.prototype gelten nicht als gültige Werte', () => {
- const raw = JSON.stringify({ ...custom, network: 'constructor', template: '__proto__', brand: 'toString' });
+ const raw = JSON.stringify({ ...custom, template: '__proto__', details: 'constructor', brand: 'toString' });
expect(parseConfig(raw)).toEqual({
...custom,
- network: DEFAULT_CONFIG.network,
template: DEFAULT_CONFIG.template,
+ details: DEFAULT_CONFIG.details,
brand: DEFAULT_CONFIG.brand,
});
});
@@ -137,21 +156,29 @@ describe('parseConfig / serializeConfig', () => {
const raw = JSON.stringify({ ...custom, evil: '