Init
This commit is contained in:
commit
c018ea3c0e
14 changed files with 381 additions and 0 deletions
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
zig-out/
|
||||||
|
zig-cache/
|
||||||
|
*.core
|
||||||
|
static/
|
||||||
|
.jetzig
|
||||||
19
LICENSE
Normal file
19
LICENSE
Normal 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
13
README.md
Normal 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
52
build.zig
Normal 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
40
build.zig.zon
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
public/jetzig.png
Normal file
BIN
public/jetzig.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
10
public/styles.css
Normal file
10
public/styles.css
Normal 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
BIN
public/zmpl.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
65
src/app/middleware/DemoMiddleware.zig
Normal file
65
src/app/middleware/DemoMiddleware.zig
Normal 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
43
src/app/views/root.zig
Normal 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);
|
||||||
|
}
|
||||||
18
src/app/views/root/_content.zmpl
Normal file
18
src/app/views/root/_content.zmpl
Normal 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>
|
||||||
20
src/app/views/root/index.zmpl
Normal file
20
src/app/views/root/index.zmpl
Normal 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
96
src/main.zig
Normal 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));
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue