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

330
lib/db.ts Normal file
View File

@@ -0,0 +1,330 @@
import Database from 'better-sqlite3'
import { join } from 'path'
// Database path - using user's .awesome directory
const DB_PATH = process.env.AWESOME_DB_PATH || join(process.env.HOME || '', '.awesome', 'awesome.db')
let db: Database.Database | null = null
export function getDb(): Database.Database {
if (!db) {
db = new Database(DB_PATH, { readonly: true })
// Enable WAL mode for better concurrency
db.pragma('journal_mode = WAL')
}
return db
}
export interface AwesomeList {
id: number
name: string
url: string
description: string | null
category: string | null
stars: number | null
forks: number | null
last_commit: string | null
level: number | null
parent_id: number | null
added_at: string | null
last_updated: string | null
}
export interface Repository {
id: number
awesome_list_id: number
name: string
url: string
description: string | null
stars: number | null
forks: number | null
watchers: number | null
language: string | null
topics: string | null
last_commit: string | null
created_at: string | null
added_at: string | null
}
export interface Readme {
id: number
repository_id: number
content: string | null
raw_content: string | null
version_hash: string | null
indexed_at: string | null
}
export interface SearchResult {
repository_id: number
repository_name: string
repository_url: string
description: string | null
stars: number | null
language: string | null
topics: string | null
awesome_list_name: string | null
awesome_list_category: string | null
rank: number
snippet: string | null
}
export interface SearchOptions {
query: string
limit?: number
offset?: number
language?: string
minStars?: number
category?: string
sortBy?: 'relevance' | 'stars' | 'recent'
}
export interface PaginatedResults<T> {
results: T[]
total: number
page: number
pageSize: number
totalPages: number
}
/**
* Full-text search using FTS5
*/
export function searchRepositories(options: SearchOptions): PaginatedResults<SearchResult> {
const db = getDb()
const {
query,
limit = 20,
offset = 0,
language,
minStars,
category,
sortBy = 'relevance'
} = options
// Build FTS query
const ftsQuery = query
.trim()
.split(/\s+/)
.map(term => `"${term}"*`)
.join(' OR ')
let sql = `
SELECT
r.id as repository_id,
r.name as repository_name,
r.url as repository_url,
r.description,
r.stars,
r.language,
r.topics,
al.name as awesome_list_name,
al.category as awesome_list_category,
fts.rank,
snippet(readmes_fts, 2, '<mark>', '</mark>', '...', 32) as snippet
FROM readmes_fts fts
JOIN repositories r ON fts.rowid = r.id
LEFT JOIN awesome_lists al ON r.awesome_list_id = al.id
WHERE readmes_fts MATCH ?
`
const params: any[] = [ftsQuery]
// Add filters
if (language) {
sql += ` AND r.language = ?`
params.push(language)
}
if (minStars !== undefined) {
sql += ` AND r.stars >= ?`
params.push(minStars)
}
if (category) {
sql += ` AND al.category = ?`
params.push(category)
}
// Add sorting
switch (sortBy) {
case 'stars':
sql += ` ORDER BY r.stars DESC, fts.rank`
break
case 'recent':
sql += ` ORDER BY r.last_commit DESC, fts.rank`
break
default:
sql += ` ORDER BY fts.rank`
}
// Count total results (use [\s\S]*? for multiline matching)
const countSql = sql.replace(/SELECT[\s\S]*?FROM/, 'SELECT COUNT(*) as total FROM')
.replace(/ORDER BY[\s\S]*$/, '')
const totalResult = db.prepare(countSql).get(...params) as { total: number }
const total = totalResult.total
// Add pagination
sql += ` LIMIT ? OFFSET ?`
params.push(limit, offset)
const results = db.prepare(sql).all(...params) as SearchResult[]
const page = Math.floor(offset / limit) + 1
const totalPages = Math.ceil(total / limit)
return {
results,
total,
page,
pageSize: limit,
totalPages
}
}
/**
* Get all awesome lists with optional category filter
*/
export function getAwesomeLists(category?: string): AwesomeList[] {
const db = getDb()
let sql = `
SELECT * FROM awesome_lists
WHERE 1=1
`
const params: any[] = []
if (category) {
sql += ` AND category = ?`
params.push(category)
}
sql += ` ORDER BY stars DESC, name ASC`
return db.prepare(sql).all(...params) as AwesomeList[]
}
/**
* Get repositories by awesome list ID
*/
export function getRepositoriesByList(listId: number, limit = 50, offset = 0): PaginatedResults<Repository> {
const db = getDb()
const countSql = `SELECT COUNT(*) as total FROM repositories WHERE awesome_list_id = ?`
const totalResult = db.prepare(countSql).get(listId) as { total: number }
const total = totalResult.total
const sql = `
SELECT * FROM repositories
WHERE awesome_list_id = ?
ORDER BY stars DESC, name ASC
LIMIT ? OFFSET ?
`
const results = db.prepare(sql).all(listId, limit, offset) as Repository[]
const page = Math.floor(offset / limit) + 1
const totalPages = Math.ceil(total / limit)
return {
results,
total,
page,
pageSize: limit,
totalPages
}
}
/**
* Get repository by ID with README content
*/
export function getRepositoryWithReadme(repositoryId: number): (Repository & { readme: Readme | null }) | null {
const db = getDb()
const repo = db.prepare(`
SELECT * FROM repositories WHERE id = ?
`).get(repositoryId) as Repository | undefined
if (!repo) {
return null
}
const readme = db.prepare(`
SELECT raw_content as content FROM readmes WHERE repository_id = ?
`).get(repositoryId) as Readme | undefined
return {
...repo,
readme: readme || null
}
}
/**
* Get all unique categories
*/
export function getCategories(): { name: string; count: number }[] {
const db = getDb()
return db.prepare(`
SELECT
category as name,
COUNT(*) as count
FROM awesome_lists
WHERE category IS NOT NULL
GROUP BY category
ORDER BY count DESC, category ASC
`).all() as { name: string; count: number }[]
}
/**
* Get all unique languages
*/
export function getLanguages(): { name: string; count: number }[] {
const db = getDb()
return db.prepare(`
SELECT
language as name,
COUNT(*) as count
FROM repositories
WHERE language IS NOT NULL
GROUP BY language
ORDER BY count DESC, language ASC
LIMIT 50
`).all() as { name: string; count: number }[]
}
/**
* Get database statistics
*/
export function getStats() {
const db = getDb()
const stats = {
lists: db.prepare('SELECT COUNT(*) as count FROM awesome_lists').get() as { count: number },
repositories: db.prepare('SELECT COUNT(*) as count FROM repositories').get() as { count: number },
readmes: db.prepare('SELECT COUNT(*) as count FROM readmes').get() as { count: number },
lastUpdated: db.prepare('SELECT MAX(last_updated) as date FROM awesome_lists').get() as { date: string | null }
}
return {
totalLists: stats.lists.count,
totalRepositories: stats.repositories.count,
totalReadmes: stats.readmes.count,
lastUpdated: stats.lastUpdated.date
}
}
/**
* Get trending repositories (most stars)
*/
export function getTrendingRepositories(limit = 10): Repository[] {
const db = getDb()
return db.prepare(`
SELECT * FROM repositories
WHERE stars IS NOT NULL
ORDER BY stars DESC
LIMIT ?
`).all(limit) as Repository[]
}

