feat: implement comprehensive UI/UX improvements

This commit implements 7 requested improvements:

1. Add footer component to all pages
   - Created reusable AppFooter component
   - Added to layout.tsx with flex-1 main container
   - Includes links to Legal, Disclaimer, Imprint, GitHub

2. Change search highlight from yellow to more discreet color
   - Updated mark styling with purple theme colors
   - Uses color-mix for theme-aware transparency
   - Added subtle border-bottom for better visibility

3. Strip markdown from search results
   - Created stripMarkdown function
   - Removes HTML tags, markdown links, images, formatting
   - Shows clean text descriptions only

4. Add page number links to pagination
   - Created getPageNumbers function with smart ellipsis
   - Shows current page ±2 pages with first/last always visible
   - Example: 1 ... 5 6 [7] 8 9 ... 20

5. Adjust README badge display to be inline
   - Custom marked renderer detects badges (shields.io, badgen, etc.)
   - Applies inline-block with !my-0 !mx-1 align-middle classes
   - Badges now display inline in paragraph flow

6. Fix relative image URLs in READMEs
   - Custom image renderer converts relative to absolute GitHub URLs
   - Handles ./path and path patterns
   - Converts to raw.githubusercontent.com URLs
   - Also handles /blob/ URLs conversion

7. Fix command menu highlight contrast
   - Reuses mark styling from search highlights
   - Consistent purple theme colors across app

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
valknarness
2025-10-29 09:27:01 +01:00
parent 109d82ed23
commit bbe7f9a22d
8 changed files with 179 additions and 47 deletions

View File

@@ -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 {

View File

@@ -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({
>
<WorkerProvider>
<CommandProvider>
<AppHeader />
{children}
<div className="flex min-h-screen flex-col">
<AppHeader />
<main className="flex-1">{children}</main>
<AppFooter />
</div>
</CommandProvider>
</WorkerProvider>
<Toaster />

View File

@@ -237,39 +237,6 @@ export default function Home() {
</Link>
</div>
</section>
{/* Footer */}
<footer className="border-t border-border/40 px-6 py-12 lg:px-8">
<div className="mx-auto max-w-7xl">
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
<div className="text-center sm:text-left">
<div className="gradient-text mb-2 text-xl font-bold">Awesome</div>
<p className="text-sm text-muted-foreground">
Built with 💜💗💛 and maximum awesomeness
</p>
</div>
<div className="flex flex-wrap justify-center gap-6 text-sm">
<Link href="/legal" className="text-muted-foreground hover:text-primary">
Legal
</Link>
<Link href="/disclaimer" className="text-muted-foreground hover:text-primary">
Disclaimer
</Link>
<Link href="/imprint" className="text-muted-foreground hover:text-primary">
Imprint
</Link>
<a
href="https://github.com/sindresorhus/awesome"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary"
>
GitHub
</a>
</div>
</div>
</div>
</footer>
</div>
)
}

View File

@@ -92,7 +92,7 @@ export default async function ReadmePage({ params }: PageProps) {
</div>
}
>
<ReadmeViewer content={data.content} />
<ReadmeViewer content={data.content} repositoryUrl={data.metadata.url} />
</Suspense>
</div>
</div>

View File

@@ -192,7 +192,7 @@ export default function RepositoryDetailPage() {
<div className="mx-auto max-w-5xl px-6 py-8">
{data.readme?.content ? (
<div className="rounded-xl border border-border bg-card p-8 shadow-sm">
<ReadmeViewer content={data.readme.content} />
<ReadmeViewer content={data.readme.content} repositoryUrl={data.url} />
</div>
) : (
<div className="rounded-xl border border-dashed border-border bg-muted/30 p-12 text-center">

View File

@@ -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() {
</div>
{result.description && (
<p className="mb-3 text-muted-foreground">{result.description}</p>
<p className="mb-3 text-muted-foreground">{stripMarkdown(result.description)}</p>
)}
{result.snippet && (
@@ -343,23 +385,43 @@ function SearchPageContent() {
{/* Pagination */}
{results.totalPages > 1 && (
<div className="mt-8 flex items-center justify-center gap-2">
<div className="mt-8 flex flex-wrap items-center justify-center gap-2">
<Button
variant="outline"
size="icon"
disabled={results.page === 1}
onClick={() => performSearch(query, results.page - 1)}
title="Previous page"
>
Previous
<ChevronLeft className="h-4 w-4" />
</Button>
<span className="mx-4 text-sm text-muted-foreground">
Page {results.page} of {results.totalPages}
</span>
{getPageNumbers(results.page, results.totalPages).map((pageNum, idx) =>
pageNum === '...' ? (
<span key={`ellipsis-${idx}`} className="px-2 text-muted-foreground">
...
</span>
) : (
<Button
key={pageNum}
variant={pageNum === results.page ? 'default' : 'outline'}
size="icon"
onClick={() => performSearch(query, pageNum as number)}
className={pageNum === results.page ? 'btn-awesome' : ''}
>
{pageNum}
</Button>
)
)}
<Button
variant="outline"
size="icon"
disabled={results.page === results.totalPages}
onClick={() => performSearch(query, results.page + 1)}
title="Next page"
>
Next
<ChevronRight className="h-4 w-4" />
</Button>
</div>
)}

View File

@@ -0,0 +1,37 @@
import Link from 'next/link'
export function AppFooter() {
return (
<footer className="border-t border-border/40 px-6 py-12 lg:px-8">
<div className="mx-auto max-w-7xl">
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
<div className="text-center sm:text-left">
<div className="gradient-text mb-2 text-xl font-bold">Awesome</div>
<p className="text-sm text-muted-foreground">
Built with 💜💗💛 and maximum awesomeness
</p>
</div>
<div className="flex flex-wrap justify-center gap-6 text-sm">
<Link href="/legal" className="text-muted-foreground hover:text-primary">
Legal
</Link>
<Link href="/disclaimer" className="text-muted-foreground hover:text-primary">
Disclaimer
</Link>
<Link href="/imprint" className="text-muted-foreground hover:text-primary">
Imprint
</Link>
<a
href="https://github.com/sindresorhus/awesome"
target="_blank"
rel="noopener noreferrer"
className="text-muted-foreground hover:text-primary"
>
GitHub
</a>
</div>
</div>
</div>
</footer>
)
}

View File

@@ -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 `<img src="${imgSrc}" alt="${text}" ${titleAttr} class="inline-block !my-0 !mx-1 align-middle" />`
}
return `<img src="${imgSrc}" alt="${text}" ${titleAttr} />`
}
}
marked.use({ renderer })
}
// Parse markdown
const parseMarkdown = async () => {
const result = await marked.parse(content)
setHtml(result)
}
parseMarkdown()
}, [content])
}, [content, repositoryUrl])
return (
<article