Skip to content

Commit

Permalink
feat: Implement bookmark highlights and metadata updates (main)
Browse files Browse the repository at this point in the history
- Add Highlight structure to bookmark DTO.
- Updates Bookmark metadata including tags, highlights, and images.
- Add swr for data fetching and mutations
- Implement protected routes
- Remove bookmark type
  • Loading branch information
vaayne committed Dec 17, 2024
1 parent 028eda8 commit 83ec2b7
Show file tree
Hide file tree
Showing 17 changed files with 456 additions and 174 deletions.
16 changes: 13 additions & 3 deletions internal/core/bookmarks/dto.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,20 @@ import (
"github.com/pgvector/pgvector-go"
)

type Highlight struct {
ID string `json:"id"`
Text string `json:"text"`
StartOffset int `json:"start_offset"`
EndOffset int `json:"end_offset"`
Note string `json:"note,omitempty"`
}

type Metadata struct {
Author string `json:"author,omitempty"`
PublishedAt time.Time `json:"published_at,omitempty"`
Tags []string `json:"tags,omitempty"`
Author string `json:"author,omitempty"`
PublishedAt time.Time `json:"published_at,omitempty"`
Tags []string `json:"tags,omitempty"`
Highlights []Highlight `json:"highlights,omitempty"`
Image string `json:"image,omitempty"`
}

// BookmarkDTO represents the domain model for a bookmark
Expand Down
6 changes: 1 addition & 5 deletions internal/port/httpserver/bookmark_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ type updateBookmarkRequest struct {
Summary string `json:"summary"`
Content string `json:"content"`
HTML string `json:"html"`
// Metadata bookmarks.Metadata `json:"metadata"`
Metadata bookmarks.Metadata `json:"metadata"`
}

// updateBookmark handles PUT /bookmarks/:bookmark-id
Expand Down Expand Up @@ -202,10 +202,6 @@ func (h *bookmarksHandler) updateBookmark(c echo.Context) error {
return ErrorResponse(c, http.StatusInternalServerError, err)
}

if req.Summary == "" && req.Content == "" && req.HTML == "" {
return ErrorResponse(c, http.StatusBadRequest, fmt.Errorf("no update fields provided"))
}

bookmark := &bookmarks.BookmarkDTO{
ID: req.BookmarkID,
UserID: user.ID,
Expand Down
Binary file modified web/bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.0.2",
"swr": "^2.2.5",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.3"
Expand Down
47 changes: 37 additions & 10 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,47 @@ import { Route, BrowserRouter as Router, Routes } from "react-router-dom";
import AuthPage from "./pages/auth";
import BookmarkPage from "./pages/BookmarkPage";
import HomePage from "./pages/HomePage";
import ProtectedRoute from "@/components/ProtectedRoute";
import { SWRConfig } from "swr";

export default function App() {
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
<Router>
<BaseLayout>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/bookmarks/:id" element={<BookmarkPage />} />
<Route path="/accounts/login" element={<AuthPage />} />
<Route path="/accounts/signup" element={<AuthPage />} />
</Routes>
</BaseLayout>
</Router>
<SWRConfig
value={{
// Define your global configuration options here
fetcher: (resource, init) =>
fetch(resource, init).then((res) => res.json()),
// ...other global configurations...
}}
>
<Router>
<BaseLayout>
<Routes>
{/* Public routes */}
<Route path="/accounts/login" element={<AuthPage />} />
<Route path="/accounts/signup" element={<AuthPage />} />
{/* Protected routes */}
<Route
path="/"
element={
<ProtectedRoute>
<HomePage />
</ProtectedRoute>
}
/>
<Route
path="/bookmarks/:id"
element={
<ProtectedRoute>
<BookmarkPage />
</ProtectedRoute>
}
/>
</Routes>
</BaseLayout>
</Router>
</SWRConfig>
</ThemeProvider>
);
}
65 changes: 29 additions & 36 deletions web/src/components/BookmarkDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,37 +9,26 @@ import {
} from "@/components/ui/card";
import { ScrollArea } from "@/components/ui/scroll-area";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Bookmark, Highlight } from "@/types/bookmark";
import { Bookmark, Highlight } from "@/lib/apis/bookmarks";
import { Calendar, ExternalLink, X } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { useMemo, useRef, useState } from "react";
import { v4 as uuidv4 } from "uuid";

interface BookmarkDetailProps {
bookmark: Bookmark;
onUpdateBookmark: (id: number, highlights: Highlight[]) => void;
onUpdateBookmark: (id: string, highlights: Highlight[]) => void;
}

