This commit is contained in:
Alan Daniels 2025-11-02 12:28:18 +11:00
commit 50117ff78d
22 changed files with 4532 additions and 0 deletions

504
src/App.zig Normal file
View file

@ -0,0 +1,504 @@
//! zap.App takes the zap.Endpoint concept one step further: instead of having
//! only per-endpoint instance data (fields of your Endpoint struct), endpoints
//! in a zap.App easily share a global 'App Context'.
//!
//! In addition to the global App Context, all Endpoint request handlers also
//! receive an arena allocator for easy, care-free allocations. There is one
//! arena allocator per thread, and arenas are reset after each request.
//!
//! Just like regular / legacy zap.Endpoints, returning errors from request
//! handlers is OK. It's decided on a per-endpoint basis how errors are dealt
//! with, via the ErrorStrategy enum field.
//!
//! See `App.Create()`.
const std = @import("std");
const Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const Thread = std.Thread;
const RwLock = Thread.RwLock;
const zap = @import("zap");
const Request = zap.Request;
const HttpListener = zap.HttpListener;
const ErrorStrategy = zap.Endpoint.ErrorStrategy;
pub const AppOpts = struct {
/// ErrorStrategy for (optional) request handler if no endpoint matches
default_error_strategy: ErrorStrategy = .log_to_console,
arena_retain_capacity: usize = 16 * 1024 * 1024,
};
/// creates an App with custom app context
///
/// About App Contexts:
///
/// ```zig
/// const MyContext = struct {
/// // You may (optionally) define the following global handlers:
/// pub fn unhandledRequest(_: *MyContext, _: Allocator, _: Request) anyerror!void {}
/// pub fn unhandledError(_: *MyContext, _: Request, _: anyerror) void {}
/// };
/// ```
pub fn Create(
/// Your user-defined "Global App Context" type
comptime Context: type,
) type {
return struct {
const App = @This();
// we make the following fields static so we can access them from a
// context-free, pure zap request handler
const InstanceData = struct {
context: *Context = undefined,
gpa: Allocator = undefined,
opts: AppOpts = undefined,
endpoints: std.ArrayListUnmanaged(*Endpoint.Interface) = .empty,
there_can_be_only_one: bool = false,
track_arenas: std.AutoHashMapUnmanaged(Thread.Id, ArenaAllocator) = .empty,
track_arena_lock: RwLock = .{},
/// the internal http listener
listener: HttpListener = undefined,
/// function pointer to handler for otherwise unhandled requests.
/// Will automatically be set if your Context provides an
/// `unhandledRequest` function of type `fn(*Context, Allocator,
/// Request) !void`.
unhandled_request: ?*const fn (*Context, Allocator, Request) anyerror!void = null,
/// function pointer to handler for unhandled errors.
/// Errors are unhandled if they are not logged but raised by the
/// ErrorStrategy. Will automatically be set if your Context
/// provides an `unhandledError` function of type `fn(*Context,
/// Allocator, Request, anyerror) void`.
unhandled_error: ?*const fn (*Context, Request, anyerror) void = null,
};
var _static: InstanceData = .{};
pub const Endpoint = struct {
pub const Interface = struct {
call: *const fn (*Interface, Request) anyerror!void = undefined,
path: []const u8,
destroy: *const fn (*Interface, Allocator) void = undefined,
};
pub fn Bind(ArbitraryEndpoint: type) type {
return struct {
endpoint: *ArbitraryEndpoint,
interface: Interface,
// tbh: unnecessary, since we have it in _static
app_context: *Context,
const Bound = @This();
pub fn unwrap(interface: *Interface) *Bound {
const self: *Bound = @alignCast(@fieldParentPtr("interface", interface));
return self;
}
pub fn destroy(interface: *Interface, allocator: Allocator) void {
const self: *Bound = @alignCast(@fieldParentPtr("interface", interface));
allocator.destroy(self);
}
pub fn onRequestInterface(interface: *Interface, r: Request) !void {
var self: *Bound = Bound.unwrap(interface);
var arena = try get_arena();
try self.onRequest(arena.allocator(), self.app_context, r);
_ = arena.reset(.{ .retain_with_limit = _static.opts.arena_retain_capacity });
}
pub fn onRequest(self: *Bound, arena: Allocator, app_context: *Context, r: Request) !void {
// TODO: simplitfy this with @tagName?
const ret = switch (r.methodAsEnum()) {
.GET => callHandlerIfExist("get", self.endpoint, arena, app_context, r),
.POST => callHandlerIfExist("post", self.endpoint, arena, app_context, r),
.PUT => callHandlerIfExist("put", self.endpoint, arena, app_context, r),
.DELETE => callHandlerIfExist("delete", self.endpoint, arena, app_context, r),
.PATCH => callHandlerIfExist("patch", self.endpoint, arena, app_context, r),
.OPTIONS => callHandlerIfExist("options", self.endpoint, arena, app_context, r),
.HEAD => callHandlerIfExist("head", self.endpoint, arena, app_context, r),
else => error.UnsupportedHtmlRequestMethod,
};
if (ret) {
// handled without error
} else |err| {
switch (self.endpoint.*.error_strategy) {
.raise => return err,
.log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505),
.log_to_console => zap.log.err(
"Error in {} {s} : {}",
.{ Bound, r.method orelse "(no method)", err },
),
}
}
}
};
}
pub fn init(ArbitraryEndpoint: type, endpoint: *ArbitraryEndpoint) Endpoint.Bind(ArbitraryEndpoint) {
checkEndpointType(ArbitraryEndpoint);
const BoundEp = Endpoint.Bind(ArbitraryEndpoint);
return .{
.endpoint = endpoint,
.interface = .{
.path = endpoint.path,
.call = BoundEp.onRequestInterface,
.destroy = BoundEp.destroy,
},
.app_context = _static.context,
};
}
pub fn checkEndpointType(T: type) void {
if (@hasField(T, "path")) {
if (@FieldType(T, "path") != []const u8) {
@compileError(@typeName(@FieldType(T, "path")) ++ " has wrong type, expected: []const u8");
}
} else {
@compileError(@typeName(T) ++ " has no path field");
}
if (@hasField(T, "error_strategy")) {
if (@FieldType(T, "error_strategy") != ErrorStrategy) {
@compileError(@typeName(@FieldType(T, "error_strategy")) ++ " has wrong type, expected: zap.Endpoint.ErrorStrategy");
}
} else {
@compileError(@typeName(T) ++ " has no error_strategy field");
}
const methods_to_check = [_][]const u8{
"get",
"post",
"put",
"delete",
"patch",
"options",
"head",
};
const params_to_check = [_]type{
*T,
Allocator,
*Context,
Request,
};
inline for (methods_to_check) |method| {
if (@hasDecl(T, method)) {
const Method = @TypeOf(@field(T, method));
const method_info = @typeInfo(Method);
if (method_info != .@"fn") {
@compileError("Expected `" ++ @typeName(T) ++ "." ++ method ++ "` to be a request handler method, got: " ++ @typeName(Method));
}
// now check parameters
const params = method_info.@"fn".params;
if (params.len != params_to_check.len) {
@compileError(std.fmt.comptimePrint(
"Expected method `{s}.{s}` to have {d} parameters, got {d}",
.{
@typeName(T),
method,
params_to_check.len,
params.len,
},
));
}
inline for (params_to_check, 0..) |param_type_expected, i| {
if (params[i].type.? != param_type_expected) {
@compileError(std.fmt.comptimePrint(
"Expected parameter {d} of method {s}.{s} to be {s}, got {s}",
.{
i + 1,
@typeName(T),
method,
@typeName(param_type_expected),
@typeName(params[i].type.?),
},
));
}
}
const ret_type = method_info.@"fn".return_type.?;
const ret_info = @typeInfo(ret_type);
if (ret_info != .error_union) {
@compileError("Expected return type of method `" ++ @typeName(T) ++ "." ++ method ++ "` to be !void, got: " ++ @typeName(ret_type));
}
if (ret_info.error_union.payload != void) {
@compileError("Expected return type of method `" ++ @typeName(T) ++ "." ++ method ++ "` to be !void, got: !" ++ @typeName(ret_info.error_union.payload));
}
}
}
}
/// Wrap an endpoint with an Authenticator
pub fn Authenticating(EndpointType: type, Authenticator: type) type {
return struct {
authenticator: *Authenticator,
ep: *EndpointType,
path: []const u8,
error_strategy: ErrorStrategy,
const AuthenticatingEndpoint = @This();
/// Init the authenticating endpoint. Pass in a pointer to the endpoint
/// you want to wrap, and the Authenticator that takes care of authenticating
/// requests.
pub fn init(e: *EndpointType, authenticator: *Authenticator) AuthenticatingEndpoint {
return .{
.authenticator = authenticator,
.ep = e,
.path = e.path,
.error_strategy = e.error_strategy,
};
}
/// Authenticates GET requests using the Authenticator.
pub fn get(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("get", self.ep, arena, context, request),
.Handled => {},
};
}
/// Authenticates POST requests using the Authenticator.
pub fn post(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("post", self.ep, arena, context, request),
.Handled => {},
};
}
/// Authenticates PUT requests using the Authenticator.
pub fn put(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("put", self.ep, arena, context, request),
.Handled => {},
};
}
/// Authenticates DELETE requests using the Authenticator.
pub fn delete(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("delete", self.ep, arena, context, request),
.Handled => {},
};
}
/// Authenticates PATCH requests using the Authenticator.
pub fn patch(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("patch", self.ep, arena, context, request),
.Handled => {},
};
}
/// Authenticates OPTIONS requests using the Authenticator.
pub fn options(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("options", self.ep, arena, context, request),
.Handled => {},
};
}
/// Authenticates HEAD requests using the Authenticator.
pub fn head(self: *AuthenticatingEndpoint, arena: Allocator, context: *Context, request: zap.Request) anyerror!void {
try switch (self.authenticator.authenticateRequest(&request)) {
.AuthFailed => callHandlerIfExist("unauthorized", self.ep, arena, context, request),
.AuthOK => callHandlerIfExist("head", self.ep, arena, context, request),
.Handled => {},
};
}
};
}
};
pub const ListenerSettings = struct {
/// IP interface, e.g. 0.0.0.0
interface: [*c]const u8 = null,
/// IP port to listen on
port: usize,
public_folder: ?[]const u8 = null,
max_clients: ?isize = null,
max_body_size: ?usize = null,
timeout: ?u8 = null,
tls: ?zap.Tls = null,
};
pub fn init(gpa_: Allocator, context_: *Context, opts_: AppOpts) !void {
if (_static.there_can_be_only_one) {
return error.OnlyOneAppAllowed;
}
_static.context = context_;
_static.gpa = gpa_;
_static.opts = opts_;
_static.there_can_be_only_one = true;
// set unhandled_request callback if provided by Context
if (@hasDecl(Context, "unhandledRequest")) {
// try if we can use it
const Unhandled = @TypeOf(@field(Context, "unhandledRequest"));
const Expected = fn (_: *Context, _: Allocator, _: Request) anyerror!void;
if (Unhandled != Expected) {
@compileError("`unhandledRequest` method of " ++ @typeName(Context) ++ " has wrong type:\n" ++ @typeName(Unhandled) ++ "\nexpected:\n" ++ @typeName(Expected));
}
_static.unhandled_request = Context.unhandledRequest;
}
if (@hasDecl(Context, "unhandledError")) {
// try if we can use it
const Unhandled = @TypeOf(@field(Context, "unhandledError"));
const Expected = fn (_: *Context, _: Request, _: anyerror) void;
if (Unhandled != Expected) {
@compileError("`unhandledError` method of " ++ @typeName(Context) ++ " has wrong type:\n" ++ @typeName(Unhandled) ++ "\nexpected:\n" ++ @typeName(Expected));
}
_static.unhandled_error = Context.unhandledError;
}
}
pub fn deinit() void {
// we created endpoint wrappers but only tracked their interfaces
// hence, we need to destroy the wrappers through their interfaces
if (false) {
var it = _static.endpoints.iterator();
while (it.next()) |kv| {
const interface = kv.value_ptr;
interface.*.destroy(_static.gpa);
}
} else {
for (_static.endpoints.items) |interface| {
interface.destroy(interface, _static.gpa);
}
}
_static.endpoints.deinit(_static.gpa);
_static.track_arena_lock.lock();
defer _static.track_arena_lock.unlock();
var it = _static.track_arenas.valueIterator();
while (it.next()) |arena| {
arena.deinit();
}
_static.track_arenas.deinit(_static.gpa);
}
// This can be resolved at comptime so *perhaps it does affect optimiazation
pub fn callHandlerIfExist(comptime fn_name: []const u8, e: anytype, arena: Allocator, ctx: *Context, r: Request) anyerror!void {
const EndPoint = @TypeOf(e.*);
if (@hasDecl(EndPoint, fn_name)) {
return @field(EndPoint, fn_name)(e, arena, ctx, r);
}
zap.log.debug(
"Unhandled `{s}` {s} request ({s} not implemented in {s})",
.{ r.method orelse "<unknown>", r.path orelse "", fn_name, @typeName(Endpoint) },
);
r.setStatus(.method_not_allowed);
try r.sendBody("405 - method not allowed\r\n");
return;
}
pub fn get_arena() !*ArenaAllocator {
const thread_id = std.Thread.getCurrentId();
_static.track_arena_lock.lockShared();
if (_static.track_arenas.getPtr(thread_id)) |arena| {
_static.track_arena_lock.unlockShared();
return arena;
} else {
_static.track_arena_lock.unlockShared();
_static.track_arena_lock.lock();
defer _static.track_arena_lock.unlock();
const arena = ArenaAllocator.init(_static.gpa);
try _static.track_arenas.put(_static.gpa, thread_id, arena);
return _static.track_arenas.getPtr(thread_id).?;
}
}
pub fn register(endpoint: anytype) !void {
for (_static.endpoints.items) |other| {
if (std.mem.eql(
u8,
other.path,
endpoint.path,
)) {
return zap.Endpoint.ListenerError.EndpointPathShadowError;
}
}
const EndpointType = @typeInfo(@TypeOf(endpoint)).pointer.child;
Endpoint.checkEndpointType(EndpointType);
const bound = try _static.gpa.create(Endpoint.Bind(EndpointType));
bound.* = Endpoint.init(EndpointType, endpoint);
try _static.endpoints.append(_static.gpa, &bound.interface);
std.mem.sort(*Endpoint.Interface, _static.endpoints.items, {}, lessThanFnEndpointInterface);
}
fn lessThanFnEndpointInterface(_: void, lh: *Endpoint.Interface, rh: *Endpoint.Interface) bool {
return lh.path.len > rh.path.len;
}
pub fn listen(l: ListenerSettings) !void {
_static.listener = HttpListener.init(.{
.interface = l.interface,
.port = l.port,
.public_folder = l.public_folder,
.max_clients = l.max_clients,
.max_body_size = l.max_body_size,
.timeout = l.timeout,
.tls = l.tls,
.on_request = onRequest,
});
try _static.listener.listen();
}
fn sanitizePath(path: []const u8) []const u8 {
if (path.len > 1 and path[path.len - 1] == '/') {
return path[0..(path.len - 1)];
}
return path;
}
fn onRequest(r: Request) !void {
if (r.path) |unsan_p| {
const p = sanitizePath(unsan_p);
for (_static.endpoints.items) |interface| {
if (std.mem.startsWith(u8, p, interface.path) and std.mem.eql(u8, p, interface.path)) {
return interface.call(interface, r) catch |err| {
// if error is not dealt with in the interface, e.g.
// if error strategy is .raise:
if (_static.unhandled_error) |error_cb| {
error_cb(_static.context, r, err);
} else {
zap.log.err(
"App.Endpoint onRequest error {} in endpoint interface {}\n",
.{ err, interface },
);
}
};
}
}
}
// this is basically the "not found" handler
if (_static.unhandled_request) |user_handler| {
var arena = try get_arena();
user_handler(_static.context, arena.allocator(), r) catch |err| {
switch (_static.opts.default_error_strategy) {
.raise => if (_static.unhandled_error) |error_cb| {
error_cb(_static.context, r, err);
} else {
zap.Logging.on_uncaught_error("App on_request", err);
},
.log_to_response => return r.sendError(err, if (@errorReturnTrace()) |t| t.* else null, 505),
.log_to_console => zap.log.err("Error in {} {s} : {}", .{ App, r.method orelse "(no method)", err }),
}
};
}
}
};
}

