Gornergrat Sunrise (Jul 11) -- Take the first train up at 7am, hike down.
Matterhorn Base Camp (Jul 12) -- Full day trek to Hornlihutte. 1500m gain.
Tre Cime di Lavaredo (Jul 15) -- Classic loop, ~4h.
Seceda Ridgeline (Jul 17) -- Gondola up, ridge walk, hike down to Ortisei.
Adventure Activities
Via Ferrata at Aiguille du Midi (Jul 8) -- Rent harness + lanyard + helmet, ~EUR 25/day
Paragliding over Zermatt (Jul 13) -- Tandem flights ~EUR 180pp
Kayaking at Lago di Braies (Jul 16) -- Turquoise glacial lake, ~EUR 15/hour
Rest Days
Jul 9: Explore Chamonix town, gear shopping
Jul 19: Free day before flying home, packing
`,
content_plain: "Detailed activity planning including hiking routes with difficulty ratings, adventure activities with costs, and rest day plans.",
+ content_format: 'html',
type: "NOTE", tags: ["hiking", "activities", "adventure"], is_pinned: false,
created_at: new Date(now - 10 * day).toISOString(), updated_at: new Date(now - 5 * hour).toISOString(),
},
{
id: "demo-note-4", title: "Gear Research",
- content: `## Gear Research
-
-### Via Ferrata Kit
-Need harness + lanyard + helmet. Can rent in Chamonix for ~EUR 25/day per person. Renting is better than buying for a one-time activity.
-
-### Paragliding
-Tandem flights in Zermatt: ~EUR 180pp. Book at **Paragliding Zermatt** (best reviews on TripAdvisor). They offer morning flights with better thermals.
-
-### Camera & Drone
-- Bring the **DJI Mini 4 Pro** for Tre Cime and Seceda
-- Check Italian drone regulations! Need ENAC registration for flights over 250g
-- ND filters for long exposure water shots at Lago di Braies
-- Extra batteries (3x) -- cold altitude drains them fast
-
-> Liam scored the DJI Mini 4 Pro over GoPro Hero 12 and Sony A7C II in our decision matrix. Best weight-to-quality ratio for hiking.
-
-### Group Gear (Shared)
-\`\`\`
-Item Cost Status Owner
--------------------------- ------- ---------- -----
-Adventure First-Aid Kit EUR 85 Funded Omar
-Water Filter (Sawyer) EUR 45 Funded Maya
-Bear Canisters 2x (BV500) EUR 120 In Cart Liam
-Camp Stove + Fuel EUR 65 Funded Priya
-DJI Mini 4 Pro Rental EUR 350 Needs Fund Liam
-Starlink Mini Rental EUR 200 Needs Fund Omar
-\`\`\`
-
-### Personal Gear Checklist
-- Hiking boots (broken in!)
-- Rain jacket (waterproof, not just resistant)
-- Headlamp + spare batteries
-- Trekking poles (collapsible for flights)
-- Sunscreen SPF 50 + lip balm
-- Wool base layers for hut nights`,
- content_plain: "Gear research including Via Ferrata rental, paragliding booking, camera and drone regulations, shared group gear status, and personal gear checklist.",
+ content: `
Gear Research
Via Ferrata Kit
Need harness + lanyard + helmet. Can rent in Chamonix for ~EUR 25/day per person.
Camera & Drone
Bring the DJI Mini 4 Pro for Tre Cime and Seceda
Check Italian drone regulations! Need ENAC registration for flights over 250g
ND filters for long exposure water shots at Lago di Braies
Extra batteries (3x) -- cold altitude drains them fast
Personal Gear Checklist
Hiking boots (broken in!)
Rain jacket (waterproof, not just resistant)
Headlamp + spare batteries
Trekking poles (collapsible for flights)
Sunscreen SPF 50 + lip balm
Wool base layers for hut nights
`,
+ content_plain: "Gear research including Via Ferrata rental, camera and drone regulations, shared group gear status, and personal gear checklist.",
+ content_format: 'html',
type: "NOTE", tags: ["gear", "equipment", "budget"], is_pinned: false,
created_at: new Date(now - 8 * day).toISOString(), updated_at: new Date(now - 2 * hour).toISOString(),
},
{
id: "demo-note-5", title: "Emergency Contacts & Safety",
- content: `## Emergency Contacts & Safety
-
-### Emergency Numbers
-- **France**: 112 (EU general), PGHM Mountain Rescue: +33 4 50 53 16 89
-- **Switzerland**: 1414 (REGA air rescue), 144 (ambulance)
-- **Italy**: 118 (medical), 112 (general emergency)
-
-### Insurance
-- Policy #: **WA-2026-7891**
-- Emergency line: +1-800-555-0199
-- Covers: mountain rescue, helicopter evacuation, medical repatriation
-- Deductible: EUR 150 per incident
-
-### Altitude Sickness Protocol
-1. Acclimatize in Chamonix (1,035m) for 2 days before going high
-2. Stay hydrated -- minimum 3L water per day above 2,500m
-3. Watch for symptoms: headache, nausea, dizziness
-4. Descend immediately if symptoms worsen
-5. Omar packed altitude sickness medication (Diamox) in the first-aid kit
-
-### Weather Contingency
-> If the Matterhorn trek gets rained out, Omar found a cheese museum in Zermatt as backup. Priya suggests the Glacier Paradise instead.
-
-- Check MeteoSwiss and Meteotrentino daily
-- Alpine weather changes fast -- always carry rain gear
-- Lightning protocol: descend ridgelines immediately, avoid isolated trees
-
-### Emergency Meeting Points
-\`\`\`
-Chamonix: Place Balmat (town center)
-Zermatt: Bahnhofplatz (train station)
-Dolomites: Rifugio Auronzo parking lot
-\`\`\``,
- content_plain: "Emergency contacts for France, Switzerland, and Italy. Insurance details, altitude sickness protocol, weather contingency plans, and emergency meeting points.",
+ content: `
Switzerland: 1414 (REGA air rescue), 144 (ambulance)
Italy: 118 (medical), 112 (general emergency)
Insurance
Policy #: WA-2026-7891
Emergency line: +1-800-555-0199
Covers: mountain rescue, helicopter evacuation, medical repatriation
Altitude Sickness Protocol
Acclimatize in Chamonix (1,035m) for 2 days before going high
Stay hydrated -- minimum 3L water per day above 2,500m
Watch for symptoms: headache, nausea, dizziness
Descend immediately if symptoms worsen
`,
+ content_plain: "Emergency contacts for France, Switzerland, and Italy. Insurance details, altitude sickness protocol, weather contingency plans.",
+ content_format: 'html',
type: "NOTE", tags: ["safety", "emergency", "contacts"], is_pinned: false,
created_at: new Date(now - 7 * day).toISOString(), updated_at: new Date(now - 6 * hour).toISOString(),
},
{
id: "demo-note-6", title: "Photo Spots & Creative Plan",
- content: `## Photo Spots & Creative Plan
-
-### Must-Capture Locations
-1. **Lac Blanc** -- Reflection of Mont Blanc at sunrise. Arrive by 5:30am. Tripod essential.
-2. **Gornergrat Panorama** -- 360-degree view with Matterhorn. Golden hour is best.
-3. **Tre Cime from Rifugio Locatelli** -- The iconic three peaks at golden hour. Drone shots here.
-4. **Seceda Ridgeline** -- Dramatic Dolomite spires. Best drone footage location.
-5. **Lago di Braies** -- Turquoise water, use ND filters for long exposure reflections.
-
-### Drone Shot List (Liam)
-- Tre Cime circular orbit (check wind < 20km/h)
-- Seceda ridge reveal shot (low to high)
-- Lago di Braies top-down turquoise pattern
-- Matterhorn time-lapse from Gornergrat (if weather permits)
-
-### Zine Plan (Maya)
-We are making an **Alpine Explorer Zine** after the trip:
-- Format: A5 risograph, 50 copies
-- Print at Chamonix Print Collective
-- Content: best photos, trail notes, hand-drawn maps
-- Price: EUR 12 per copy on rCart
-
-> Bring ND filters for long exposure water shots. Pack the circular polarizer for cutting glare on alpine lakes.
-
-### Video Plan
-- Daily 30-second clips for trip recap
-- Lac Blanc sunrise test footage already uploaded to rTube (3:42)
-- Final edit: 5-8 minute highlight reel`,
+ content: `
Photo Spots & Creative Plan
Must-Capture Locations
Lac Blanc -- Reflection of Mont Blanc at sunrise. Arrive by 5:30am. Tripod essential.
Gornergrat Panorama -- 360-degree view with Matterhorn. Golden hour is best.
Tre Cime from Rifugio Locatelli -- The iconic three peaks at golden hour. Drone shots here.
Seceda Ridgeline -- Dramatic Dolomite spires. Best drone footage location.
Lago di Braies -- Turquoise water, use ND filters for long exposure reflections.
Zine Plan (Maya)
We are making an Alpine Explorer Zine after the trip:
Format: A5 risograph, 50 copies
Print at Chamonix Print Collective
Content: best photos, trail notes, hand-drawn maps
Price: EUR 12 per copy on rCart
`,
content_plain: "Photography and creative plan including must-capture locations, drone shot list, zine production details, and video plan.",
+ content_format: 'html',
type: "NOTE", tags: ["photography", "creative", "planning"], is_pinned: false,
created_at: new Date(now - 5 * day).toISOString(), updated_at: new Date(now - 4 * hour).toISOString(),
},
@@ -309,130 +188,25 @@ We are making an **Alpine Explorer Zine** after the trip:
const packingNotes: Note[] = [
{
id: "demo-note-7", title: "Packing Checklist",
- content: `## Packing Checklist
-
-### Footwear
-- [x] Hiking boots (broken in!)
-- [ ] Camp sandals / flip-flops
-- [ ] Extra laces
-
-### Clothing
-- [x] Rain jacket (Gore-Tex)
-- [ ] Down jacket for hut nights
-- [ ] 3x wool base layers
-- [ ] 2x hiking pants
-- [ ] Sun hat + warm beanie
-- [ ] Gloves (lightweight)
-
-### Gear
-- [x] Headlamp + spare batteries
-- [ ] Trekking poles (collapsible)
-- [x] First aid kit
-- [ ] Sunscreen SPF 50
-- [ ] Water filter (Sawyer Squeeze)
-- [ ] Dry bags (2x)
-- [ ] Repair kit (duct tape, zip ties)
-
-### Electronics
-- [ ] Camera + 3 batteries
-- [ ] Drone + 3 batteries (Liam)
-- [x] Power bank (20,000mAh)
-- [ ] Universal adapter (EU plugs)
-
-### Documents
-- [x] Passports
-- [ ] Travel insurance printout
-- [x] Hut reservation confirmations
-- [ ] Italian drone registration
-- [ ] Emergency contacts card
-
-### Food & Water
-- [ ] Reusable water bottles (1L each)
-- [ ] Trail snacks (energy bars, nuts)
-- [ ] Electrolyte tablets
-- [ ] Coffee/tea for hut mornings`,
- content_plain: "Complete packing checklist organized by category: footwear, clothing, gear, electronics, documents, and food. Includes checked-off items.",
+ content: `
Packing Checklist
Footwear
Hiking boots (broken in!)
Camp sandals / flip-flops
Extra laces
Clothing
Rain jacket (Gore-Tex)
Down jacket for hut nights
3x wool base layers
2x hiking pants
Sun hat + warm beanie
Gear
Headlamp + spare batteries
Trekking poles (collapsible)
First aid kit
Sunscreen SPF 50
Water filter (Sawyer Squeeze)
`,
+ content_plain: "Complete packing checklist organized by category: footwear, clothing, gear, electronics, documents, and food.",
+ content_format: 'html',
type: "NOTE", tags: ["packing", "gear", "checklist"], is_pinned: true,
created_at: new Date(now - 6 * day).toISOString(), updated_at: new Date(now - hour).toISOString(),
},
{
id: "demo-note-8", title: "Food & Cooking Plan",
- content: `## Food & Cooking Plan
-
-### Hut Meals (Half-Board)
-Lac Blanc and Locatelli include dinner + breakfast. Budget EUR 0 for those nights.
-
-### Self-Catering Days
-We have a kitchen in the Chamonix Airbnb and Zermatt apartment.
-
-**Chamonix Grocery Run (Omar)**
-- Pasta, rice, couscous
-- Cheese, bread, cured meats
-- Fresh vegetables
-- Coffee, tea, milk
-- Trail mix ingredients
-
-> Omar already spent EUR 93 at Chamonix Carrefour. Receipts in rFunds.
-
-### Trail Lunches
-Pack these the night before each hike:
-- Sandwiches (baguette + cheese + ham)
-- Energy bars (2 per person)
-- Nuts and dried fruit
-- Chocolate (the altitude calls for it)
-- 1.5L water minimum
-
-### Special Meals
-- **Jul 10**: Fondue at Chez Vrony, Zermatt (won the dinner vote 5-4 over pizza)
-- **Jul 18**: Cooking class in Bolzano (South Tyrolean cuisine)
-
-### Dietary Notes
-\`\`\`
-Maya: Vegetarian (eggs & dairy OK)
-Priya: No shellfish allergy
-Others: No restrictions
-\`\`\``,
- content_plain: "Food and cooking plan covering hut meals, self-catering, trail lunches, special restaurant meals, and dietary notes for all travelers.",
+ content: `
Food & Cooking Plan
Hut Meals (Half-Board)
Lac Blanc and Locatelli include dinner + breakfast. Budget EUR 0 for those nights.
Self-Catering Days
We have a kitchen in the Chamonix Airbnb and Zermatt apartment.
Trail Lunches
Pack these the night before each hike:
Sandwiches (baguette + cheese + ham)
Energy bars (2 per person)
Nuts and dried fruit
Chocolate (the altitude calls for it)
1.5L water minimum
`,
+ content_plain: "Food and cooking plan covering hut meals, self-catering, trail lunches, special restaurant meals, and dietary notes.",
+ content_format: 'html',
type: "NOTE", tags: ["food", "planning", "budget"], is_pinned: false,
created_at: new Date(now - 4 * day).toISOString(), updated_at: new Date(now - 8 * hour).toISOString(),
},
{
id: "demo-note-9", title: "Transport & Logistics",
- content: `## Transport & Logistics
-
-### Getting There
-- **Jul 6**: Fly to Geneva (everyone arrives by 14:00)
-- Geneva to Chamonix shuttle: EUR 186 for 4 pax (Maya paid, tracked in rFunds)
-
-### Between Destinations
-- **Jul 10**: Chamonix to Zermatt
- - Option A: Train via Martigny (~3.5h, scenic)
- - Option B: Drive via Grand St Bernard tunnel (~2.5h)
- - *Decision: Train (Liam prefers scenic route)*
-
-- **Jul 14**: Zermatt to Dolomites
- - Bernina Express is 6 hours but spectacular
- - Rental car is 3.5 hours
- - *Under discussion in rForum -- Priya prefers the train*
-
-### Local Transport
-- **Chamonix**: Free local bus with guest card
-- **Zermatt**: Car-free! Electric taxis + Gornergrat railway
-- **Dolomites**: Need rental car or local bus (limited schedule)
-
-### Getting Home
-- **Jul 20**: Drive to Innsbruck airport (~2.5h from Val Gardena)
-- Return flights booked separately
-
-### Important Timetables
-\`\`\`
-Gornergrat Railway (first): 07:00 from Zermatt
-Lac Blanc trailhead shuttle: 06:30 from Chamonix
-Seceda gondola (first): 08:30 from Ortisei
-\`\`\`
-
-> Sam is researching rail passes -- the Swiss Travel Pass might save us money for the Zermatt segment.`,
- content_plain: "Transport and logistics plan covering flights, inter-city transfers, local transport options, return journey, and important timetables.",
+ content: `
Transport & Logistics
Getting There
Jul 6: Fly to Geneva (everyone arrives by 14:00)
Geneva to Chamonix shuttle: EUR 186 for 4 pax
Between Destinations
Jul 10: Chamonix to Zermatt -- Train via Martigny (~3.5h, scenic)
Jul 14: Zermatt to Dolomites -- Bernina Express (6 hours but spectacular)
Local Transport
Chamonix: Free local bus with guest card
Zermatt: Car-free! Electric taxis + Gornergrat railway
Dolomites: Need rental car or local bus (limited schedule)
`,
+ content_plain: "Transport and logistics plan covering flights, inter-city transfers, local transport options, return journey, and timetables.",
+ content_format: 'html',
type: "NOTE", tags: ["transport", "logistics"], is_pinned: false,
created_at: new Date(now - 9 * day).toISOString(), updated_at: new Date(now - 5 * hour).toISOString(),
},
@@ -441,104 +215,25 @@ Seceda gondola (first): 08:30 from Ortisei
const itineraryNotes: Note[] = [
{
id: "demo-note-10", title: "Full Itinerary -- Alpine Explorer 2026",
- content: `## Full Itinerary -- Alpine Explorer 2026
-**Jul 6-20 | France, Switzerland, Italy**
-**Travelers: Maya, Liam, Priya, Omar, Alex, Sam**
-
-### Week 1: Chamonix, France (Jul 6-10)
-| Date | Activity | Category |
-|------|----------|----------|
-| Jul 6 | Fly Geneva, shuttle to Chamonix | Travel |
-| Jul 7 | Acclimatization hike -- Lac Blanc | Hike |
-| Jul 8 | Via Ferrata -- Aiguille du Midi | Adventure |
-| Jul 9 | Rest day / Chamonix town | Rest |
-| Jul 10 | Train to Zermatt | Travel |
-
-### Week 2: Zermatt, Switzerland (Jul 10-14)
-| Date | Activity | Category |
-|------|----------|----------|
-| Jul 10 | Arrive Zermatt, settle in | Travel |
-| Jul 11 | Gornergrat sunrise hike | Hike |
-| Jul 12 | Matterhorn base camp trek | Hike |
-| Jul 13 | Paragliding over Zermatt | Adventure |
-| Jul 14 | Transfer to Dolomites | Travel |
-
-### Week 3: Dolomites, Italy (Jul 14-20)
-| Date | Activity | Category |
-|------|----------|----------|
-| Jul 14 | Arrive Val Gardena | Travel |
-| Jul 15 | Tre Cime di Lavaredo loop | Hike |
-| Jul 16 | Lago di Braies kayaking | Adventure |
-| Jul 17 | Seceda ridgeline hike | Hike |
-| Jul 18 | Cooking class in Bolzano | Culture |
-| Jul 19 | Free day -- shopping & packing | Rest |
-| Jul 20 | Fly home from Innsbruck | Travel |
-
-> This itinerary is also tracked in rTrips and synced to the shared rCal calendar.`,
+ content: `
Full Itinerary -- Alpine Explorer 2026
Jul 6-20 | France, Switzerland, Italy
Week 1: Chamonix, France (Jul 6-10)
Jul 6: Fly Geneva, shuttle to Chamonix
Jul 7: Acclimatization hike -- Lac Blanc
Jul 8: Via Ferrata -- Aiguille du Midi
Jul 9: Rest day / Chamonix town
Jul 10: Train to Zermatt
Week 2: Zermatt, Switzerland (Jul 10-14)
Jul 10: Arrive Zermatt, settle in
Jul 11: Gornergrat sunrise hike
Jul 12: Matterhorn base camp trek
Jul 13: Paragliding over Zermatt
Jul 14: Transfer to Dolomites
Week 3: Dolomites, Italy (Jul 14-20)
Jul 14: Arrive Val Gardena
Jul 15: Tre Cime di Lavaredo loop
Jul 16: Lago di Braies kayaking
Jul 17: Seceda ridgeline hike
Jul 18: Cooking class in Bolzano
Jul 19: Free day -- shopping & packing
Jul 20: Fly home from Innsbruck
`,
content_plain: "Complete day-by-day itinerary for the Alpine Explorer 2026 trip covering three weeks across Chamonix, Zermatt, and the Dolomites.",
+ content_format: 'html',
type: "NOTE", tags: ["itinerary", "planning"], is_pinned: true,
created_at: new Date(now - 15 * day).toISOString(), updated_at: new Date(now - 2 * hour).toISOString(),
},
{
id: "demo-note-11", title: "Mountain Hut Reservations",
- content: `## Mountain Hut Reservations
-
-### Confirmed
-- **Refuge du Lac Blanc** (Jul 7)
- - 4 beds reserved, half-board
- - Confirmation: #LB2026-234
- - Check-in after 15:00, dinner at 19:00
- - Bring sleeping bag liner (required)
-
-- **Rifugio Locatelli** (Jul 15)
- - 4 beds reserved, half-board
- - Confirmation: #TRE2026-089
- - Famous for Tre Cime sunset views
- - Cash only! Bring EUR 55pp
-
-### Waitlisted
-- **Hornlihutte** (Matterhorn base, Jul 12)
- - Waitlisted -- will know by Jul 1
- - If no luck, camp at Zermatt and do a long day hike instead
- - Alex is monitoring the cancellation list
-
-### Hut Etiquette Reminders
-1. Arrive before 17:00 if possible
-2. Remove boots at entrance (bring hut shoes or thick socks)
-3. Lights out by 22:00
-4. Pack out all trash
-5. Tip is appreciated but not required
-
-> Priya handled all the hut bookings. She has the confirmations printed and digital copies in rFiles.`,
- content_plain: "Mountain hut reservations with confirmation numbers, check-in details, and hut etiquette reminders. Two confirmed, one waitlisted.",
+ content: `
Hornlihutte (Matterhorn base, Jul 12) -- will know by Jul 1
Hut Etiquette Reminders
Arrive before 17:00 if possible
Remove boots at entrance (bring hut shoes or thick socks)
Lights out by 22:00
Pack out all trash
Tip is appreciated but not required
`,
+ content_plain: "Mountain hut reservations with confirmation numbers, check-in details, and hut etiquette reminders.",
+ content_format: 'html',
type: "NOTE", tags: ["accommodation", "hiking"], is_pinned: false,
created_at: new Date(now - 11 * day).toISOString(), updated_at: new Date(now - day).toISOString(),
},
{
id: "demo-note-12", title: "Group Decisions & Votes",
- content: `## Group Decisions & Votes
-
-### Decided
-- **Camera Gear**: DJI Mini 4 Pro (Liam's decision matrix: 8.5/10)
-- **First Night Dinner in Zermatt**: Fondue at Chez Vrony (won 5-4 over pizza)
-- **Day 5 Activity**: Via Ferrata at Aiguille du Midi (won 7-3 over kayaking)
-
-### Active Votes (in rVote)
-- **Zermatt to Dolomites transfer**: Train vs rental car
- - Train: 3 votes (Liam, Priya, Sam)
- - Car: 2 votes (Maya, Omar)
- - Alex: undecided
-
-### Pending Decisions
-- Val Gardena accommodation (agriturismo vs apartment)
-- Whether to rent the Starlink Mini (EUR 200, needs funding)
-- Trip zine print run size (50 vs 100 copies)
-
-### Decision Framework
-We are using a simple majority vote for group activities. For expenses over EUR 100, we need consensus (all 6 agree). Individual expenses are each person's choice.
-
-> All votes are tracked in rVote and synced to the canvas.`,
- content_plain: "Summary of group decisions made and active votes. Covers camera gear, dining, activities, and pending decisions with the group's voting framework.",
+ content: `
Group Decisions & Votes
Decided
Camera Gear: DJI Mini 4 Pro (Liam's decision matrix: 8.5/10)
First Night Dinner in Zermatt: Fondue at Chez Vrony (won 5-4 over pizza)
Day 5 Activity: Via Ferrata at Aiguille du Midi (won 7-3 over kayaking)
Active Votes (in rVote)
Zermatt to Dolomites transfer: Train vs rental car -- Train leading 3-2
Pending Decisions
Val Gardena accommodation (agriturismo vs apartment)
Whether to rent the Starlink Mini (EUR 200, needs funding)
Trip zine print run size (50 vs 100 copies)
`,
+ content_plain: "Summary of group decisions made and active votes. Covers camera gear, dining, activities, and pending decisions.",
+ content_format: 'html',
type: "NOTE", tags: ["decisions", "planning"], is_pinned: false,
created_at: new Date(now - 3 * day).toISOString(), updated_at: new Date(now - 3 * hour).toISOString(),
},
@@ -548,17 +243,17 @@ We are using a simple majority vote for group activities. For expenses over EUR
{
id: "demo-nb-1", title: "Alpine Explorer Planning", description: "Shared knowledge base for our July 2026 trip across France, Switzerland, and Italy",
cover_color: "#f59e0b", note_count: "6", updated_at: new Date(now - hour).toISOString(),
- notes: tripPlanningNotes, space: "demo",
+ notes: tripPlanningNotes,
} as any,
{
id: "demo-nb-2", title: "Packing & Logistics", description: "Checklists, food plans, and transport details",
cover_color: "#22c55e", note_count: "3", updated_at: new Date(now - hour).toISOString(),
- notes: packingNotes, space: "demo",
+ notes: packingNotes,
} as any,
{
id: "demo-nb-3", title: "Itinerary & Decisions", description: "Day-by-day schedule, hut reservations, and group votes",
cover_color: "#6366f1", note_count: "3", updated_at: new Date(now - 2 * hour).toISOString(),
- notes: itineraryNotes, space: "demo",
+ notes: itineraryNotes,
} as any,
];
@@ -597,7 +292,12 @@ We are using a simple majority vote for group activities. For expenses over EUR
private demoLoadNote(id: string) {
const allNotes = this.demoNotebooks.flatMap(nb => nb.notes);
this.selectedNote = allNotes.find(n => n.id === id) || null;
- this.render();
+ if (this.selectedNote) {
+ this.view = "note";
+ this.renderNav();
+ this.renderMeta();
+ this.mountEditor(this.selectedNote);
+ }
}
private demoCreateNotebook() {
@@ -608,7 +308,6 @@ We are using a simple majority vote for group activities. For expenses over EUR
id: `demo-nb-${now}`, title, description: "",
cover_color: "#8b5cf6", note_count: "0",
updated_at: new Date(now).toISOString(), notes: [] as Note[],
- space: "demo",
} as any;
this.demoNotebooks.push(nb);
this.notebooks = this.demoNotebooks.map(({ notes, ...rest }) => rest as Notebook);
@@ -621,10 +320,10 @@ We are using a simple majority vote for group activities. For expenses over EUR
const noteId = `demo-note-${now}`;
const newNote: Note = {
id: noteId, title: "Untitled Note", content: "", content_plain: "",
+ content_format: 'tiptap-json',
type: "NOTE", tags: null, is_pinned: false,
created_at: new Date(now).toISOString(), updated_at: new Date(now).toISOString(),
};
- // Add to the matching demoNotebook
const demoNb = this.demoNotebooks.find(n => n.id === this.selectedNotebook!.id);
if (demoNb) {
demoNb.notes.push(newNote);
@@ -634,10 +333,13 @@ We are using a simple majority vote for group activities. For expenses over EUR
this.selectedNotebook.note_count = String(this.selectedNotebook.notes.length);
this.selectedNote = newNote;
this.view = "note";
- this.render();
+ this.renderNav();
+ this.renderMeta();
+ this.mountEditor(newNote);
}
disconnectedCallback() {
+ this.destroyEditor();
this.disconnectSync();
}
@@ -650,13 +352,11 @@ We are using a simple majority vote for group activities. For expenses over EUR
this.ws.onopen = () => {
this.syncConnected = true;
- // Keepalive ping every 30s
this.pingInterval = setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: "ping", timestamp: Date.now() }));
}
}, 30000);
- // If we had a pending subscription, re-subscribe
if (this.subscribedDocId && this.doc) {
this.subscribeNotebook(this.subscribedDocId.split(":").pop()!);
}
@@ -668,7 +368,6 @@ We are using a simple majority vote for group activities. For expenses over EUR
if (msg.type === "sync" && msg.docId === this.subscribedDocId) {
this.handleSyncMessage(new Uint8Array(msg.data));
}
- // pong and other messages are ignored
} catch {
// ignore parse errors
}
@@ -677,21 +376,18 @@ We are using a simple majority vote for group activities. For expenses over EUR
this.ws.onclose = () => {
this.syncConnected = false;
if (this.pingInterval) clearInterval(this.pingInterval);
- // Reconnect after 3s
setTimeout(() => {
if (this.isConnected) this.connectSync();
}, 3000);
};
- this.ws.onerror = () => {
- // onclose will fire after this
- };
+ this.ws.onerror = () => {};
}
private disconnectSync() {
if (this.pingInterval) clearInterval(this.pingInterval);
if (this.ws) {
- this.ws.onclose = null; // prevent reconnect
+ this.ws.onclose = null;
this.ws.close();
this.ws = null;
}
@@ -707,7 +403,6 @@ We are using a simple majority vote for group activities. For expenses over EUR
this.doc = newDoc;
this.syncState = newSyncState;
- // Send reply if needed
const [nextState, reply] = Automerge.generateSyncMessage(this.doc, this.syncState);
this.syncState = nextState;
if (reply && this.ws?.readyState === WebSocket.OPEN) {
@@ -727,10 +422,8 @@ We are using a simple majority vote for group activities. For expenses over EUR
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
- // Send subscribe
this.ws.send(JSON.stringify({ type: "subscribe", docIds: [this.subscribedDocId] }));
- // Send initial sync message to kick off handshake
const [s, m] = Automerge.generateSyncMessage(this.doc, this.syncState);
this.syncState = s;
if (m) {
@@ -757,7 +450,7 @@ We are using a simple majority vote for group activities. For expenses over EUR
const nb = this.doc.notebook;
const items = this.doc.items;
- if (!nb) return; // doc not yet synced
+ if (!nb) return;
// Build notebook data from doc
const notes: Note[] = [];
@@ -768,6 +461,7 @@ We are using a simple majority vote for group activities. For expenses over EUR
title: item.title || "Untitled",
content: item.content || "",
content_plain: item.contentPlain || "",
+ content_format: (item.contentFormat as Note['content_format']) || undefined,
type: item.type || "NOTE",
tags: item.tags?.length ? Array.from(item.tags) : null,
is_pinned: item.isPinned || false,
@@ -777,7 +471,6 @@ We are using a simple majority vote for group activities. For expenses over EUR
}
}
- // Sort: pinned first, then by sort order, then by updated_at desc
notes.sort((a, b) => {
if (a.is_pinned !== b.is_pinned) return a.is_pinned ? -1 : 1;
return new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime();
@@ -793,7 +486,61 @@ We are using a simple majority vote for group activities. For expenses over EUR
notes,
};
- // If viewing a specific note, update it from doc too
+ // If viewing a note and editor is mounted, update editor content from remote
+ if (this.view === "note" && this.selectedNote && this.editor && this.editorNoteId === this.selectedNote.id) {
+ const noteItem = items?.[this.selectedNote.id];
+ if (noteItem) {
+ this.selectedNote = {
+ id: noteItem.id,
+ title: noteItem.title || "Untitled",
+ content: noteItem.content || "",
+ content_plain: noteItem.contentPlain || "",
+ content_format: (noteItem.contentFormat as Note['content_format']) || undefined,
+ type: noteItem.type || "NOTE",
+ tags: noteItem.tags?.length ? Array.from(noteItem.tags) : null,
+ is_pinned: noteItem.isPinned || false,
+ created_at: noteItem.createdAt ? new Date(noteItem.createdAt).toISOString() : new Date().toISOString(),
+ updated_at: noteItem.updatedAt ? new Date(noteItem.updatedAt).toISOString() : new Date().toISOString(),
+ };
+
+ // Update editor content if different (remote change)
+ const remoteContent = noteItem.content || "";
+ const currentContent = noteItem.contentFormat === 'tiptap-json'
+ ? JSON.stringify(this.editor.getJSON())
+ : this.editor.getHTML();
+
+ if (remoteContent !== currentContent) {
+ this.isRemoteUpdate = true;
+ try {
+ if (noteItem.contentFormat === 'tiptap-json') {
+ try {
+ this.editor.commands.setContent(JSON.parse(remoteContent), { emitUpdate: false });
+ } catch {
+ this.editor.commands.setContent(remoteContent, { emitUpdate: false });
+ }
+ } else {
+ this.editor.commands.setContent(remoteContent, { emitUpdate: false });
+ }
+ } finally {
+ this.isRemoteUpdate = false;
+ }
+ }
+
+ // Update title input if it exists
+ const titleInput = this.shadow.querySelector('#note-title-input') as HTMLInputElement;
+ if (titleInput && document.activeElement !== titleInput && titleInput !== this.shadow.activeElement) {
+ titleInput.value = noteItem.title || "Untitled";
+ }
+
+ // Only update nav/meta, skip contentZone
+ this.renderNav();
+ this.renderMeta();
+ this.loading = false;
+ return;
+ }
+ }
+
+ // If viewing a specific note without editor mounted, update selectedNote
if (this.view === "note" && this.selectedNote) {
const noteItem = items?.[this.selectedNote.id];
if (noteItem) {
@@ -802,6 +549,7 @@ We are using a simple majority vote for group activities. For expenses over EUR
title: noteItem.title || "Untitled",
content: noteItem.content || "",
content_plain: noteItem.contentPlain || "",
+ content_format: (noteItem.contentFormat as Note['content_format']) || undefined,
type: noteItem.type || "NOTE",
tags: noteItem.tags?.length ? Array.from(noteItem.tags) : null,
is_pinned: noteItem.isPinned || false,
@@ -831,6 +579,7 @@ We are using a simple majority vote for group activities. For expenses over EUR
title: "Untitled Note",
content: "",
contentPlain: "",
+ contentFormat: "tiptap-json",
type: "NOTE",
tags: [],
isPinned: false,
@@ -843,14 +592,17 @@ We are using a simple majority vote for group activities. For expenses over EUR
this.sendSyncAfterChange();
this.renderFromDoc();
- // Open the new note for editing
+ // Open the new note
this.selectedNote = {
id: noteId, title: "Untitled Note", content: "", content_plain: "",
+ content_format: 'tiptap-json',
type: "NOTE", tags: null, is_pinned: false,
created_at: new Date(now).toISOString(), updated_at: new Date(now).toISOString(),
};
this.view = "note";
- this.render();
+ this.renderNav();
+ this.renderMeta();
+ this.mountEditor(this.selectedNote);
}
private updateNoteField(noteId: string, field: string, value: string) {
@@ -905,13 +657,9 @@ We are using a simple majority vote for group activities. For expenses over EUR
this.loading = true;
this.render();
- // Unsubscribe from any previous notebook
this.unsubscribeNotebook();
-
- // Subscribe to the new notebook via Automerge
this.subscribeNotebook(id);
- // Set a timeout — if doc doesn't arrive in 5s, fall back to REST
setTimeout(() => {
if (this.loading && this.view === "notebook") {
this.loadNotebookREST(id);
@@ -919,7 +667,6 @@ We are using a simple majority vote for group activities. For expenses over EUR
}, 5000);
}
- /** REST fallback for notebook detail */
private async loadNotebookREST(id: string) {
try {
const base = this.getApiBase();
@@ -933,7 +680,7 @@ We are using a simple majority vote for group activities. For expenses over EUR
}
private loadNote(id: string) {
- // Note is already in the Automerge doc — just select it
+ // Note is already in the Automerge doc
if (this.doc?.items?.[id]) {
const item = this.doc.items[id];
this.selectedNote = {
@@ -941,6 +688,7 @@ We are using a simple majority vote for group activities. For expenses over EUR
title: item.title || "Untitled",
content: item.content || "",
content_plain: item.contentPlain || "",
+ content_format: (item.contentFormat as Note['content_format']) || undefined,
type: item.type || "NOTE",
tags: item.tags?.length ? Array.from(item.tags) : null,
is_pinned: item.isPinned || false,
@@ -948,10 +696,14 @@ We are using a simple majority vote for group activities. For expenses over EUR
updated_at: item.updatedAt ? new Date(item.updatedAt).toISOString() : new Date().toISOString(),
};
} else if (this.selectedNotebook?.notes) {
- // Fallback: find in REST-loaded data
this.selectedNote = this.selectedNotebook.notes.find(n => n.id === id) || null;
}
- this.render();
+
+ if (this.selectedNote) {
+ this.renderNav();
+ this.renderMeta();
+ this.mountEditor(this.selectedNote);
+ }
}
private async searchNotes(query: string) {
@@ -988,16 +740,275 @@ We are using a simple majority vote for group activities. For expenses over EUR
}
}
+ // ── Tiptap Editor ──
+
+ private mountEditor(note: Note) {
+ this.destroyEditor();
+
+ // Build content zone
+ const isDemo = this.space === "demo";
+ const isAutomerge = !!(this.doc?.items?.[note.id]);
+ const isEditable = isAutomerge || isDemo;
+
+ this.contentZone.innerHTML = `
+