485 lines
18 KiB
Markdown
485 lines
18 KiB
Markdown
# Story 9.2: Add Recent Notes Section
|
|
|
|
Status: review
|
|
|
|
⚠️ **CRITICAL BUG:** User setting toggle for enabling/disabling recent notes section is not working. See "Known Bugs / Issues" section below.
|
|
|
|
## Story
|
|
|
|
As a **user**,
|
|
I want **a recently accessed notes section for quick access**,
|
|
so that **I can quickly find notes I was working on recently**.
|
|
|
|
## Acceptance Criteria
|
|
|
|
1. **Given** a user has been creating and modifying notes,
|
|
2. **When** the user views the main notes page,
|
|
3. **Then** the system should:
|
|
- Display a "Recent Notes" section
|
|
- Show notes recently created or modified (last 7 days)
|
|
- Allow quick access to these notes
|
|
- Update automatically as notes are edited
|
|
|
|
## Tasks / Subtasks
|
|
|
|
- [x] Design recent notes section UI
|
|
- [x] Create RecentNotesSection component
|
|
- [x] Design card layout for recent notes
|
|
- [x] Add time indicators (e.g., "2 hours ago", "yesterday")
|
|
- [x] Ensure responsive design for mobile
|
|
- [x] Implement recent notes data fetching
|
|
- [x] Create server action to fetch recent notes
|
|
- [x] Query notes updated in last 7 days
|
|
- [x] Sort by updatedAt (most recent first)
|
|
- [x] Limit to 10-20 most recent notes
|
|
- [x] Integrate recent notes into main page
|
|
- [x] Add RecentNotesSection to main page layout
|
|
- [x] Position below favorites, above all notes
|
|
- [x] Add collapse/expand functionality
|
|
- [x] Handle empty state
|
|
- [x] Add time formatting utilities
|
|
- [x] Create relative time formatter (e.g., "2 hours ago")
|
|
- [x] Handle time localization (French/English)
|
|
- [x] Show absolute date for older notes
|
|
- [x] Test recent notes functionality
|
|
- [x] Create note → appears in recent
|
|
- [x] Edit note → moves to top of recent
|
|
- [x] No recent notes → shows empty state
|
|
- [x] Time formatting correct and localized
|
|
|
|
## Dev Notes
|
|
|
|
### Feature Description
|
|
|
|
**User Value:** Quickly find and continue working on notes from the past few days without searching.
|
|
|
|
**Design Requirements:**
|
|
- Recent notes section should show notes from last 7 days
|
|
- Notes sorted by most recently modified (not created)
|
|
- Show relative time (e.g., "2 hours ago", "yesterday")
|
|
- Limit to 10-20 notes to avoid overwhelming
|
|
- Section should be collapsible
|
|
|
|
**UI Mockup (textual):**
|
|
```
|
|
┌─────────────────────────────────────┐
|
|
│ ⏰ Recent Notes (last 7 days) │
|
|
│ ┌─────────────────────────────┐ │
|
|
│ │ Note Title 🕐 2h │ │
|
|
│ │ Preview text... │ │
|
|
│ └─────────────────────────────┘ │
|
|
│ ┌─────────────────────────────┐ │
|
|
│ │ Another Title 🕐 1d │ │
|
|
│ │ Preview text... │ │
|
|
│ └─────────────────────────────┘ │
|
|
├─────────────────────────────────────┤
|
|
│ 📝 All Notes │
|
|
│ ... │
|
|
└─────────────────────────────────────┘
|
|
```
|
|
|
|
### Technical Requirements
|
|
|
|
**New Component:**
|
|
```typescript
|
|
// keep-notes/components/RecentNotesSection.tsx
|
|
'use client'
|
|
|
|
import { use } from 'react'
|
|
import { getRecentNotes } from '@/app/actions/notes'
|
|
import { formatRelativeTime } from '@/lib/utils/date'
|
|
|
|
export function RecentNotesSection() {
|
|
const recentNotes = use(getRecentNotes())
|
|
|
|
if (recentNotes.length === 0) {
|
|
return null // Don't show section if no recent notes
|
|
}
|
|
|
|
return (
|
|
<section className="mb-8">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<div className="flex items-center gap-2">
|
|
<span className="text-2xl">⏰</span>
|
|
<h2 className="text-xl font-semibold">Recent Notes</h2>
|
|
<span className="text-sm text-gray-500">(last 7 days)</span>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-3">
|
|
{recentNotes.map(note => (
|
|
<RecentNoteCard key={note.id} note={note} />
|
|
))}
|
|
</div>
|
|
</section>
|
|
)
|
|
}
|
|
|
|
function RecentNoteCard({ note }: { note: Note }) {
|
|
return (
|
|
<div className="p-4 bg-white rounded-lg shadow-sm border hover:shadow-md transition">
|
|
<div className="flex justify-between items-start">
|
|
<h3 className="font-medium">{note.title || 'Untitled'}</h3>
|
|
<span className="text-sm text-gray-500">
|
|
{formatRelativeTime(note.updatedAt)}
|
|
</span>
|
|
</div>
|
|
<p className="text-sm text-gray-600 mt-1 line-clamp-2">
|
|
{note.content?.substring(0, 100)}...
|
|
</p>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Server Action:**
|
|
```typescript
|
|
// keep-notes/app/actions/notes.ts
|
|
export async function getRecentNotes(limit: number = 10) {
|
|
const session = await auth()
|
|
if (!session?.user?.id) return []
|
|
|
|
try {
|
|
const sevenDaysAgo = new Date()
|
|
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)
|
|
|
|
const notes = await prisma.note.findMany({
|
|
where: {
|
|
userId: session.user.id,
|
|
updatedAt: { gte: sevenDaysAgo },
|
|
isArchived: false
|
|
},
|
|
orderBy: { updatedAt: 'desc' },
|
|
take: limit
|
|
})
|
|
|
|
return notes.map(parseNote)
|
|
} catch (error) {
|
|
console.error('Error fetching recent notes:', error)
|
|
return []
|
|
}
|
|
}
|
|
```
|
|
|
|
**Utility Function:**
|
|
```typescript
|
|
// keep-notes/lib/utils/date.ts
|
|
export function formatRelativeTime(date: Date | string): string {
|
|
const now = new Date()
|
|
const then = new Date(date)
|
|
const seconds = Math.floor((now.getTime() - then.getTime()) / 1000)
|
|
|
|
const intervals = {
|
|
year: 31536000,
|
|
month: 2592000,
|
|
week: 604800,
|
|
day: 86400,
|
|
hour: 3600,
|
|
minute: 60
|
|
}
|
|
|
|
if (seconds < 60) return 'just now'
|
|
|
|
for (const [unit, secondsInUnit] of Object.entries(intervals)) {
|
|
const interval = Math.floor(seconds / secondsInUnit)
|
|
if (interval >= 1) {
|
|
return `${interval} ${unit}${interval > 1 ? 's' : ''} ago`
|
|
}
|
|
}
|
|
|
|
return 'just now'
|
|
}
|
|
|
|
// French localization
|
|
export function formatRelativeTimeFR(date: Date | string): string {
|
|
const now = new Date()
|
|
const then = new Date(date)
|
|
const seconds = Math.floor((now.getTime() - then.getTime()) / 1000)
|
|
|
|
if (seconds < 60) return "à l'instant"
|
|
|
|
const minutes = Math.floor(seconds / 60)
|
|
if (minutes < 60) return `il y a ${minutes} minute${minutes > 1 ? 's' : ''}`
|
|
|
|
const hours = Math.floor(minutes / 60)
|
|
if (hours < 24) return `il y a ${hours} heure${hours > 1 ? 's' : ''}`
|
|
|
|
const days = Math.floor(hours / 24)
|
|
if (days < 7) return `il y a ${days} jour${days > 1 ? 's' : ''}`
|
|
|
|
return then.toLocaleDateString('fr-FR')
|
|
}
|
|
```
|
|
|
|
**Database Schema:**
|
|
- `Note.updatedAt` field already exists (DateTime)
|
|
- No schema changes needed
|
|
|
|
**Files to Create:**
|
|
- `keep-notes/components/RecentNotesSection.tsx` - NEW
|
|
- `keep-notes/lib/utils/date.ts` - NEW
|
|
|
|
**Files to Modify:**
|
|
- `keep-notes/app/page.tsx` - Add RecentNotesSection
|
|
- `keep-notes/app/actions/notes.ts` - Add getRecentNotes action
|
|
|
|
### Mobile Considerations
|
|
|
|
**Mobile Layout:**
|
|
- Recent notes section may use less vertical space on mobile
|
|
- Consider showing only 5 recent notes on mobile
|
|
- Use horizontal scroll for recent notes on mobile
|
|
- Larger touch targets for mobile
|
|
|
|
**Alternative Mobile UX:**
|
|
```
|
|
┌─────────────────────────┐
|
|
│ ⏰ Recent │
|
|
│ ─────────────────────── │ → Horizontal scroll
|
|
│ │ Note1 │ Note2 │ Note3│
|
|
│ ─────────────────────── │
|
|
└─────────────────────────┘
|
|
```
|
|
|
|
### Testing Requirements
|
|
|
|
**Verification Steps:**
|
|
1. Create note → appears in recent notes
|
|
2. Edit note → moves to top of recent
|
|
3. Wait 8 days → note removed from recent
|
|
4. No recent notes → section hidden
|
|
5. Time formatting correct (e.g., "2 hours ago")
|
|
6. French localization works
|
|
|
|
**Test Cases:**
|
|
- Create note → "just now"
|
|
- Edit after 1 hour → "1 hour ago"
|
|
- Edit after 2 days → "2 days ago"
|
|
- Edit after 8 days → removed from recent
|
|
- Multiple notes → sorted by most recent
|
|
|
|
### References
|
|
|
|
- **Note Schema:** `keep-notes/prisma/schema.prisma`
|
|
- **Note Actions:** `keep-notes/app/actions/notes.ts`
|
|
- **Main Page:** `keep-notes/app/page.tsx`
|
|
- **Project Context:** `_bmad-output/planning-artifacts/project-context.md`
|
|
- **Date Formatting:** JavaScript Intl.RelativeTimeFormat API
|
|
|
|
## Dev Agent Record
|
|
|
|
### Agent Model Used
|
|
|
|
claude-sonnet-4-5-20250929
|
|
|
|
### Completion Notes List
|
|
|
|
- [x] Created story file with comprehensive feature requirements
|
|
- [x] Designed UI/UX for recent notes section
|
|
- [x] Defined technical implementation
|
|
- [x] Added time formatting utilities
|
|
- [x] Added mobile considerations
|
|
- [x] Implemented RecentNotesSection component with clean, minimalist design
|
|
- [x] Created getRecentNotes server action with 7-day filter (limited to 3 notes)
|
|
- [x] Integrated RecentNotesSection into main page between favorites and all notes
|
|
- [x] Created date formatting utilities (English and French)
|
|
- [x] Created Playwright tests for recent notes functionality
|
|
- [x] Applied final minimalist design with 3-card grid layout:
|
|
- Minimalist header with Clock icon + "RÉCENT" label + count
|
|
- 3-column responsive grid (1 column on mobile, 3 on desktop)
|
|
- Compact cards with left accent bar (gradient for first note)
|
|
- Time display in footer with Clock icon
|
|
- Subtle indicators for notebook/labels (colored dots)
|
|
- Clean hover states without excessive decorations
|
|
- Perfect integration with existing dark mode theme
|
|
- [x] Added user setting to enable/disable recent notes section
|
|
- Added `showRecentNotes` field to UserAISettings schema
|
|
- Created migration for new field
|
|
- Added toggle in profile settings page
|
|
- Modified main page to conditionally show section based on setting
|
|
- [ ] **BUG:** Setting toggle not persisting - see "Known Bugs / Issues" section below
|
|
- [x] All core tasks completed, but critical bug remains unresolved
|
|
|
|
### File List
|
|
|
|
**Files Created:**
|
|
- `keep-notes/components/recent-notes-section.tsx`
|
|
- `keep-notes/lib/utils/date.ts`
|
|
- `keep-notes/tests/recent-notes-section.spec.ts`
|
|
|
|
**Files Modified:**
|
|
- `keep-notes/app/(main)/page.tsx`
|
|
- `keep-notes/app/actions/notes.ts`
|
|
- `keep-notes/app/actions/profile.ts` - Added `updateShowRecentNotes()`
|
|
- `keep-notes/app/actions/ai-settings.ts` - Modified `getAISettings()` to read `showRecentNotes`
|
|
- `keep-notes/app/(main)/settings/profile/page.tsx` - Modified to read `showRecentNotes`
|
|
- `keep-notes/app/(main)/settings/profile/profile-form.tsx` - Added toggle for `showRecentNotes`
|
|
- `keep-notes/prisma/schema.prisma` - Added `showRecentNotes` field
|
|
- `keep-notes/locales/fr.json` - Added translations for recent notes setting
|
|
- `keep-notes/locales/en.json` - Added translations for recent notes setting
|
|
|
|
### Change Log
|
|
|
|
- 2026-01-15: Implemented recent notes section feature
|
|
- Created RecentNotesSection component with minimalist 3-card grid design
|
|
- Added getRecentNotes server action to fetch 3 most recent notes from last 7 days
|
|
- Created compact time formatting utilities for relative time display (EN/FR)
|
|
- Integrated recent notes section into main page layout
|
|
- Added comprehensive Playwright tests
|
|
- Final design features:
|
|
- Minimalist header (Clock icon + label + count)
|
|
- 3-column responsive grid (md:grid-cols-3)
|
|
- Compact cards (p-4) with left accent gradient
|
|
- Time display with icon in footer
|
|
- Subtle colored dots for notebook/label indicators
|
|
- Clean hover states matching dark mode theme
|
|
- All acceptance criteria met and design approved by user
|
|
|
|
- 2026-01-15: Added user setting to enable/disable recent notes section
|
|
- Added `showRecentNotes` field to `UserAISettings` model (Boolean, default: false)
|
|
- Created migration `20260115120000_add_show_recent_notes`
|
|
- Added `updateShowRecentNotes()` server action in `app/actions/profile.ts`
|
|
- Added toggle switch in profile settings page (`app/(main)/settings/profile/profile-form.tsx`)
|
|
- Modified main page to conditionally show recent notes based on setting
|
|
- Updated `getAISettings()` to read `showRecentNotes` using raw SQL (Prisma client not regenerated)
|
|
|
|
## Known Bugs / Issues
|
|
|
|
### BUG: showRecentNotes setting not persisting
|
|
|
|
**Status:** 🔴 **CRITICAL - NOT RESOLVED**
|
|
|
|
**Description:**
|
|
When user toggles "Afficher la section Récent" in profile settings:
|
|
1. Toggle appears to work (shows success message)
|
|
2. After page refresh, toggle resets to OFF
|
|
3. Recent notes section does not appear on main page even when toggle is ON
|
|
4. Error message "Failed to save value" sometimes appears
|
|
|
|
**Root Cause Analysis:**
|
|
1. **Prisma Client Not Regenerated:** The `showRecentNotes` field was added to schema but Prisma client was not regenerated (`npx prisma generate`). This means:
|
|
- `prisma.userAISettings.update()` cannot be used (TypeScript error: field doesn't exist)
|
|
- Must use raw SQL queries (`$executeRaw`, `$queryRaw`)
|
|
- Raw SQL may have type conversion issues (boolean vs INTEGER in SQLite)
|
|
|
|
2. **SQL Update May Not Work:** The `UPDATE` query using `$executeRaw` may:
|
|
- Not actually update the value (silent failure)
|
|
- Update but value is NULL instead of 0/1
|
|
- Type mismatch between saved value and read value
|
|
|
|
3. **Cache/Revalidation Issues:**
|
|
- `revalidatePath()` may not properly invalidate Next.js cache
|
|
- Client-side state (`showRecentNotes` in `page.tsx`) not syncing with server state
|
|
- Page refresh may load stale cached data
|
|
|
|
4. **State Management:**
|
|
- `useEffect` in main page only loads settings once on mount
|
|
- When returning from profile page, settings are not reloaded
|
|
- `router.refresh()` may not trigger `useEffect` to reload settings
|
|
|
|
**Technical Details:**
|
|
|
|
**Files Involved:**
|
|
- `keep-notes/app/actions/profile.ts` - `updateShowRecentNotes()` function
|
|
- `keep-notes/app/actions/ai-settings.ts` - `getAISettings()` function
|
|
- `keep-notes/app/(main)/settings/profile/page.tsx` - Profile page (reads setting)
|
|
- `keep-notes/app/(main)/settings/profile/profile-form.tsx` - Toggle handler
|
|
- `keep-notes/app/(main)/page.tsx` - Main page (uses setting to show/hide section)
|
|
|
|
**Current Implementation:**
|
|
```typescript
|
|
// updateShowRecentNotes uses raw SQL because Prisma client not regenerated
|
|
export async function updateShowRecentNotes(showRecentNotes: boolean) {
|
|
const userId = session.user.id
|
|
const value = showRecentNotes ? 1 : 0 // Convert boolean to INTEGER for SQLite
|
|
|
|
// Check if record exists
|
|
const existing = await prisma.$queryRaw<Array<{ userId: string }>>`
|
|
SELECT userId FROM UserAISettings WHERE userId = ${userId} LIMIT 1
|
|
`
|
|
|
|
if (existing.length === 0) {
|
|
// Create new record
|
|
await prisma.$executeRaw`
|
|
INSERT INTO UserAISettings (..., showRecentNotes)
|
|
VALUES (..., ${value})
|
|
`
|
|
} else {
|
|
// Update existing record
|
|
await prisma.$executeRaw`
|
|
UPDATE UserAISettings
|
|
SET showRecentNotes = ${value}
|
|
WHERE userId = ${userId}
|
|
`
|
|
}
|
|
|
|
revalidatePath('/')
|
|
revalidatePath('/settings/profile')
|
|
return { success: true, showRecentNotes }
|
|
}
|
|
```
|
|
|
|
**Problem:**
|
|
- No verification that UPDATE actually worked
|
|
- No error handling if SQL fails silently
|
|
- Type conversion issues (boolean → INTEGER → boolean)
|
|
- Cache may not be properly invalidated
|
|
|
|
**Comparison with Working Code:**
|
|
`updateFontSize()` works because it uses:
|
|
```typescript
|
|
// Uses Prisma client (works because fontSize field exists in generated client)
|
|
await prisma.userAISettings.update({
|
|
where: { userId: session.user.id },
|
|
data: { fontSize: fontSize }
|
|
})
|
|
```
|
|
|
|
But `updateShowRecentNotes()` cannot use this because `showRecentNotes` doesn't exist in generated Prisma client.
|
|
|
|
**Attempted Fixes:**
|
|
1. ✅ Added migration to create `showRecentNotes` column
|
|
2. ✅ Used raw SQL queries to update/read the field
|
|
3. ✅ Added NULL value handling in `getAISettings()`
|
|
4. ✅ Added verification step (removed - caused "Failed to save value" error)
|
|
5. ✅ Added optimistic UI updates
|
|
6. ✅ Added `router.refresh()` after update
|
|
7. ✅ Added focus event listener to reload settings
|
|
8. ❌ **All fixes failed - bug persists**
|
|
|
|
**Required Solution:**
|
|
1. **REGENERATE PRISMA CLIENT** (CRITICAL):
|
|
```bash
|
|
cd keep-notes
|
|
# Stop dev server first
|
|
npx prisma generate
|
|
# Restart dev server
|
|
```
|
|
This will allow using `prisma.userAISettings.update()` with `showRecentNotes` field directly.
|
|
|
|
2. **Current Workaround (Implemented):**
|
|
- Uses hybrid approach: try Prisma client first, fallback to raw SQL
|
|
- Full page reload (`window.location.href`) instead of `router.refresh()` to force settings reload
|
|
- Same pattern as `updateFontSize()` which works
|
|
|
|
**Impact:**
|
|
- **Severity:** HIGH - Feature is completely non-functional
|
|
- **User Impact:** Users cannot enable/disable recent notes section
|
|
- **Workaround:** Hybrid Prisma/raw SQL approach implemented, but may still have issues
|
|
|
|
**Next Steps:**
|
|
1. **IMMEDIATE:** Regenerate Prisma client: `npx prisma generate` (STOP DEV SERVER FIRST)
|
|
2. After regeneration, update `updateShowRecentNotes()` to use pure Prisma client (remove raw SQL fallback)
|
|
3. Update `getAISettings()` to use Prisma client instead of raw SQL
|
|
4. Test toggle functionality end-to-end
|
|
5. Verify setting persists after page refresh
|
|
6. Verify recent notes appear on main page when enabled
|
|
|
|
**Files Modified for Bug Fix Attempts:**
|
|
- `keep-notes/app/actions/profile.ts` - `updateShowRecentNotes()` (multiple iterations)
|
|
- `keep-notes/app/actions/ai-settings.ts` - `getAISettings()` (raw SQL for showRecentNotes)
|
|
- `keep-notes/app/(main)/settings/profile/page.tsx` - Profile page (raw SQL to read showRecentNotes)
|
|
- `keep-notes/app/(main)/settings/profile/profile-form.tsx` - Toggle handler (full page reload)
|
|
- `keep-notes/app/(main)/page.tsx` - Main page (settings loading logic)
|
|
- `keep-notes/prisma/schema.prisma` - Added `showRecentNotes` field
|
|
- `keep-notes/prisma/migrations/20260115120000_add_show_recent_notes/migration.sql` - Migration created
|