83
src/context.zig Normal file
View file

@ -0,0 +1,83 @@
const zap = @import("zap");
const sqlite = @import("sqlite");
const std = @import("std");
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const optimize_mode = builtin.mode;
db_connection: sqlite.Db,
counter: u32 = 0,
pub fn init(connection: sqlite.Db) @This() {
return .{
.db_connection = connection,
};
}
pub fn SendInertiaResponse(endpoint: anytype, r: zap.Request, arena: Allocator, component: []const u8, props: anytype) !void {
const inertia_json = try std.fmt.allocPrint(
arena,
"{f}",
.{std.json.fmt(.{
.component = component,
.props = props,
.url = endpoint.path,
.version = "",
}, .{
.escape_unicode = true,
})},
);
if (r.getHeader("x-inertia")) |_| {
try r.setHeader("X-Inertia", "true");
try r.setHeader("Content-Type", "application/json");
r.setStatus(.ok);
try r.sendBody(inertia_json);
} else {
try r.setHeader("Content-Type", "text/html");
const show_unbuilt_assets = (optimize_mode == .Debug) and (r.getHeader("x-vite") != null);
const response_text = try std.fmt.allocPrint(
arena,
@embedFile("root.html"),
.{
(if (show_unbuilt_assets)
"/src/main.ts"
else
"/build/main.js"),
(if (show_unbuilt_assets)
"/src/style.css"
else
"/build/style.css"),
HtmlEncodeFormatter{ .input = inertia_json },
},
);
r.setStatus(.ok);
try r.sendBody(response_text);
}
}
pub const HtmlEncodeFormatter = struct {
input: []const u8,
pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void {
for (self.input) |char_byte| {
switch (char_byte) {
'<' => _ = try writer.write("&lt;"),
'>' => _ = try writer.write("&gt;"),
'&' => _ = try writer.write("&amp;"),
'"' => _ = try writer.write("&quot;"),
'\'' => _ = try writer.write("&apos;"),
else => _ = try writer.writeByte(char_byte),
}
}
}
};
pub fn unhandledRequest(ctx: *@This(), arena: Allocator, r: zap.Request) anyerror!void {
_ = ctx;
try SendInertiaResponse(.{ .path = r.path.? }, r, arena, "404", .{});
}

