a new start

This commit is contained in:
valknarness
2025-10-25 16:09:02 +02:00
commit b63592f153
94 changed files with 23058 additions and 0 deletions

View File

@@ -0,0 +1,177 @@
'use client'
import * as React from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import { Search, Home, BookOpen, Menu, List as ListIcon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { ThemeSwitcher } from '@/components/theme/theme-switcher'
import { Badge } from '@/components/ui/badge'
import { AwesomeIcon } from '@/components/ui/awesome-icon'
import { cn } from '@/lib/utils'
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '@/components/ui/sheet'
import { usePersonalListStore } from '@/lib/personal-list-store'
export function AppHeader() {
const pathname = usePathname()
const [isScrolled, setIsScrolled] = React.useState(false)
const { items } = usePersonalListStore()
React.useEffect(() => {
const handleScroll = () => {
setIsScrolled(window.scrollY > 10)
}
window.addEventListener('scroll', handleScroll)
return () => window.removeEventListener('scroll', handleScroll)
}, [])
const navigation = [
{
name: 'Home',
href: '/',
icon: Home,
},
{
name: 'Search',
href: '/search',
icon: Search,
},
{
name: 'Browse',
href: '/browse',
icon: BookOpen,
},
]
return (
<header
className={cn(
'sticky top-0 z-50 w-full border-b transition-all duration-300',
isScrolled
? 'border-border/40 bg-background/95 shadow-lg backdrop-blur-xl'
: 'border-transparent bg-background/80 backdrop-blur-sm'
)}
>
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between gap-4 px-6">
{/* Logo */}
<Link href="/" className="flex items-center gap-2 transition-transform hover:scale-105">
<div className="relative flex h-10 w-10 items-center justify-center rounded-lg bg-background p-1.5 shadow-lg ring-1 ring-primary/10 transition-all hover:ring-primary/30 hover:shadow-xl hover:shadow-primary/20">
<AwesomeIcon size={32} className="drop-shadow-sm" />
</div>
<span className="gradient-text hidden text-xl font-bold sm:inline">
Awesome
</span>
</Link>
{/* Desktop Navigation */}
<nav className="hidden items-center gap-1 md:flex">
{navigation.map((item) => {
const Icon = item.icon
const isActive = pathname === item.href
return (
<Link
key={item.name}
href={item.href}
className={cn(
'flex items-center gap-2 rounded-lg px-4 py-2 text-sm font-medium transition-all',
isActive
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-primary/5 hover:text-foreground'
)}
>
<Icon className="h-4 w-4" />
{item.name}
</Link>
)
})}
</nav>
{/* Right Side */}
<div className="flex items-center gap-2">
{/* Search Button - Hidden on search page */}
{pathname !== '/search' && (
<Link href="/search">
<Button
variant="outline"
size="sm"
className="hidden gap-2 border-primary/20 sm:inline-flex"
>
<Search className="h-4 w-4" />
<span className="hidden lg:inline">Search</span>
<kbd className="hidden rounded bg-muted px-1.5 py-0.5 text-xs lg:inline">
K
</kbd>
</Button>
</Link>
)}
{/* Personal List Button */}
<Link href="/my-list">
<Button
variant="outline"
size="sm"
className="hidden gap-2 border-primary/20 sm:inline-flex"
>
<ListIcon className="h-4 w-4" />
<span className="hidden lg:inline">My List</span>
{items.length > 0 && (
<Badge variant="default" className="h-5 min-w-5 px-1 text-xs">
{items.length}
</Badge>
)}
</Button>
</Link>
{/* Theme Switcher */}
<ThemeSwitcher />
{/* Mobile Menu */}
<Sheet>
<SheetTrigger asChild className="md:hidden">
<Button variant="outline" size="icon" className="border-primary/20">
<Menu className="h-5 w-5" />
<span className="sr-only">Toggle menu</span>
</Button>
</SheetTrigger>
<SheetContent side="right" className="w-[300px]">
<SheetHeader>
<SheetTitle className="flex items-center gap-2">
<AwesomeIcon size={20} />
<span className="gradient-text">Awesome</span>
</SheetTitle>
</SheetHeader>
<nav className="mt-6 flex flex-col gap-2">
{navigation.map((item) => {
const Icon = item.icon
const isActive = pathname === item.href
return (
<Link
key={item.name}
href={item.href}
className={cn(
'flex items-center gap-3 rounded-lg px-4 py-3 text-sm font-medium transition-all',
isActive
? 'bg-primary/10 text-primary'
: 'text-muted-foreground hover:bg-primary/5 hover:text-foreground'
)}
>
<Icon className="h-5 w-5" />
{item.name}
</Link>
)
})}
</nav>
</SheetContent>
</Sheet>
</div>
</div>
</header>
)
}

View File

@@ -0,0 +1,265 @@
'use client'
import * as React from 'react'
import Link from 'next/link'
import { usePathname } from 'next/navigation'
import {
ChevronRight,
Home,
Search,
Star,
BookOpen,
Code,
Layers,
Package,
Globe,
} from 'lucide-react'
import {
Sidebar,
SidebarContent,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarMenuSub,
SidebarMenuSubButton,
SidebarMenuSubItem,
} from '@/components/ui/sidebar'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/utils'
import { AwesomeIcon } from '@/components/ui/awesome-icon'
interface Category {
name: string
icon: React.ReactNode
lists: ListItem[]
expanded?: boolean
}
interface ListItem {
id: string
name: string
url: string
stars?: number
}
export function AppSidebar() {
const pathname = usePathname()
const [searchQuery, setSearchQuery] = React.useState('')
const [expandedCategories, setExpandedCategories] = React.useState<Set<string>>(
new Set(['Front-end Development'])
)
// Mock data - will be replaced with actual API call
const categories: Category[] = [
{
name: 'Front-end Development',
icon: <Code className="h-4 w-4" />,
lists: [
{ id: 'react', name: 'React', url: '/list/react', stars: 45000 },
{ id: 'vue', name: 'Vue.js', url: '/list/vue', stars: 38000 },
{ id: 'angular', name: 'Angular', url: '/list/angular', stars: 32000 },
{ id: 'svelte', name: 'Svelte', url: '/list/svelte', stars: 28000 },
{ id: 'css', name: 'CSS', url: '/list/css', stars: 25000 },
{ id: 'tailwind', name: 'Tailwind CSS', url: '/list/tailwind', stars: 22000 },
],
},
{
name: 'Back-end Development',
icon: <Layers className="h-4 w-4" />,
lists: [
{ id: 'nodejs', name: 'Node.js', url: '/list/nodejs', stars: 38000 },
{ id: 'python', name: 'Python', url: '/list/python', stars: 52000 },
{ id: 'go', name: 'Go', url: '/list/go', stars: 35000 },
{ id: 'rust', name: 'Rust', url: '/list/rust', stars: 30000 },
{ id: 'java', name: 'Java', url: '/list/java', stars: 28000 },
{ id: 'dotnet', name: '.NET', url: '/list/dotnet', stars: 24000 },
],
},
{
name: 'Programming Languages',
icon: <Code className="h-4 w-4" />,
lists: [
{ id: 'javascript', name: 'JavaScript', url: '/list/javascript', stars: 48000 },
{ id: 'typescript', name: 'TypeScript', url: '/list/typescript', stars: 42000 },
{ id: 'python-lang', name: 'Python', url: '/list/python-lang', stars: 52000 },
{ id: 'rust-lang', name: 'Rust', url: '/list/rust-lang', stars: 30000 },
{ id: 'go-lang', name: 'Go', url: '/list/go-lang', stars: 35000 },
],
},
{
name: 'Platforms',
icon: <Globe className="h-4 w-4" />,
lists: [
{ id: 'docker', name: 'Docker', url: '/list/docker', stars: 40000 },
{ id: 'kubernetes', name: 'Kubernetes', url: '/list/kubernetes', stars: 38000 },
{ id: 'aws', name: 'AWS', url: '/list/aws', stars: 35000 },
{ id: 'azure', name: 'Azure', url: '/list/azure', stars: 28000 },
],
},
{
name: 'Tools',
icon: <Package className="h-4 w-4" />,
lists: [
{ id: 'vscode', name: 'VS Code', url: '/list/vscode', stars: 45000 },
{ id: 'git', name: 'Git', url: '/list/git', stars: 42000 },
{ id: 'vim', name: 'Vim', url: '/list/vim', stars: 38000 },
{ id: 'cli', name: 'CLI', url: '/list/cli', stars: 35000 },
],
},
]
const toggleCategory = (categoryName: string) => {
setExpandedCategories((prev) => {
const next = new Set(prev)
if (next.has(categoryName)) {
next.delete(categoryName)
} else {
next.add(categoryName)
}
return next
})
}
const filteredCategories = React.useMemo(() => {
if (!searchQuery) return categories
return categories
.map((category) => ({
...category,
lists: category.lists.filter((list) =>
list.name.toLowerCase().includes(searchQuery.toLowerCase())
),
}))
.filter((category) => category.lists.length > 0)
}, [searchQuery])
return (
<Sidebar>
<SidebarContent>
{/* Header */}
<SidebarGroup>
<div className="px-4 py-4">
<Link href="/" className="flex items-center gap-2">
<AwesomeIcon size={24} />
<span className="gradient-text text-xl font-bold">Awesome</span>
</Link>
</div>
</SidebarGroup>
{/* Search Input */}
<SidebarGroup>
<div className="px-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
type="search"
placeholder="Search lists..."
className="pl-9"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
</div>
</SidebarGroup>
{/* Main Navigation */}
<SidebarGroup>
<SidebarGroupContent>
<SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={pathname === '/'}>
<Link href="/">
<Home className="h-4 w-4" />
<span>Home</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={pathname === '/search'}>
<Link href="/search">
<Search className="h-4 w-4" />
<span>Search</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
<SidebarMenuItem>
<SidebarMenuButton asChild isActive={pathname === '/browse'}>
<Link href="/browse">
<BookOpen className="h-4 w-4" />
<span>Browse</span>
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroupContent>
</SidebarGroup>
{/* Categories */}
<ScrollArea className="flex-1">
{filteredCategories.map((category) => {
const isExpanded = expandedCategories.has(category.name)
return (
<SidebarGroup key={category.name}>
<SidebarGroupLabel asChild>
<button
onClick={() => toggleCategory(category.name)}
className="group flex w-full items-center justify-between px-2 py-1.5 text-sm font-semibold transition-colors hover:bg-accent"
>
<div className="flex items-center gap-2">
{category.icon}
<span>{category.name}</span>
</div>
<ChevronRight
className={cn(
'h-4 w-4 transition-transform',
isExpanded && 'rotate-90'
)}
/>
</button>
</SidebarGroupLabel>
{isExpanded && (
<SidebarGroupContent>
<SidebarMenuSub>
{category.lists.map((list) => (
<SidebarMenuSubItem key={list.id}>
<SidebarMenuSubButton
asChild
isActive={pathname === list.url}
>
<Link href={list.url}>
<span className="flex-1">{list.name}</span>
{list.stars && (
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<Star className="h-3 w-3 fill-current" />
{(list.stars / 1000).toFixed(0)}k
</span>
)}
</Link>
</SidebarMenuSubButton>
</SidebarMenuSubItem>
))}
</SidebarMenuSub>
</SidebarGroupContent>
)}
</SidebarGroup>
)
})}
</ScrollArea>
{/* Footer */}
<SidebarGroup className="mt-auto border-t">
<div className="px-4 py-3 text-xs text-muted-foreground">
<div className="mb-1 font-semibold">Built with 💜💗💛</div>
<div>Updated every 6 hours</div>
</div>
</SidebarGroup>
</SidebarContent>
</Sidebar>
)
}

View File

@@ -0,0 +1,171 @@
'use client'
import * as React from 'react'
import { useRouter } from 'next/navigation'
import {
CommandDialog,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command'
import { Search, Star, BookOpen, Home, FileText, Code } from 'lucide-react'
interface CommandMenuProps {
open: boolean
setOpen: (open: boolean) => void
}
export function CommandMenu({ open, setOpen }: CommandMenuProps) {
const router = useRouter()
const [search, setSearch] = React.useState('')
const [results, setResults] = React.useState([])
const [loading, setLoading] = React.useState(false)
// declare the async data fetching function
const fetchData = React.useCallback(async () => {
const response = await fetch(`/api/search?q=${encodeURIComponent(search)}`)
const data = await response.json()
setResults(...data.results);
}, [])
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if (e.key === 'k' && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setOpen(!open)
}
}
document.addEventListener('keydown', down)
return () => document.removeEventListener('keydown', down)
}, [open, setOpen])
React.useEffect(() => {
if (!search) {
return
}
setLoading(true)
fetchData()
console.log(results)
setLoading(false)
}, [search])
const runCommand = React.useCallback((command: () => void) => {
setOpen(false)
command()
}, [setOpen])
const pages = [
{
id: 'home',
type: 'page',
title: 'Home',
url: '/',
},
{
id: 'browse',
type: 'page',
title: 'Browse Collections',
url: '/browse',
},
{
id: 'search',
type: 'page',
title: 'Search',
url: '/search',
},
]
const getIcon = (type: string) => {
switch (type) {
case 'list':
return <Star className="mr-2 h-4 w-4" />
case 'repo':
return <Code className="mr-2 h-4 w-4" />
case 'page':
return <FileText className="mr-2 h-4 w-4" />
default:
return <BookOpen className="mr-2 h-4 w-4" />
}
}
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<CommandInput
placeholder="Search awesome lists, repos, and more..."
value={search}
onValueChange={setSearch}
/>
<CommandList>
<CommandEmpty>
{loading ? (
<div className="flex items-center justify-center py-6">
<div className="spinner-awesome h-8 w-8" />
</div>
) : (
<div className="py-6 text-center text-sm">
No results found for &quot;{search}&quot;
</div>
)}
</CommandEmpty>
{!search && (
<React.Fragment key="pages-group">
<CommandGroup heading="Pages">
{pages.map((page) => (
<CommandItem
key={page.id}
value={page.title}
onSelect={() => runCommand(() => router.push(page.url))}
>
{getIcon(page.type)}
<span>{page.title}</span>
</CommandItem>
))}
</CommandGroup>
<CommandSeparator />
</React.Fragment>
)}
{results.length > 0 && (
<CommandGroup heading="Search Results">
{results.map((result: any) => (
<CommandItem
key={result.repository_id}
value={result.repository_name}
onSelect={() => runCommand(() => router.push(result.url))}
>
{getIcon(result.type)}
<div className="flex flex-1 flex-col gap-1">
<div className="flex items-center gap-2">
<span className="font-medium">{result.title}</span>
{result.stars && (
<span className="flex items-center gap-1 text-xs text-muted-foreground">
<Star className="h-3 w-3 fill-current" />
{result.stars.toLocaleString()}
</span>
)}
</div>
{result.description && (
<span className="text-xs text-muted-foreground line-clamp-1">
{result.description}
</span>
)}
{result.category && (
<span className="text-xs text-primary">
{result.category}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
)}
</CommandList>
</CommandDialog>
)
}