This commit is contained in:
mitteneer 2024-04-02 11:48:20 -04:00
commit c018ea3c0e
14 changed files with 381 additions and 0 deletions

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
zig-out/
zig-cache/
*.core
static/
.jetzig

19
LICENSE Normal file
View file

@ -0,0 +1,19 @@
Copyright (c) 2024 miteneer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

13
README.md Normal file
View file

@ -0,0 +1,13 @@
# Zuletzt
**Zuletzt** gives you the statistics of your music listening habits.
Inspired by Last.fm, Maloja, and Lastfmstats.com.
**Z**uletzt is written in **Z**ig as a means of learning the
language, reintroducing myself to programming, and combining
the functionality of the aforementioned inspirations.
Zuletzt means "last" in German.
Licensed under MIT.

52
build.zig Normal file
View file

@ -0,0 +1,52 @@
const std = @import("std");
const jetzig = @import("jetzig");
pub fn build(b: *std.Build) !void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "zuletzt",
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
// Example dependency:
const iguanas_dep = b.dependency("iguanas", .{ .optimize = optimize, .target = target });
exe.root_module.addImport("iguanas", iguanas_dep.module("iguanas"));
// All dependencies **must** be added to imports above this line.
try jetzig.jetzigInit(b, exe, .{});
b.installArtifact(exe);
const run_cmd = b.addRunArtifact(exe);
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| run_cmd.addArgs(args);
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const lib_unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests);
const exe_unit_tests = b.addTest(.{
.root_source_file = .{ .path = "src/main.zig" },
.target = target,
.optimize = optimize,
});
const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_lib_unit_tests.step);
test_step.dependOn(&run_exe_unit_tests.step);
}

40
build.zig.zon Normal file
View file

@ -0,0 +1,40 @@
.{
.name = "zuletzt",
// This is a [Semantic Version](https://semver.org/).
// In a future version of Zig it will be used for package deduplication.
.version = "0.0.0",
// This field is optional.
// This is currently advisory only; Zig does not yet do anything
// with this value.
//.minimum_zig_version = "0.11.0",
// This field is optional.
// Each dependency must either provide a `url` and `hash`, or a `path`.
// `zig build --fetch` can be used to fetch all dependencies of a package, recursively.
// Once all dependencies are fetched, `zig build` no longer requires
// internet connectivity.
.dependencies = .{
.jetzig = .{
.url = "https://github.com/jetzig-framework/jetzig/archive/800b72eeb9b146d4ee4901e164fa691dc5332fb5.tar.gz",
.hash = "122031827c1329f8dbd9135e7463c27cc09e9fcbbebde6f2a8411ee122984afe0a4c",
},
.iguanas = .{
.url = "https://github.com/jetzig-framework/iguanas/archive/89c2abf29de0bc31054a9a6feac5a6a83bab0459.tar.gz",
.hash = "12202fd319a5ab4e124b00e8ddea474d07c19c4e005d77b6c29fc44860904ea01a5c",
},
},
.paths = .{
// This makes *all* files, recursively, included in this package. It is generally
// better to explicitly list the files and directories instead, to insure that
// fetching from tarballs, file system paths, and version control all result
// in the same contents hash.
"",
// For example...
//"build.zig",
//"build.zig.zon",
//"src",
//"LICENSE",
//"README.md",
},
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
public/jetzig.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

10
public/styles.css Normal file
View file

@ -0,0 +1,10 @@
/* Root stylesheet. Load into your Zmpl template with:
*
* <link rel="stylesheet" href="/styles.css" />
*
*/
.message {
font-weight: bold;
font-size: 3rem;
}

BIN
public/zmpl.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View file

@ -0,0 +1,65 @@
/// Demo middleware. Assign middleware by declaring `pub const middleware` in the
/// `jetzig_options` defined in your application's `src/main.zig`.
///
/// Middleware is called before and after the request, providing full access to the active
/// request, allowing you to execute any custom code for logging, tracking, inserting response
/// headers, etc.
///
/// This middleware is configured in the demo app's `src/main.zig`:
///
/// ```
/// pub const jetzig_options = struct {
/// pub const middleware: []const type = &.{@import("app/middleware/DemoMiddleware.zig")};
/// };
/// ```
const std = @import("std");
const jetzig = @import("jetzig");
/// Define any custom data fields you want to store here. Assigning to these fields in the `init`
/// function allows you to access them in various middleware callbacks defined below, where they
/// can also be modified.
my_custom_value: []const u8,
const Self = @This();
/// Initialize middleware.
pub fn init(request: *jetzig.http.Request) !*Self {
var middleware = try request.allocator.create(Self);
middleware.my_custom_value = "initial value";
return middleware;
}
/// Invoked immediately after the request is received but before it has started processing.
/// Any calls to `request.render` or `request.redirect` will prevent further processing of the
/// request, including any other middleware in the chain.
pub fn afterRequest(self: *Self, request: *jetzig.http.Request) !void {
try request.server.logger.DEBUG(
"[DemoMiddleware:afterRequest] my_custom_value: {s}",
.{self.my_custom_value},
);
self.my_custom_value = @tagName(request.method);
}
/// Invoked immediately before the response renders to the client.
/// The response can be modified here if needed.
pub fn beforeResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
try request.server.logger.DEBUG(
"[DemoMiddleware:beforeResponse] my_custom_value: {s}, response status: {s}",
.{ self.my_custom_value, @tagName(response.status_code) },
);
}
/// Invoked immediately after the response has been finalized and sent to the client.
/// Response data can be accessed for logging, but any modifications will have no impact.
pub fn afterResponse(self: *Self, request: *jetzig.http.Request, response: *jetzig.http.Response) !void {
_ = self;
_ = response;
try request.server.logger.DEBUG("[DemoMiddleware:afterResponse] response completed", .{});
}
/// Invoked after `afterResponse` is called. Use this function to do any clean-up.
/// Note that `request.allocator` is an arena allocator, so any allocations are automatically
/// freed before the next request starts processing.
pub fn deinit(self: *Self, request: *jetzig.http.Request) void {
request.allocator.destroy(self);
}

