Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions HIGHLIGHTS_DEMO.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Text Highlights & Annotations Demo

This document describes the text highlighting and annotation feature for the DevVerse blog.

## Features

### Text Selection & Highlighting
- Select any text in an article to reveal the highlighting toolbar
- Choose from 5 color options: yellow, green, blue, pink, orange
- Add optional personal notes to your highlights
- Highlights persist across devices and sessions

### Sidebar Panel
- Collapsible "Notes" panel on the right side of articles
- View all highlights for the current article
- Filter by color or notes
- Edit or delete existing highlights
- Jump to any highlight with smooth scrolling

### Global Notes Page
- Visit `/notes` to see all highlights across all articles
- Search through highlight text and notes
- Filter by article or color
- Organized by article with timestamps
- Direct links to jump back to highlighted text

### Deep Linking
- Share specific quotes with others
- Automatic text fragment support for short, ASCII text
- Custom anchor fallback for longer or special text
- Smooth scrolling with visual pulse animation

### Database Integration
- Built on Supabase with Row Level Security
- W3C Web Annotation-style selectors for robust text anchoring
- Optimistic UI updates with React Query
- Real-time sync across devices

## Implementation Details

### Text Selector Algorithm
The highlighting system uses W3C Web Annotation TextQuoteSelector pattern:
- `exact`: The highlighted text
- `prefix`: Text before the selection for disambiguation
- `suffix`: Text after the selection for disambiguation
- Fallback to position-based selectors when needed

### Performance
- Efficient DOM manipulation with minimal re-renders
- Lazy loading of highlights
- Client-side caching with React Query
- Debounced search and filtering

### Accessibility
- ARIA labels for screen readers
- Keyboard navigation support
- Color contrast compliance
- Focus management for toolbar and panels

## Usage

1. **Creating Highlights**: Select text → Choose color → Add note (optional) → Click "Highlight"
2. **Managing**: Use sidebar panel to view, edit, filter, and delete highlights
3. **Searching**: Global `/notes` page with full-text search
4. **Sharing**: Copy quote links to share specific passages

This feature enhances the reading experience by allowing users to create their own personalized study notes and annotations directly within articles.
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,11 +127,49 @@ Feel free to explore the content and features of the blog! 🚀
- **User Authentication:** User authentication with JWT tokens.
- **User Registration/Sign-In:** User registration and sign-in functionality.
- **Favorites:** Add articles to favorites for easy access (authenticated users only).
- **Text Highlighting & Annotations:** Select and highlight text in articles with personal notes, color coding, and shareable quote links (authenticated users only).
- **Global Notes Management:** View, search, and organize all your highlights and notes across articles in a dedicated `/notes` page.
- **Deep Linking:** Share specific quotes and highlights with others via direct links that automatically scroll to the highlighted text.
- **Responsive Design:** Mobile-friendly layout with responsive design.
- **Dark Mode:** Toggle between light and dark themes.
- **SEO-Friendly:** Optimized for search engines with meta tags and structured data, with SSR and SSG.
- **Linting & Formatting:** ESLint and Prettier configurations for consistent code quality.

## Using Text Highlights & Annotations

The blog includes a powerful text highlighting and annotation system that allows authenticated users to:

### Creating Highlights
1. **Select Text**: Select any text within an article
2. **Choose Color**: Pick from yellow, green, blue, pink, or orange highlights
3. **Add Notes**: Optionally add personal notes to your highlights
4. **Save**: Click "Highlight" to save your annotation

### Managing Highlights
- **Sidebar Panel**: Click "Notes" panel on the right to view all highlights for the current article
- **Filter & Search**: Filter highlights by color or notes, search across all content
- **Edit**: Click the edit button to modify notes or change colors
- **Delete**: Remove highlights you no longer need

### Sharing Highlights
- **Copy Links**: Generate shareable links to specific quotes
- **Deep Linking**: Links automatically scroll to the highlighted text
- **Public/Private**: Control whether your highlights are visible to others

### Global Notes Management
- **Visit `/notes`** to see all your highlights across all articles
- **Search**: Find specific highlights using the search functionality
- **Organize**: Group by article, filter by color or notes
- **Navigate**: Click any highlight to jump directly to its location in the article

### Database Setup
To enable highlights functionality, run the SQL schema in `supabase/article_highlights.sql` in your Supabase project:

```sql
-- Creates the article_highlights table with RLS policies
-- See supabase/article_highlights.sql for the complete schema
```

## Project Structure

