Skip to content

Commit

Permalink
🚀 feat: add ArticleReader component and dependencies (main)
Browse files Browse the repository at this point in the history
- Introduced `ArticleReader` component for rendering articles with summary, highlights, and content sections.
- Added new dependencies: `@radix-ui/react-accordion`, `prism-react-renderer`, `react-markdown`, `rehype-raw`, `remark-gfm`, and `@tailwindcss/typography`.
- Updated `BookmarkPage` to use `ArticleReader` instead of `BookmarkDetail`.
- Added keyframe animations for accordion behavior in Tailwind CSS configuration.
  • Loading branch information
vaayne committed Dec 19, 2024
1 parent 9a7ec64 commit 91de71e
Show file tree
Hide file tree
Showing 7 changed files with 334 additions and 21 deletions.
Binary file modified web/bun.lockb
Binary file not shown.
8 changes: 8 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"generate-pwa-assets": "pwa-assets-generator"
},
"dependencies": {
"@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-avatar": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.2",
"@radix-ui/react-dropdown-menu": "^2.1.2",
Expand All @@ -23,23 +24,30 @@
"clsx": "^2.1.1",
"lucide-react": "^0.468.0",
"next-themes": "^0.4.4",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"react-router-dom": "^7.0.2",
"rehype-raw": "^7.0.0",
"remark-gfm": "^4.0.0",
"swr": "^2.2.5",
"tailwind-merge": "^2.5.5",
"tailwindcss-animate": "^1.0.7",
"uuid": "^11.0.3"
},
"devDependencies": {
"@biomejs/biome": "1.9.4",
"@tailwindcss/typography": "^0.5.15",
"@types/node": "^22.10.1",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react-syntax-highlighter": "^15.5.13",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"globals": "^15.12.0",
"postcss": "^8.4.49",
"react-syntax-highlighter": "^15.6.1",
"tailwindcss": "^3.4.16",
"typescript": "~5.6.2",
"vite": "^6.0.1"
Expand Down
107 changes: 107 additions & 0 deletions web/src/components/content-reader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import MarkdownRenderer from "@/components/markdown-render";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import type { Bookmark as BookmarkType } from "@/lib/apis/bookmarks";
import { Bookmark, Share2, ThumbsUp } from "lucide-react";
import type React from "react";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
interface ArticleReaderProps {
bookmark: BookmarkType;
}

export const ArticleReader: React.FC<ArticleReaderProps> = ({ bookmark }) => {
const { title, summary, content, metadata } = bookmark;

const handleShare = () => {
// Implement share functionality
console.log("Sharing article:", bookmark.url);
};

const handleLike = () => {
// Implement like functionality
console.log("Liking article:", bookmark.id);
};

const handleSaveBookmark = () => {
// Implement save bookmark functionality
console.log("Saving bookmark:", bookmark.id);
};

return (
<div className="container mx-auto px-4 py-8 max-w-screen-lg">
<Card className="p-8 shadow-lg">
<h1 className="text-3xl font-bold mb-4">{title}</h1>

{/* Summary Section using Accordion */}
<Accordion type="single" collapsible className="mb-8">
<AccordionItem value="summary" className="border-none">
<AccordionTrigger className="bg-background rounded-t-lg px-6 py-4 hover:no-underline">
<h2 className="text-lg font-semibold">Summary</h2>
</AccordionTrigger>
<AccordionContent className="bg-muted rounded-b-lg px-6 pb-6">
<div className="prose dark:prose-invert prose-sm max-w-none pt-4">
<p style={{ whiteSpace: "pre-line" }}>{summary}</p>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>

{/* {metadata?.tags && (
<div className="flex flex-wrap gap-2 mb-6">
{metadata.tags.map((tag, index) => (
<Badge key={index} variant="secondary">
{tag}
</Badge>
))}
</div>
)} */}

<div className="flex space-x-4 mb-8">
<Button onClick={handleShare} variant="outline">
<Share2 className="mr-2 h-4 w-4" /> Share
</Button>
<Button onClick={handleLike} variant="outline">
<ThumbsUp className="mr-2 h-4 w-4" /> Like
</Button>
<Button onClick={handleSaveBookmark} variant="outline">
<Bookmark className="mr-2 h-4 w-4" /> Save
</Button>
</div>

{metadata?.highlights && (
<div className="mb-8">
<h2 className="text-xl font-semibold mb-4">Highlights</h2>
{metadata.highlights.map((highlight) => (
<div
key={highlight.id}
className="bg-yellow-100 dark:bg-yellow-900/30 p-4 rounded-md mb-4"
>
<p className="text-gray-800 dark:text-gray-200">
{highlight.text}
</p>
{highlight.note && (
<p className="text-sm text-gray-600 dark:text-gray-400 mt-2">
Note: {highlight.note}
</p>
)}
</div>
))}
</div>
)}

{/* Main Content */}
<div className="mt-8 border-t pt-8">
<div className="prose dark:prose-invert prose-lg max-w-none">
<MarkdownRenderer content={content ?? ""} />
</div>
</div>
</Card>
</div>
);
};
138 changes: 138 additions & 0 deletions web/src/components/markdown-render.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import React from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import {
oneDark,
oneLight,
} from "react-syntax-highlighter/dist/esm/styles/prism";
import { Card, CardContent } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { useTheme } from "next-themes";

interface MarkdownRendererProps {
content: string;
className?: string;
}

const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
content,
className,
}) => {
const { theme } = useTheme();

// Custom components for markdown elements
const components = {
// Heading components
h1: ({ node, ...props }) => (
<h1
{...props}
className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl mb-4"
/>
),
h2: ({ node, ...props }) => (
<h2
{...props}
className="scroll-m-20 text-3xl font-semibold tracking-tight mb-3"
/>
),
h3: ({ node, ...props }) => (
<h3
{...props}
className="scroll-m-20 text-2xl font-semibold tracking-tight mb-2"
/>
),

// Paragraph and text elements
p: ({ node, ...props }) => (
<p {...props} className="leading-7 [&:not(:first-child)]:mt-6" />
),
a: ({ node, ...props }) => (
<a
{...props}
className="font-medium text-primary underline underline-offset-4 hover:text-primary/80"
target="_blank"
rel="noopener noreferrer"
/>
),

// List elements
ul: ({ node, ...props }) => (
<ul {...props} className="my-6 ml-6 list-disc [&>li]:mt-2" />
),
ol: ({ node, ...props }) => (
<ol {...props} className="my-6 ml-6 list-decimal [&>li]:mt-2" />
),

// Code blocks
code: ({ node, inline, className, children, ...props }) => {
const match = /language-(\w+)/.exec(className || "");
return !inline && match ? (
<Card className="my-4">
<CardContent className="p-0">
<SyntaxHighlighter
style={theme === "dark" ? oneDark : oneLight}
language={match[1]}
PreTag="div"
customStyle={{
margin: 0,
}}
{...props}
>
{String(children).replace(/\n$/, "")}
</SyntaxHighlighter>
</CardContent>
</Card>
) : (
<code
className="relative rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm font-semibold"
{...props}
>
{children}
</code>
);
},

// Blockquote
blockquote: ({ node, ...props }) => (
<blockquote
{...props}
className="mt-6 border-l-2 border-primary pl-6 italic"
/>
),

// Table elements
table: ({ node, ...props }) => (
<div className="my-6 w-full overflow-y-auto">
<table {...props} className="w-full" />
</div>
),
th: ({ node, ...props }) => (
<th
{...props}
className="border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right"
/>
),
td: ({ node, ...props }) => (
<td
{...props}
className="border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right"
/>
),
};

return (
<div className={cn("markdown-content", className)}>
<ReactMarkdown
components={components}
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
>
{content}
</ReactMarkdown>
</div>
);
};

export default MarkdownRenderer;
55 changes: 55 additions & 0 deletions web/src/components/ui/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as React from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDown } from "lucide-react";

import { cn } from "@/lib/utils";

const Accordion = AccordionPrimitive.Root;

const AccordionItem = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn("border-b", className)}
{...props}
/>
));
AccordionItem.displayName = "AccordionItem";

const AccordionTrigger = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
ref={ref}
className={cn(
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
className,
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
));
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;

const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
AccordionContent.displayName = AccordionPrimitive.Content.displayName;

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
21 changes: 3 additions & 18 deletions web/src/pages/BookmarkPage.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import BookmarkDetail from "@/components/BookmarkDetail";
import { useBookmark, useBookmarkMutations } from "@/lib/apis/bookmarks";
import { ArticleReader } from "@/components/content-reader";
import { useBookmark } from "@/lib/apis/bookmarks";
import { useParams } from "react-router-dom";

export default function BookmarkPage() {
const { id } = useParams<{ id: string }>();
const { data: bookmark, error } = useBookmark(id!);
const { updateBookmark } = useBookmarkMutations();

if (error) {
return <div className="container mx-auto p-4">Error loading bookmark</div>;
Expand All @@ -17,21 +16,7 @@ export default function BookmarkPage() {

return (
<div className="container mx-auto p-4 max-w-4xl">
<BookmarkDetail
bookmark={bookmark}
onUpdateBookmark={async (id, highlights) => {
try {
await updateBookmark(id, {
metadata: {
...bookmark.metadata,
highlights,
},
});
} catch (error) {
console.error("Failed to update bookmark:", error);
}
}}
/>
<ArticleReader bookmark={bookmark} />
</div>
);
}
Loading

0 comments on commit 91de71e

Please sign in to comment.