19
src/layout/Layout.vue Normal file
View file

@ -0,0 +1,19 @@
<script setup>
import { Link } from "@inertiajs/vue3";
</script>
<template>
<header class="p-3 bg-rose-800 dark:bg-rose-950">
<div class="max-w-5xl flex gap-2 mx-auto">
<Link href="/" class="text-white font-extrabold">
<img src="../../favicon.svg" class="h-8 inline" />
<span class="px-1 relative top-0.5">OwnMedia</span>
</Link>
</div>
</header>
<div class="p-4">
<div class="p-4 bg-rose-950 rounded">
<slot />
</div>
</div>
</template>

15
src/main.ts Normal file
View file

@ -0,0 +1,15 @@
import { createApp, h } from 'vue'
import { createInertiaApp } from '@inertiajs/vue3'
createInertiaApp({
resolve: name => {
const modules = import.meta.glob('./modules/**/*.vue', { eager: false })
return modules[`./modules/${name}.vue`]();
},
setup({ el, App, props, plugin }) {
createApp({ render: () => h(App, props) })
.use(plugin)
.mount(el)
},
id: "app"
})

121
src/main.zig Normal file
View file

@ -0,0 +1,121 @@
//!
//! Part of the Zap examples.
//!
//! Build me with `zig build app_basic`.
//! Run me with `zig build run-app_basic`.
//!
const std = @import("std");
const Allocator = std.mem.Allocator;
const builtin = @import("builtin");
const optimize_mode = builtin.mode;
const zap = @import("zap");
const sqlite = @import("sqlite");
// The global Application Context
const Context = @import("Context");
// A very simple endpoint handling only GET requests
const SimpleEndpoint = struct {
// zap.App.Endpoint Interface part
path: []const u8,
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,
// data specific for this endpoint
component: []const u8,
pub fn init(path: []const u8, data: []const u8) SimpleEndpoint {
return .{
.path = path,
.component = data,
};
}
// handle GET requests
pub fn get(e: *SimpleEndpoint, arena: Allocator, context: *Context, r: zap.Request) !void {
const thread_id = std.Thread.getCurrentId();
try Context.SendInertiaResponse(e, r, arena, e.component, .{
.thread_id = thread_id,
.counter = context.counter,
});
context.*.counter += 1;
}
};
const StopEndpoint = struct {
path: []const u8,
error_strategy: zap.Endpoint.ErrorStrategy = .log_to_response,
pub fn get(_: *StopEndpoint, _: Allocator, context: *Context, _: zap.Request) !void {
std.debug.print(
\\Before I stop, let me dump the app context:
\\db_connection='{}'
\\
\\
, .{context.*.db_connection});
zap.stop();
}
};
pub fn main() !void {
// setup allocations
var gpa: std.heap.GeneralPurposeAllocator(.{
// just to be explicit
.thread_safe = true,
}) = .{};
defer std.debug.print("\n\nLeaks detected: {}\n\n", .{gpa.deinit() != .ok});
const allocator = gpa.allocator();
var db = try sqlite.Db.init(.{
.mode = sqlite.Db.Mode{ .File = "./storage/mydata.db" },
.open_flags = .{
.write = true,
.create = true,
},
.threading_mode = .MultiThread,
});
defer db.deinit();
// create an app context
var my_context = Context.init(db);
// create an App instance
// const App = zap.App.Create(Context);
const App = @import("./App.zig").Create(Context);
try App.init(allocator, &my_context, .{});
defer App.deinit();
// create the endpoints
var home_endpoint = SimpleEndpoint.init("/", "home/home");
var test_endpoint = SimpleEndpoint.init("/test", "home/test");
var stop_endpoint: StopEndpoint = .{ .path = "/stop" };
// register the endpoints with the App
try App.register(&stop_endpoint);
try App.register(&home_endpoint);
try App.register(&test_endpoint);
// listen on the network
try App.listen(.{
.interface = "0.0.0.0",
.port = 3000,
.public_folder = "./public",
});
std.debug.print("Listening on 0.0.0.0:3000\n", .{});
std.debug.print(
\\ Try me via:
\\ curl http://localhost:3000/test
\\ Stop me via:
\\ curl http://localhost:3000/stop
\\
, .{});
// start worker threads -- only 1 process!!!
zap.start(.{
.threads = 2,
.workers = 1,
});
}