```
Expand All @@ -143,11 +181,17 @@ devverse-cs-swe-blog/
│ ├── not-found.tsx # 404 page component
│ ├── favorites/
│ │ ├── page.tsx # Favorites page component
│ ├── notes/
│ │ ├── page.tsx # Global notes/highlights page
│ ├── api/
│ │ ├── reset-password/
│ │ │ ├── route.ts # Reset password API route
│ │ ├── favorites/
│ │ │ ├── route.ts # Favorites API route
│ │ ├── highlights/
│ │ │ ├── route.ts # Highlights CRUD API
│ │ │ └── [id]/
│ │ │ └── route.ts # Individual highlight API
│ ├── auth/
│ │ ├── login/
│ │ │ ├── page.tsx # Login page component
Expand All @@ -160,6 +204,11 @@ devverse-cs-swe-blog/
├── components/ # Reusable React components
│ ├── ArticleList.tsx # Component for displaying a list of articles
│ ├── ArticleContent.tsx # Article content component
│ ├── highlights/ # Text highlighting components
│ │ ├── ArticleHighlights.tsx # Main highlights wrapper
│ │ ├── SelectionToolbar.tsx # Text selection toolbar
│ │ ├── HighlightRenderer.tsx # DOM highlight rendering
│ │ └── HighlightsPanel.tsx # Sidebar highlights panel
│ ├── FavoritesList.tsx # Component for displaying a list of favorite articles
│ ├── FavButton.tsx # Favorite button component
│ ├── Footer.tsx # Footer component
Expand All @@ -176,12 +225,21 @@ devverse-cs-swe-blog/
│ ├── CodeBlock.tsx # Code block component
│ ├── InlineCode.tsx # Inline code component
│ └── PreBlock.tsx # Preformatted block component
├── lib/ # Core application libraries
│ └── highlights/ # Text highlighting utilities
│ └── selectors.ts # W3C Web Annotation selectors
├── hooks/ # React hooks for data management
│ └── useHighlights.ts # React Query hooks for highlights
├── provider/ # Context providers
│ ├── DarkModeProvider.tsx # Dark mode context
│ └── QueryProvider.tsx # React Query provider
├── supabase/ # Supabase client configuration and queries
│ ├── supabaseClient.ts # Supabase client configuration
│ ├── auth.ts # Authentication functions
│ ├── avatar.ts # Avatar functions
│ ├── favorites.ts # Favorites functions
│ ├── profile.ts # Profile functions
│ ├── article_highlights.sql # Highlights table schema
│ └── (other sql files for database setup)
├── public/ # Static files (images, fonts, etc.)
├── content/ # MDX content for blog posts
Expand Down
157 changes: 157 additions & 0 deletions app/api/highlights/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { NextRequest, NextResponse } from "next/server";
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import { z } from "zod";

const updateHighlightSchema = z.object({
note: z.string().optional(),
color: z.string().optional(),
is_public: z.boolean().optional(),
});

export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const cookieStore = await cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {}
},
},
}
);

// Get current user
const {
data: { user },
error: userError,
} = await supabase.auth.getUser();

if (userError || !user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const body = await request.json();
const validatedData = updateHighlightSchema.parse(body);

// Check if highlight exists and belongs to user
const { data: existingHighlight, error: fetchError } = await supabase
.from("article_highlights")
.select("id")
.eq("id", id)
.eq("user_id", user.id)
.single();

if (fetchError || !existingHighlight) {
return NextResponse.json(
{ error: "Highlight not found" },
{ status: 404 }
);
}

const { data, error } = await supabase
.from("article_highlights")
.update(validatedData)
.eq("id", id)
.eq("user_id", user.id)
.select()
.single();

if (error) {
console.error("Error updating highlight:", error);
return NextResponse.json(
{ error: "Failed to update highlight" },
{ status: 500 }
);
}

return NextResponse.json({ highlight: data });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ error: "Invalid request data", details: error.errors },
{ status: 400 }
);
}

console.error("Error in PATCH /api/highlights/[id]:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}

export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params;
const cookieStore = await cookies();
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {}
},
},
}
);

// Get current user
const {
data: { user },
error: userError,
} = await supabase.auth.getUser();

if (userError || !user) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}

const { error } = await supabase
.from("article_highlights")
.delete()
.eq("id", id)
.eq("user_id", user.id);

if (error) {
console.error("Error deleting highlight:", error);
return NextResponse.json(
{ error: "Failed to delete highlight" },
{ status: 500 }
);
}

return NextResponse.json({ success: true });
} catch (error) {
console.error("Error in DELETE /api/highlights/[id]:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
Loading