a new start
This commit is contained in:
136
components/readme/readme-header.tsx
Normal file
136
components/readme/readme-header.tsx
Normal file
@@ -0,0 +1,136 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Star, Share2, ExternalLink, Copy, Mail, MessageSquare } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface ReadmeHeaderProps {
|
||||
metadata: {
|
||||
title: string
|
||||
description: string
|
||||
stars: number
|
||||
lastUpdated: string
|
||||
url: string
|
||||
}
|
||||
}
|
||||
|
||||
export function ReadmeHeader({ metadata }: ReadmeHeaderProps) {
|
||||
const [isSticky, setIsSticky] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleScroll = () => {
|
||||
setIsSticky(window.scrollY > 100)
|
||||
}
|
||||
|
||||
window.addEventListener('scroll', handleScroll)
|
||||
return () => window.removeEventListener('scroll', handleScroll)
|
||||
}, [])
|
||||
|
||||
const handleCopyLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(window.location.href)
|
||||
toast.success('Link copied to clipboard!')
|
||||
} catch (error) {
|
||||
toast.error('Failed to copy link')
|
||||
}
|
||||
}
|
||||
|
||||
const handleShare = (type: 'twitter' | 'email' | 'reddit') => {
|
||||
const url = encodeURIComponent(window.location.href)
|
||||
const text = encodeURIComponent(`Check out ${metadata.title}: ${metadata.description}`)
|
||||
|
||||
const shareUrls = {
|
||||
twitter: `https://twitter.com/intent/tweet?text=${text}&url=${url}`,
|
||||
email: `mailto:?subject=${encodeURIComponent(metadata.title)}&body=${text}%20${url}`,
|
||||
reddit: `https://reddit.com/submit?url=${url}&title=${encodeURIComponent(metadata.title)}`,
|
||||
}
|
||||
|
||||
window.open(shareUrls[type], '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`sticky top-0 z-50 border-b transition-all duration-300 ${
|
||||
isSticky
|
||||
? 'bg-background/95 shadow-lg backdrop-blur-sm'
|
||||
: 'bg-background/80 backdrop-blur-sm'
|
||||
}`}
|
||||
>
|
||||
<div className="mx-auto max-w-7xl px-6 py-4">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
{/* Left side */}
|
||||
<div className="flex-1">
|
||||
<h1 className="gradient-text mb-2 text-2xl font-bold sm:text-3xl">
|
||||
{metadata.title}
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground sm:text-base">
|
||||
{metadata.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Right side - Actions */}
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Stars */}
|
||||
<div className="flex items-center gap-1.5 rounded-lg border border-primary/20 bg-primary/10 px-3 py-2 text-sm font-medium text-primary">
|
||||
<Star className="h-4 w-4 fill-current" />
|
||||
<span>{metadata.stars.toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
{/* Share Button */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="default" className="gap-2">
|
||||
<Share2 className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Share</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={handleCopyLink}>
|
||||
<Copy className="mr-2 h-4 w-4" />
|
||||
Copy Link
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleShare('twitter')}>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Share on Twitter
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleShare('reddit')}>
|
||||
<MessageSquare className="mr-2 h-4 w-4" />
|
||||
Share on Reddit
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleShare('email')}>
|
||||
<Mail className="mr-2 h-4 w-4" />
|
||||
Share via Email
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
{/* GitHub Link */}
|
||||
<Button asChild variant="default" size="default" className="gap-2">
|
||||
<Link href={metadata.url} target="_blank" rel="noopener noreferrer">
|
||||
<ExternalLink className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">View on GitHub</span>
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Last updated */}
|
||||
<div className="mt-2 text-xs text-muted-foreground">
|
||||
Last updated: {new Date(metadata.lastUpdated).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
47
components/readme/readme-viewer.tsx
Normal file
47
components/readme/readme-viewer.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
'use client'
|
||||
|
||||
import * as React from 'react'
|
||||
import { Marked } from 'marked'
|
||||
import { markedHighlight } from 'marked-highlight'
|
||||
import hljs from 'highlight.js'
|
||||
import 'highlight.js/styles/github-dark.css'
|
||||
|
||||
interface ReadmeViewerProps {
|
||||
content: string
|
||||
}
|
||||
|
||||
export function ReadmeViewer({ content }: ReadmeViewerProps) {
|
||||
const [html, setHtml] = React.useState('')
|
||||
|
||||
React.useEffect(() => {
|
||||
const marked = new Marked(
|
||||
markedHighlight({
|
||||
langPrefix: 'hljs language-',
|
||||
highlight(code, lang) {
|
||||
const language = hljs.getLanguage(lang) ? lang : 'plaintext'
|
||||
return hljs.highlight(code, { language }).value
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
// Configure marked options
|
||||
marked.setOptions({
|
||||
gfm: true,
|
||||
breaks: true,
|
||||
})
|
||||
|
||||
// Parse markdown
|
||||
const parseMarkdown = async () => {
|
||||
const result = await marked.parse(content)
|
||||
setHtml(result)
|
||||
}
|
||||
parseMarkdown()
|
||||
}, [content])
|
||||
|
||||
return (
|
||||
<article
|
||||
className="prose prose-lg dark:prose-invert max-w-none prose-headings:gradient-text prose-headings:font-bold prose-a:text-primary prose-a:no-underline hover:prose-a:underline prose-code:rounded prose-code:bg-muted prose-code:px-1.5 prose-code:py-0.5 prose-code:font-mono prose-code:text-sm prose-pre:rounded-lg prose-pre:border prose-pre:border-primary/20 prose-pre:bg-muted/50 prose-img:rounded-lg prose-hr:border-primary/20"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user