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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts

# sqlite
*.db
66 changes: 20 additions & 46 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Adanomad Online Assessment
# PDF Highlighter

## Project Overview

Expand All @@ -14,15 +14,17 @@ This project is a PDF viewer and keyword search application developed as part of
- Text highlighting for search matches
- Sidebar for search results and navigation
- Responsive design for various screen sizes
- Persistent storage of highlights using SQLite or Supabase

## Technologies Used

- Next.js
- React
- TypeScript
- react-pdf library for PDF rendering
- Tailwind CSS for styling
- Custom highlight storage solution
- Tailwind CSS for stylinge
- SQLite for local highlight storage
- Supabase for cloud-based highlight storage (optional)

## Getting Started

Expand All @@ -33,43 +35,11 @@ This project is a PDF viewer and keyword search application developed as part of

## Project Structure

```
.
├── app
│ ├── components
│ │ ├── App.tsx
│ │ ├── Button.tsx
│ │ ├── HighlightPopup.tsx
│ │ ├── Input.tsx
│ │ ├── KeywordSearch.tsx
│ │ ├── PdfUploader.tsx
│ │ ├── PdfViewer.tsx
│ │ ├── Sidebar.tsx
│ │ └── Spinner.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.js
│ ├── page.js
│ ├── styles
│ │ ├── output.css
│ │ └── styles.css
│ └── utils
│ ├── highlightStorage.ts
│ └── pdfUtils.ts
├── public
│ ├── sample.pdf
│ └── ...
├── scripts
│ └── comment-file-path.sh
├── README.md
├── screenshot.png
└── ...
```

- `app/page.js`: Main entry point of the application
- `app/components/`: React components for various parts of the application
- `app/utils/`: Utility functions for PDF processing and highlight storage
- `app/styles/`: CSS files for styling
- `app/api/`: API routes for handling highlight operations

## Key Components

Expand All @@ -78,31 +48,35 @@ This project is a PDF viewer and keyword search application developed as part of
- `KeywordSearch.tsx`: Manages keyword search functionality
- `HighlightPopup.tsx`: Displays information about highlighted text
- `Sidebar.tsx`: Shows search results and navigation options
- `highlightStorage.ts`: Manages highlight storage operations
- `sqliteUtils.ts`: Handles SQLite database operations

## Screenshot
## Features

![Application Screenshot](./screenshot.png)
- Has a highlight storage system supporting both SQLite and Supabase
- API routes for creating, retrieving, updating, and deleting highlights
- User authentication and document permissions (currently disabled)
- Export/import as JSON functionality for highlights
- Scroll the sidebar highlighted area into view across different PDFs.

*The screenshot above shows the main interface of the PDF viewer application, including the document display, search functionality, and sidebar.*

## Future Improvements

- Implement annotation tools (e.g., freehand drawing, text notes)
- Add support for multiple document comparison
- Add support for multiple document search
- Pre-process batch PDFs for quicker highlights
- Enhance mobile responsiveness for better small-screen experience
- Implement user authentication and document permissions
- Optimize performance for large PDF files

## Contributing