43
src/app/views/root.zig Normal file
View file

@ -0,0 +1,43 @@
const jetzig = @import("jetzig");
/// `src/app/views/root.zig` represents the root URL `/`
/// The `index` view function is invoked when when the HTTP verb is `GET`.
/// Other view types are invoked either by passing a resource ID value (e.g. `/1234`) or by using
/// a different HTTP verb:
///
/// GET / => index(request, data)
/// GET /1234 => get(id, request, data)
/// POST / => post(request, data)
/// PUT /1234 => put(id, request, data)
/// PATCH /1234 => patch(id, request, data)
/// DELETE /1234 => delete(id, request, data)
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
// The first call to `data.object()` or `data.array()` sets the root response data value.
// JSON requests return a JSON string representation of the root data value.
// Zmpl templates can access all values in the root data value.
var root = try data.object();
// Add a string to the root object.
try root.put("message", data.string("Welcome to Jetzig!"));
// Request params have the same type as a `data.object()` so they can be inserted them
// directly into the response data. Fetch `http://localhost:8080/?message=hello` to set the
// param. JSON data is also accepted when the `content-type: application/json` header is
// present.
const params = try request.params();
if (params.get("message")) |value| {
try root.put("message_param", value);
}
// Set arbitrary response headers as required. `content-type` is automatically assigned for
// HTML, JSON responses.
//
// Static files located in `public/` in the root of your project directory are accessible
// from the root path (e.g. `public/jetzig.png`) is available at `/jetzig.png` and the
// content type is inferred from the extension using MIME types.
try request.response.headers.append("x-example-header", "example header value");
// Render the response and set the response code.
return request.render(.ok);
}

View file

@ -0,0 +1,18 @@
// Renders the `message` response data value.
<h3 class="message text-[#39b54a]">{.message}</h3>
<div><img class="p-3 mx-auto" src="/jetzig.png" /></div>
<div>
<a href="https://github.com/jetzig-framework/zmpl">
<img class="p-3 m-3 mx-auto" src="/zmpl.png" />
</a>
</div>
<div>Visit <a class="font-bold text-[#39b54a]" href="https://jetzig.dev/">jetzig.dev</a> to get started.
<div>Join our Discord server and introduce yourself:</div>
<div>
<a class="font-bold text-[#39b54a]" href="https://discord.gg/eufqssz7X6">https://discord.gg/eufqssz7X6</a>
</div>
</div>

