-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
🚀 feat: add ArticleReader component and dependencies (main)
- 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
Showing
7 changed files
with
334 additions
and
21 deletions.
There are no files selected for viewing
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.