Contributions, issues, and feature requests are welcome. Feel free to check [issues page](https://github.com/yourusername/your-repo-name/issues) if you want to contribute.
- Upload the PDF into the database.

## License

[MIT License](https://opensource.org/licenses/MIT)

## Acknowledgements

- [Next.js](https://nextjs.org/) for the React framework
- [SQLite](https://www.sqlite.org/) for local database storage
- [Supabase](https://supabase.io/) for cloud database capabilities
- [react-pdf](https://github.com/wojtekmaj/react-pdf) for PDF rendering capabilities
- [Tailwind CSS](https://tailwindcss.com/) for utility-first CSS framework
- [Next.js](https://nextjs.org/) for the React framework
2 changes: 2 additions & 0 deletions app/api/auth/[...nextauth]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// app/api/auth/[...nextauth]/route.ts
export { GET, POST } from "../../../utils/auth";
49 changes: 49 additions & 0 deletions app/api/highlight/get/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// app/api/highlight/get/route.ts
import HighlightStorage from "../../../utils/highlightStorage";
import { storageMethod } from "../../../utils/env";
import { StorageMethod } from "../../../utils/types";
import { getHighlightsForPdf as supabaseGetHighlightsForPdf } from "../../../utils/supabase";

async function handleRequest(req: Request): Promise<Response> {
let db: HighlightStorage | undefined;
try {
const body = await req.json();
let highlights;

if (storageMethod === StorageMethod.sqlite) {
db = new HighlightStorage();
highlights = await db.getHighlightsForPdf(body.pdfId);
} else {
highlights = await supabaseGetHighlightsForPdf(body.pdfId);
}

return new Response(JSON.stringify(highlights), {
status: 200,
headers: { "Content-Type": "application/json" },
});
} catch (error) {
console.error("Error in handleRequest:", error);
return new Response(
JSON.stringify({
error: "Internal Server Error",
details: error.message,
}),
{
status: 500,
headers: { "Content-Type": "application/json" },
}
);
} finally {
if (db) {
try {
await db.close();
} catch (closeError) {
console.error("Error closing database:", closeError);
}
}
}
}

export async function POST(req: Request): Promise<Response> {
return handleRequest(req);
}
78 changes: 78 additions & 0 deletions app/api/highlight/update/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// app/api/highlight/update/route.ts

import HighlightStorage from "../../../utils/highlightStorage";
import { storageMethod } from "../../../utils/env";
import {
deleteHighlight as supabaseDeleteHighlight,
saveBulkHighlights as supabaseSaveBulkHighlights,
saveHighlight as supabaseSaveHighlight,
} from "../../../utils/supabase";
import { StorageMethod, StoredHighlight } from "../../../utils/types";

async function handleRequest(
req: Request,
action: (body: any, db?: HighlightStorage) => Promise<void>
): Promise<Response> {
let db: HighlightStorage | undefined;
try {
const body = await req.json();
if (storageMethod === StorageMethod.sqlite) {
db = new HighlightStorage();
}
await action(body, db);
return new Response(null, { status: 200 });
} catch (error) {
console.error(error);
return new Response(null, { status: 500 });
} finally {
if (db) {
await db.close();
}
}
}

async function saveHighlights(body: any, db?: HighlightStorage): Promise<void> {
if (db) {
if (Array.isArray(body.highlights)) {
await db.saveBulkHighlights(ensureKeywords(body.highlights));
} else {
await db.saveHighlight(ensureKeyword(body.highlights));
}
} else {
if (Array.isArray(body)) {
await supabaseSaveBulkHighlights(ensureKeywords(body));
} else {
await supabaseSaveHighlight(ensureKeyword(body));
}
}
}

async function removeHighlight(
body: any,
db?: HighlightStorage
): Promise<void> {
if (db) {
await db.deleteHighlight(body.pdfId, body.id);
} else {
await supabaseDeleteHighlight(body);
}
}

function ensureKeyword(highlight: StoredHighlight): StoredHighlight {
return {
...highlight,
keyword: highlight.keyword || "",
};
}

function ensureKeywords(highlights: StoredHighlight[]): StoredHighlight[] {
return highlights.map(ensureKeyword);
}

export async function POST(req: Request): Promise<Response> {
return handleRequest(req, saveHighlights);
}

export async function DELETE(req: Request): Promise<Response> {
return handleRequest(req, removeHighlight);
}
27 changes: 27 additions & 0 deletions app/api/index/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// app/api/index/route.ts
import { storageMethod } from "../../utils/env";
import HighlightStorage from "../../utils/highlightStorage";
import { StorageMethod } from "../../utils/types";

export async function POST(req: Request) {
let response;
let db;
try {
const body = await req.json();
if (storageMethod === StorageMethod.sqlite) {
db = new HighlightStorage(body.pdfId);
await db.indexWords(body.pdfId, body.words);
} else {
throw new Error("Index via supabase has not been implemented");
}
response = new Response(null, { status: 200 });
} catch (error) {
console.log(error);
response = new Response(null, { status: 500 });
} finally {
if (db) {
await db.close();
}
return response;
}
}
Loading