diff --git a/src/lib/components/NoteGrid.svelte b/src/lib/components/NoteGrid.svelte
index 8d4c22d..0f7e93a 100644
--- a/src/lib/components/NoteGrid.svelte
+++ b/src/lib/components/NoteGrid.svelte
@@ -1,23 +1,62 @@
-{#if notes.length > 0}
+{#if displayItems.length > 0}
{#if label}
{label}
{/if}
-
- {#each notes as note (note.id)}
-
- {/each}
-
+ {#if draggable}
+
+ {#each localItems as note (note.id)}
+
+
+
+ {/each}
+
+ {:else}
+
+ {#each displayItems as note (note.id)}
+
+ {/each}
+
+ {/if}
{/if}
diff --git a/src/lib/components/SortSelector.svelte b/src/lib/components/SortSelector.svelte
new file mode 100644
index 0000000..10c5acc
--- /dev/null
+++ b/src/lib/components/SortSelector.svelte
@@ -0,0 +1,29 @@
+
+
+
+ {#each modes as mode}
+
+ {/each}
+
diff --git a/src/lib/stores/notes.ts b/src/lib/stores/notes.ts
index 0ac3aad..9d580a2 100644
--- a/src/lib/stores/notes.ts
+++ b/src/lib/stores/notes.ts
@@ -3,6 +3,7 @@ import type { Note, NoteFilter, NoteCreate, NoteUpdate } from '$lib/types/index.
import { addToSyncQueue, putNote, deleteNoteFromIdb, getAllNotes, clearNotes as clearIdbNotes } from '$lib/sync/idb.js';
import { showToast } from '$lib/stores/toast.js';
import { extractTags } from '$lib/utils/tags.js';
+import { sortMode, type SortMode } from '$lib/stores/sort.js';
export const notes = writable([]);
export const currentFilter = writable('all');
@@ -27,12 +28,25 @@ export const filteredNotes = derived(
}
);
-export const pinnedNotes = derived(filteredNotes, ($notes) =>
- $notes.filter((n) => n.pinned)
+function sortNotes(list: Note[], mode: SortMode): Note[] {
+ return [...list].sort((a, b) => {
+ if (mode === 'created') {
+ return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime();
+ }
+ if (mode === 'custom') {
+ return a.sortOrder - b.sortOrder;
+ }
+ // default: updated
+ return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
+ });
+}
+
+export const pinnedNotes = derived([filteredNotes, sortMode], ([$notes, $sortMode]) =>
+ sortNotes($notes.filter((n) => n.pinned), $sortMode)
);
-export const unpinnedNotes = derived(filteredNotes, ($notes) =>
- $notes.filter((n) => !n.pinned)
+export const unpinnedNotes = derived([filteredNotes, sortMode], ([$notes, $sortMode]) =>
+ sortNotes($notes.filter((n) => !n.pinned), $sortMode)
);
export const allTags = derived(notes, ($notes) => {
@@ -199,3 +213,25 @@ export async function unarchiveNote(id: string): Promise {
export async function togglePin(id: string, currentPinned: boolean): Promise {
return updateNote(id, { pinned: !currentPinned });
}
+
+export async function updateSortOrders(orders: { id: string; sortOrder: number }[]): Promise {
+ // Optimistic update
+ notes.update((list) =>
+ list.map((n) => {
+ const order = orders.find((o) => o.id === n.id);
+ return order ? { ...n, sortOrder: order.sortOrder } : n;
+ })
+ );
+
+ try {
+ const res = await fetch('/api/notes/reorder', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ orders })
+ });
+ return res.ok;
+ } catch {
+ // Offline — optimistic update stands
+ return false;
+ }
+}
diff --git a/src/lib/stores/sort.ts b/src/lib/stores/sort.ts
new file mode 100644
index 0000000..d037410
--- /dev/null
+++ b/src/lib/stores/sort.ts
@@ -0,0 +1,24 @@
+import { writable } from 'svelte/store';
+import { browser } from '$app/environment';
+
+export type SortMode = 'updated' | 'created' | 'custom';
+
+const STORAGE_KEY = 'crumbs-sort-mode';
+
+function getInitial(): SortMode {
+ if (browser) {
+ const stored = localStorage.getItem(STORAGE_KEY);
+ if (stored === 'updated' || stored === 'created' || stored === 'custom') {
+ return stored;
+ }
+ }
+ return 'updated';
+}
+
+export const sortMode = writable(getInitial());
+
+if (browser) {
+ sortMode.subscribe((value) => {
+ localStorage.setItem(STORAGE_KEY, value);
+ });
+}
diff --git a/src/routes/(app)/+page.svelte b/src/routes/(app)/+page.svelte
index 6b66013..c8249dd 100644
--- a/src/routes/(app)/+page.svelte
+++ b/src/routes/(app)/+page.svelte
@@ -2,7 +2,9 @@
import NoteGrid from '$lib/components/NoteGrid.svelte';
import NoteEditor from '$lib/components/NoteEditor.svelte';
import TagFilter from '$lib/components/TagFilter.svelte';
- import { pinnedNotes, unpinnedNotes, selectedTag, currentFilter, notes, notesLoaded } from '$lib/stores/notes.js';
+ import SortSelector from '$lib/components/SortSelector.svelte';
+ import { pinnedNotes, unpinnedNotes, selectedTag, currentFilter, notes, notesLoaded, updateSortOrders } from '$lib/stores/notes.js';
+ import { sortMode } from '$lib/stores/sort.js';
import { onMount } from 'svelte';
import { get } from 'svelte/store';
import type { Note } from '$lib/types/index.js';
@@ -22,6 +24,11 @@
history.replaceState(null, '', location.pathname);
}
+ function handleReorder(noteIds: string[]) {
+ const orders = noteIds.map((id, index) => ({ id, sortOrder: index }));
+ updateSortOrders(orders);
+ }
+
onMount(() => {
const hash = location.hash.slice(1);
if (!hash) return;
@@ -53,9 +60,16 @@
{/if}
-
-
-
+{#if $currentFilter === 'all'}
+
+
+
+
+{:else}
+
+
+
+{/if}
{#if $currentFilter === 'archived'}
Archive
@@ -65,7 +79,7 @@
{#if $pinnedNotes.length > 0}
-
+
{/if}
@@ -73,6 +87,8 @@
notes={$unpinnedNotes}
label={$pinnedNotes.length > 0 ? 'Others' : ''}
onEdit={openEditor}
+ draggable={$sortMode === 'custom'}
+ onReorder={handleReorder}
/>
{#if $notesLoaded && $pinnedNotes.length === 0 && $unpinnedNotes.length === 0}
diff --git a/src/routes/api/notes/reorder/+server.ts b/src/routes/api/notes/reorder/+server.ts
new file mode 100644
index 0000000..94550a2
--- /dev/null
+++ b/src/routes/api/notes/reorder/+server.ts
@@ -0,0 +1,22 @@
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from './$types.js';
+import { db } from '$lib/server/db/index.js';
+import { notes } from '$lib/server/db/schema.js';
+import { eq } from 'drizzle-orm';
+
+export const POST: RequestHandler = async ({ request }) => {
+ const body = await request.json();
+ const orders: { id: string; sortOrder: number }[] = body.orders;
+
+ if (!Array.isArray(orders)) {
+ return json({ error: 'Invalid payload' }, { status: 400 });
+ }
+
+ db.transaction((tx) => {
+ for (const { id, sortOrder } of orders) {
+ tx.update(notes).set({ sortOrder }).where(eq(notes.id, id)).run();
+ }
+ });
+
+ return json({ ok: true });
+};
diff --git a/tests/e2e/organization.spec.ts b/tests/e2e/organization.spec.ts
index 73e8d30..041da9b 100644
--- a/tests/e2e/organization.spec.ts
+++ b/tests/e2e/organization.spec.ts
@@ -68,3 +68,64 @@ test.describe('Organization Features', () => {
}
});
});
+
+test.describe('Note Sorting', () => {
+ test('Scenario: Notes can be sorted by last updated', async ({ authenticatedPage: page }) => {
+ // Given two notes exist with different update times
+ await page.getByTestId('new-note-btn').click();
+ await page.getByTestId('note-title-input').fill('Sort-First');
+ await page.getByTestId('close-editor-btn').click();
+ await expect(noteCard(page, 'Sort-First')).toBeVisible();
+
+ await page.getByTestId('new-note-btn').click();
+ await page.getByTestId('note-title-input').fill('Sort-Second');
+ await page.getByTestId('close-editor-btn').click();
+ await expect(noteCard(page, 'Sort-Second')).toBeVisible();
+
+ // When the user selects "Updated" sort mode
+ await page.getByTestId('sort-updated').click();
+
+ // Then Sort-Second (most recently updated) appears before Sort-First
+ const sortFirst = noteCard(page, 'Sort-First');
+ const sortSecond = noteCard(page, 'Sort-Second');
+ const firstBox = await sortSecond.boundingBox();
+ const secondBox = await sortFirst.boundingBox();
+ expect(firstBox!.y).toBeLessThanOrEqual(secondBox!.y);
+ });
+
+ test('Scenario: Notes can be sorted by creation date', async ({ authenticatedPage: page }) => {
+ // Given two notes exist
+ await page.getByTestId('new-note-btn').click();
+ await page.getByTestId('note-title-input').fill('Created-Older');
+ await page.getByTestId('close-editor-btn').click();
+ await expect(noteCard(page, 'Created-Older')).toBeVisible();
+
+ await page.getByTestId('new-note-btn').click();
+ await page.getByTestId('note-title-input').fill('Created-Newer');
+ await page.getByTestId('close-editor-btn').click();
+ await expect(noteCard(page, 'Created-Newer')).toBeVisible();
+
+ // When the user selects "Created" sort mode
+ await page.getByTestId('sort-created').click();
+
+ // Then Created-Newer appears before Created-Older
+ const newer = noteCard(page, 'Created-Newer');
+ const older = noteCard(page, 'Created-Older');
+ const newerBox = await newer.boundingBox();
+ const olderBox = await older.boundingBox();
+ expect(newerBox!.y).toBeLessThanOrEqual(olderBox!.y);
+ });
+
+ test('Scenario: Sort preference persists across page reload', async ({ authenticatedPage: page }) => {
+ // Given the user selects "Created" sort mode
+ await page.getByTestId('sort-created').click();
+
+ // When the page is reloaded
+ await page.reload();
+ await page.waitForLoadState('networkidle');
+
+ // Then the "Created" sort mode is still active
+ const createdBtn = page.getByTestId('sort-created');
+ await expect(createdBtn).toHaveClass(/bg-\[var\(--primary\)\]/);
+ });
+});