export default function BookmarkDetail({
bookmark,
onUpdateBookmark,
}: BookmarkDetailProps) {
const [highlights, setHighlights] = useState<Highlight[]>([]);
const [highlights, setHighlights] = useState<Highlight[]>(
bookmark.metadata?.highlights || [],
);
const [isHighlighting, setIsHighlighting] = useState(false);
const contentRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const savedHighlights = localStorage.getItem(`highlights-${bookmark.id}`);
if (savedHighlights) {
setHighlights(JSON.parse(savedHighlights));
}
}, [bookmark.id]);

useEffect(() => {
const highlightsJson = JSON.stringify(highlights);
localStorage.setItem(`highlights-${bookmark.id}`, highlightsJson);
onUpdateBookmark(bookmark.id, highlights);
}, [highlights, bookmark.id, onUpdateBookmark]);

const handleHighlight = () => {
const selection = window.getSelection();
if (selection && !selection.isCollapsed && contentRef.current) {
Expand All @@ -56,15 +45,18 @@ export default function BookmarkDetail({
};

setHighlights([...highlights, newHighlight]);
onUpdateBookmark(bookmark.id, [...highlights, newHighlight]);
}
};

const removeHighlight = (id: string) => {
setHighlights(highlights.filter((h) => h.id !== id));
const updatedHighlights = highlights.filter((h) => h.id !== id);
setHighlights(updatedHighlights);
onUpdateBookmark(bookmark.id, updatedHighlights);
};

const highlightedContent = useMemo(() => {
let content = bookmark.content;
let content = bookmark.html || bookmark.content || "";
highlights.forEach((highlight) => {
const before = content.slice(0, highlight.startOffset);
const highlighted = content.slice(
Expand All @@ -75,13 +67,13 @@ export default function BookmarkDetail({
content = `${before}<mark class="bg-yellow-200 dark:bg-yellow-800">${highlighted}</mark>${after}`;
});
return content;
}, [bookmark.content, highlights]);
}, [bookmark.html, bookmark.content, highlights]);

return (
<div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
<div>
<h1 className="text-3xl font-bold">{bookmark.title}</h1>
<h1 className="text-3xl font-bold">{bookmark.title || "Untitled"}</h1>
<p className="text-muted-foreground mt-1">
<a
href={bookmark.url}
Expand All @@ -97,26 +89,23 @@ export default function BookmarkDetail({
<div className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-muted-foreground" />
<span className="text-sm text-muted-foreground">
Added on{" "}
{bookmark.dateAdded
? new Date(bookmark.dateAdded).toLocaleDateString()
: "Unknown date"}
Added on {new Date(bookmark.created_at).toLocaleDateString()}
</span>
</div>
</div>

<div className="flex flex-wrap gap-2">
{bookmark.tags.map((tag) => (
{bookmark.metadata?.tags?.map((tag) => (
<Badge key={tag} variant="secondary">
{tag}
</Badge>
))}
</div>

{bookmark.image && (
{bookmark.screenshot && (
<img
src={bookmark.image}
alt={`Thumbnail for ${bookmark.title}`}
src={bookmark.screenshot}
alt={`Screenshot of ${bookmark.title || bookmark.url}`}
className="w-full h-64 object-cover rounded-lg"
/>
)}
Expand Down Expand Up @@ -144,12 +133,16 @@ export default function BookmarkDetail({
</Button>
</div>
<ScrollArea className="h-[60vh]">
<div
ref={contentRef}
className="prose dark:prose-invert max-w-none"
dangerouslySetInnerHTML={{ __html: highlightedContent }}
onMouseUp={isHighlighting ? handleHighlight : undefined}
/>
{bookmark.html || bookmark.content ? (
<div
ref={contentRef}
className="prose dark:prose-invert max-w-none"
dangerouslySetInnerHTML={{ __html: highlightedContent }}
onMouseUp={isHighlighting ? handleHighlight : undefined}
/>
) : (
<p className="text-muted-foreground">No content available</p>
)}
</ScrollArea>
</CardContent>
</Card>
Expand All @@ -163,7 +156,7 @@ export default function BookmarkDetail({
</CardDescription>
</CardHeader>
<CardContent>
<p>{bookmark.summary}</p>
<p>{bookmark.summary || "No summary available"}</p>
</CardContent>
</Card>
</TabsContent>
Expand Down
15 changes: 8 additions & 7 deletions web/src/components/BookmarkGrid.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Bookmark } from "@/types/bookmark";
import { Bookmark } from "@/lib/apis/bookmarks";
import { ExternalLink, Highlighter } from "lucide-react";
import { Link } from "react-router-dom";

Expand All @@ -14,9 +14,9 @@ export default function BookmarkGrid({ bookmarks }: BookmarkGridProps) {
{bookmarks.map((bookmark) => (
<Link to={`/bookmarks/${bookmark.id}`} key={bookmark.id}>
<Card className="h-full cursor-pointer hover:shadow-md transition-all duration-300 ease-in-out transform hover:-translate-y-1">
{bookmark.image && (
{bookmark.metadata?.image && (
<img
src={bookmark.image}
src={bookmark.metadata?.image}
alt={`Thumbnail for ${bookmark.title}`}
className="w-full h-40 object-cover"
/>
Expand All @@ -25,9 +25,10 @@ export default function BookmarkGrid({ bookmarks }: BookmarkGridProps) {
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2 truncate">
{bookmark.title}
{bookmark.highlights && bookmark.highlights.length > 0 && (
<Highlighter className="h-4 w-4 text-yellow-500 flex-shrink-0" />
)}
{bookmark.metadata?.highlights &&
bookmark.metadata?.highlights.length > 0 && (
<Highlighter className="h-4 w-4 text-yellow-500 flex-shrink-0" />
)}
</span>
<a
href={bookmark.url}
Expand All @@ -42,7 +43,7 @@ export default function BookmarkGrid({ bookmarks }: BookmarkGridProps) {
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{bookmark.tags.map((tag) => (
{bookmark.metadata?.tags?.map((tag) => (
<Badge key={tag} variant="secondary">
{tag}
</Badge>
Expand Down
16 changes: 9 additions & 7 deletions web/src/components/BookmarkList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Bookmark } from "@/types/bookmark";
import { Bookmark } from "@/lib/apis/bookmarks";
import { ExternalLink, Highlighter } from "lucide-react";
import { Link } from "react-router-dom";

Expand All @@ -21,22 +21,24 @@ export default function BookmarkList({ bookmarks }: BookmarkListProps) {
<Link to={`/bookmarks/${bookmark.id}`} key={bookmark.id}>
<Card className="cursor-pointer hover:shadow-md transition-all duration-300 ease-in-out transform hover:-translate-y-1 overflow-hidden">
<div className="flex">
{bookmark.image && (
{bookmark.metadata?.image && (
<div className="w-1/4 min-w-[100px]">
<img
src={bookmark.image}
src={bookmark.metadata?.image}
alt={`Thumbnail for ${bookmark.title}`}
className="w-full h-full object-cover"
/>
</div>
)}
<div className={`flex-1 ${bookmark.image ? "w-3/4" : "w-full"}`}>
<div
className={`flex-1 ${bookmark.metadata?.image ? "w-3/4" : "w-full"}`}
>
<CardHeader>
<CardTitle className="flex items-center justify-between">
<span className="flex items-center gap-2 truncate">
{bookmark.title}
{bookmark.highlights &&
bookmark.highlights.length > 0 && (
{bookmark.metadata?.highlights &&
bookmark.metadata?.highlights.length > 0 && (
<Highlighter className="h-4 w-4 text-yellow-500 flex-shrink-0" />
)}
</span>
Expand All @@ -56,7 +58,7 @@ export default function BookmarkList({ bookmarks }: BookmarkListProps) {
</CardHeader>
<CardContent>
<div className="flex flex-wrap gap-2">
{bookmark.tags.map((tag) => (
{bookmark.metadata?.tags?.map((tag) => (
<Badge key={tag} variant="secondary">
{tag}
</Badge>
Expand Down
22 changes: 22 additions & 0 deletions web/src/components/ProtectedRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React from "react";
import { useUser } from "@/lib/apis/auth";
import { Navigate } from "react-router-dom";

interface ProtectedRouteProps {
children: React.ReactElement;
}

export default function ProtectedRoute({ children }: ProtectedRouteProps) {
const { user, isLoading } = useUser();

if (isLoading) {
// Render a loading indicator or null
return null; // or your loading component
}

if (!user) {
return <Navigate to="/accounts/login" replace />;
}

return children;
}
5 changes: 3 additions & 2 deletions web/src/components/layout/header.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Button } from "@/components/ui/button";
import { Link, useNavigate } from "react-router-dom";
import { UserNav } from "./user-nav";
import { useUser } from "@/lib/apis/auth";

export default function Header() {
const isAuthenticated = true;
const { user, isLoading } = useUser();
const navigate = useNavigate();

return (
Expand Down Expand Up @@ -31,7 +32,7 @@ export default function Header() {
</nav>
</div>
<div className="flex items-center">
{isAuthenticated ? (
{user ? (
<UserNav />
) : (
<Button
Expand Down
Loading

0 comments on commit 83ec2b7

Please sign in to comment.