feat: add template inheritance (extends/block) support

- ViewEngine now supports extends and named blocks
- Each route gets exclusive cached AST (no shared parent layouts)
- Fix iteration over struct arrays in each loops
- Add demo app with full e-commerce layout using extends
- Serve static files from public folder
- Bump version to 0.3.0
This commit is contained in:
2026-01-25 15:23:57 +05:30
parent 776f8a68f5
commit 1b2da224be
52 changed files with 2962 additions and 728 deletions

View File

@@ -0,0 +1,752 @@
/* Pugz Store - Clean Modern CSS */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
:root {
--primary: #3b82f6;
--primary-dark: #2563eb;
--text: #1f2937;
--text-muted: #6b7280;
--bg: #ffffff;
--bg-alt: #f9fafb;
--border: #e5e7eb;
--success: #10b981;
--radius: 8px;
}
body {
font-family: system-ui, -apple-system, sans-serif;
line-height: 1.6;
color: var(--text);
background: var(--bg);
}
a {
color: var(--primary);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
/* Layout */
.container {
max-width: 1100px;
margin: 0 auto;
padding: 0 20px;
}
/* Header */
.header {
background: var(--bg);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 0;
}
.logo {
font-size: 1.5rem;
font-weight: 700;
color: var(--primary);
}
.logo:hover {
text-decoration: none;
}
.nav {
display: flex;
gap: 24px;
}
.nav-link {
color: var(--text-muted);
font-weight: 500;
}
.nav-link:hover {
color: var(--primary);
text-decoration: none;
}
.header-actions {
display: flex;
align-items: center;
}
.cart-link {
color: var(--text);
font-weight: 500;
}
/* Footer */
.footer {
background: var(--text);
color: white;
padding: 40px 0;
margin-top: 60px;
}
.footer-content {
text-align: center;
}
.footer-content p {
color: #9ca3af;
}
/* Buttons */
.btn {
display: inline-block;
padding: 10px 20px;
font-size: 14px;
font-weight: 500;
border-radius: var(--radius);
border: none;
cursor: pointer;
text-align: center;
transition: all 0.2s;
}
.btn:hover {
text-decoration: none;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-outline {
background: transparent;
border: 1px solid var(--border);
color: var(--text);
}
.btn-outline:hover {
border-color: var(--primary);
color: var(--primary);
}
.btn-sm {
padding: 6px 12px;
font-size: 13px;
}
.btn-block {
display: block;
width: 100%;
}
/* Hero Section */
.hero {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
padding: 80px 0;
text-align: center;
}
.hero h1 {
font-size: 2.5rem;
margin-bottom: 16px;
}
.hero p {
font-size: 1.1rem;
opacity: 0.9;
margin-bottom: 32px;
}
.hero-actions {
display: flex;
gap: 12px;
justify-content: center;
}
.hero .btn-outline {
border-color: rgba(255, 255, 255, 0.4);
color: white;
}
.hero .btn-outline:hover {
border-color: white;
background: rgba(255, 255, 255, 0.1);
}
/* Sections */
.section {
padding: 60px 0;
}
.section-alt {
background: var(--bg-alt);
}
.section h2 {
font-size: 1.75rem;
margin-bottom: 32px;
}
.page-header {
background: var(--bg-alt);
padding: 40px 0;
border-bottom: 1px solid var(--border);
}
.page-header h1 {
font-size: 2rem;
margin-bottom: 8px;
}
.page-header p {
color: var(--text-muted);
}
/* Feature Grid */
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 24px;
}
.feature-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
}
.feature-card h3 {
font-size: 1.1rem;
margin-bottom: 12px;
}
.feature-card p {
color: var(--text-muted);
font-size: 14px;
}
.feature-card ul {
margin: 0;
padding-left: 20px;
color: var(--text-muted);
font-size: 14px;
}
.feature-card li {
margin-bottom: 4px;
}
/* Category Grid */
.category-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 24px;
}
.category-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 32px 24px;
text-align: center;
transition: all 0.2s;
}
.category-card:hover {
border-color: var(--primary);
text-decoration: none;
transform: translateY(-2px);
}
.category-icon {
width: 60px;
height: 60px;
margin: 0 auto 16px;
background: var(--bg-alt);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.5rem;
font-weight: 600;
color: var(--primary);
}
.category-card h3 {
font-size: 1rem;
color: var(--text);
margin-bottom: 4px;
}
.category-card span {
font-size: 14px;
color: var(--text-muted);
}
/* Product Grid */
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
margin-bottom: 40px;
}
.product-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
overflow: hidden;
transition: all 0.2s;
}
.product-card:hover {
border-color: var(--primary);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.product-image {
position: relative;
height: 180px;
background: var(--bg-alt);
}
.product-badge {
position: absolute;
top: 12px;
left: 12px;
background: #ef4444;
color: white;
padding: 4px 10px;
font-size: 12px;
font-weight: 600;
border-radius: 4px;
}
.product-info {
padding: 16px;
}
.product-category {
font-size: 12px;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.product-name {
font-size: 1rem;
margin: 6px 0 12px;
}
.product-price {
font-size: 1.1rem;
font-weight: 600;
color: var(--text);
margin-bottom: 12px;
}
/* Products Toolbar */
.products-toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
}
.results-count {
color: var(--text-muted);
}
.sort-options {
display: flex;
align-items: center;
gap: 8px;
}
.sort-options label {
color: var(--text-muted);
font-size: 14px;
}
.sort-options select {
padding: 8px 12px;
border: 1px solid var(--border);
border-radius: var(--radius);
font-size: 14px;
}
/* Pagination */
.pagination {
display: flex;
justify-content: center;
gap: 8px;
}
.page-link {
padding: 8px 14px;
border: 1px solid var(--border);
border-radius: var(--radius);
color: var(--text-muted);
font-size: 14px;
}
.page-link:hover {
border-color: var(--primary);
color: var(--primary);
text-decoration: none;
}
.page-link.active {
background: var(--primary);
border-color: var(--primary);
color: white;
}
/* Cart */
.cart-layout {
display: grid;
grid-template-columns: 1fr 340px;
gap: 32px;
}
.cart-items {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
}
.cart-item {
display: grid;
grid-template-columns: 1fr auto auto auto;
gap: 20px;
padding: 20px;
align-items: center;
border-bottom: 1px solid var(--border);
}
.cart-item:last-child {
border-bottom: none;
}
.cart-item-info h3 {
font-size: 1rem;
margin-bottom: 4px;
}
.cart-item-price {
color: var(--text-muted);
font-size: 14px;
}
.cart-item-qty {
display: flex;
align-items: center;
}
.qty-btn {
width: 32px;
height: 32px;
border: 1px solid var(--border);
background: var(--bg);
cursor: pointer;
font-size: 16px;
}
.qty-input {
width: 48px;
height: 32px;
border: 1px solid var(--border);
border-left: none;
border-right: none;
text-align: center;
font-size: 14px;
}
.cart-item-total {
font-weight: 600;
min-width: 80px;
text-align: right;
}
.cart-item-remove {
width: 32px;
height: 32px;
border: none;
background: none;
color: var(--text-muted);
cursor: pointer;
font-size: 18px;
}
.cart-item-remove:hover {
color: #ef4444;
}
.cart-actions {
padding: 20px;
border-top: 1px solid var(--border);
}
.cart-summary {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
height: fit-content;
}
.cart-summary h3 {
font-size: 1.1rem;
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border);
}
.summary-row {
display: flex;
justify-content: space-between;
margin-bottom: 12px;
font-size: 14px;
}
.summary-total {
font-size: 1.1rem;
font-weight: 600;
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
/* About Page */
.about-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 40px;
}
.about-main h2 {
margin-bottom: 16px;
}
.about-main h3 {
margin: 24px 0 12px;
font-size: 1.1rem;
}
.about-main p {
color: var(--text-muted);
margin-bottom: 12px;
}
.feature-list {
list-style: none;
padding: 0;
}
.feature-list li {
padding: 10px 0;
border-bottom: 1px solid var(--border);
font-size: 14px;
}
.about-sidebar {
display: flex;
flex-direction: column;
gap: 20px;
}
.info-card {
background: var(--bg-alt);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
}
.info-card h3 {
font-size: 1rem;
margin-bottom: 12px;
}
.info-card ul {
margin: 0;
padding-left: 18px;
font-size: 14px;
color: var(--text-muted);
}
.info-card li {
margin-bottom: 6px;
}
/* Product Detail */
.product-detail {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
}
.product-detail-image {
background: var(--bg-alt);
border-radius: var(--radius);
aspect-ratio: 1;
}
.product-image-placeholder {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
}
.product-detail-info h1 {
font-size: 2rem;
margin: 8px 0 16px;
}
.product-price-large {
font-size: 1.75rem;
font-weight: 600;
color: var(--primary);
margin-bottom: 16px;
}
.product-description {
color: var(--text-muted);
margin-bottom: 24px;
line-height: 1.7;
}
.product-actions {
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 24px;
}
.quantity-selector {
display: flex;
align-items: center;
gap: 8px;
}
.quantity-selector label {
font-weight: 500;
}
.product-meta {
padding-top: 24px;
border-top: 1px solid var(--border);
}
.product-meta p {
color: var(--text-muted);
font-size: 14px;
margin-bottom: 8px;
}
.breadcrumb {
font-size: 14px;
color: var(--text-muted);
}
.breadcrumb a {
color: var(--text-muted);
}
.breadcrumb a:hover {
color: var(--primary);
}
.breadcrumb span {
margin: 0 8px;
}
/* Error Page */
.error-page {
padding: 100px 0;
text-align: center;
}
.error-code {
font-size: 8rem;
font-weight: 700;
color: var(--border);
line-height: 1;
}
.error-content h2 {
margin: 16px 0 8px;
}
.error-content p {
color: var(--text-muted);
margin-bottom: 32px;
}
.error-actions {
display: flex;
gap: 12px;
justify-content: center;
}
/* Utility Classes */
.text-success {
color: var(--success);
}
.text-muted {
color: var(--text-muted);
}
/* Responsive */
@media (max-width: 768px) {
.header-content {
flex-wrap: wrap;
gap: 16px;
}
.nav {
order: 3;
width: 100%;
justify-content: center;
}
.hero h1 {
font-size: 2rem;
}
.hero-actions {
flex-direction: column;
align-items: center;
}
.cart-layout,
.about-grid {
grid-template-columns: 1fr;
}
.cart-item {
grid-template-columns: 1fr;
gap: 12px;
}
}