13
src/modules/404.vue Normal file
View file

@ -0,0 +1,13 @@
<script setup>
import Layout from "@/layout/Layout.vue";
import { Head, Link } from "@inertiajs/vue3";
defineProps({ thread_id: Number, counter: Number });
</script>
<template>
<Layout>
<h1>404</h1>
<Link href="/" class="p-button mt-4">Back Home</Link>
</Layout>
</template>

17
src/modules/home/home.vue Normal file
View file

@ -0,0 +1,17 @@
<script setup>
import Layout from "@/layout/Layout.vue";
import { Head, Link } from "@inertiajs/vue3";
defineProps({ thread_id: Number, counter: Number });
</script>
<template>
<Layout>
<h1>Home</h1>
<Head title="Welcome" />
<h1 class="">Welcome</h1>
<p>The thread id is '{{ thread_id }}'.</p>
<p>The counter is at '{{ counter }}'.</p>
<Link href="/test" class="p-button mt-4">To Test</Link>
</Layout>
</template>

18
src/modules/home/test.vue Normal file
View file

@ -0,0 +1,18 @@
<script setup>
import Layout from "@/layout/Layout.vue";
import { Head, Link } from "@inertiajs/vue3";
defineProps({ thread_id: Number, counter: Number });
</script>
<template>
<Layout>
<h1>/test</h1>
<Head title="Welcome" />
<h1 class="">Welcome</h1>
<p>The thread id is '{{ thread_id }}'.</p>
<p>The counter is at '{{ counter }}'.</p>
<Link href="/test" class="p-button mt-4 mr-2">Again!</Link>
<Link href="/" class="p-button mt-4">Back Home</Link>
</Layout>
</template>

11
src/root.html Normal file
View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<link rel="icon" type="image/svg+xml" href="/favicon.svg"/>
<script type="module" src="{s}"></script>
<link href="{s}" rel="stylesheet">
</head>
<body>
<div id="app" data-page="{f}"></div>
</body>
</html>

13
src/style.css Normal file
View file

@ -0,0 +1,13 @@
@import "tailwindcss";
/* @custom-variant dark (&:where(.dark, .dark *)); */
body {
background: var(--color-slate-100);
color: black;
@variant dark {
color: white;
background: var(--color-slate-900);
}
}