View file

@ -0,0 +1,20 @@
<html>
<head>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="/styles.css" />
</head>
<body>
<div class="text-center pt-10 m-auto">
// If present, renders the `message_param` response data value, add `?message=hello` to the
// URL to see the output:
<h2 class="param text-3xl text-[#f7931e]">{.message_param}</h2>
// Renders `src/app/views/root/_content.zmpl` with the same template data available:
<div>{^root/content}</div>
</div>
</body>
</html>

96
src/main.zig Normal file
View file

@ -0,0 +1,96 @@
const std = @import("std");
pub const jetzig = @import("jetzig");
pub const routes = @import("routes").routes;
// Override default settings in `jetzig.config` here:
pub const jetzig_options = struct {
/// Middleware chain. Add any custom middleware here, or use middleware provided in
/// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`).
pub const middleware: []const type = &.{
// htmx middleware skips layouts when `HX-Target` header is present and issues
// `HX-Redirect` instead of a regular HTTP redirect when `request.redirect` is called.
jetzig.middleware.HtmxMiddleware,
// Demo middleware included with new projects. Remove once you are familiar with Jetzig's
// middleware system.
@import("app/middleware/DemoMiddleware.zig"),
};
// Maximum bytes to allow in request body.
// pub const max_bytes_request_body: usize = std.math.pow(usize, 2, 16);
// Maximum filesize for `public/` content.
// pub const max_bytes_public_content: usize = std.math.pow(usize, 2, 20);
// Maximum filesize for `static/` content (applies only to apps using `jetzig.http.StaticRequest`).
// pub const max_bytes_static_content: usize = std.math.pow(usize, 2, 18);
// Path relative to cwd() to serve public content from. Symlinks are not followed.
// pub const public_content_path = "public";
// HTTP buffer. Must be large enough to store all headers. This should typically not be modified.
// pub const http_buffer_size: usize = std.math.pow(usize, 2, 16);
// Set custom fragments for rendering markdown templates. Any values will fall back to
// defaults provided by Zmd (https://github.com/bobf/zmd/blob/main/src/zmd/html.zig).
pub const markdown_fragments = struct {
pub const root = .{
"<div class='p-5'>",
"</div>",
};
pub const h1 = .{
"<h1 class='text-2xl mb-3 font-bold'>",
"</h1>",
};
pub const h2 = .{
"<h2 class='text-xl mb-3 font-bold'>",
"</h2>",
};
pub const h3 = .{
"<h3 class='text-lg mb-3 font-bold'>",
"</h3>",
};
pub const paragraph = .{
"<p class='p-3'>",
"</p>",
};
pub const code = .{
"<span class='font-mono bg-gray-900 p-2 text-white'>",
"</span>",
};
pub const unordered_list = .{
"<ul class='list-disc ms-8 leading-8'>",
"</ul>",
};
pub const ordered_list = .{
"<ul class='list-decimal ms-8 leading-8'>",
"</ul>",
};
pub fn block(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 {
return try std.fmt.allocPrint(allocator,
\\<pre class="w-1/2 font-mono mt-4 ms-3 bg-gray-900 p-2 text-white"><code class="language-{?s}">{s}</code></pre>
, .{ node.meta, node.content });
}
pub fn link(allocator: std.mem.Allocator, node: jetzig.zmd.Node) ![]const u8 {
return try std.fmt.allocPrint(allocator,
\\<a class="underline decoration-sky-500" href="{0s}" title={1s}>{1s}</a>
, .{ node.href.?, node.title.? });
}
};
};
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer std.debug.assert(gpa.deinit() == .ok);
const allocator = gpa.allocator();
const app = try jetzig.init(allocator);
defer app.deinit();
try app.start(comptime jetzig.route(routes));
}