//! 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, \\ \\ \\Error \\ \\

500 - Server Error

\\

Error: {s}

\\ \\ , .{@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(); }