Consolidate examples into unified production config with static CSS
- New sqlite_serve.conf: all features on single port 8080 - New zenburn.css: hex color palette, all styles in one cacheable file - New Zenburn templates: header/footer/card partials - New start.sh: unified launcher with all endpoint docs - Removed 3 separate example configs and start scripts 15 files changed, 980 insertions(+), 258 deletions(-)
This commit is contained in:
114
conf/sqlite_serve.conf
Normal file
114
conf/sqlite_serve.conf
Normal file
@ -0,0 +1,114 @@
|
||||
# sqlite-serve - Unified Production Example
|
||||
# Demonstrates all features: static queries, positional params, named params, templates
|
||||
|
||||
load_module target/debug/libsqlite_serve.dylib;
|
||||
|
||||
worker_processes auto;
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
error_log logs/error.log warn;
|
||||
|
||||
http {
|
||||
# Global templates for shared components
|
||||
sqlite_global_templates "server_root/global_templates";
|
||||
|
||||
# MIME types
|
||||
types {
|
||||
text/html html htm;
|
||||
text/css css;
|
||||
application/javascript js;
|
||||
}
|
||||
default_type text/html;
|
||||
|
||||
# Performance optimizations
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
gzip on;
|
||||
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
|
||||
root "server_root";
|
||||
charset utf-8;
|
||||
|
||||
# Serve static CSS file
|
||||
location /static/ {
|
||||
add_header "Content-Type" "text/css; charset=utf-8";
|
||||
add_header "Cache-Control" "public, max-age=31536000";
|
||||
}
|
||||
|
||||
# Homepage - Redirect to catalog
|
||||
location = / {
|
||||
return 302 /books;
|
||||
}
|
||||
|
||||
# Browse all books
|
||||
location = /books {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
add_header "Cache-Control" "public, max-age=300";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books ORDER BY rating DESC, title";
|
||||
sqlite_template "catalog.hbs";
|
||||
}
|
||||
|
||||
# Book detail by ID (named parameter)
|
||||
location = /book {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
add_header "Cache-Control" "public, max-age=300";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE id = :book_id";
|
||||
sqlite_param :book_id $arg_id;
|
||||
sqlite_template "detail.hbs";
|
||||
}
|
||||
|
||||
# Filter by genre (named parameter)
|
||||
location = /genre {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
add_header "Cache-Control" "public, max-age=300";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE genre = :genre ORDER BY rating DESC, title";
|
||||
sqlite_param :genre $arg_genre;
|
||||
sqlite_template "catalog.hbs";
|
||||
}
|
||||
|
||||
# Search by title (named parameter with LIKE)
|
||||
location = /search {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE title LIKE '%' || :q || '%' ORDER BY rating DESC";
|
||||
sqlite_param :q $arg_q;
|
||||
sqlite_template "catalog.hbs";
|
||||
}
|
||||
|
||||
# Filter by minimum rating (named parameter)
|
||||
location = /top {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE rating >= :min_rating ORDER BY rating DESC, title";
|
||||
sqlite_param :min_rating $arg_min;
|
||||
sqlite_template "catalog.hbs";
|
||||
}
|
||||
|
||||
# Year range filter (multiple named parameters)
|
||||
location = /era {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
sqlite_db "book_catalog.db";
|
||||
sqlite_query "SELECT * FROM books WHERE year >= :from AND year <= :to ORDER BY year DESC, title";
|
||||
sqlite_param :from $arg_from;
|
||||
sqlite_param :to $arg_to;
|
||||
sqlite_template "catalog.hbs";
|
||||
}
|
||||
|
||||
# 404 handler
|
||||
location @notfound {
|
||||
add_header "Content-Type" "text/html; charset=utf-8";
|
||||
return 404 "<html><body><h1>404 Not Found</h1><p><a href='/books'>Back to catalog</a></p></body></html>";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,140 +1,50 @@
|
||||
{{> header}}
|
||||
{{> zenburn_header}}
|
||||
|
||||
<style>
|
||||
.book-detail {
|
||||
max-width: 800px;
|
||||
margin: 2rem auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
.book-cover {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
height: 300px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
font-size: 3rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.book-title {
|
||||
font-size: 2.5rem;
|
||||
color: #2d3748;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.book-author {
|
||||
font-size: 1.5rem;
|
||||
color: #667eea;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.book-meta {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.meta-item {
|
||||
background: #f8f9fa;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #667eea;
|
||||
}
|
||||
.meta-label {
|
||||
font-size: 0.875rem;
|
||||
color: #6c757d;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.meta-value {
|
||||
font-size: 1.25rem;
|
||||
color: #2d3748;
|
||||
font-weight: 600;
|
||||
}
|
||||
.book-description {
|
||||
background: #f8f9fa;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
line-height: 1.8;
|
||||
font-size: 1.125rem;
|
||||
color: #4a5568;
|
||||
}
|
||||
.rating {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.stars {
|
||||
color: #fbbf24;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-top: 2rem;
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 8px;
|
||||
font-weight: 500;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.back-link:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: #6c757d;
|
||||
}
|
||||
</style>
|
||||
<div class="detail-container">
|
||||
<div class="breadcrumb">
|
||||
<a href="/books">← Back to catalog</a>
|
||||
</div>
|
||||
|
||||
{{#if results.[0]}}
|
||||
{{#with results.[0]}}
|
||||
<div class="book-detail">
|
||||
<div class="book-cover">
|
||||
📖
|
||||
</div>
|
||||
{{#if results.[0]}}
|
||||
{{#with results.[0]}}
|
||||
<div class="book-hero">
|
||||
<h1 class="book-hero-title">{{title}}</h1>
|
||||
<p class="book-hero-author">by {{author}}</p>
|
||||
|
||||
<h1 class="book-title">{{title}}</h1>
|
||||
<p class="book-author">by {{author}}</p>
|
||||
|
||||
<div class="book-meta">
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Rating</div>
|
||||
<div class="meta-value rating">
|
||||
<span class="stars">⭐</span>
|
||||
{{rating}} / 5.0
|
||||
<div class="meta-grid">
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Rating</div>
|
||||
<div class="meta-value rating">⭐ {{rating}}</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Published</div>
|
||||
<div class="meta-value">{{year}}</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Genre</div>
|
||||
<div class="meta-value">{{genre}}</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">ISBN</div>
|
||||
<div class="meta-value" style="font-size: 1rem; font-family: monospace;">{{isbn}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Published</div>
|
||||
<div class="meta-value">{{year}}</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">Genre</div>
|
||||
<div class="meta-value">{{genre}}</div>
|
||||
</div>
|
||||
<div class="meta-item">
|
||||
<div class="meta-label">ISBN</div>
|
||||
<div class="meta-value" style="font-family: monospace; font-size: 1rem;">{{isbn}}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="book-description">
|
||||
<strong>About this book:</strong><br><br>
|
||||
{{description}}
|
||||
<div class="description-section">
|
||||
<h3>About This Book</h3>
|
||||
<p class="description-text">{{description}}</p>
|
||||
</div>
|
||||
|
||||
<a href="/books/all" class="back-link">← Back to Catalog</a>
|
||||
</div>
|
||||
{{/with}}
|
||||
{{else}}
|
||||
<div class="not-found">
|
||||
<h2>Book Not Found</h2>
|
||||
<p>The requested book could not be found in our catalog.</p>
|
||||
<a href="/books/all" class="back-link">← Back to Catalog</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{> footer}}
|
||||
<a href="/books" class="back-button">← Back to Catalog</a>
|
||||
{{/with}}
|
||||
{{else}}
|
||||
<div class="not-found">
|
||||
<h2>Book Not Found</h2>
|
||||
<p>The requested book could not be found in our catalog.</p>
|
||||
<a href="/books" class="back-button">← Back to Catalog</a>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{> zenburn_footer}}
|
||||
|
||||
37
server_root/books/catalog.hbs
Normal file
37
server_root/books/catalog.hbs
Normal file
@ -0,0 +1,37 @@
|
||||
{{> zenburn_header}}
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Book Catalog</h2>
|
||||
<p>Browse our curated collection of technical literature</p>
|
||||
</div>
|
||||
|
||||
{{#if results}}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{#each results}}{{#if @last}}{{@index}}{{/if}}{{/each}}</span>
|
||||
<span class="stat-label">Books Found</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">📚</span>
|
||||
<span class="stat-label">Categories</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">⭐</span>
|
||||
<span class="stat-label">Curated</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="book-grid">
|
||||
{{#each results}}
|
||||
{{> zenburn_card}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<h3>No Books Found</h3>
|
||||
<p>Try adjusting your search or <a href="/books">browse all books</a></p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{> zenburn_footer}}
|
||||
|
||||
37
server_root/era/catalog.hbs
Normal file
37
server_root/era/catalog.hbs
Normal file
@ -0,0 +1,37 @@
|
||||
{{> zenburn_header}}
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Book Catalog</h2>
|
||||
<p>Browse our curated collection of technical literature</p>
|
||||
</div>
|
||||
|
||||
{{#if results}}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{#each results}}{{#if @last}}{{@index}}{{/if}}{{/each}}</span>
|
||||
<span class="stat-label">Books Found</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">📚</span>
|
||||
<span class="stat-label">Categories</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">⭐</span>
|
||||
<span class="stat-label">Curated</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="book-grid">
|
||||
{{#each results}}
|
||||
{{> zenburn_card}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<h3>No Books Found</h3>
|
||||
<p>Try adjusting your search or <a href="/books">browse all books</a></p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{> zenburn_footer}}
|
||||
|
||||
37
server_root/genre/catalog.hbs
Normal file
37
server_root/genre/catalog.hbs
Normal file
@ -0,0 +1,37 @@
|
||||
{{> zenburn_header}}
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Book Catalog</h2>
|
||||
<p>Browse our curated collection of technical literature</p>
|
||||
</div>
|
||||
|
||||
{{#if results}}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{#each results}}{{#if @last}}{{@index}}{{/if}}{{/each}}</span>
|
||||
<span class="stat-label">Books Found</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">📚</span>
|
||||
<span class="stat-label">Categories</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">⭐</span>
|
||||
<span class="stat-label">Curated</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="book-grid">
|
||||
{{#each results}}
|
||||
{{> zenburn_card}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<h3>No Books Found</h3>
|
||||
<p>Try adjusting your search or <a href="/books">browse all books</a></p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{> zenburn_footer}}
|
||||
|
||||
15
server_root/global_templates/zenburn_card.hbs
Normal file
15
server_root/global_templates/zenburn_card.hbs
Normal file
@ -0,0 +1,15 @@
|
||||
<article class="book-card">
|
||||
<div class="book-header">
|
||||
<h3 class="book-title">{{title}}</h3>
|
||||
<div class="book-rating">⭐ {{rating}}</div>
|
||||
</div>
|
||||
<p class="book-author">{{author}}</p>
|
||||
<p class="book-description">{{description}}</p>
|
||||
<div class="book-meta">
|
||||
<span class="genre-badge">{{genre}}</span>
|
||||
<span class="year-badge">{{year}}</span>
|
||||
{{#if isbn}}<span class="isbn">{{isbn}}</span>{{/if}}
|
||||
</div>
|
||||
<a href="/book?id={{id}}" class="detail-link">View Details →</a>
|
||||
</article>
|
||||
|
||||
9
server_root/global_templates/zenburn_footer.hbs
Normal file
9
server_root/global_templates/zenburn_footer.hbs
Normal file
@ -0,0 +1,9 @@
|
||||
</main>
|
||||
<footer>
|
||||
<p>sqlite-serve v0.1.0</p>
|
||||
<p>Rust + NGINX + SQLite + Handlebars</p>
|
||||
<p>45 tests | Type-safe | Read-only</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
31
server_root/global_templates/zenburn_header.hbs
Normal file
31
server_root/global_templates/zenburn_header.hbs
Normal file
@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>sqlite-serve | Book Catalog</title>
|
||||
<link rel="stylesheet" href="/static/zenburn.css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="header-content">
|
||||
<h1>📚 sqlite-serve</h1>
|
||||
<p>Dynamic content from SQLite via NGINX</p>
|
||||
</div>
|
||||
</header>
|
||||
<nav>
|
||||
<div class="nav-content">
|
||||
<a href="/books">All Books</a>
|
||||
<a href="/genre?genre=Programming">Programming</a>
|
||||
<a href="/genre?genre=Databases">Databases</a>
|
||||
<a href="/genre?genre=Computer%20Science">Computer Science</a>
|
||||
<a href="/top?min=4.7">Top Rated</a>
|
||||
<a href="/era?from=2015&to=2024">Recent (2015-2024)</a>
|
||||
<form class="search-form" action="/search" method="get">
|
||||
<input type="text" name="q" placeholder="Search titles..." />
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
<main>
|
||||
|
||||
37
server_root/search/catalog.hbs
Normal file
37
server_root/search/catalog.hbs
Normal file
@ -0,0 +1,37 @@
|
||||
{{> zenburn_header}}
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Book Catalog</h2>
|
||||
<p>Browse our curated collection of technical literature</p>
|
||||
</div>
|
||||
|
||||
{{#if results}}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{#each results}}{{#if @last}}{{@index}}{{/if}}{{/each}}</span>
|
||||
<span class="stat-label">Books Found</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">📚</span>
|
||||
<span class="stat-label">Categories</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">⭐</span>
|
||||
<span class="stat-label">Curated</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="book-grid">
|
||||
{{#each results}}
|
||||
{{> zenburn_card}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<h3>No Books Found</h3>
|
||||
<p>Try adjusting your search or <a href="/books">browse all books</a></p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{> zenburn_footer}}
|
||||
|
||||
509
server_root/static/zenburn.css
Normal file
509
server_root/static/zenburn.css
Normal file
@ -0,0 +1,509 @@
|
||||
/* Zenburn theme for sqlite-serve */
|
||||
/* Generated palette via: zenburn -j -e oklab */
|
||||
|
||||
:root {
|
||||
--fg-plus-2: #FFFFEF;
|
||||
--fg-plus-1: #F5F5D6;
|
||||
--fg: #DCDCCC;
|
||||
--fg-1: #A6A689;
|
||||
--fg-2: #656555;
|
||||
--black: #000000;
|
||||
--bg-2: #000000;
|
||||
--bg-1: #111112;
|
||||
--bg-05: #383838;
|
||||
--bg: #2A2B2E;
|
||||
--bg-plus-05: #494949;
|
||||
--bg-plus-1: #4F4F4F;
|
||||
--bg-plus-2: #5F5F5F;
|
||||
--bg-plus-3: #6F6F6F;
|
||||
--red-plus-2: #ECB3B3;
|
||||
--red-plus-1: #DCA3A3;
|
||||
--red: #CC9393;
|
||||
--red-1: #BC8383;
|
||||
--red-2: #AC7373;
|
||||
--red-3: #9C6363;
|
||||
--red-4: #8C5353;
|
||||
--red-5: #7C4343;
|
||||
--red-6: #6C3333;
|
||||
--orange: #DFAF8F;
|
||||
--yellow: #F0DFAF;
|
||||
--yellow-1: #E0CF9F;
|
||||
--yellow-2: #D0BF8F;
|
||||
--green-5: #2F4F2F;
|
||||
--green-4: #3F5F3F;
|
||||
--green-3: #4F6F4F;
|
||||
--green-2: #5F7F5F;
|
||||
--green-1: #6F8F6F;
|
||||
--green: #7F9F7F;
|
||||
--green-plus-1: #8FB28F;
|
||||
--green-plus-2: #9FC59F;
|
||||
--green-plus-3: #AFD8AF;
|
||||
--green-plus-4: #BFEBBF;
|
||||
--cyan: #93E0E3;
|
||||
--blue-plus-3: #BDE0F3;
|
||||
--blue-plus-2: #ACE0E3;
|
||||
--blue-plus-1: #94BFF3;
|
||||
--blue: #8CD0D3;
|
||||
--blue-1: #7CB8BB;
|
||||
--blue-2: #6CA0A3;
|
||||
--blue-3: #5C888B;
|
||||
--blue-4: #4C7073;
|
||||
--blue-5: #366060;
|
||||
--magenta: #DC8CC3;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
|
||||
line-height: 1.6;
|
||||
color: var(--fg);
|
||||
background: var(--bg);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
header {
|
||||
background: var(--bg-1);
|
||||
border-bottom: 2px solid var(--blue-1);
|
||||
padding: 1.5rem 2rem;
|
||||
box-shadow: 0 2px 8px oklab(0.000% 0.000 0.000 / 0.5);
|
||||
}
|
||||
|
||||
.header-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
header h1 {
|
||||
color: var(--yellow);
|
||||
font-size: 1.8rem;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
header p {
|
||||
color: var(--fg-1);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* Navigation */
|
||||
nav {
|
||||
background: var(--bg-plus-1);
|
||||
padding: 0.75rem 2rem;
|
||||
border-bottom: 1px solid var(--bg-1);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
box-shadow: 0 2px 4px oklab(0.000% 0.000 0.000 / 0.3);
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: var(--blue-1);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
transition: color 0.2s;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
color: var(--green-plus-1);
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
color: var(--yellow);
|
||||
}
|
||||
|
||||
.search-form {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.search-form input {
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--fg-2);
|
||||
color: var(--fg);
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 4px;
|
||||
font-family: inherit;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.search-form input:focus {
|
||||
outline: none;
|
||||
border-color: var(--blue-1);
|
||||
}
|
||||
|
||||
.search-form button {
|
||||
background: var(--blue-1);
|
||||
color: var(--bg-1);
|
||||
border: none;
|
||||
padding: 0.4rem 1rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.search-form button:hover {
|
||||
background: var(--green-plus-1);
|
||||
}
|
||||
|
||||
/* Main content */
|
||||
main {
|
||||
padding: 2rem;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
background: var(--bg-1);
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
border-top: 2px solid var(--blue-1);
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
footer p {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
footer p:first-child {
|
||||
color: var(--yellow);
|
||||
font-weight: 600;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
footer p:not(:first-child) {
|
||||
color: var(--fg-1);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Page header */
|
||||
.page-header {
|
||||
background: var(--bg-plus-1);
|
||||
border: 1px solid var(--bg-1);
|
||||
border-left: 3px solid var(--green-plus-1);
|
||||
border-radius: 6px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
color: var(--yellow);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: var(--fg-1);
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
/* Stats grid */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: var(--bg-plus-1);
|
||||
border: 1px solid var(--bg-1);
|
||||
border-radius: 6px;
|
||||
padding: 1.5rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: var(--orange);
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: var(--fg-1);
|
||||
font-size: 0.9rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* Book grid and cards */
|
||||
.book-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.book-card {
|
||||
background: var(--bg-plus-1);
|
||||
border: 1px solid var(--bg-1);
|
||||
border-left: 3px solid var(--blue-1);
|
||||
border-radius: 6px;
|
||||
padding: 1.5rem;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.book-card:hover {
|
||||
border-left-color: var(--green-plus-1);
|
||||
box-shadow: 0 4px 12px oklab(0.000% 0.000 0.000 / 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.book-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: start;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
color: var(--yellow);
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.book-rating {
|
||||
background: var(--bg-1);
|
||||
color: var(--orange);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 12px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.book-author {
|
||||
color: var(--blue-1);
|
||||
font-weight: 500;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.book-description {
|
||||
color: var(--fg-1);
|
||||
line-height: 1.5;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.book-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.genre-badge {
|
||||
background: var(--green);
|
||||
color: var(--bg-1);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.year-badge {
|
||||
background: var(--bg-1);
|
||||
color: var(--fg-1);
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.isbn {
|
||||
color: var(--fg-1);
|
||||
font-family: monospace;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.detail-link {
|
||||
color: var(--blue-1);
|
||||
text-decoration: none;
|
||||
font-weight: 500;
|
||||
font-size: 0.9rem;
|
||||
margin-top: 0.5rem;
|
||||
align-self: flex-start;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.detail-link:hover {
|
||||
color: var(--green-plus-1);
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
color: var(--fg-1);
|
||||
}
|
||||
|
||||
.empty-state h3 {
|
||||
color: var(--orange);
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.empty-state a {
|
||||
color: var(--blue-1);
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.empty-state a:hover {
|
||||
color: var(--green-plus-1);
|
||||
}
|
||||
|
||||
/* Book detail page */
|
||||
.detail-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.breadcrumb {
|
||||
color: var(--fg-1);
|
||||
margin-bottom: 1.5rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.breadcrumb a {
|
||||
color: var(--blue-1);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.breadcrumb a:hover {
|
||||
color: var(--green-plus-1);
|
||||
}
|
||||
|
||||
.book-hero {
|
||||
background: var(--bg-plus-1);
|
||||
border: 1px solid var(--bg-1);
|
||||
border-left: 4px solid var(--orange);
|
||||
border-radius: 6px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.book-hero-title {
|
||||
color: var(--yellow);
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.75rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.book-hero-author {
|
||||
color: var(--blue-1);
|
||||
font-size: 1.3rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.meta-item {
|
||||
background: var(--bg-1);
|
||||
padding: 1.25rem;
|
||||
border-radius: 6px;
|
||||
border-left: 3px solid var(--blue-1);
|
||||
}
|
||||
|
||||
.meta-label {
|
||||
color: var(--fg-1);
|
||||
font-size: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.meta-value {
|
||||
color: var(--yellow);
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.meta-value.rating {
|
||||
color: var(--orange);
|
||||
}
|
||||
|
||||
.description-section {
|
||||
background: var(--bg-plus-1);
|
||||
border: 1px solid var(--bg-1);
|
||||
border-radius: 6px;
|
||||
padding: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.description-section h3 {
|
||||
color: var(--green-plus-1);
|
||||
font-size: 1.3rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.description-text {
|
||||
color: var(--fg);
|
||||
line-height: 1.8;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.back-button {
|
||||
display: inline-block;
|
||||
background: var(--blue-1);
|
||||
color: var(--bg-1);
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: 6px;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.back-button:hover {
|
||||
background: var(--green-plus-1);
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
.not-found {
|
||||
text-align: center;
|
||||
padding: 4rem 2rem;
|
||||
}
|
||||
|
||||
.not-found h2 {
|
||||
color: var(--red);
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.not-found p {
|
||||
color: var(--fg-1);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
37
server_root/top/catalog.hbs
Normal file
37
server_root/top/catalog.hbs
Normal file
@ -0,0 +1,37 @@
|
||||
{{> zenburn_header}}
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Book Catalog</h2>
|
||||
<p>Browse our curated collection of technical literature</p>
|
||||
</div>
|
||||
|
||||
{{#if results}}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">{{#each results}}{{#if @last}}{{@index}}{{/if}}{{/each}}</span>
|
||||
<span class="stat-label">Books Found</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">📚</span>
|
||||
<span class="stat-label">Categories</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-value">⭐</span>
|
||||
<span class="stat-label">Curated</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="book-grid">
|
||||
{{#each results}}
|
||||
{{> zenburn_card}}
|
||||
{{/each}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="empty-state">
|
||||
<h3>No Books Found</h3>
|
||||
<p>Try adjusting your search or <a href="/books">browse all books</a></p>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
{{> zenburn_footer}}
|
||||
|
||||
75
start.sh
Executable file
75
start.sh
Executable file
@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
# Production start script for sqlite-serve
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Starting sqlite-serve..."
|
||||
echo ""
|
||||
|
||||
# Check if database exists
|
||||
if [ ! -f "book_catalog.db" ]; then
|
||||
echo "📊 Database not found. Running setup..."
|
||||
./setup_book_catalog.sh
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Build if needed
|
||||
if [ ! -f "target/debug/libsqlite_serve.dylib" ]; then
|
||||
echo "🔨 Building module..."
|
||||
direnv exec "$PWD" cargo build
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Stop any existing instance
|
||||
./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/sqlite_serve.conf -p . 2>/dev/null || true
|
||||
sleep 1
|
||||
|
||||
# Start nginx
|
||||
echo "▶️ Starting NGINX on http://localhost:8080"
|
||||
./ngx_src/nginx-1.28.0/objs/nginx -c conf/sqlite_serve.conf -p .
|
||||
|
||||
echo ""
|
||||
echo "✅ sqlite-serve is running!"
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo " Endpoints:"
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo " 📚 Browse"
|
||||
echo " http://localhost:8080/books"
|
||||
echo " → All books in catalog"
|
||||
echo ""
|
||||
echo " 🔍 Search"
|
||||
echo " http://localhost:8080/search?q=Rust"
|
||||
echo " → Search by title"
|
||||
echo ""
|
||||
echo " 📖 Book Detail"
|
||||
echo " http://localhost:8080/book?id=1"
|
||||
echo " → View individual book"
|
||||
echo ""
|
||||
echo " 🏷️ Filter by Genre"
|
||||
echo " http://localhost:8080/genre?genre=Programming"
|
||||
echo " → Programming books"
|
||||
echo ""
|
||||
echo " ⭐ Top Rated"
|
||||
echo " http://localhost:8080/top?min=4.7"
|
||||
echo " → Books rated 4.7 or higher"
|
||||
echo ""
|
||||
echo " 📅 By Era"
|
||||
echo " http://localhost:8080/era?from=2015&to=2024"
|
||||
echo " → Books from 2015-2024"
|
||||
echo ""
|
||||
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||
echo ""
|
||||
echo "Features Demonstrated:"
|
||||
echo " ✓ SQLite integration with parameterized queries"
|
||||
echo " ✓ Named & positional SQL parameters"
|
||||
echo " ✓ Handlebars templates with inheritance"
|
||||
echo " ✓ Global & local template overrides"
|
||||
echo " ✓ Type-safe configuration (Parse, Don't Validate)"
|
||||
echo " ✓ Dependency injection architecture"
|
||||
echo " ✓ 45 unit tests"
|
||||
echo ""
|
||||
echo "To stop: ./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/sqlite_serve.conf -p ."
|
||||
echo ""
|
||||
|
||||
@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Quick start script for the book catalog example
|
||||
|
||||
set -e
|
||||
|
||||
echo "📚 Starting Book Catalog..."
|
||||
echo ""
|
||||
|
||||
# Check if database exists
|
||||
if [ ! -f "book_catalog.db" ]; then
|
||||
echo "Database not found. Running setup..."
|
||||
./setup_book_catalog.sh
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check if module is built
|
||||
if [ ! -f "target/debug/libsqlite_serve.dylib" ]; then
|
||||
echo "Module not built. Building..."
|
||||
direnv exec "$PWD" cargo build
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Start nginx
|
||||
echo "Starting nginx on http://localhost:8080"
|
||||
./ngx_src/nginx-1.28.0/objs/nginx -c conf/book_catalog.conf -p .
|
||||
|
||||
echo ""
|
||||
echo "✅ Book Catalog is running!"
|
||||
echo ""
|
||||
echo "Visit:"
|
||||
echo " • http://localhost:8080/books/all - All books"
|
||||
echo " • http://localhost:8080/books/programming - Programming books"
|
||||
echo " • http://localhost:8080/books/databases - Database books"
|
||||
echo " • http://localhost:8080/books/computer-science - CS books"
|
||||
echo ""
|
||||
echo "To stop: ./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/book_catalog.conf -p ."
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start script for the book detail example with path parameters
|
||||
|
||||
set -e
|
||||
|
||||
echo "📚 Starting Book Detail Example with Path Parameters..."
|
||||
echo ""
|
||||
|
||||
# Check if database exists
|
||||
if [ ! -f "book_catalog.db" ]; then
|
||||
echo "Database not found. Running setup..."
|
||||
./setup_book_catalog.sh
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check if module is built
|
||||
if [ ! -f "target/debug/libsqlite_serve.dylib" ]; then
|
||||
echo "Module not built. Building..."
|
||||
direnv exec "$PWD" cargo build
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Start nginx
|
||||
echo "Starting nginx on http://localhost:8081"
|
||||
./ngx_src/nginx-1.28.0/objs/nginx -c conf/book_detail.conf -p .
|
||||
|
||||
echo ""
|
||||
echo "✅ Book Detail Example is running!"
|
||||
echo ""
|
||||
echo "Try these URLs:"
|
||||
echo " • http://localhost:8081/book?id=1 - View book #1"
|
||||
echo " • http://localhost:8081/book?id=5 - View book #5"
|
||||
echo " • http://localhost:8081/genre?genre=Programming - Programming books"
|
||||
echo " • http://localhost:8081/genre?genre=Databases - Database books"
|
||||
echo " • http://localhost:8081/years?min=2000&max=2010 - Books from 2000-2010"
|
||||
echo " • http://localhost:8081/years?min=2015&max=2024 - Books from 2015-2024"
|
||||
echo ""
|
||||
echo "To stop: ./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/book_detail.conf -p ."
|
||||
|
||||
@ -1,50 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# Start script for named parameters example
|
||||
|
||||
set -e
|
||||
|
||||
echo "📚 Starting Named Parameters Example..."
|
||||
echo ""
|
||||
|
||||
# Check if database exists
|
||||
if [ ! -f "book_catalog.db" ]; then
|
||||
echo "Database not found. Running setup..."
|
||||
./setup_book_catalog.sh
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Check if module is built
|
||||
if [ ! -f "target/debug/libsqlite_serve.dylib" ]; then
|
||||
echo "Module not built. Building..."
|
||||
direnv exec "$PWD" cargo build
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# Start nginx
|
||||
echo "Starting nginx on http://localhost:8082"
|
||||
./ngx_src/nginx-1.28.0/objs/nginx -c conf/book_named_params.conf -p .
|
||||
|
||||
echo ""
|
||||
echo "✅ Named Parameters Example is running!"
|
||||
echo ""
|
||||
echo "Named Parameter Examples:"
|
||||
echo " • http://localhost:8082/book?id=1"
|
||||
echo " Query: SELECT * FROM books WHERE id = :book_id"
|
||||
echo " Param: :book_id = \$arg_id"
|
||||
echo ""
|
||||
echo " • http://localhost:8082/genre?genre=Programming"
|
||||
echo " Query: ... WHERE genre = :genre_name"
|
||||
echo " Param: :genre_name = \$arg_genre"
|
||||
echo ""
|
||||
echo " • http://localhost:8082/years?min=2015&max=2024"
|
||||
echo " Query: ... WHERE year >= :min_year AND year <= :max_year"
|
||||
echo " Params: :min_year = \$arg_min, :max_year = \$arg_max"
|
||||
echo ""
|
||||
echo " • http://localhost:8082/search?q=Rust"
|
||||
echo " Search by title with named parameter"
|
||||
echo ""
|
||||
echo " • http://localhost:8082/top-rated?rating=4.7"
|
||||
echo " Filter by minimum rating"
|
||||
echo ""
|
||||
echo "To stop: ./ngx_src/nginx-1.28.0/objs/nginx -s stop -c conf/book_named_params.conf -p ."
|
||||
|
||||
Reference in New Issue
Block a user