diff --git a/app/globals.css b/app/globals.css index 0e0228e..a678eff 100644 --- a/app/globals.css +++ b/app/globals.css @@ -237,6 +237,21 @@ kbd { color: inherit; } +/* Search Highlight - Discreet styling */ +mark { + background: color-mix(in oklab, var(--primary) 15%, transparent); + color: var(--primary); + font-weight: 600; + padding: 0.125rem 0.25rem; + border-radius: 0.25rem; + border-bottom: 2px solid color-mix(in oklab, var(--primary) 40%, transparent); +} + +.dark mark { + background: color-mix(in oklab, var(--primary) 20%, transparent); + color: var(--primary-foreground); +} + /* Loading Spinner */ @keyframes spin-awesome { from { diff --git a/app/layout.tsx b/app/layout.tsx index abeabbb..ff02cf0 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -6,6 +6,7 @@ import { Toaster } from '@/components/ui/sonner' import { WorkerProvider } from '@/components/providers/worker-provider' import { CommandProvider } from '@/components/providers/command-provider' import { AppHeader } from '@/components/layout/app-header' +import { AppFooter } from '@/components/layout/app-footer' import { ThemeProvider } from 'next-themes' const inter = Inter({ subsets: ['latin'] }) @@ -203,8 +204,11 @@ export default function RootLayout({ > - - {children} +
+ +
{children}
+ +
diff --git a/app/page.tsx b/app/page.tsx index 532c759..5217e4e 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -237,39 +237,6 @@ export default function Home() { - - {/* Footer */} - ) } diff --git a/app/readme/[owner]/[repo]/page.tsx b/app/readme/[owner]/[repo]/page.tsx index b3ee514..fefb4e3 100644 --- a/app/readme/[owner]/[repo]/page.tsx +++ b/app/readme/[owner]/[repo]/page.tsx @@ -92,7 +92,7 @@ export default async function ReadmePage({ params }: PageProps) { } > - + diff --git a/app/repository/[id]/page.tsx b/app/repository/[id]/page.tsx index 1eedbfc..d6cce82 100644 --- a/app/repository/[id]/page.tsx +++ b/app/repository/[id]/page.tsx @@ -192,7 +192,7 @@ export default function RepositoryDetailPage() {
{data.readme?.content ? (
- +
) : (
diff --git a/app/search/page.tsx b/app/search/page.tsx index 5099f9b..5c4ae5d 100644 --- a/app/search/page.tsx +++ b/app/search/page.tsx @@ -3,7 +3,7 @@ import * as React from 'react' import { Suspense } from 'react' import { useSearchParams, useRouter } from 'next/navigation' -import { Search, Star, Filter, SlidersHorizontal, ExternalLink } from 'lucide-react' +import { Search, Star, Filter, SlidersHorizontal, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react' import { Input } from '@/components/ui/input' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' @@ -129,6 +129,48 @@ function SearchPageContent() { setFilters(prev => ({ ...prev, [key]: value })) } + // Strip markdown and HTML from descriptions + const stripMarkdown = (text: string | null): string => { + if (!text) return '' + return text + .replace(/<[^>]*>/g, '') // Remove HTML tags + .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // Convert [text](url) to text + .replace(/!\[([^\]]*)\]\([^)]+\)/g, '$1') // Convert ![alt](url) to alt text + .replace(/[*_~`#]/g, '') // Remove markdown formatting chars + .replace(/\s+/g, ' ') // Normalize whitespace + .trim() + } + + // Generate page numbers for pagination + const getPageNumbers = (current: number, total: number): (number | string)[] => { + const delta = 2 + const range: (number | string)[] = [] + const rangeWithDots: (number | string)[] = [] + let l: number | undefined + + for (let i = 1; i <= total; i++) { + if (i === 1 || i === total || (i >= current - delta && i <= current + delta)) { + range.push(i) + } + } + + for (const i of range) { + if (l !== undefined && typeof i === 'number') { + if (i - l === 2) { + rangeWithDots.push(l + 1) + } else if (i - l !== 1) { + rangeWithDots.push('...') + } + } + rangeWithDots.push(i) + if (typeof i === 'number') { + l = i + } + } + + return rangeWithDots + } + React.useEffect(() => { if (query) { performSearch(query) @@ -314,7 +356,7 @@ function SearchPageContent() {
{result.description && ( -

{result.description}

+

{stripMarkdown(result.description)}

)} {result.snippet && ( @@ -343,23 +385,43 @@ function SearchPageContent() { {/* Pagination */} {results.totalPages > 1 && ( -
+
- - Page {results.page} of {results.totalPages} - + + {getPageNumbers(results.page, results.totalPages).map((pageNum, idx) => + pageNum === '...' ? ( + + ... + + ) : ( + + ) + )} +
)} diff --git a/components/layout/app-footer.tsx b/components/layout/app-footer.tsx new file mode 100644 index 0000000..a6f386c --- /dev/null +++ b/components/layout/app-footer.tsx @@ -0,0 +1,37 @@ +import Link from 'next/link' + +export function AppFooter() { + return ( +
+
+
+
+
Awesome
+

+ Built with 💜💗💛 and maximum awesomeness +

+
+
+ + Legal + + + Disclaimer + + + Imprint + + + GitHub + +
+
+
+
+ ) +} diff --git a/components/readme/readme-viewer.tsx b/components/readme/readme-viewer.tsx index c73457c..4075b2a 100644 --- a/components/readme/readme-viewer.tsx +++ b/components/readme/readme-viewer.tsx @@ -8,9 +8,10 @@ import 'highlight.js/styles/github-dark.css' interface ReadmeViewerProps { content: string + repositoryUrl?: string } -export function ReadmeViewer({ content }: ReadmeViewerProps) { +export function ReadmeViewer({ content, repositoryUrl }: ReadmeViewerProps) { const [html, setHtml] = React.useState('') React.useEffect(() => { @@ -30,13 +31,59 @@ export function ReadmeViewer({ content }: ReadmeViewerProps) { breaks: true, }) + // Custom renderer to fix relative image URLs + if (repositoryUrl) { + const renderer = { + image(href: string, title: string | null, text: string) { + let imgSrc = href + + // Convert GitHub URLs to raw content URLs + if (repositoryUrl.includes('github.com')) { + const match = repositoryUrl.match(/github\.com\/([^\/]+)\/([^\/]+)/) + if (match) { + const [, owner, repo] = match + const cleanRepo = repo.replace(/\.git$/, '') + + // Handle relative URLs + if (!imgSrc.startsWith('http://') && !imgSrc.startsWith('https://') && !imgSrc.startsWith('//')) { + // Remove leading ./ + imgSrc = imgSrc.replace(/^\.\//, '') + // Build raw GitHub URL (main/master branch assumed) + imgSrc = `https://raw.githubusercontent.com/${owner}/${cleanRepo}/master/${imgSrc}` + } + // Handle GitHub blob URLs - convert to raw + else if (imgSrc.includes('github.com') && imgSrc.includes('/blob/')) { + imgSrc = imgSrc.replace('github.com', 'raw.githubusercontent.com').replace('/blob/', '/') + } + } + } + + const titleAttr = title ? ` title="${title}"` : '' + + // Check if it's a badge (shields.io, badgen, etc.) + const isBadge = imgSrc.includes('shields.io') || + imgSrc.includes('badgen.net') || + imgSrc.includes('badge') || + imgSrc.includes('img.shields') || + imgSrc.match(/\/badges?\//) + + if (isBadge) { + return `${text}` + } + + return `${text}` + } + } + marked.use({ renderer }) + } + // Parse markdown const parseMarkdown = async () => { const result = await marked.parse(content) setHtml(result) } parseMarkdown() - }, [content]) + }, [content, repositoryUrl]) return (