Skip to content

Commit 8be769a

Browse files
committed
Merge branch 'cardsboard' of https://github.com/edufeed-org/kanban-editor into cardsboard
2 parents 509c49e + 9e0c6d1 commit 8be769a

File tree

5 files changed

+193
-33
lines changed

5 files changed

+193
-33
lines changed

src/lib/stores/kanbanStore.svelte.ts

Lines changed: 167 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Board, Chat, type CardProps, type ColumnProps, type BoardProps } from '../classes/BoardModel.js';
1+
import { Board, Card, Chat, type CardProps, type ColumnProps, type BoardProps } from '../classes/BoardModel.js';
22
import { BoardRole, type BoardShare } from '../types/sharing.js';
33
import { authStore } from './authStore.svelte.js';
44
import { settingsStore } from './settingsStore.svelte.js';
@@ -69,8 +69,15 @@ export class BoardStore {
6969

7070
private initializeBoard(): void {
7171
const currentBoardId = this.board.id;
72+
73+
// ⚠️ FIX: Don't save placeholder board!
74+
if (currentBoardId === 'placeholder-board') {
75+
console.log('📝 Placeholder board active - nicht speichern');
76+
return;
77+
}
78+
7279
if (!this.boardIds.includes(currentBoardId)) {
73-
console.log('🔥 Erstes Laden: Füge Default Board zur Liste hinzu:', currentBoardId);
80+
console.log('🔥 Erstes Laden: Füge Board zur Liste hinzu:', currentBoardId);
7481
this.boardIds = [...this.boardIds, currentBoardId];
7582
// BoardStorage.saveBoardIds() removed - deprecated, auto-discovered from localStorage
7683
this.saveToStorage();
@@ -254,8 +261,68 @@ export class BoardStore {
254261
const boardIds = BoardStorage.loadBoardIds();
255262

256263
if (boardIds.length === 0) {
257-
console.log('📝 Keine Boards gefunden, erstelle Default-Board');
258-
return BoardStorage.createDefaultBoard();
264+
// ⚠️ FIX: Anonymous users should load demo board directly!
265+
// Authenticated users get placeholder to create their first board
266+
const currentUserPubkey = this.getCurrentUserPubkey();
267+
const isAnonymous = !currentUserPubkey;
268+
269+
if (isAnonymous) {
270+
// Anonymous user: Load demo board directly
271+
console.log('📝 Keine Boards gefunden - lade Demo-Board für anonymen Benutzer');
272+
const demoBoardId = 'demo-board';
273+
let demoBoard = BoardStorage.loadBoard(demoBoardId);
274+
275+
if (!demoBoard) {
276+
// Demo board doesn't exist yet - create it
277+
demoBoard = this.createDefaultDemoBoard();
278+
BoardStorage.saveBoard(demoBoard);
279+
}
280+
281+
// Check if we should load from configured source
282+
const sourceAddress = settingsStore.settings.demoBoardSourceAddress;
283+
if (sourceAddress && (
284+
demoBoard.name === '🎯 Demo-Board - Testen Sie die App!' ||
285+
demoBoard.name === '⏳ Demo-Board wird geladen...'
286+
)) {
287+
// Set loading placeholder
288+
demoBoard.name = '⏳ Demo-Board wird geladen...';
289+
demoBoard.description = 'Das Demo-Board wird von der konfigurierten Quelle geladen. Bitte warten Sie einen Moment.';
290+
BoardStorage.saveBoard(demoBoard);
291+
292+
// Load asynchronously in background
293+
this.demoBoardLoadInProgress = true;
294+
this.loadDemoBoardFromSourceAsync(sourceAddress).then(loadedBoard => {
295+
if (loadedBoard) {
296+
BoardStorage.saveBoard(loadedBoard);
297+
this.board = loadedBoard;
298+
this._columnOrder = loadedBoard.columns.map(c => c.id);
299+
this.demoBoardLoadInProgress = false;
300+
this.triggerUpdate();
301+
} else {
302+
this.demoBoardLoadInProgress = false;
303+
}
304+
}).catch(() => {
305+
this.demoBoardLoadInProgress = false;
306+
});
307+
}
308+
309+
return demoBoard;
310+
} else {
311+
// Authenticated user: Create placeholder to prompt board creation
312+
console.log('📝 Keine Boards gefunden - erstelle Platzhalter-Board für authentifizierten Benutzer');
313+
const placeholder = new Board({
314+
id: 'placeholder-board',
315+
name: 'Willkommen',
316+
description: 'Erstellen Sie Ihr erstes Board',
317+
author: currentUserPubkey || 'anonymous',
318+
columns: []
319+
});
320+
// Add default columns for tests compatibility
321+
placeholder.addColumn({ name: 'To Do', color: 'blue' });
322+
placeholder.addColumn({ name: 'In Progress', color: 'orange' });
323+
placeholder.addColumn({ name: 'Done', color: 'green' });
324+
return placeholder;
325+
}
259326
}
260327

261328
const boards = BoardStorage.getAllBoardsMetadata(boardIds);
@@ -278,11 +345,36 @@ export class BoardStore {
278345
}
279346
}
280347

281-
console.log('⚠️ Keine Boards gefunden, erstelle Default-Board');
282-
return BoardStorage.createDefaultBoard();
348+
// ⚠️ FIX: This should not happen (boards.length > 0 but couldn't load any)
349+
// Return placeholder instead of creating default board
350+
console.log('⚠️ Boards in list but couldn\'t load any - erstelle Platzhalter');
351+
return new Board({
352+
id: 'placeholder-board',
353+
name: 'Willkommen',
354+
description: 'Erstellen Sie Ihr erstes Board oder laden Sie die Demo',
355+
author: 'anonymous',
356+
columns: []
357+
});
283358
}
284359

285360
private saveToStorage(): void {
361+
// ⚠️ FIX: Don't save placeholder board UNLESS it has been modified with real data
362+
// A pristine placeholder board isn't worth saving, but if it has:
363+
// - Different name, OR
364+
// - Maintainers, OR
365+
// - Different column structure than default (3 cols: To Do, In Progress, Done)
366+
// then it's been modified and should be saved
367+
if (this.board.id === 'placeholder-board' &&
368+
this.board.name === 'Willkommen' &&
369+
(!this.board.maintainers || this.board.maintainers.length === 0) &&
370+
this.board.columns.length === 3 &&
371+
this.board.columns[0].name === 'To Do' &&
372+
this.board.columns[1].name === 'In Progress' &&
373+
this.board.columns[2].name === 'Done') {
374+
console.log('⏭️ Skipping save for unmodified placeholder board');
375+
return;
376+
}
377+
286378
BoardStorage.saveBoard(this.board);
287379
console.log(`💾 Saved board "${this.board.name}" with lastAccessedAt:`, this.board.lastAccessedAt);
288380
}
@@ -338,8 +430,8 @@ export class BoardStore {
338430
return this.getDemoBoardsForAnonymousUser();
339431
}
340432

341-
// ⚡ FIX: Authentifizierte Benutzer - Demo-Board explizit ausschließen
342-
const filteredBoardIds = this.boardIds.filter(id => id !== 'demo-board');
433+
// ⚡ FIX: Authentifizierte Benutzer - Demo-Board und Placeholder explizit ausschließen
434+
const filteredBoardIds = this.boardIds.filter(id => id !== 'demo-board' && id !== 'placeholder-board');
343435
const allBoards = BoardStorage.getAllBoardsMetadata(filteredBoardIds);
344436

345437
// 🔍 DEBUG: Log all boards and their authors
@@ -3177,6 +3269,24 @@ export class BoardStore {
31773269
// DEMO BOARD & USER MANAGEMENT
31783270
// ============================================================================
31793271

3272+
/**
3273+
* Invalidiert den Demo-Board Cache und erzwingt Neuladen beim nächsten Zugriff
3274+
* Wird z.B. beim Logout aufgerufen, um sicherzustellen dass anonyme User
3275+
* immer ein frisches Demo-Board von der Quelle erhalten
3276+
*/
3277+
public invalidateDemoBoardCache(): void {
3278+
const demoBoardId = 'demo-board';
3279+
// Setze Demo-Board auf Platzhalter-Name, um Reload zu triggern
3280+
const demoBoard = BoardStorage.loadBoard(demoBoardId);
3281+
if (demoBoard) {
3282+
demoBoard.name = '⏳ Demo-Board wird geladen...';
3283+
BoardStorage.saveBoard(demoBoard);
3284+
console.log('🔄 Demo-Board Cache invalidiert - wird beim nächsten Zugriff neu geladen');
3285+
}
3286+
// Reset load-in-progress flag falls es hängen geblieben ist
3287+
this.demoBoardLoadInProgress = false;
3288+
}
3289+
31803290
/**
31813291
* Erstellt oder lädt Demo-Board für anonyme Benutzer
31823292
*/
@@ -3245,12 +3355,15 @@ export class BoardStore {
32453355
BoardStorage.saveBoard(demoBoard);
32463356
}
32473357

3248-
// 🎯 WICHTIG: Immer frisch aus storage laden um neueste Version zu haben
3249-
// (falls async update stattgefunden hat)
3250-
const freshDemoBoard = BoardStorage.loadBoard(demoBoardId);
3251-
if (freshDemoBoard) {
3252-
demoBoard = freshDemoBoard;
3358+
// 🎯 WICHTIG: Nur frisch aus storage laden wenn NICHT gerade geladen wird
3359+
// Wenn async loading in progress ist, würden wir sonst das leere Platzhalter-Board zurückgeben!
3360+
if (!this.demoBoardLoadInProgress) {
3361+
const freshDemoBoard = BoardStorage.loadBoard(demoBoardId);
3362+
if (freshDemoBoard) {
3363+
demoBoard = freshDemoBoard;
3364+
}
32533365
}
3366+
// else: Keep existing demoBoard reference while async loading completes
32543367

32553368
return [{
32563369
id: demoBoard.id,
@@ -3359,6 +3472,9 @@ export class BoardStore {
33593472
// Board aus Event konvertieren
33603473
const boardProps = nostrEventToBoard(boardEvent);
33613474

3475+
console.log(`📋 Board hat ${boardProps.columns?.length || 0} Spalten:`,
3476+
boardProps.columns?.map(c => `${c.name} (${c.id})`) || []);
3477+
33623478
// Board mit neuer Demo-ID erstellen
33633479
// Use a valid dummy pubkey (64 zeros) instead of 'demo' to avoid Nostr validation errors
33643480
const board = new Board({
@@ -3382,37 +3498,57 @@ export class BoardStore {
33823498

33833499
console.log(`✅ ${cardEventArray.length} Card Events gefunden`);
33843500

3385-
// Cards zum Board hinzufügen
3501+
if (cardEventArray.length === 0) return board;
3502+
3503+
// ⚠️ CRITICAL: Reuse exact logic from working [naddr]/+page.svelte
3504+
// DON'T use addCard() - it does array reassignment which doesn't work before board is in store!
3505+
// Instead: Create Card instance and push directly to column.cards array
33863506
for (const cardEvent of cardEventArray) {
33873507
try {
3388-
const cardProps = nostrEventToCard(cardEvent as any) as any;
3508+
const cardProps = nostrEventToCard(cardEvent as any) as CardProps & { columnName?: string };
33893509
if (!cardProps.id) continue;
3390-
3391-
// Finde Spalte
3510+
3511+
// Finde oder erstelle Spalte (columnName kommt via @ts-ignore aus nostrEventToCard)
33923512
const columnName = cardProps.columnName || 'To Do';
33933513
let column = board.columns.find(c => c.name === columnName);
33943514

33953515
if (!column) {
3396-
// Spalte existiert nicht, erstelle sie
3516+
// Spalte existiert nicht im Board Event, erstelle sie
33973517
column = board.addColumn({ name: columnName });
33983518
console.log(`📁 Spalte erstellt: ${columnName}`);
33993519
}
3400-
3401-
// Card mit Demo-Attribution hinzufügen
3402-
column.addCard({
3403-
...cardProps,
3404-
author: '0000000000000000000000000000000000000000000000000000000000000000', // Valid hex pubkey
3405-
authorName: 'Demo User'
3406-
});
3520+
3521+
// Prüfe ob Card bereits existiert
3522+
const existingCard = column.findCard(cardProps.id);
3523+
if (existingCard) {
3524+
// Update existierende Card (LWW)
3525+
const existingTime = existingCard.updatedAt ? new Date(existingCard.updatedAt).getTime() : 0;
3526+
const newTime = cardProps.updatedAt ? new Date(cardProps.updatedAt).getTime() : 0;
3527+
3528+
if (newTime > existingTime) {
3529+
existingCard.update(cardProps);
3530+
}
3531+
} else {
3532+
// ⚠️ CRITICAL: Use same pattern as [naddr]/+page.svelte
3533+
// Create Card and push to array directly (NOT addCard which does reassignment)
3534+
const card = new Card({
3535+
...cardProps,
3536+
author: '0000000000000000000000000000000000000000000000000000000000000000',
3537+
authorName: 'Demo User'
3538+
});
3539+
column.cards.push(card);
3540+
}
34073541
} catch (error) {
3408-
console.warn('⚠️ Fehler beim Hinzufügen einer Card:', error);
3542+
console.warn('⚠️ Fehler beim Verarbeiten von Card Event:', error);
34093543
}
34103544
}
3411-
3412-
// ⚡ CRITICAL: Sortiere Cards nach rank pro Spalte!
3413-
// ndk.fetchEvents() liefert keine garantierte Reihenfolge.
3414-
for (const col of board.columns) {
3415-
col.cards.sort((a: any, b: any) => {
3545+
3546+
// ⚡ CRITICAL: Sortiere Cards nach Rank pro Spalte!
3547+
// ndk.fetchEvents() liefert ein Set ohne garantierte Reihenfolge.
3548+
// Ohne Sortierung hängt die Card-Reihenfolge davon ab, welcher Relay
3549+
// zuerst antwortet → unterschiedliche Reihenfolge auf verschiedenen Browsern.
3550+
for (const column of board.columns) {
3551+
column.cards.sort((a: any, b: any) => {
34163552
const rankA = a.rank ?? Number.MAX_SAFE_INTEGER;
34173553
const rankB = b.rank ?? Number.MAX_SAFE_INTEGER;
34183554
return rankA - rankB;

src/routes/cardsboard/BoardsList.svelte

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,16 @@
247247
*/
248248
function navigateToBoardUrl() {
249249
const board = boardStore.data;
250-
if (board?.author) {
250+
251+
// Demo-Board Check: Keine naddr-URL für Demo-Boards erstellen
252+
const isDemoBoard = board?.id === 'demo-board' ||
253+
board?.author === 'demo' ||
254+
board?.author === '0000000000000000000000000000000000000000000000000000000000000000';
255+
256+
if (isDemoBoard) {
257+
// Demo-Board: Bleibe auf /cardsboard/ (keine naddr)
258+
goto('/cardsboard/', { replaceState: true });
259+
} else if (board?.author) {
251260
try {
252261
const naddrUrl = createBoardNaddrUrl(board.id, board.author);
253262
goto(naddrUrl, { replaceState: true });

src/routes/cardsboard/LeftSidebarFooter.svelte

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import * as Avatar from "$lib/components/ui/avatar/index.js";
88
import * as DropdownMenu from "$lib/components/ui/dropdown-menu/index.js";
99
import * as Dialog from "$lib/components/ui/dialog/index.js";
10+
import { goto } from "$app/navigation";
1011
import { settingsStore } from "$lib/stores/settingsStore.svelte.js";
1112
import { authStore } from "$lib/stores/authStore.svelte.js";
13+
import { boardStore } from "$lib/stores/kanbanStore.svelte.js";
1214
import LoginDialog from "./LoginDialog.svelte";
1315
import SettingsDialog from "./SettingsDialog.svelte";
1416
import SettingsPanel from "$lib/components/settings/SettingsPanel.svelte";
@@ -49,7 +51,14 @@
4951
5052
async function handleLogout() {
5153
authStore.logout();
54+
// Invalidate demo board cache so anonymous users get fresh board from source
55+
boardStore.invalidateDemoBoardCache();
5256
loginDialogOpen = false;
57+
58+
// Redirect to main cardsboard if on naddr path
59+
if (typeof window !== 'undefined' && window.location.pathname.includes('/cardsboard/naddr')) {
60+
goto('/cardsboard');
61+
}
5362
}
5463
5564
async function handleDemoSession() {

src/routes/willkommen/+page.svelte

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { Button } from "$lib/components/ui/button/index.js";
88
import { Badge } from "$lib/components/ui/badge/index.js";
99
import * as Card from "$lib/components/ui/card/index.js";
10+
import { boardStore } from "$lib/stores/kanbanStore.svelte.js";
1011
1112
import ArrowRightIcon from "@lucide/svelte/icons/arrow-right";
1213
import BookOpenIcon from "@lucide/svelte/icons/book-open";
@@ -37,7 +38,12 @@
3738
3839
async function handleLogout() {
3940
authStore.logout();
41+
// Invalidate demo board cache so anonymous users get fresh board from source
42+
boardStore.invalidateDemoBoardCache();
4043
loginDialogOpen = false;
44+
45+
// Redirect to cardsboard after logout
46+
goto('/cardsboard');
4147
}
4248
4349
function handleGoToBoards() {

static/config.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@
5353
},
5454

5555
"demoBoard": {
56-
"sourceAddress": "naddr1qvzqqqrkt5pzpt8g7spkqtvt5wqau6xx2g98ptyyydzqetg7h47928hdml6f7stjqprxymmpwfjz6etxvd3rvdtrxcmrwcfkxvensdnxxgckye35vscn2d3j8quxzdpkxu6nxdrzvycxxwfjv56nge35vc6kgepnxvcrwd3hxf3rxepnxu6s7kr5h0",
56+
"sourceAddress": null,
5757
"$comment": "⚙️ Optional: Nostr address (naddr) of a board to use as demo template. If null, uses default demo board. Example: 'naddr1...'"
5858
},
5959

0 commit comments

Comments
 (0)