View File

@@ -1,11 +1,13 @@
//! Pugz Demo - ViewEngine Template Rendering
//! Pugz Store Demo - A comprehensive e-commerce demo showcasing Pugz capabilities
//!
//! This demo shows how to use ViewEngine for server-side rendering.
//!
//! Routes:
//! GET / - Home page
//! GET /users - Users list
//! GET /page-a - Page with data
//! Features demonstrated:
//! - Template inheritance (extends/block)
//! - Partial includes (header, footer)
//! - Mixins with parameters (product-card, rating, forms)
//! - Conditionals and loops
//! - Data binding
//! - Pretty printing
//! - LRU cache with TTL
const std = @import("std");
const httpz = @import("httpz");
@@ -13,28 +15,367 @@ const pugz = @import("pugz");
const Allocator = std.mem.Allocator;
/// Application state shared across all requests
// ============================================================================
// Data Types
// ============================================================================
const Product = struct {
id: []const u8,
name: []const u8,
price: []const u8,
image: []const u8,
rating: u8,
category: []const u8,
categorySlug: []const u8,
sale: bool = false,
description: []const u8 = "",
reviewCount: []const u8 = "0",
};
const Category = struct {
name: []const u8,
slug: []const u8,
icon: []const u8,
count: []const u8,
active: bool = false,
};
const CartItem = struct {
id: []const u8,
name: []const u8,
price: []const u8,
image: []const u8,
variant: []const u8,
quantity: []const u8,
total: []const u8,
};
const Cart = struct {
items: []const CartItem,
subtotal: []const u8,
shipping: []const u8,
discount: ?[]const u8 = null,
discountCode: ?[]const u8 = null,
tax: []const u8,
total: []const u8,
};
const ShippingMethod = struct {
id: []const u8,
name: []const u8,
time: []const u8,
price: []const u8,
};
const State = struct {
code: []const u8,
name: []const u8,
};
// ============================================================================
// Sample Data
// ============================================================================
const sample_products = [_]Product{
.{
.id = "1",
.name = "Wireless Headphones",
.price = "79.99",
.image = "/images/headphones.jpg",
.rating = 4,
.category = "Electronics",
.categorySlug = "electronics",
.sale = true,
.description = "Premium wireless headphones with noise cancellation",
.reviewCount = "128",
},
.{
.id = "2",
.name = "Smart Watch Pro",
.price = "199.99",
.image = "/images/watch.jpg",
.rating = 5,
.category = "Electronics",
.categorySlug = "electronics",
.description = "Advanced fitness tracking and notifications",
.reviewCount = "256",
},
.{
.id = "3",
.name = "Laptop Stand",
.price = "49.99",
.image = "/images/stand.jpg",
.rating = 4,
.category = "Accessories",
.categorySlug = "accessories",
.description = "Ergonomic aluminum laptop stand",
.reviewCount = "89",
},
.{
.id = "4",
.name = "USB-C Hub",
.price = "39.99",
.image = "/images/hub.jpg",
.rating = 4,
.category = "Accessories",
.categorySlug = "accessories",
.sale = true,
.description = "7-in-1 USB-C hub with HDMI and card reader",
.reviewCount = "312",
},
.{
.id = "5",
.name = "Mechanical Keyboard",
.price = "129.99",
.image = "/images/keyboard.jpg",
.rating = 5,
.category = "Electronics",
.categorySlug = "electronics",
.description = "RGB mechanical keyboard with Cherry MX switches",
.reviewCount = "445",
},
.{
.id = "6",
.name = "Desk Lamp",
.price = "34.99",
.image = "/images/lamp.jpg",
.rating = 4,
.category = "Home Office",
.categorySlug = "home-office",
.description = "LED desk lamp with adjustable brightness",
.reviewCount = "67",
},
};
const sample_categories = [_]Category{
.{ .name = "Electronics", .slug = "electronics", .icon = "E", .count = "24" },
.{ .name = "Accessories", .slug = "accessories", .icon = "A", .count = "18" },
.{ .name = "Home Office", .slug = "home-office", .icon = "H", .count = "12" },
.{ .name = "Clothing", .slug = "clothing", .icon = "C", .count = "36" },
};
const sample_cart_items = [_]CartItem{
.{
.id = "1",
.name = "Wireless Headphones",
.price = "79.99",
.image = "/images/headphones.jpg",
.variant = "Black",
.quantity = "1",
.total = "79.99",
},
.{
.id = "5",
.name = "Mechanical Keyboard",
.price = "129.99",
.image = "/images/keyboard.jpg",
.variant = "RGB",
.quantity = "1",
.total = "129.99",
},
};
const sample_cart = Cart{
.items = &sample_cart_items,
.subtotal = "209.98",
.shipping = "0",
.tax = "18.90",
.total = "228.88",
};
const shipping_methods = [_]ShippingMethod{
.{ .id = "standard", .name = "Standard Shipping", .time = "5-7 business days", .price = "0" },
.{ .id = "express", .name = "Express Shipping", .time = "2-3 business days", .price = "9.99" },
.{ .id = "overnight", .name = "Overnight Shipping", .time = "Next business day", .price = "19.99" },
};
const us_states = [_]State{
.{ .code = "CA", .name = "California" },
.{ .code = "NY", .name = "New York" },
.{ .code = "TX", .name = "Texas" },
.{ .code = "FL", .name = "Florida" },
.{ .code = "WA", .name = "Washington" },
};
// ============================================================================
// Application
// ============================================================================
const App = struct {
allocator: Allocator,
view: pugz.ViewEngine,
pub fn init(allocator: Allocator) App {
pub fn init(allocator: Allocator) !App {
return .{
.allocator = allocator,
.view = pugz.ViewEngine.init(.{
.view = try pugz.ViewEngine.init(allocator, .{
.views_dir = "views",
.pretty = true,
.max_cached_templates = 50,
.cache_ttl_seconds = 10, // 10s TTL for development
}),
};
}
pub fn deinit(self: *App) void {
self.view.deinit();
}
};
// ============================================================================
// Request Handlers
// ============================================================================
fn home(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "pages/home", .{
.title = "Home",
.cartCount = "2",
.authenticated = true,
.items = &[_][]const u8{ "Wireless Headphones", "Smart Watch", "Laptop Stand", "USB-C Hub" },
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn products(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "pages/products", .{
.title = "All Products",
.cartCount = "2",
.productCount = "6",
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn productDetail(app: *App, req: *httpz.Request, res: *httpz.Response) !void {
const id = req.param("id") orelse "1";
_ = id;
const html = app.view.render(res.arena, "pages/product-detail", .{
.cartCount = "2",
.productName = "Wireless Headphones",
.category = "Electronics",
.price = "79.99",
.description = "Premium wireless headphones with active noise cancellation. Experience crystal-clear audio whether you're working, traveling, or relaxing at home.",
.sku = "WH-001-BLK",
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn cart(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "pages/cart", .{
.title = "Shopping Cart",
.cartCount = "2",
.cartItems = &sample_cart_items,
.subtotal = sample_cart.subtotal,
.shipping = sample_cart.shipping,
.tax = sample_cart.tax,
.total = sample_cart.total,
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn about(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "pages/about", .{
.title = "About",
.cartCount = "2",
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn notFound(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
res.status = 404;
const html = app.view.render(res.arena, "pages/404", .{
.title = "Page Not Found",
.cartCount = "2",
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn renderError(res: *httpz.Response, err: anyerror) void {
res.status = 500;
res.content_type = .HTML;
res.body = std.fmt.allocPrint(res.arena,
\\<!DOCTYPE html>
\\<html>
\\<head><title>Error</title></head>
\\<body>
\\<h1>500 - Server Error</h1>
\\<p>Error: {s}</p>
\\</body>
\\</html>
, .{@errorName(err)}) catch "Internal Server Error";
}
// ============================================================================
// Static Files
// ============================================================================
fn serveStatic(_: *App, req: *httpz.Request, res: *httpz.Response) !void {
const path = req.url.path;
// Strip leading slash and prepend public folder
const rel_path = if (path.len > 0 and path[0] == '/') path[1..] else path;
const full_path = std.fmt.allocPrint(res.arena, "public/{s}", .{rel_path}) catch {
res.status = 500;
res.body = "Internal Server Error";
return;
};
// Read file from disk
const content = std.fs.cwd().readFileAlloc(res.arena, full_path, 10 * 1024 * 1024) catch {
res.status = 404;
res.body = "Not Found";
return;
};
// Set content type based on extension
if (std.mem.endsWith(u8, path, ".css")) {
res.content_type = .CSS;
} else if (std.mem.endsWith(u8, path, ".js")) {
res.content_type = .JS;
} else if (std.mem.endsWith(u8, path, ".html")) {
res.content_type = .HTML;
}
res.body = content;
}
// ============================================================================
// Main
// ============================================================================
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer if (gpa.deinit() == .leak) @panic("leak");
const allocator = gpa.allocator();
var app = App.init(allocator);
var app = try App.init(allocator);
defer app.deinit();
const port = 8081;
var server = try httpz.Server(*App).init(allocator, .{ .port = port }, &app);
@@ -42,83 +383,37 @@ pub fn main() !void {
var router = try server.router(.{});
router.get("/", index, .{});
router.get("/users", users, .{});
router.get("/page-a", pageA, .{});
router.get("/mixin-test", mixinTest, .{});
// Pages
router.get("/", home, .{});
router.get("/products", products, .{});
router.get("/products/:id", productDetail, .{});
router.get("/cart", cart, .{});
router.get("/about", about, .{});
// Static files
router.get("/css/*", serveStatic, .{});
std.debug.print(
\\
\\Pugz Demo - ViewEngine Template Rendering
\\==========================================
\\Server running at http://localhost:{d}
\\ ____ ____ _
\\ | _ \ _ _ __ _ ____ / ___|| |_ ___ _ __ ___
\\ | |_) | | | |/ _` |_ / \___ \| __/ _ \| '__/ _ \
\\ | __/| |_| | (_| |/ / ___) | || (_) | | | __/
\\ |_| \__,_|\__, /___| |____/ \__\___/|_| \___|
\\ |___/
\\
\\Routes:
\\ GET / - Home page
\\ GET /users - Users list
\\ GET /page-a - Page with data
\\ GET /mixin-test - Mixin test page
\\ Server running at http://localhost:{d}
\\
\\Press Ctrl+C to stop.
\\ Routes:
\\ GET / - Home page
\\ GET /products - Products page
\\ GET /products/:id - Product detail
\\ GET /cart - Shopping cart
\\ GET /about - About page
\\
\\ Press Ctrl+C to stop.
\\
, .{port});
try server.listen();
}
/// GET / - Home page
fn index(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "index", .{
.title = "Welcome",
.authenticated = true,
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// GET /users - Users list
fn users(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "users", .{
.title = "Users",
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// GET /page-a - Page with data
fn pageA(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "page-a", .{
.title = "Page A - Pets",
.items = &[_][]const u8{ "A", "B", "C" },
.n = 0,
}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}
/// GET /mixin-test - Mixin test page
fn mixinTest(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = app.view.render(res.arena, "mixin-test", .{}) catch |err| {
res.status = 500;
res.body = @errorName(err);
return;
};
res.content_type = .HTML;
res.body = html;
}

View File

@@ -1,2 +0,0 @@
p
| Route no found

View File

@@ -1,336 +0,0 @@
//! Auto-generated by pugz.compileTemplates()
//! Do not edit manually - regenerate by running: zig build
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList(u8);
// ─────────────────────────────────────────────────────────────────────────────
// Helpers
// ─────────────────────────────────────────────────────────────────────────────
const esc_lut: [256]?[]const u8 = blk: {
var t: [256]?[]const u8 = .{null} ** 256;
t['&'] = "&amp;";
t['<'] = "&lt;";
t['>'] = "&gt;";
t['"'] = "&quot;";
t['\''] = "&#x27;";
break :blk t;
};
fn esc(o: *ArrayList, a: Allocator, s: []const u8) Allocator.Error!void {
var i: usize = 0;
for (s, 0..) |c, j| {
if (esc_lut[c]) |e| {
if (j > i) try o.appendSlice(a, s[i..j]);
try o.appendSlice(a, e);
i = j + 1;
}
}
if (i < s.len) try o.appendSlice(a, s[i..]);
}
fn truthy(v: anytype) bool {
return switch (@typeInfo(@TypeOf(v))) {
.bool => v,
.optional => v != null,
.pointer => |p| if (p.size == .slice) v.len > 0 else true,
.int, .comptime_int => v != 0,
else => true,
};
}
var int_buf: [32]u8 = undefined;
fn strVal(v: anytype) []const u8 {
const T = @TypeOf(v);
switch (@typeInfo(T)) {
.pointer => |p| switch (p.size) {
.slice => return v,
.one => {
// For pointer-to-array, slice it
const child_info = @typeInfo(p.child);
if (child_info == .array) {
const arr_info = child_info.array;
const ptr: [*]const arr_info.child = @ptrCast(v);
return ptr[0..arr_info.len];
}
return strVal(v.*);
},
else => @compileError("unsupported pointer type"),
},
.array => @compileError("arrays must be passed by pointer"),
.int, .comptime_int => return std.fmt.bufPrint(&int_buf, "{d}", .{v}) catch "0",
.optional => return if (v) |val| strVal(val) else "",
else => @compileError("strVal: unsupported type " ++ @typeName(T)),
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Templates
// ─────────────────────────────────────────────────────────────────────────────
pub fn index(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<!DOCTYPE html><html><head><title>hello</title></head><body><p>some thing</p>ballahballah");
{
const text = "click me ";
const @"type" = "secondary";
try o.appendSlice(a, "<button");
try o.appendSlice(a, " class=\"");
try o.appendSlice(a, "btn btn-");
try o.appendSlice(a, strVal(@"type"));
try o.appendSlice(a, "\"");
try o.appendSlice(a, ">");
try esc(&o, a, strVal(text));
try o.appendSlice(a, "</button>");
}
try o.appendSlice(a, "</body><br /><a href=\"//google.com\" target=\"_blank\">Google 1</a><br /><a class=\"button\" href=\"//google.com\" target=\"_blank\">Google 2</a><br /><a class=\"button\" href=\"//google.com\" target=\"_blank\">Google 3</a></html>");
_ = d;
return o.items;
}
pub fn sub_layout(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<html><head><title>My Site - ");
try esc(&o, a, strVal(@field(d, "title")));
try o.appendSlice(a, "</title><script src=\"/jquery.js\"></script></head><body><div class=\"sidebar\"><p>nothing</p></div><div class=\"primary\"><p>nothing</p></div><div id=\"footer\"><p>some footer content</p></div></body></html>");
return o.items;
}
pub fn _404(a: Allocator, d: anytype) Allocator.Error![]u8 {
_ = .{ a, d };
return "<p>Route no found</p>";
}
pub fn mixins_alert(a: Allocator, d: anytype) Allocator.Error![]u8 {
_ = .{ a, d };
return "";
}
pub fn mixins_buttons(a: Allocator, d: anytype) Allocator.Error![]u8 {
_ = .{ a, d };
return "";
}
pub fn mixins_cards(a: Allocator, d: anytype) Allocator.Error![]u8 {
_ = .{ a, d };
return "";
}
pub fn mixins_alert_error(a: Allocator, d: anytype) Allocator.Error![]u8 {
_ = .{ a, d };
return "";
}
pub fn mixins_input_text(a: Allocator, d: anytype) Allocator.Error![]u8 {
_ = .{ a, d };
return "";
}
pub fn home(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<!DOCTYPE html><html><head><title>");
try esc(&o, a, strVal(@field(d, "title")));
try o.appendSlice(a, "</title><link rel=\"stylesheet\" href=\"/style.css\" /></head><body><header><h1>");
try esc(&o, a, strVal(@field(d, "title")));
try o.appendSlice(a, "</h1>");
if (@hasField(@TypeOf(d), "authenticated") and truthy(@field(d, "authenticated"))) {
try o.appendSlice(a, "<span class=\"user\">Welcome back!</span>");
}
try o.appendSlice(a, "</header><main><p>This page is rendered using a compiled template.</p><p>Compiled templates are 3x faster than Pug.js!</p></main><footer><p>&copy; 2024 Pugz Demo</p></footer></body></html>");
return o.items;
}
pub fn page_a(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<html><head><title>My Site - ");
try esc(&o, a, strVal(@field(d, "title")));
try o.appendSlice(a, "</title><script src=\"/jquery.js\"></script><script src=\"/pets.js\"></script></head><body><h1>");
try esc(&o, a, strVal(@field(d, "title")));
try o.appendSlice(a, "</h1><p>Welcome to the pets page!</p><ul><li>Cat</li><li>Dog</li></ul><ul>");
for (@field(d, "items")) |val| {
try o.appendSlice(a, "<li>");
try esc(&o, a, strVal(val));
try o.appendSlice(a, "</li>");
}
try o.appendSlice(a, "</ul><input data-json=\"{ &quot;very-long&quot;: &quot;piece of &quot;, &quot;data&quot;: true }\" /><br /><div class=\"div-class\" (click)=\"play()\">one</div><div class=\"div-class\" (click)=\"play()\">two</div><a style=\"color:red;background:green;\">sdfsdfs</a><a class=\"button\">btn</a><br /><form method=\"post\">");
{
const name = "firstName";
const label = "First Name";
const placeholder = "first name";
try o.appendSlice(a, "<fieldset class=\"fieldset\"><legend class=\"fieldset-legend\">");
try esc(&o, a, strVal(label));
try o.appendSlice(a, "</legend><input class=\"input\" type=\"text\"");
try o.appendSlice(a, " name=\"");
try o.appendSlice(a, strVal(name));
try o.appendSlice(a, "\"");
try o.appendSlice(a, " placeholder=\"");
try o.appendSlice(a, strVal(placeholder));
try o.appendSlice(a, "\"");
try o.appendSlice(a, " /></fieldset>");
}
try o.appendSlice(a, "<br />");
{
const name = "lastName";
const label = "Last Name";
const placeholder = "last name";
try o.appendSlice(a, "<fieldset class=\"fieldset\"><legend class=\"fieldset-legend\">");
try esc(&o, a, strVal(label));
try o.appendSlice(a, "</legend><input class=\"input\" type=\"text\"");
try o.appendSlice(a, " name=\"");
try o.appendSlice(a, strVal(name));
try o.appendSlice(a, "\"");
try o.appendSlice(a, " placeholder=\"");
try o.appendSlice(a, strVal(placeholder));
try o.appendSlice(a, "\"");
try o.appendSlice(a, " /></fieldset>");
}
try o.appendSlice(a, "<submit>sumit</submit>");
if (@hasField(@TypeOf(d), "error") and truthy(@field(d, "error"))) {
{
const message = @field(d, "error");
{
const mixin_attrs_1: struct {
class: []const u8 = "",
id: []const u8 = "",
style: []const u8 = "",
} = .{
.class = "alert-error",
};
try o.appendSlice(a, "<div");
try o.appendSlice(a, " class=\"");
try o.appendSlice(a, "alert ");
try o.appendSlice(a, strVal(mixin_attrs_1.class));
try o.appendSlice(a, "\"");
try o.appendSlice(a, " role=\"alert\"><svg class=\"h-6 w-6 shrink-0 stroke-current\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\" d=\"M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z\"></path></svg><span>");
try esc(&o, a, strVal(message));
try o.appendSlice(a, "</span></div>");
}
}
}
try o.appendSlice(a, "</form><div id=\"footer\"><p>some footer content</p></div></body></html>");
return o.items;
}
pub fn mixin_test(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<!DOCTYPE html><html><head><title>Mixin Test</title></head><body><h1>Mixin Test Page</h1><p>Testing button mixin:</p>");
{
const text = "Click Me";
const @"type" = "primary";
try o.appendSlice(a, "<button");
try o.appendSlice(a, " class=\"");
try o.appendSlice(a, "btn btn-");
try o.appendSlice(a, strVal(@"type"));
try o.appendSlice(a, "\"");
try o.appendSlice(a, ">");
try esc(&o, a, strVal(text));
try o.appendSlice(a, "</button>");
}
{
const text = "Cancel";
const @"type" = "btn btn-secondary";
try o.appendSlice(a, "<button");
try o.appendSlice(a, " class=\"");
try o.appendSlice(a, "btn btn-");
try o.appendSlice(a, strVal(@"type"));
try o.appendSlice(a, "\"");
try o.appendSlice(a, ">");
try esc(&o, a, strVal(text));
try o.appendSlice(a, "</button>");
}
try o.appendSlice(a, "<p>Testing link mixin:</p>");
{
const href = "/home";
const text = "Go Home";
try o.appendSlice(a, "<a class=\"btn btn-link\"");
try o.appendSlice(a, " href=\"");
try o.appendSlice(a, strVal(href));
try o.appendSlice(a, "\"");
try o.appendSlice(a, ">");
try esc(&o, a, strVal(text));
try o.appendSlice(a, "</a>");
}
try o.appendSlice(a, "</body></html>");
_ = d;
return o.items;
}
pub fn page_b(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<html><head><title>My Site - ");
try esc(&o, a, strVal(@field(d, "title")));
try o.appendSlice(a, "</title><script src=\"/jquery.js\"></script></head><body><div class=\"sidebar\"><p>nothing</p></div><div class=\"primary\"><p>nothing</p></div><div id=\"footer\"><p>some footer content</p></div></body></html>");
return o.items;
}
pub fn layout_2(a: Allocator, d: anytype) Allocator.Error![]u8 {
_ = .{ a, d };
return "<html><head><script src=\"/vendor/jquery.js\"></script><script src=\"/vendor/caustic.js\"></script></head><body></body></html>";
}
pub fn layout(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<html><head><title>My Site - ");
try esc(&o, a, strVal(@field(d, "title")));
try o.appendSlice(a, "</title><script src=\"/jquery.js\"></script></head><body><div id=\"footer\"><p>some footer content</p></div></body></html>");
return o.items;
}
pub fn page_append(a: Allocator, d: anytype) Allocator.Error![]u8 {
_ = .{ a, d };
return "<html><head><script src=\"/vendor/jquery.js\"></script><script src=\"/vendor/caustic.js\"></script><script src=\"/vendor/three.js\"></script><script src=\"/game.js\"></script></head><body><p>cheks manually the head section<br />hello there</p></body></html>";
}
pub fn users(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<!DOCTYPE html><html><head><title>Users</title></head><body><h1>User List</h1><ul class=\"user-list\">");
for (@field(d, "users")) |user| {
try o.appendSlice(a, "<li class=\"user\"><strong>");
try esc(&o, a, strVal(user.name));
try o.appendSlice(a, "</strong><span class=\"email\">");
try esc(&o, a, strVal(user.email));
try o.appendSlice(a, "</span></li>");
}
try o.appendSlice(a, "</ul></body></html>");
return o.items;
}
pub fn page_appen_optional_blk(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<html><head><title>My Site - ");
try esc(&o, a, strVal(@field(d, "title")));
try o.appendSlice(a, "</title><script src=\"/jquery.js\"></script></head><body><div id=\"footer\"><p>some footer content</p></div></body></html>");
return o.items;
}
pub fn pet(a: Allocator, d: anytype) Allocator.Error![]u8 {
var o: ArrayList = .empty;
try o.appendSlice(a, "<p>");
try esc(&o, a, strVal(@field(d, "petName")));
try o.appendSlice(a, "</p>");
return o.items;
}
pub const template_names = [_][]const u8{
"index",
"sub_layout",
"_404",
"mixins_alert",
"mixins_buttons",
"mixins_cards",
"mixins_alert_error",
"mixins_input_text",
"home",
"page_a",
"mixin_test",
"page_b",
"layout_2",
"layout",
"page_append",
"users",
"page_appen_optional_blk",
"pet",
};

View File

@@ -1,15 +0,0 @@
doctype html
html
head
title #{title}
link(rel="stylesheet" href="/style.css")
body
header
h1 #{title}
if authenticated
span.user Welcome back!
main
p This page is rendered using a compiled template.
p Compiled templates are 3x faster than Pug.js!
footer
p &copy; 2024 Pugz Demo

View File

@@ -1,15 +0,0 @@
doctype html
html
head
title hello
body
p some thing
| ballah
| ballah
+btn("click me ", "secondary")
br
a(href='//google.com' target="_blank") Google 1
br
a(class='button' href='//google.com' target="_blank") Google 2
br
a(class='button', href='//google.com' target="_blank") Google 3

View File

@@ -1,7 +0,0 @@
html
head
block head
script(src='/vendor/jquery.js')
script(src='/vendor/caustic.js')
body
block content

View File

@@ -1,10 +0,0 @@
html
head
title My Site - #{title}
block scripts
script(src='/jquery.js')
body
block content
block foot
#footer
p some footer content

View File

@@ -0,0 +1,28 @@
doctype html
html(lang="en")
head
meta(charset="UTF-8")
meta(name="viewport" content="width=device-width, initial-scale=1.0")
link(rel="stylesheet" href="/css/style.css")
block title
title Pugz Store
body
header.header
.container
.header-content
a.logo(href="/") Pugz Store
nav.nav
a.nav-link(href="/") Home
a.nav-link(href="/products") Products
a.nav-link(href="/about") About
.header-actions
a.cart-link(href="/cart")
| Cart (#{cartCount})
main
block content
footer.footer
.container
.footer-content
p Built with Pugz - A Pug template engine for Zig

View File

@@ -1,15 +0,0 @@
include mixins/buttons.pug
doctype html
html
head
title Mixin Test
body
h1 Mixin Test Page
p Testing button mixin:
+btn("Click Me")
+btn("Cancel", "secondary")
p Testing link mixin:
+btn-link("/home", "Go Home")

View File

@@ -1,5 +0,0 @@
mixin alert(message)
div.alert(role="alert" class!=attributes.class)
svg(xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24")
path(stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z")
span= message

View File

@@ -1,2 +0,0 @@
mixin alert_error(message)
+alert(message)(class="alert-error")

View File

@@ -0,0 +1,12 @@
//- Alert/notification mixins
mixin alert(message, type)
- var alertClass = type ? "alert alert-" + type : "alert alert-info"
.alert(class=alertClass)
p= message
mixin alert-dismissible(message, type)
- var alertClass = type ? "alert alert-" + type : "alert alert-info"
.alert.alert-dismissible(class=alertClass)
p= message
button.alert-close(type="button" aria-label="Close") x

View File

@@ -1,5 +1,15 @@
mixin btn(text, type="primary")
button(class="btn btn-" + type)= text
//- Button mixins with various styles
mixin btn-link(href, text)
a.btn.btn-link(href=href)= text
mixin btn(text, type)
- var btnClass = type ? "btn btn-" + type : "btn btn-primary"
button(class=btnClass)= text
mixin btn-link(href, text, type)
- var btnClass = type ? "btn btn-" + type : "btn btn-primary"
a(href=href class=btnClass)= text
mixin btn-icon(icon, text, type)
- var btnClass = type ? "btn btn-" + type : "btn btn-primary"
button(class=btnClass)
span.icon= icon
span= text

View File

@@ -1,11 +0,0 @@
mixin card(title)
.card
.card-header
h3= title
.card-body
block
mixin card-simple(title, body)
.card
h3= title
p= body

View File

@@ -0,0 +1,17 @@
//- Cart item display
mixin cart-item(item)
.cart-item
.cart-item-image
img(src=item.image alt=item.name)
.cart-item-details
h4.cart-item-name #{item.name}
p.cart-item-variant #{item.variant}
span.cart-item-price $#{item.price}
.cart-item-quantity
button.qty-btn.qty-minus -
input.qty-input(type="number" value=item.quantity min="1")
button.qty-btn.qty-plus +
.cart-item-total
span $#{item.total}
button.cart-item-remove(aria-label="Remove item") x

View File

@@ -0,0 +1,25 @@
//- Form input mixins
mixin input(name, label, type, placeholder)
.form-group
label(for=name)= label
input.form-control(type=type id=name name=name placeholder=placeholder)
mixin input-required(name, label, type, placeholder)
.form-group
label(for=name)
= label
span.required *
input.form-control(type=type id=name name=name placeholder=placeholder required)
mixin select(name, label, options)
.form-group
label(for=name)= label
select.form-control(id=name name=name)
each opt in options
option(value=opt.value)= opt.label
mixin textarea(name, label, placeholder, rows)
.form-group
label(for=name)= label
textarea.form-control(id=name name=name placeholder=placeholder rows=rows)

View File

@@ -1,4 +0,0 @@
mixin input_text(name, label, placeholder)
fieldset.fieldset
legend.fieldset-legend= label
input(type="text" name=name class="input" placeholder=placeholder)

View File

@@ -0,0 +1,38 @@
//- Product card mixin - displays a product in grid/list view
//- Parameters:
//- product: { id, name, price, image, rating, category }
mixin product-card(product)
article.product-card
a.product-image(href="/products/" + product.id)
img(src=product.image alt=product.name)
if product.sale
span.badge.badge-sale Sale
.product-info
span.product-category #{product.category}
h3.product-name
a(href="/products/" + product.id) #{product.name}
.product-rating
+rating(product.rating)
.product-footer
span.product-price $#{product.price}
button.btn.btn-primary.btn-sm(data-product=product.id) Add to Cart
//- Featured product card with larger display
mixin product-featured(product)
article.product-card.product-featured
.product-image-large
img(src=product.image alt=product.name)
if product.sale
span.badge.badge-sale Sale
.product-details
span.product-category #{product.category}
h2.product-name #{product.name}
p.product-description #{product.description}
.product-rating
+rating(product.rating)
span.review-count (#{product.reviewCount} reviews)
.product-price-large $#{product.price}
.product-actions
button.btn.btn-primary.btn-lg Add to Cart
button.btn.btn-outline Wishlist

View File

@@ -0,0 +1,13 @@
//- Star rating display
//- Parameters:
//- stars: number of stars (1-5)
mixin rating(stars)
.stars
- var i = 1
while i <= 5
if i <= stars
span.star.star-filled
else
span.star.star-empty
- i = i + 1

View File

@@ -1,36 +0,0 @@
extends layout.pug
block scripts
script(src='/jquery.js')
script(src='/pets.js')
block content
h1= title
p Welcome to the pets page!
ul
li Cat
li Dog
ul
each val in items
li= val
input(data-json=`
{
"very-long": "piece of ",
"data": true
}
`)
br
div(class='div-class', (click)='play()') one
div(class='div-class' '(click)'='play()') two
a(style={color: 'red', background: 'green'}) sdfsdfs
a.button btn
br
form(method="post")
+input_text("firstName", "First Name", "first name")
br
+input_text("lastName", "Last Name", "last name")
submit sumit
if error
+alert_error(error)

View File

@@ -1,5 +0,0 @@
extends layout
append head
script(src='/vendor/three.js')
script(src='/game.js')

View File

@@ -1,11 +0,0 @@
extends layout-2.pug
block append head
script(src='/vendor/three.js')
script(src='/game.js')
block content
p
| cheks manually the head section
br
| hello there

View File

@@ -1,9 +0,0 @@
extends sub-layout.pug
block content
.sidebar
block sidebar
p nothing
.primary
block primary
p nothing

View File

@@ -0,0 +1,15 @@
extends layouts/base.pug
block title
title #{title} | Pugz Store
block content
section.error-page
.container
.error-content
h1.error-code 404
h2 Page Not Found
p The page you are looking for does not exist or has been moved.
.error-actions
a.btn.btn-primary(href="/") Go Home
a.btn.btn-outline(href="/products") View Products

View File

@@ -0,0 +1,51 @@
extends layouts/base.pug
block title
title #{title} | Pugz Store
block content
section.page-header
.container
h1 About Pugz
p A Pug template engine written in Zig
section.section
.container
.about-grid
.about-main
h2 What is Pugz?
p Pugz is a high-performance Pug template engine implemented in Zig. It provides both runtime interpretation and build-time compilation for maximum flexibility.
h3 Key Features
ul.feature-list
li Template inheritance with extends and blocks
li Partial includes for modular templates
li Mixins for reusable components
li Conditionals (if/else/unless)
li Iteration with each loops
li Variable interpolation
li Pretty-printed output
li LRU caching with TTL
h3 Performance
p Compiled templates run approximately 3x faster than Pug.js, with zero runtime parsing overhead.
.about-sidebar
.info-card
h3 This Demo Shows
ul
li Template inheritance (extends)
li Named blocks
li Conditional rendering
li Variable interpolation
li Simple iteration
.info-card
h3 Links
ul
li
a(href="https://github.com/ankitpatial/pugz") GitHub Repository
li
a(href="/products") View Products
li
a(href="/") Back to Home

View File

@@ -0,0 +1,47 @@
extends layouts/base.pug
block title
title #{title} | Pugz Store
block content
section.page-header
.container
h1 Shopping Cart
p Review your items before checkout
section.section
.container
.cart-layout
.cart-main
.cart-items
each item in cartItems
.cart-item
.cart-item-info
h3 #{name}
p.text-muted #{variant}
span.cart-item-price $#{price}
.cart-item-qty
button.qty-btn -
input.qty-input(type="text" value=quantity)
button.qty-btn +
.cart-item-total $#{total}
button.cart-item-remove x
.cart-actions
a.btn.btn-outline(href="/products") Continue Shopping
.cart-summary
h3 Order Summary
.summary-row
span Subtotal
span $#{subtotal}
.summary-row
span Shipping
span.text-success Free
.summary-row
span Tax
span $#{tax}
.summary-row.summary-total
span Total
span $#{total}
a.btn.btn-primary.btn-block(href="/checkout") Proceed to Checkout

View File

@@ -0,0 +1,144 @@
extends layouts/base.pug
include mixins/forms.pug
include mixins/alerts.pug
include mixins/buttons.pug
block content
h1 Checkout
if errors
+alert("Please correct the errors below", "error")
.checkout-layout
form.checkout-form(action="/checkout" method="POST")
//- Shipping Information
section.checkout-section
h2 Shipping Information
.form-row
+input-required("firstName", "First Name", "text", "John")
+input-required("lastName", "Last Name", "text", "Doe")
+input-required("email", "Email Address", "email", "john@example.com")
+input-required("phone", "Phone Number", "tel", "+1 (555) 123-4567")
+input-required("address", "Street Address", "text", "123 Main St")
+input("address2", "Apartment, suite, etc.", "text", "Apt 4B")
.form-row
+input-required("city", "City", "text", "New York")
.form-group
label(for="state")
| State
span.required *
select.form-control#state(name="state" required)
option(value="") Select State
each state in states
option(value=state.code)= state.name
+input-required("zip", "ZIP Code", "text", "10001")
.form-group
label(for="country")
| Country
span.required *
select.form-control#country(name="country" required)
option(value="US" selected) United States
option(value="CA") Canada
//- Shipping Method
section.checkout-section
h2 Shipping Method
.shipping-options
each method in shippingMethods
label.shipping-option
input(type="radio" name="shipping" value=method.id checked=method.id == "standard")
.shipping-info
span.shipping-name #{method.name}
span.shipping-time #{method.time}
span.shipping-price
if method.price > 0
| $#{method.price}
else
| Free
//- Payment Information
section.checkout-section
h2 Payment Information
.payment-methods-select
label.payment-method
input(type="radio" name="paymentMethod" value="card" checked)
span Credit/Debit Card
label.payment-method
input(type="radio" name="paymentMethod" value="paypal")
span PayPal
.card-details(id="card-details")
+input-required("cardNumber", "Card Number", "text", "1234 5678 9012 3456")
.form-row
+input-required("expiry", "Expiration Date", "text", "MM/YY")
+input-required("cvv", "CVV", "text", "123")
+input-required("cardName", "Name on Card", "text", "John Doe")
.form-group
label.checkbox-label
input(type="checkbox" name="saveCard")
span Save card for future purchases
//- Billing Address
section.checkout-section
.form-group
label.checkbox-label
input(type="checkbox" name="sameAsShipping" checked)
span Billing address same as shipping
.billing-address(id="billing-address" style="display: none")
+input-required("billingAddress", "Street Address", "text", "")
.form-row
+input-required("billingCity", "City", "text", "")
+input-required("billingState", "State", "text", "")
+input-required("billingZip", "ZIP Code", "text", "")
button.btn.btn-primary.btn-lg(type="submit") Place Order
//- Order Summary Sidebar
aside.order-summary
h3 Order Summary
.summary-items
each item in cart.items
.summary-item
img(src=item.image alt=item.name)
.item-info
span.item-name #{item.name}
span.item-qty x#{item.quantity}
span.item-price $#{item.total}
.summary-details
.summary-row
span Subtotal
span $#{cart.subtotal}
if cart.discount
.summary-row.discount
span Discount
span -$#{cart.discount}
.summary-row
span Shipping
span#shipping-cost $#{selectedShipping.price}
.summary-row
span Tax
span $#{cart.tax}
.summary-row.total
span Total
span $#{cart.total}
.secure-checkout
span Secure Checkout
p Your information is protected with 256-bit SSL encryption

View File

@@ -0,0 +1,56 @@
extends layouts/base.pug
block title
title #{title} | Pugz Store
block content
section.hero
.container
h1 Welcome to Pugz Store
p Discover amazing products powered by Zig
.hero-actions
a.btn.btn-primary(href="/products") Shop Now
a.btn.btn-outline(href="/about") Learn More
section.section
.container
h2 Template Features
.feature-grid
.feature-card
h3 Conditionals
if authenticated
p.text-success You are logged in!
else
p.text-muted Please log in to continue.
.feature-card
h3 Variables
p Title: #{title}
p Cart Items: #{cartCount}
.feature-card
h3 Iteration
ul
each item in items
li= item
.feature-card
h3 Clean Syntax
p Pug templates compile to HTML with minimal overhead.
section.section.section-alt
.container
h2 Shop by Category
.category-grid
a.category-card(href="/products?cat=electronics")
.category-icon E
h3 Electronics
span 24 products
a.category-card(href="/products?cat=accessories")
.category-icon A
h3 Accessories
span 18 products
a.category-card(href="/products?cat=home")
.category-icon H
h3 Home Office
span 12 products

View File

@@ -0,0 +1,65 @@
extends layouts/base.pug
block title
title #{productName} | Pugz Store
block content
section.page-header
.container
.breadcrumb
a(href="/") Home
span /
a(href="/products") Products
span /
span #{productName}
section.section
.container
.product-detail
.product-detail-image
.product-image-placeholder
.product-detail-info
span.product-category #{category}
h1 #{productName}
.product-price-large $#{price}
p.product-description #{description}
.product-actions
.quantity-selector
label Quantity:
button.qty-btn -
input.qty-input(type="text" value="1")
button.qty-btn +
a.btn.btn-primary.btn-lg(href="/cart") Add to Cart
.product-meta
p SKU: #{sku}
p Category: #{category}
section.section.section-alt
.container
h2 You May Also Like
.product-grid
.product-card
.product-image
.product-info
span.product-category Electronics
h3.product-name Smart Watch Pro
.product-price $199.99
a.btn.btn-sm(href="/products/2") View Details
.product-card
.product-image
.product-info
span.product-category Accessories
h3.product-name Laptop Stand
.product-price $49.99
a.btn.btn-sm(href="/products/3") View Details
.product-card
.product-image
.product-info
span.product-category Accessories
h3.product-name USB-C Hub
.product-price $39.99
a.btn.btn-sm(href="/products/4") View Details

View File

@@ -0,0 +1,79 @@
extends layouts/base.pug
block title
title #{title} | Pugz Store
block content
section.page-header
.container
h1 All Products
p Browse our selection of quality products
section.section
.container
.products-toolbar
span.results-count #{productCount} products
.sort-options
label Sort by:
select
option(value="featured") Featured
option(value="price-low") Price: Low to High
option(value="price-high") Price: High to Low
.product-grid
.product-card
.product-image
.product-badge Sale
.product-info
span.product-category Electronics
h3.product-name Wireless Headphones
.product-price $79.99
a.btn.btn-sm(href="/products/1") View Details
.product-card
.product-image
.product-info
span.product-category Electronics
h3.product-name Smart Watch Pro
.product-price $199.99
a.btn.btn-sm(href="/products/2") View Details
.product-card
.product-image
.product-info
span.product-category Accessories
h3.product-name Laptop Stand
.product-price $49.99
a.btn.btn-sm(href="/products/3") View Details
.product-card
.product-image
.product-badge Sale
.product-info
span.product-category Accessories
h3.product-name USB-C Hub
.product-price $39.99
a.btn.btn-sm(href="/products/4") View Details
.product-card
.product-image
.product-info
span.product-category Electronics
h3.product-name Mechanical Keyboard
.product-price $129.99
a.btn.btn-sm(href="/products/5") View Details
.product-card
.product-image
.product-info
span.product-category Home Office
h3.product-name Desk Lamp
.product-price $34.99
a.btn.btn-sm(href="/products/6") View Details
.pagination
a.page-link(href="#") Prev
a.page-link.active(href="#") 1
a.page-link(href="#") 2
a.page-link(href="#") 3
a.page-link(href="#") Next

View File

@@ -0,0 +1,4 @@
footer.footer
.container
.footer-content
p Built with Pugz - A Pug template engine for Zig

View File

@@ -0,0 +1,3 @@
meta(charset="UTF-8")
meta(name="viewport" content="width=device-width, initial-scale=1.0")
link(rel="stylesheet" href="/css/style.css")

View File

@@ -0,0 +1,11 @@
header.header
.container
.header-content
a.logo(href="/") Pugz Store
nav.nav
a.nav-link(href="/") Home
a.nav-link(href="/products") Products
a.nav-link(href="/about") About
.header-actions
a.cart-link(href="/cart")
| Cart (#{cartCount})

View File

@@ -1 +0,0 @@
p= petName

View File

@@ -1,9 +0,0 @@
extends layout.pug
block content
.sidebar
block sidebar
p nothing
.primary
block primary
p nothing

View File

@@ -1,11 +0,0 @@
doctype html
html
head
title Users
body
h1 User List
ul.user-list
each user in users
li.user
strong= user.name
span.email= user.email