diff --git a/db/database.php b/db/database.php index 19dbec5..a5248e9 100644 --- a/db/database.php +++ b/db/database.php @@ -1,63 +1,29 @@ db = new SQLite3(__DIR__ . '/votes.db'); - $this->initDatabase(); - } - - private function initDatabase() { try { - // Create tables if they don't exist - $schema = file_get_contents(__DIR__ . '/schema.sql'); - $this->db->exec($schema); + $this->db = new SQLite3(__DIR__ . '/votes.db'); + $this->db->enableExceptions(true); } catch (Exception $e) { - error_log("Database initialization warning: " . $e->getMessage()); + error_log("Database connection error: " . $e->getMessage()); + throw $e; } } - public function getImplementations($includeRequested = true) { + public function getImplementations() { try { - $query = "SELECT * FROM implementations"; - if (!$includeRequested) { - $query .= " WHERE is_requested = 0"; - } - $query .= " ORDER BY CASE WHEN is_requested = 1 THEN votes END DESC, name ASC"; - - $result = $this->db->query($query); - if ($result === false) { - error_log("Database error: Failed to fetch implementations"); - return []; - } - - $implementations = []; - while ($row = $result->fetchArray(SQLITE3_ASSOC)) { - $implementations[] = $row; - } - - return $implementations; - } catch (Exception $e) { - error_log("Database error: " . $e->getMessage()); - return []; - } - } - - public function getRequestedImplementations() { - try { - $query = "SELECT * FROM implementations WHERE is_requested = 1 ORDER BY votes DESC"; - $result = $this->db->query($query); - - if ($result === false) { - error_log("Database error: Failed to fetch requested implementations"); - return []; - } - + $stmt = $this->db->prepare(' + SELECT * FROM implementations + WHERE is_draft = 0 + ORDER BY is_featured DESC, name ASC + '); + $result = $stmt->execute(); $implementations = []; while ($row = $result->fetchArray(SQLITE3_ASSOC)) { $implementations[] = $row; } - return $implementations; } catch (Exception $e) { error_log("Database error: " . $e->getMessage()); @@ -67,52 +33,75 @@ public function getRequestedImplementations() { public function addImplementation($data) { try { - // Generate logo path from name - if (!empty($data['logo_url'])) { - $ext = pathinfo($data['logo_url'], PATHINFO_EXTENSION); - $filename = get_logo_filename($data['name']); - $data['logo_url'] = '/logos/' . $filename . '.' . $ext; + // Check if implementation with this URL already exists + $existing = $this->getImplementationByUrl($data['llms_txt_url']); + if ($existing) { + error_log("Implementation with URL {$data['llms_txt_url']} already exists"); + return false; } - - $stmt = $this->db->prepare('INSERT INTO implementations ( - name, logo_url, description, llms_txt_url, has_full, is_featured, is_requested, votes - ) VALUES ( - :name, :logo_url, :description, :llms_txt_url, :has_full, :is_featured, :is_requested, :votes - )'); + + // Set default values for optional fields + $defaults = [ + 'logo_url' => null, + 'description' => null, + 'has_full' => 0, + 'is_featured' => 0, + 'is_requested' => 0, + 'is_draft' => 1, + 'votes' => 0 + ]; + + // Merge defaults with provided data + $data = array_merge($defaults, $data); + + $stmt = $this->db->prepare(' + INSERT INTO implementations ( + name, logo_url, description, llms_txt_url, + has_full, is_featured, is_requested, + is_draft, votes + ) VALUES ( + :name, :logo_url, :description, :llms_txt_url, + :has_full, :is_featured, :is_requested, + :is_draft, :votes + ) + '); $stmt->bindValue(':name', $data['name'], SQLITE3_TEXT); $stmt->bindValue(':logo_url', $data['logo_url'], SQLITE3_TEXT); $stmt->bindValue(':description', $data['description'], SQLITE3_TEXT); $stmt->bindValue(':llms_txt_url', $data['llms_txt_url'], SQLITE3_TEXT); - $stmt->bindValue(':has_full', $data['has_full'], SQLITE3_INTEGER); - $stmt->bindValue(':is_featured', $data['is_featured'] ?? 0, SQLITE3_INTEGER); - $stmt->bindValue(':is_requested', $data['is_requested'], SQLITE3_INTEGER); - $stmt->bindValue(':votes', $data['votes'] ?? 0, SQLITE3_INTEGER); + $stmt->bindValue(':has_full', (int)$data['has_full'], SQLITE3_INTEGER); + $stmt->bindValue(':is_featured', (int)$data['is_featured'], SQLITE3_INTEGER); + $stmt->bindValue(':is_requested', (int)$data['is_requested'], SQLITE3_INTEGER); + $stmt->bindValue(':is_draft', (int)$data['is_draft'], SQLITE3_INTEGER); + $stmt->bindValue(':votes', (int)$data['votes'], SQLITE3_INTEGER); return $stmt->execute(); } catch (Exception $e) { error_log("Failed to add implementation: " . $e->getMessage()); - return false; + throw $e; } } public function updateImplementation($id, $data) { try { - // Generate logo path from name if logo is being updated - if (!empty($data['logo_url'])) { - $ext = pathinfo($data['logo_url'], PATHINFO_EXTENSION); - $filename = get_logo_filename($data['name']); - $data['logo_url'] = '/logos/' . $filename . '.' . $ext; - } - $fields = []; $values = []; - // Build update fields dynamically + // Only include fields that are actually provided + $allowedFields = [ + 'name', 'logo_url', 'description', 'llms_txt_url', + 'has_full', 'is_featured', 'is_requested', 'is_draft', 'votes' + ]; + foreach ($data as $key => $value) { - if ($key !== 'id') { + if (in_array($key, $allowedFields)) { $fields[] = "$key = :$key"; - $values[":$key"] = $value; + if (in_array($key, ['has_full', 'is_featured', 'is_requested', 'is_draft', 'votes'])) { + $values[":$key"] = (int)$value; + } else { + $values[":$key"] = $value; + } } } @@ -133,7 +122,41 @@ public function updateImplementation($id, $data) { return $stmt->execute(); } catch (Exception $e) { error_log("Failed to update implementation: " . $e->getMessage()); - return false; + throw $e; + } + } + + public function getFeaturedImplementations() { + try { + $stmt = $this->db->prepare(' + SELECT * FROM implementations + WHERE is_featured = 1 AND is_draft = 0 + ORDER BY name ASC + '); + $result = $stmt->execute(); + $implementations = []; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $implementations[] = $row; + } + return $implementations; + } catch (Exception $e) { + error_log("Database error: " . $e->getMessage()); + return []; + } + } + + public function getRequestedImplementations() { + try { + $stmt = $this->db->prepare(' + SELECT * FROM implementations + WHERE is_requested = 1 AND is_draft = 0 + ORDER BY name ASC + '); + $result = $stmt->execute(); + return $result->fetchArray(SQLITE3_ASSOC) ? $result : []; + } catch (Exception $e) { + error_log("Database error: " . $e->getMessage()); + return []; } } @@ -218,7 +241,7 @@ public function addVote($implementationId) { public function getRecentlyAddedImplementations($limit = 6) { try { - $query = "SELECT * FROM implementations WHERE is_requested = 0 ORDER BY id DESC LIMIT :limit"; + $query = "SELECT * FROM implementations WHERE is_draft = 0 ORDER BY id DESC LIMIT :limit"; $stmt = $this->db->prepare($query); $stmt->bindValue(':limit', $limit, SQLITE3_INTEGER); $result = $stmt->execute(); @@ -245,35 +268,25 @@ public function getImplementationByUrl($url) { $stmt = $this->db->prepare('SELECT * FROM implementations WHERE llms_txt_url = :url LIMIT 1'); $stmt->bindValue(':url', $url, SQLITE3_TEXT); $result = $stmt->execute(); - - if ($result === false) { - error_log("Database error: Failed to fetch implementation by URL"); - return null; - } - - return $result->fetchArray(SQLITE3_ASSOC); + $row = $result->fetchArray(SQLITE3_ASSOC); + return $row ?: null; } catch (Exception $e) { error_log("Database error: " . $e->getMessage()); return null; } } - public function getFeaturedImplementations() { + public function getRecentImplementations($limit = 5) { try { - $query = "SELECT * FROM implementations WHERE is_featured = 1 AND is_requested = 0 ORDER BY id ASC"; - $result = $this->db->query($query); - - if ($result === false) { - error_log("Database error: Failed to fetch featured implementations"); - return []; - } - - $implementations = []; - while ($row = $result->fetchArray(SQLITE3_ASSOC)) { - $implementations[] = $row; - } - - return $implementations; + $stmt = $this->db->prepare(' + SELECT * FROM implementations + WHERE is_draft = 0 + ORDER BY created_at DESC + LIMIT :limit + '); + $stmt->bindValue(':limit', $limit, SQLITE3_INTEGER); + $result = $stmt->execute(); + return $result->fetchArray(SQLITE3_ASSOC) ? $result : []; } catch (Exception $e) { error_log("Database error: " . $e->getMessage()); return []; diff --git a/db/init.php b/db/init.php index f28f42e..aa07f43 100644 --- a/db/init.php +++ b/db/init.php @@ -1,113 +1,83 @@ query('SELECT COUNT(*) as count FROM implementations'); -$count = $result->fetchArray(SQLITE3_ASSOC)['count']; + $db = new Database(); + $result = $db->db->exec($schema); + if ($result === false) { + throw new Exception("Failed to execute schema: " . $db->db->lastErrorMsg()); + } -if ($count > 0) { - echo "Database already contains data. Skipping initialization.\n"; - exit; -} + // Sample implementations + $implementations = [ + [ + 'name' => 'Superwall', + 'logo_url' => '/logos/superwall.svg', + 'description' => 'Paywall infrastructure for mobile apps', + 'llms_txt_url' => 'https://docs.superwall.com/llms.txt', + 'has_full' => 1, + 'is_featured' => 1, + 'is_requested' => 0, + 'is_draft' => 0, + 'votes' => 15 + ], + [ + 'name' => 'Anthropic', + 'logo_url' => '/logos/anthropic.svg', + 'description' => 'AI research company and creator of Claude', + 'llms_txt_url' => 'https://docs.anthropic.com/llms.txt', + 'has_full' => 1, + 'is_featured' => 1, + 'is_requested' => 0, + 'is_draft' => 0, + 'votes' => 25 + ] + ]; -// Regular implementations -$implementations = [ - [ - 'name' => 'Superwall', - 'logo_url' => 'https://superwall.com/logo.svg', - 'llms_txt_url' => 'https://superwall.com/docs/llms.txt', - 'has_full' => true, - 'is_requested' => false - ], - [ - 'name' => 'Anthropic', - 'logo_url' => 'https://upload.wikimedia.org/wikipedia/commons/0/0f/Anthropic_logo.svg', - 'llms_txt_url' => 'http://docs.anthropic.com/llms.txt', - 'has_full' => true, - 'is_requested' => false - ], - [ - 'name' => 'Cursor', - 'logo_url' => 'https://cursor.sh/cursor.svg', - 'llms_txt_url' => 'https://docs.cursor.com/llms.txt', - 'has_full' => true, - 'is_requested' => false - ], - [ - 'name' => 'FastHTML', - 'logo_url' => 'https://fastht.ml/logo.png', - 'llms_txt_url' => 'https://docs.fastht.ml/llms.txt', - 'has_full' => false, - 'is_requested' => false - ], - [ - 'name' => 'nbdev', - 'logo_url' => 'https://nbdev.fast.ai/images/logo.png', - 'llms_txt_url' => 'https://nbdev.fast.ai/llms.txt', - 'has_full' => true, - 'is_requested' => false - ], - [ - 'name' => 'fastcore', - 'logo_url' => 'https://fastcore.fast.ai/images/logo.png', - 'llms_txt_url' => 'https://fastcore.fast.ai/llms.txt', - 'has_full' => true, - 'is_requested' => false - ], - [ - 'name' => 'Answer.AI', - 'logo_url' => 'https://answer.ai/logo.png', - 'llms_txt_url' => 'https://answer.ai/llms.txt', - 'has_full' => true, - 'is_requested' => false - ] -]; + // Requested implementations + $requested = [ + [ + 'name' => 'Vercel', + 'logo_url' => '/logos/vercel.png', + 'description' => 'Frontend cloud platform and framework provider', + 'llms_txt_url' => 'https://vercel.com/docs/llms.txt', + 'has_full' => 0, + 'is_featured' => 0, + 'is_requested' => 1, + 'is_draft' => 0, + 'votes' => 42 + ], + [ + 'name' => 'Next.js', + 'logo_url' => '/logos/nextjs.png', + 'description' => 'React framework for production', + 'llms_txt_url' => 'https://nextjs.org/docs/llms.txt', + 'has_full' => 0, + 'is_featured' => 0, + 'is_requested' => 1, + 'is_draft' => 0, + 'votes' => 38 + ] + ]; -// Requested implementations -$requested = [ - [ - 'name' => 'Vercel', - 'logo_url' => 'https://assets.vercel.com/image/upload/front/favicon/vercel/180x180.png', - 'description' => 'Frontend cloud platform and framework provider', - 'llms_txt_url' => '', - 'has_full' => false, - 'is_requested' => true, - 'votes' => 42 - ], - [ - 'name' => 'Next.js', - 'logo_url' => 'https://nextjs.org/static/favicon/favicon-32x32.png', - 'description' => 'React framework for production-grade applications', - 'llms_txt_url' => '', - 'has_full' => false, - 'is_requested' => true, - 'votes' => 38 - ], - [ - 'name' => 'Stripe', - 'logo_url' => 'https://stripe.com/img/v3/home/twitter.png', - 'description' => 'Payment processing platform for internet businesses', - 'llms_txt_url' => '', - 'has_full' => false, - 'is_requested' => true, - 'votes' => 35 - ] -]; + // Insert all implementations + foreach (array_merge($implementations, $requested) as $impl) { + if (!$db->addImplementation($impl)) { + error_log("Failed to add implementation: {$impl['name']}"); + } + } -// Insert all implementations -foreach (array_merge($implementations, $requested) as $impl) { - $stmt = $pdo->prepare('INSERT INTO implementations (name, logo_url, description, llms_txt_url, has_full, is_requested, votes) VALUES (:name, :logo_url, :description, :llms_txt_url, :has_full, :is_requested, :votes)'); - $stmt->bindValue(':name', $impl['name'], SQLITE3_TEXT); - $stmt->bindValue(':logo_url', $impl['logo_url'], SQLITE3_TEXT); - $stmt->bindValue(':description', $impl['description'] ?? '', SQLITE3_TEXT); - $stmt->bindValue(':llms_txt_url', $impl['llms_txt_url'], SQLITE3_TEXT); - $stmt->bindValue(':has_full', $impl['has_full'] ? 1 : 0, SQLITE3_INTEGER); - $stmt->bindValue(':is_requested', $impl['is_requested'] ? 1 : 0, SQLITE3_INTEGER); - $stmt->bindValue(':votes', $impl['votes'] ?? 0, SQLITE3_INTEGER); - $stmt->execute(); + echo "Database initialized successfully.\n"; +} catch (Exception $e) { + echo "Error: " . $e->getMessage() . "\n"; + exit(1); } - -echo "Database initialized with sample data.\n"; diff --git a/db/schema.sql b/db/schema.sql index e0038a6..52ed3ba 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,13 +1,16 @@ CREATE TABLE IF NOT EXISTS implementations ( - id INTEGER PRIMARY KEY, + id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, - logo_url TEXT NOT NULL, + logo_url TEXT, description TEXT, - llms_txt_url TEXT NOT NULL, - has_full BOOLEAN DEFAULT 0, - is_requested BOOLEAN DEFAULT 0, - is_featured BOOLEAN DEFAULT 0, - votes INTEGER DEFAULT 0 + llms_txt_url TEXT NOT NULL UNIQUE, + has_full INTEGER DEFAULT 0, + is_featured INTEGER DEFAULT 0, + is_requested INTEGER DEFAULT 0, + is_draft INTEGER DEFAULT 1, + votes INTEGER DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS votes ( diff --git a/public/admin/index.php b/public/admin/index.php index 3f68ebc..ff67814 100644 --- a/public/admin/index.php +++ b/public/admin/index.php @@ -116,6 +116,7 @@ 'llms_txt_url' => $_POST['llms_txt_url'], 'has_full' => isset($_POST['has_full']) ? 1 : 0, 'is_featured' => isset($_POST['is_featured']) ? 1 : 0, + 'is_draft' => isset($_POST['is_draft']) ? 1 : 0, 'is_requested' => isset($_POST['is_requested']) ? 1 : 0 ]; @@ -151,6 +152,7 @@ 'llms_txt_url' => $_POST['llms_txt_url'], 'has_full' => isset($_POST['has_full']) ? 1 : 0, 'is_featured' => isset($_POST['is_featured']) ? 1 : 0, + 'is_draft' => isset($_POST['is_draft']) ? 1 : 0, 'is_requested' => isset($_POST['is_requested']) ? 1 : 0 ]; @@ -426,6 +428,7 @@ llms.txt URL Has Full Is Featured + Is Draft Type Votes Actions @@ -435,13 +438,14 @@ - + - + + - +
@@ -463,41 +467,49 @@

Add New Implementation

- - + +
- +
- +
- - -
-

Supported formats: SVG, PNG, JPEG. Will be displayed at 32x32px.

+ +
- +
- - + +
- - + + +
+
+ +
+
@@ -507,10 +519,10 @@ Is Requested Implementation - +
@@ -520,13 +532,13 @@ function showAddModal() { document.getElementById('modalTitle').textContent = 'Add New Implementation'; document.getElementById('formAction').value = 'add'; - document.getElementById('formId').value = ''; + document.getElementById('edit-id').value = ''; document.getElementById('name').value = ''; - document.getElementById('logo').value = ''; document.getElementById('description').value = ''; document.getElementById('llms_txt_url').value = ''; document.getElementById('has_full').checked = false; document.getElementById('is_featured').checked = false; + document.getElementById('is_draft').checked = false; document.getElementById('is_requested').checked = false; document.querySelector('.logo-preview').style.display = 'none'; document.getElementById('modal').style.display = 'flex'; @@ -535,19 +547,19 @@ function showAddModal() { function showEditModal(impl) { document.getElementById('modalTitle').textContent = 'Edit Implementation'; document.getElementById('formAction').value = 'edit'; - document.getElementById('formId').value = impl.id; - document.getElementById('name').value = impl.name; + document.getElementById('edit-id').value = impl.id; + document.getElementById('name').value = impl.name || ''; document.getElementById('description').value = impl.description || ''; document.getElementById('llms_txt_url').value = impl.llms_txt_url || ''; document.getElementById('has_full').checked = impl.has_full === 1; document.getElementById('is_featured').checked = impl.is_featured === 1; + document.getElementById('is_draft').checked = impl.is_draft === 1; document.getElementById('is_requested').checked = impl.is_requested === 1; - // Show current logo const preview = document.querySelector('.logo-preview'); if (impl.logo_url) { - preview.innerHTML = `Current logo`; preview.style.display = 'block'; + preview.querySelector('img').src = impl.logo_url; } else { preview.style.display = 'none'; } @@ -555,33 +567,9 @@ function showEditModal(impl) { document.getElementById('modal').style.display = 'flex'; } - function hideModal() { + function closeModal() { document.getElementById('modal').style.display = 'none'; } - - // Close modal when clicking outside - document.getElementById('modal').addEventListener('click', function(e) { - if (e.target === this) { - hideModal(); - } - }); - - // Handle file input change - document.getElementById('logo').addEventListener('change', function(e) { - const preview = document.querySelector('.logo-preview'); - const file = e.target.files[0]; - - if (file) { - const reader = new FileReader(); - reader.onload = function(e) { - preview.innerHTML = `Logo preview`; - preview.style.display = 'block'; - }; - reader.readAsDataURL(file); - } else { - preview.style.display = 'none'; - } - });