209
lib/personal-list-store.ts Normal file
View File

@@ -0,0 +1,209 @@
'use client'
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
export interface PersonalListItem {
id: string
title: string
description: string
url: string
repository?: string
addedAt: number
tags?: string[]
category?: string
}
export interface PersonalListState {
items: PersonalListItem[]
markdown: string
isEditorOpen: boolean
activeView: 'editor' | 'preview' | 'split'
// Actions
addItem: (item: Omit<PersonalListItem, 'id' | 'addedAt'>) => void
removeItem: (id: string) => void
updateItem: (id: string, updates: Partial<PersonalListItem>) => void
setMarkdown: (markdown: string) => void
toggleEditor: () => void
openEditor: () => void
closeEditor: () => void
setActiveView: (view: 'editor' | 'preview' | 'split') => void
clearList: () => void
importList: (items: PersonalListItem[]) => void
exportList: () => PersonalListItem[]
generateMarkdown: () => string
syncMarkdownToItems: () => void
}
const DEFAULT_MARKDOWN = `# My Awesome List
> A curated list of my favorite resources, tools, and projects.
## Contents
- [Getting Started](#getting-started)
- [Resources](#resources)
## Getting Started
Start adding items to your personal awesome list by clicking the "Push to my list" button on any repository or resource you find interesting!
## Resources
`
export const usePersonalListStore = create<PersonalListState>()(
persist(
(set, get) => ({
items: [],
markdown: DEFAULT_MARKDOWN,
isEditorOpen: false,
activeView: 'split',
addItem: (itemData) => {
const newItem: PersonalListItem = {
...itemData,
id: `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
addedAt: Date.now(),
}
set((state) => {
const newItems = [...state.items, newItem]
return {
items: newItems,
markdown: generateMarkdownFromItems(newItems, state.markdown),
}
})
},
removeItem: (id) => {
set((state) => {
const newItems = state.items.filter((item) => item.id !== id)
return {
items: newItems,
markdown: generateMarkdownFromItems(newItems, state.markdown),
}
})
},
updateItem: (id, updates) => {
set((state) => {
const newItems = state.items.map((item) =>
item.id === id ? { ...item, ...updates } : item
)
return {
items: newItems,
markdown: generateMarkdownFromItems(newItems, state.markdown),
}
})
},
setMarkdown: (markdown) => {
set({ markdown })
},
toggleEditor: () => {
set((state) => ({ isEditorOpen: !state.isEditorOpen }))
},
openEditor: () => {
set({ isEditorOpen: true })
},
closeEditor: () => {
set({ isEditorOpen: false })
},
setActiveView: (view) => {
set({ activeView: view })
},
clearList: () => {
set({ items: [], markdown: DEFAULT_MARKDOWN })
},
importList: (items) => {
set({
items,
markdown: generateMarkdownFromItems(items, DEFAULT_MARKDOWN),
})
},
exportList: () => {
return get().items
},
generateMarkdown: () => {
const items = get().items
return generateMarkdownFromItems(items, get().markdown)
},
syncMarkdownToItems: () => {
// This would parse markdown back to items - for now, we'll keep it simple
// and prioritize items as source of truth
const items = get().items
set({ markdown: generateMarkdownFromItems(items, get().markdown) })
},
}),
{
name: 'personal-awesome-list',
version: 1,
}
)
)
// Helper function to generate markdown from items
function generateMarkdownFromItems(
items: PersonalListItem[],
currentMarkdown: string
): string {
if (items.length === 0) {
return DEFAULT_MARKDOWN
}
// Group items by category
const categorized = items.reduce((acc, item) => {
const category = item.category || 'Uncategorized'
if (!acc[category]) {
acc[category] = []
}
acc[category].push(item)
return acc
}, {} as Record<string, PersonalListItem[]>)
// Build markdown
let markdown = `# My Awesome List\n\n`
markdown += `> A curated list of my favorite resources, tools, and projects.\n\n`
// Table of contents
markdown += `## Contents\n\n`
Object.keys(categorized).forEach((category) => {
const slug = category.toLowerCase().replace(/\s+/g, '-')
markdown += `- [${category}](#${slug})\n`
})
markdown += `\n`
// Categories and items
Object.entries(categorized).forEach(([category, categoryItems]) => {
markdown += `## ${category}\n\n`
categoryItems.forEach((item) => {
markdown += `### [${item.title}](${item.url})\n\n`
markdown += `${item.description}\n\n`
if (item.repository) {
markdown += `**Repository:** \`${item.repository}\`\n\n`
}
if (item.tags && item.tags.length > 0) {
markdown += `**Tags:** ${item.tags.map(tag => `\`${tag}\``).join(', ')}\n\n`
}
})
})
markdown += `---\n\n`
markdown += `*Generated with [Awesome](https://awesome.com) 💜💗💛*\n`
return markdown
}

254
lib/themes.ts Normal file
View File

@@ -0,0 +1,254 @@
export interface ColorPalette {
id: string
name: string
description: string
colors: {
primary: string
primaryLight: string
primaryDark: string
secondary: string
secondaryLight: string
secondaryDark: string
accent: string
accentLight: string
accentDark: string
}
gradient: string
}
export const colorPalettes: ColorPalette[] = [
{
id: 'awesome',
name: 'Awesome Purple',
description: 'Our signature purple, pink, and gold theme',
colors: {
primary: '#DA22FF',
primaryLight: '#E855FF',
primaryDark: '#9733EE',
secondary: '#FF69B4',
secondaryLight: '#FFB6D9',
secondaryDark: '#FF1493',
accent: '#FFD700',
accentLight: '#FFE44D',
accentDark: '#FFC700',
},
gradient: 'linear-gradient(135deg, #DA22FF 0%, #9733EE 50%, #FFD700 100%)',
},
{
id: 'royal',
name: 'Royal Violet',
description: 'Deep purple with regal blue and silver accents',
colors: {
primary: '#7C3AED',
primaryLight: '#A78BFA',
primaryDark: '#5B21B6',
secondary: '#6366F1',
secondaryLight: '#818CF8',
secondaryDark: '#4F46E5',
accent: '#94A3B8',
accentLight: '#CBD5E1',
accentDark: '#64748B',
},
gradient: 'linear-gradient(135deg, #7C3AED 0%, #6366F1 50%, #94A3B8 100%)',
},
{
id: 'cosmic',
name: 'Cosmic Purple',
description: 'Deep space purple with cyan and magenta',
colors: {
primary: '#8B5CF6',
primaryLight: '#A78BFA',
primaryDark: '#6D28D9',
secondary: '#EC4899',
secondaryLight: '#F472B6',
secondaryDark: '#DB2777',
accent: '#06B6D4',
accentLight: '#22D3EE',
accentDark: '#0891B2',
},
gradient: 'linear-gradient(135deg, #8B5CF6 0%, #EC4899 50%, #06B6D4 100%)',
},
{
id: 'sunset',
name: 'Purple Sunset',
description: 'Warm purple with orange and coral tones',
colors: {
primary: '#A855F7',
primaryLight: '#C084FC',
primaryDark: '#7E22CE',
secondary: '#F97316',
secondaryLight: '#FB923C',
secondaryDark: '#EA580C',
accent: '#FB7185',
accentLight: '#FDA4AF',
accentDark: '#F43F5E',
},
gradient: 'linear-gradient(135deg, #A855F7 0%, #F97316 50%, #FB7185 100%)',
},
{
id: 'lavender',
name: 'Lavender Dreams',
description: 'Soft purple with pastel pink and mint',
colors: {
primary: '#C084FC',
primaryLight: '#D8B4FE',
primaryDark: '#A855F7',
secondary: '#F9A8D4',
secondaryLight: '#FBC8E7',
secondaryDark: '#F472B6',
accent: '#86EFAC',
accentLight: '#BBF7D0',
accentDark: '#4ADE80',
},
gradient: 'linear-gradient(135deg, #C084FC 0%, #F9A8D4 50%, #86EFAC 100%)',
},
{
id: 'neon',
name: 'Neon Purple',
description: 'Electric purple with bright neon accents',
colors: {
primary: '#D946EF',
primaryLight: '#E879F9',
primaryDark: '#C026D3',
secondary: '#F0ABFC',
secondaryLight: '#F5D0FE',
secondaryDark: '#E879F9',
accent: '#22D3EE',
accentLight: '#67E8F9',
accentDark: '#06B6D4',
},
gradient: 'linear-gradient(135deg, #D946EF 0%, #F0ABFC 50%, #22D3EE 100%)',
},
{
id: 'galaxy',
name: 'Galaxy Purple',
description: 'Deep cosmic purple with star-like shimmer',
colors: {
primary: '#6D28D9',
primaryLight: '#8B5CF6',
primaryDark: '#5B21B6',
secondary: '#7C3AED',
secondaryLight: '#A78BFA',
secondaryDark: '#6D28D9',
accent: '#FBBF24',
accentLight: '#FCD34D',
accentDark: '#F59E0B',
},
gradient: 'linear-gradient(135deg, #6D28D9 0%, #7C3AED 50%, #FBBF24 100%)',
},
{
id: 'berry',
name: 'Berry Blast',
description: 'Rich purple with berry and wine tones',
colors: {
primary: '#9333EA',
primaryLight: '#A855F7',
primaryDark: '#7E22CE',
secondary: '#BE123C',
secondaryLight: '#E11D48',
secondaryDark: '#9F1239',
accent: '#FB923C',
accentLight: '#FDBA74',
accentDark: '#F97316',
},
gradient: 'linear-gradient(135deg, #9333EA 0%, #BE123C 50%, #FB923C 100%)',
},
]
export type ThemeMode = 'light' | 'dark'
export interface ThemeConfig {
mode: ThemeMode
palette: string
}
export function getThemeVariables(palette: ColorPalette, mode: ThemeMode) {
const isDark = mode === 'dark'
return {
// Base colors
background: isDark ? '0 0% 3.9%' : '0 0% 100%',
foreground: isDark ? '0 0% 98%' : '0 0% 3.9%',
// Card
card: isDark ? '0 0% 3.9%' : '0 0% 100%',
cardForeground: isDark ? '0 0% 98%' : '0 0% 3.9%',
// Popover
popover: isDark ? '0 0% 3.9%' : '0 0% 100%',
popoverForeground: isDark ? '0 0% 98%' : '0 0% 3.9%',
// Primary (from palette)
primary: palette.colors.primary,
primaryLight: palette.colors.primaryLight,
primaryDark: palette.colors.primaryDark,
primaryForeground: isDark ? '0 0% 9%' : '0 0% 98%',
// Secondary (from palette)
secondary: palette.colors.secondary,
secondaryLight: palette.colors.secondaryLight,
secondaryDark: palette.colors.secondaryDark,
secondaryForeground: isDark ? '0 0% 98%' : '0 0% 9%',
// Accent (from palette)
accent: palette.colors.accent,
accentLight: palette.colors.accentLight,
accentDark: palette.colors.accentDark,
accentForeground: isDark ? '0 0% 98%' : '0 0% 9%',
// Muted
muted: isDark ? '0 0% 14.9%' : '0 0% 96.1%',
mutedForeground: isDark ? '0 0% 63.9%' : '0 0% 45.1%',
// Destructive
destructive: isDark ? '0 62.8% 30.6%' : '0 84.2% 60.2%',
destructiveForeground: '0 0% 98%',
// Border
border: isDark ? '0 0% 14.9%' : '0 0% 89.8%',
input: isDark ? '0 0% 14.9%' : '0 0% 89.8%',
ring: palette.colors.primary,
// Gradient
gradient: palette.gradient,
}
}
export function hexToHsl(hex: string): string {
// Remove the hash if present
hex = hex.replace(/^#/, '')
// Parse the hex values
const r = parseInt(hex.substring(0, 2), 16) / 255
const g = parseInt(hex.substring(2, 4), 16) / 255
const b = parseInt(hex.substring(4, 6), 16) / 255
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
let h = 0
let s = 0
const l = (max + min) / 2
if (max !== min) {
const d = max - min
s = l > 0.5 ? d / (2 - max - min) : d / (max + min)
switch (max) {
case r:
h = ((g - b) / d + (g < b ? 6 : 0)) / 6
break
case g:
h = ((b - r) / d + 2) / 6
break
case b:
h = ((r - g) / d + 4) / 6
break
}
}
h = Math.round(h * 360)
s = Math.round(s * 100)
const lightness = Math.round(l * 100)
return `${h} ${s}% ${lightness}%`
}

6
lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}