a new start
This commit is contained in:
330
lib/db.ts
Normal file
330
lib/db.ts
Normal 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
209
lib/personal-list-store.ts
Normal 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
254
lib/themes.ts
Normal 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
6
lib/utils.ts
Normal 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))
|
||||
}
|
||||
Reference in New Issue
Block a user