A modern, full-stack todo and note-taking application built with Next.js, demonstrating clean architecture patterns and best practices for React applications.
This application provides a personal assistant interface for managing todos and notes with rich markdown editing capabilities. It showcases a well-structured architecture following the MVP (Model-View-Presenter) pattern with headless UI hooks, ensuring separation of concerns and maintainability.
- Todo Management: Create, update, complete, and delete todos with due dates
- Note Taking: Rich markdown editor with Mermaid diagram support
- Zen Mode: Distraction-free editing experience for notes
- Search: Filter todos and notes by search query
- Responsive UI: Clean, modern interface built with Tailwind CSS and Radix UI
This application follows a Model-View-Presenter (MVP) architecture pattern, ensuring clear separation of concerns and maintainable code structure.
| Layer | Responsibility | Location | Pattern |
|---|---|---|---|
| Model | Encapsulates business logic and data structures | src/lib/models/ |
Domain entities (Todo, Note) |
| View | Renders UI components (pure functions of props) | src/components/ |
No data fetching or state management |
| Presenter | Handles data fetching, state management, routing, and behavior | src/lib/hooks/use-{domain}-page.ts |
Headless UI hooks |
| Technology | Purpose | Location |
|---|---|---|
| Next.js | React framework for server-side rendering, routing, and API routes | src/app/ |
| React | UI component library | src/components/ |
| TypeScript | Type safety and developer experience | All .ts/.tsx files |
| TanStack Query | Data fetching, caching, and synchronization | src/lib/query/ |
| TanStack Form | Headless form state management and validation | Form components |
| Drizzle ORM | Type-safe database queries and migrations | src/lib/db/ |
| SQLite (Bun) | Embedded database for local data persistence | todo.db |
| Tailwind CSS | Utility-first CSS framework for styling | src/app/globals.css |
| Radix UI | Headless UI primitives (dialog, checkbox, label) | src/components/ui/ |
| MDXEditor | Rich markdown editor with plugin support | src/components/markdown/ |
| Zod | Schema validation and type inference | Models and forms |
Headless UI hooks are custom React hooks that encapsulate all application concerns (data fetching, state, routing, mutations) without any UI rendering logic. They return a structured object containing data, state, actions, and pending states.
Pattern: Presenter hooks (use-{domain}-page.ts) encapsulate data, state, routing, and behavior. Pages consume hooks and compose pure UI components.
Example Structure:
// src/lib/hooks/use-todos-page.ts
export function useTodosPage() {
const { data: todos, ... } = useTodos();
const createMutation = useCreateTodo();
return {
todos,
createTodo: createMutation.mutate,
isPending: createMutation.isPending,
// ... other state and actions
};
}Benefits:
- Separation of concerns: UI logic separate from business logic
- Testability: Hooks can be tested independently
- Reusability: Same hook can power different UI implementations
- Type safety: Full TypeScript support
Pure components receive all data and behavior via props. They have no direct data fetching, state management, or side effects.
Pattern: Components in src/components/{domain}/ or src/components/ui/ are pure functions of their props.
Example:
// Pure component - receives everything via props
function TodoItem({ todo, onToggle, onDelete }: TodoItemProps) {
return (
<div>
<input type="checkbox" checked={todo.completed} onChange={onToggle} />
<span>{todo.title}</span>
<button onClick={onDelete}>Delete</button>
</div>
);
}Container components handle data retrieval and orchestration. Pages (src/app/) are always containers.
Pattern: Pages use presenter hooks and pass data/actions to pure components.
Example:
// Container component - connects presenter to views
export default function TodosPage() {
const { todos, createTodo, ... } = useTodosPage();
return (
<div>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} {...actions} />
))}
</div>
);
}High-Level Variants use discriminated union components to provide a narrow entry point with deep internal variation. Export a single component with a variant prop that dispatches to internal variant implementations.
Pattern: Reduces component proliferation while maintaining type safety.
Example:
// Single component with variant prop
interface NoteProps {
variant: 'sidebar' | 'view' | 'edit';
// ... other props
}
export function Note({ variant, ...props }: NoteProps) {
switch (variant) {
case 'sidebar': return <SidebarNote {...props} />;
case 'view': return <ViewNote {...props} />;
case 'edit': return <EditNote {...props} />;
}
}Hook Composition allows composing multiple atomic headless UI hooks into aggregate hooks using an enabled options object. Aggregate hooks default all sub-hooks to false (opt-in pattern).
Pattern: Access composed hooks via namespaced return object.
Example:
const { todos, notes } = useApp({
enabled: { todos: true, notes: true }
});Conditional Fetching ensures atomic hooks accept enabled?: boolean (defaults to false). Aggregate hooks use nested enabled object with opt-in defaults. Always provide a way to disable fetching to prevent over-fetching.
TanStack Form is used for all form state management, validation, and submission handling. It provides headless form state management with type-safe APIs, field-level validation, and optimized re-renders.
Pattern: All forms use useForm from @tanstack/react-form and form.Field components for field rendering.
- Bun (latest) - Runtime and package manager
- Node.js 18+ (if not using Bun)
- Git
-
Clone the repository:
git clone <repository-url> cd todo-demo
-
Install dependencies:
bun install
Or with npm/yarn/pnpm:
npm install # or yarn install # or pnpm install
-
Set up the database:
bun run db:create
This creates the SQLite database file (
todo.db) and initializes the schema. -
Run database migrations (if needed):
bun run db:push
Or generate migrations:
bun run db:generate bun run db:migrate
-
Start the development server:
bun dev
Or with npm/yarn/pnpm:
npm run dev # or yarn dev # or pnpm dev
-
Open your browser: Navigate to http://localhost:3000
| Command | Description |
|---|---|
bun dev |
Start development server |
bun build |
Build for production |
bun start |
Start production server |
bun lint |
Run ESLint |
bun db:generate |
Generate Drizzle migrations |
bun db:migrate |
Run database migrations |
bun db:push |
Push schema changes to database |
bun db:studio |
Open Drizzle Studio (database GUI) |
bun db:create |
Create database and tables |
todo-demo/
├── .cursor/ # Cursor IDE rules and architecture docs
│ └── rules/
│ ├── architecture.mdc # Architecture decisions
│ ├── design.mdc # Design patterns and strategies
│ └── strategy/ # Detailed strategy documentation
├── src/
│ ├── app/ # Next.js App Router pages (containers)
│ │ ├── layout.tsx # Root layout
│ │ ├── page.tsx # Todos page
│ │ ├── todos/ # Todos routes
│ │ └── notes/ # Notes routes
│ ├── components/ # React components
│ │ ├── markdown/ # Markdown editor components
│ │ ├── navigation/ # Navigation components
│ │ └── ui/ # Reusable UI primitives
│ └── lib/ # Core application logic
│ ├── actions/ # Server actions
│ ├── db/ # Database schema and connection
│ ├── hooks/ # Presenter hooks (use-*-page.ts)
│ ├── models/ # Domain models
│ └── query/ # TanStack Query hooks
├── drizzle/ # Database migrations
├── scripts/ # Utility scripts
│ └── create-db.ts # Database initialization
├── drizzle.config.ts # Drizzle ORM configuration
├── next.config.ts # Next.js configuration
├── package.json # Dependencies and scripts
└── todo.db # SQLite database file
src/app/: Next.js pages (container components) that use presenter hookssrc/components/: Pure UI components organized by domainsrc/lib/hooks/: Presenter hooks that encapsulate data fetching and statesrc/lib/models/: Domain models with business logicsrc/lib/actions/: Server actions for mutationssrc/lib/query/: TanStack Query hooks for data fetchingsrc/lib/db/: Database schema and connection logic
-
Define the Model (
src/lib/models/):- Create domain model with business logic
- Define Zod schemas for validation
-
Create Query Hooks (
src/lib/query/):- Create TanStack Query hooks for data fetching
- Use conditional fetching pattern
-
Create Server Actions (
src/lib/actions/):- Define mutations for create/update/delete operations
- Use Zod for validation
-
Create Presenter Hook (
src/lib/hooks/):- Compose query hooks and mutations
- Return structured object with data, state, and actions
-
Create UI Components (
src/components/):- Build pure components that receive props
- Use high-level variants pattern if needed
-
Create Page (
src/app/):- Use presenter hook
- Compose pure components
- Handle routing and URL state
-
Update Schema (
src/lib/db/schema.ts):export const newTable = sqliteTable('new_table', { // ... columns });
-
Generate Migration:
bun run db:generate
-
Apply Migration:
bun run db:push
Or use migrations:
bun run db:migrate
- Follow TypeScript strict mode
- Use functional components with hooks
- Prefer composition over inheritance
- Keep components small and focused
- Use meaningful variable and function names
- Document complex logic with comments
For detailed architecture decisions and patterns, see:
- Architecture:
.cursor/rules/architecture.mdc - Design Patterns:
.cursor/rules/design.mdc - Strategy Guides:
.cursor/rules/strategy/
Each architectural decision is uniquely identified (e.g., MVP-001, HOOK-001) and linked to detailed strategy documentation.
When contributing to this project:
- Review relevant architecture patterns in
.cursor/rules/ - Follow the established patterns (MVP, Headless UI, etc.)
- Ensure consistency with existing code structure
- Update architecture documentation when introducing new patterns
- Assign unique IDs to new architectural decisions
[Add your license here]