Files
pugz/examples/demo/src/main.zig
Ankit Patial e2025d7de8 - demo build fix.
- README changes for bench values.
2026-01-28 19:38:59 +05:30

518 lines
15 KiB
Zig

//! Pugz Store Demo - A comprehensive e-commerce demo showcasing Pugz capabilities
//!
//! Features demonstrated:
//! - Template inheritance (extends/block)
//! - Partial includes (header, footer)
//! - Mixins with parameters (product-card, rating, forms)
//! - Conditionals and loops
//! - Data binding
//! - Pretty printing
const std = @import("std");
const httpz = @import("httpz");
const pugz = @import("pugz");
const Allocator = std.mem.Allocator;
// Mode selection: set to true to use compiled templates
// Run `zig build compile-templates` to generate templates first
const USE_COMPILED_TEMPLATES = true;
// ============================================================================
// 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 = "2",
.name = "Laptop",
.price = "500.00",
.image = "/images/keyboard.jpg",
.variant = "BLK",
.quantity = "1",
.total = "500.00",
},
.{
.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 {
return .{
.allocator = allocator,
.view = pugz.ViewEngine.init(.{
.views_dir = "views",
.pretty = true,
}),
};
}
pub fn deinit(self: *App) void {
self.view.deinit();
}
};
// ============================================================================
// Request Handlers
// ============================================================================
fn home(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = if (USE_COMPILED_TEMPLATES) blk: {
const templates = @import("templates");
break :blk try templates.pages_home.render(res.arena, .{
.title = "Home",
.cartCount = "2",
.authenticated = "true",
});
} else app.view.render(res.arena, "pages/home", .{
.title = "Home",
.cartCount = "2",
.authenticated = true,
.items = sample_products,
}) 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 = if (USE_COMPILED_TEMPLATES) blk: {
const templates = @import("templates");
break :blk try templates.pages_products.render(res.arena, .{
.title = "All Products",
.cartCount = "2",
.productCount = "6",
});
} else 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 = if (USE_COMPILED_TEMPLATES) blk: {
const templates = @import("templates");
break :blk try templates.pages_product_detail.render(res.arena, .{
.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",
});
} else 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 = if (USE_COMPILED_TEMPLATES) blk: {
const templates = @import("templates");
break :blk try templates.pages_cart.render(res.arena, .{
.cartCount = "2",
.subtotal = sample_cart.subtotal,
.tax = sample_cart.tax,
.total = sample_cart.total,
});
} else 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 = if (USE_COMPILED_TEMPLATES) blk: {
const templates = @import("templates");
break :blk try templates.pages_about.render(res.arena, .{
.title = "About",
.cartCount = "2",
});
} else 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 includeDemo(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
const html = if (USE_COMPILED_TEMPLATES) blk: {
const templates = @import("templates");
break :blk try templates.pages_include_demo.render(res.arena, .{
.cartCount = "2",
});
} else app.view.render(res.arena, "pages/include-demo", .{
.title = "Include Demo",
.cartCount = "2",
}) catch |err| {
return renderError(res, err);
};
res.content_type = .HTML;
res.body = html;
}
fn simpleCompiled(app: *App, _: *httpz.Request, res: *httpz.Response) !void {
if (USE_COMPILED_TEMPLATES) {
const templates = @import("templates");
const html = try templates.pages_simple.render(res.arena, .{
.title = "Compiled Template Demo",
.heading = "Hello from Compiled Templates!",
.siteName = "Pugz Demo",
});
res.content_type = .HTML;
res.body = html;
} else {
const html = app.view.render(res.arena, "pages/simple", .{
.title = "Simple Page",
.heading = "Hello from Runtime Templates!",
.siteName = "Pugz Demo",
}) 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 = if (USE_COMPILED_TEMPLATES) blk: {
const templates = @import("templates");
break :blk try templates.pages_404.render(res.arena, .{
.title = "Page Not Found",
.cartCount = "2",
});
} else 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 = try App.init(allocator);
defer app.deinit();
const port = 8081;
var server = try httpz.Server(*App).init(allocator, .{ .port = port }, &app);
defer server.deinit();
var router = try server.router(.{});
// Pages
router.get("/", home, .{});
router.get("/products", products, .{});
router.get("/products/:id", productDetail, .{});
router.get("/cart", cart, .{});
router.get("/about", about, .{});
router.get("/include-demo", includeDemo, .{});
router.get("/simple", simpleCompiled, .{});
// Static files
router.get("/css/*", serveStatic, .{});
std.debug.print(
\\
\\ ____ ____ _
\\ | _ \ _ _ __ _ ____ / ___|| |_ ___ _ __ ___
\\ | |_) | | | |/ _` |_ / \___ \| __/ _ \| '__/ _ \
\\ | __/| |_| | (_| |/ / ___) | || (_) | | | __/
\\ |_| \__,_|\__, /___| |____/ \__\___/|_| \___|
\\ |___/
\\
\\ Server running at http://localhost:{d}
\\
\\ Routes:
\\ GET / - Home page
\\ GET /products - Products page
\\ GET /products/:id - Product detail
\\ GET /cart - Shopping cart
\\ GET /about - About page
\\ GET /include-demo - Include directive demo
\\ GET /simple - Simple compiled template demo
\\
\\ Press Ctrl+C to stop.
\\
, .{port});
try server.listen();
}