Compare commits
10 Commits
76c670b287
...
73a1d1a7e8
Author | SHA1 | Date | |
---|---|---|---|
73a1d1a7e8 | |||
97b991f90f | |||
74041c968e | |||
fa11ca9575 | |||
281043f96f | |||
f9e148f9ee | |||
bca22601e1 | |||
d0a8d8d969 | |||
056b8918d3 | |||
045d282298 |
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
.zig-cache/
|
||||||
|
zig-out/
|
11
.gitignore
vendored
11
.gitignore
vendored
@ -1,6 +1,17 @@
|
|||||||
zig-out/
|
zig-out/
|
||||||
zig-cache/
|
zig-cache/
|
||||||
|
data/
|
||||||
*.core
|
*.core
|
||||||
.jetzig
|
.jetzig
|
||||||
.zig-cache/
|
.zig-cache/
|
||||||
|
# local should be custom
|
||||||
|
Dockerfile
|
||||||
|
compose.yml
|
||||||
|
node_modules/
|
||||||
|
static/
|
||||||
|
jetzig_downloads.json
|
||||||
|
.bundle
|
||||||
|
bundle.tar.gz
|
||||||
|
log/
|
||||||
|
.env.*
|
||||||
.env
|
.env
|
||||||
|
36
Dockerfile
Normal file
36
Dockerfile
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
FROM alpine:latest AS build
|
||||||
|
RUN apk add jq curl tar xz git vim
|
||||||
|
RUN curl --output /zig.tar.xz "$(curl -s 'https://ziglang.org/download/index.json' | jq -r '.master."aarch64-linux".tarball')"
|
||||||
|
|
||||||
|
RUN mkdir /zig
|
||||||
|
WORKDIR /zig
|
||||||
|
RUN tar -xf /zig.tar.xz
|
||||||
|
|
||||||
|
RUN mkdir /app
|
||||||
|
COPY . /app/
|
||||||
|
WORKDIR /app/
|
||||||
|
RUN /zig/zig-*/zig build -Denvironment=production install
|
||||||
|
|
||||||
|
run mkdir /jetzig
|
||||||
|
WORKDIR /jetzig
|
||||||
|
RUN git clone https://git.yuzucchii.xyz/yuzucchii/jetzig .
|
||||||
|
WORKDIR ./cli
|
||||||
|
RUN /zig/zig-*/zig build install
|
||||||
|
|
||||||
|
FROM alpine:latest
|
||||||
|
|
||||||
|
# copy zig over
|
||||||
|
RUN mkdir /.zig
|
||||||
|
COPY --from=build /zig/zig-*/ /.zig
|
||||||
|
ENV PATH="/.zig:$PATH"
|
||||||
|
|
||||||
|
# copy jetzig over
|
||||||
|
COPY --from=build /jetzig/cli/zig-out/bin/jetzig /usr/local/bin/jetzig
|
||||||
|
|
||||||
|
RUN mkdir -p /app/public
|
||||||
|
COPY --from=build /app /app
|
||||||
|
COPY --from=build /app/zig-out/bin/yuzucchiidotxyz /app/server
|
||||||
|
WORKDIR /app/
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT ["./server"]
|
||||||
|
CMD ["/app/server", "--bind", "0.0.0.0", "--port", "8080"]
|
2
build.sh
Normal file
2
build.sh
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
npx tailwind --output public/main.css
|
||||||
|
zig build -Denvironment=production install
|
15
build.zig
15
build.zig
@ -3,17 +3,20 @@ const jetzig = @import("jetzig");
|
|||||||
|
|
||||||
pub fn build(b: *std.Build) !void {
|
pub fn build(b: *std.Build) !void {
|
||||||
const target = b.standardTargetOptions(.{});
|
const target = b.standardTargetOptions(.{});
|
||||||
const optimize = b.standardOptimizeOption(.{});
|
const optimize = .ReleaseSafe;
|
||||||
|
|
||||||
const exe = b.addExecutable(.{
|
const exe = b.addExecutable(.{
|
||||||
.name = "yuzucchii.xyz",
|
.name = "yuzucchiidotxyz",
|
||||||
.root_source_file = b.path("src/main.zig"),
|
.root_source_file = b.path("src/main.zig"),
|
||||||
.target = target,
|
.target = target,
|
||||||
.optimize = optimize,
|
.optimize = optimize
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = b.addOptions();
|
const options = b.addOptions();
|
||||||
options.addOption([]const u8, "BLOGS_PASSWORD", "admin");
|
|
||||||
|
options.addOption([]const u8, "BLOGS_PASSWORD", std.posix.getenv("BLOGS_PASSWORD") orelse "admin");
|
||||||
|
|
||||||
|
options.addOption([]const u8, "DOMAIN", std.posix.getenv("DOMAIN") orelse "localhost");
|
||||||
|
|
||||||
exe.root_module.addOptions("dev", options);
|
exe.root_module.addOptions("dev", options);
|
||||||
|
|
||||||
@ -26,7 +29,9 @@ pub fn build(b: *std.Build) !void {
|
|||||||
exe.root_module.addImport("uuid", uuid.module("uuid"));
|
exe.root_module.addImport("uuid", uuid.module("uuid"));
|
||||||
// ^ Add all dependencies before `jetzig.jetzigInit()` ^
|
// ^ Add all dependencies before `jetzig.jetzigInit()` ^
|
||||||
|
|
||||||
try jetzig.jetzigInit(b, exe, .{});
|
try jetzig.jetzigInit(b, exe, .{
|
||||||
|
.zmpl_version = .v2
|
||||||
|
});
|
||||||
|
|
||||||
b.installArtifact(exe);
|
b.installArtifact(exe);
|
||||||
|
|
||||||
|
@ -24,14 +24,14 @@
|
|||||||
// Once all dependencies are fetched, `zig build` no longer requires
|
// Once all dependencies are fetched, `zig build` no longer requires
|
||||||
// internet connectivity.
|
// internet connectivity.
|
||||||
.dependencies = .{
|
.dependencies = .{
|
||||||
.jetzig = .{
|
|
||||||
.url = "https://github.com/jetzig-framework/jetzig/archive/1feb18fb74e626fe068ec67532318640a9cb83be.tar.gz",
|
|
||||||
.hash = "jetzig-0.0.0-IpAgLfMzDwDyAqZ05btcLDd9dfE_bxUbfOI_Wx7a19ed",
|
|
||||||
},
|
|
||||||
.uuid = .{
|
.uuid = .{
|
||||||
.url = "git+https://github.com/r4gus/uuid-zig#9d66e23e32cd7208d1becb4fd9352f8a27f89551",
|
.url = "git+https://github.com/r4gus/uuid-zig#9d66e23e32cd7208d1becb4fd9352f8a27f89551",
|
||||||
.hash = "uuid-0.3.0-oOieIYF1AAA_BtE7FvVqqTn5uEYTvvz7ycuVnalCOf8C",
|
.hash = "uuid-0.3.0-oOieIYF1AAA_BtE7FvVqqTn5uEYTvvz7ycuVnalCOf8C",
|
||||||
},
|
},
|
||||||
|
.jetzig = .{
|
||||||
|
.url = "git+https://git.yuzucchii.xyz/yuzucchii/jetzig#6baa8c114ad739e5461584dfc79df5b7792557bf",
|
||||||
|
.hash = "jetzig-0.0.0-IpAgLURbDwD5jrmRznyPbGcMGFusxX1xXU1_FVFozIBL",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
.paths = .{
|
.paths = .{
|
||||||
"build.zig",
|
"build.zig",
|
||||||
|
24
compose.yml
24
compose.yml
@ -1,8 +1,30 @@
|
|||||||
services:
|
services:
|
||||||
|
server:
|
||||||
|
env_file: ".env"
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- postgres
|
||||||
|
networks:
|
||||||
|
- postgres-network
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16
|
image: postgres:16
|
||||||
|
env_file: ".env"
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: ${JETQUERY_USERNAME}
|
POSTGRES_USER: ${JETQUERY_USERNAME}
|
||||||
POSTGRES_PASSWORD: ${JETQUERY_PASSWORD}
|
POSTGRES_PASSWORD: ${JETQUERY_PASSWORD}
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- ./data/:/var/lib/postgresql/data/
|
||||||
|
networks:
|
||||||
|
- postgres-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
postgres-network:
|
||||||
|
driver: bridge
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
pub const database = .{
|
pub const database = .{
|
||||||
.development = .{
|
.development = .{
|
||||||
.adapter = .postgresql,
|
.adapter = .postgresql,
|
||||||
|
.hostname = "localhost",
|
||||||
},
|
},
|
||||||
.testing = .{
|
.testing = .{
|
||||||
.adapter = .postgresql,
|
.adapter = .postgresql,
|
||||||
},
|
},
|
||||||
.production = .{
|
.production = .{
|
||||||
.adapter = .postgresql,
|
.adapter = .postgresql,
|
||||||
|
.hostname = "postgres"
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
2409
package-lock.json
generated
Normal file
2409
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
18
package.json
Normal file
18
package.json
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"name": "yuzucchii.xyz",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"author": "",
|
||||||
|
"license": "ISC",
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/typography": "^0.5.16",
|
||||||
|
"tailwindcss": "^3.4.17"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tailwindcss/cli": "^4.1.5"
|
||||||
|
}
|
||||||
|
}
|
BIN
public/banner.jpg
Normal file
BIN
public/banner.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 89 KiB |
Binary file not shown.
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 126 KiB |
@ -1,10 +0,0 @@
|
|||||||
/* Root stylesheet. Load into your Zmpl template with:
|
|
||||||
*
|
|
||||||
* <link rel="stylesheet" href="/styles.css" />
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
.message {
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 3rem;
|
|
||||||
}
|
|
@ -6,6 +6,7 @@ pub const Blog = jetquery.Model(
|
|||||||
struct {
|
struct {
|
||||||
id: i32,
|
id: i32,
|
||||||
title: []const u8,
|
title: []const u8,
|
||||||
|
blob: []const u8,
|
||||||
content: ?[]const u8,
|
content: ?[]const u8,
|
||||||
created_at: jetquery.DateTime,
|
created_at: jetquery.DateTime,
|
||||||
updated_at: jetquery.DateTime,
|
updated_at: jetquery.DateTime,
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const jetquery = @import("jetquery");
|
||||||
|
const t = jetquery.schema.table;
|
||||||
|
|
||||||
|
pub fn up(repo: anytype) !void {
|
||||||
|
try repo.createTable(
|
||||||
|
"blogs",
|
||||||
|
&.{
|
||||||
|
t.primaryKey("id", .{}),
|
||||||
|
t.column("title", .string, .{}),
|
||||||
|
t.column("content", .text, .{ .optional = true }),
|
||||||
|
t.column("blob", .text, .{}),
|
||||||
|
t.timestamps(.{}),
|
||||||
|
},
|
||||||
|
.{},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn down(repo: anytype) !void {
|
||||||
|
try repo.dropTable("blogs", .{});
|
||||||
|
}
|
@ -5,6 +5,8 @@ const Zmd = @import("zmd").Zmd;
|
|||||||
const tokens = @import("zmd").tokens;
|
const tokens = @import("zmd").tokens;
|
||||||
const Node = @import("zmd").Node;
|
const Node = @import("zmd").Node;
|
||||||
|
|
||||||
|
pub const layout = "panel";
|
||||||
|
|
||||||
/// Default fragments. Pass this to `Zmd.toHtml` or provide your own.
|
/// Default fragments. Pass this to `Zmd.toHtml` or provide your own.
|
||||||
/// Formatters can be functions receiving an allocator, the current node, and the rendered
|
/// Formatters can be functions receiving an allocator, the current node, and the rendered
|
||||||
/// content, or a 2-element tuple containing the open and close for each node.
|
/// content, or a 2-element tuple containing the open and close for each node.
|
||||||
@ -123,11 +125,35 @@ pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
|
|||||||
var root = try request.data(.object);
|
var root = try request.data(.object);
|
||||||
try root.put("blog", eb);
|
try root.put("blog", eb);
|
||||||
|
|
||||||
|
const cookies = try request.cookies();
|
||||||
|
const allowed = blk: {
|
||||||
|
const session = cookies.get("session") orelse break :blk false;
|
||||||
|
|
||||||
|
const session_query = jetzig.database.Query(.Session)
|
||||||
|
.findBy(.{ .session_id = session.value });
|
||||||
|
_ = request.repo.execute(session_query) catch break :blk false;
|
||||||
|
break :blk true;
|
||||||
|
};
|
||||||
|
|
||||||
|
try root.put("allowed", allowed);
|
||||||
|
|
||||||
try request.process();
|
try request.process();
|
||||||
return request.render(.ok);
|
return request.render(.ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new(request: *jetzig.Request) !jetzig.View {
|
pub fn new(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||||
|
const cookies = try request.cookies();
|
||||||
|
const allowed = blk: {
|
||||||
|
const session = cookies.get("session") orelse break :blk false;
|
||||||
|
|
||||||
|
const session_query = jetzig.database.Query(.Session)
|
||||||
|
.findBy(.{ .session_id = session.value });
|
||||||
|
_ = request.repo.execute(session_query) catch break :blk false;
|
||||||
|
break :blk true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const root = try data.object();
|
||||||
|
try root.put("allowed", allowed);
|
||||||
return request.render(.ok);
|
return request.render(.ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -153,7 +179,15 @@ pub fn post(request: *jetzig.Request) !jetzig.View {
|
|||||||
return request.fail(.unprocessable_entity);
|
return request.fail(.unprocessable_entity);
|
||||||
};
|
};
|
||||||
|
|
||||||
try request.repo.insert(.Blog, .{ .title = title, .content = content });
|
const preview = params.getT(.string, "preview") orelse {
|
||||||
|
return request.fail(.unprocessable_entity);
|
||||||
|
};
|
||||||
|
|
||||||
|
try request.repo.insert(.Blog, .{
|
||||||
|
.title = title,
|
||||||
|
.blob = preview,
|
||||||
|
.content = content,
|
||||||
|
});
|
||||||
|
|
||||||
return request.redirect("/blogs", .moved_permanently);
|
return request.redirect("/blogs", .moved_permanently);
|
||||||
} else {
|
} else {
|
||||||
@ -162,6 +196,31 @@ pub fn post(request: *jetzig.Request) !jetzig.View {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn delete(id: []const u8, request: *jetzig.Request) !jetzig.View {
|
||||||
|
// check "session" cookie for authentication
|
||||||
|
const cookies = try request.cookies();
|
||||||
|
const session = cookies.get("session") orelse {
|
||||||
|
return request.fail(.not_found);
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if session is valid
|
||||||
|
|
||||||
|
const query = jetzig.database.Query(.Session)
|
||||||
|
.findBy(.{ .session_id = session.value });
|
||||||
|
|
||||||
|
if (try request.repo.execute(query)) |_| {
|
||||||
|
const delete_query = jetzig.database.Query(.Blog)
|
||||||
|
.find(try std.fmt.parseInt(i32, id, 10));
|
||||||
|
|
||||||
|
if (try request.repo.execute(delete_query)) |blog_post| {
|
||||||
|
try request.repo.delete(blog_post);
|
||||||
|
}
|
||||||
|
|
||||||
|
return request.redirect("/blogs", .moved_permanently);
|
||||||
|
} else {
|
||||||
|
return request.fail(.not_found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
test "index" {
|
test "index" {
|
||||||
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
var app = try jetzig.testing.app(std.testing.allocator, @import("routes"));
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<div>
|
<div>
|
||||||
<h1>{{.blog.title}}</h1>
|
<h1>{{.blog.title}}</h1>
|
||||||
<div class="mt-6 prose prose-lg dark:prose-dark">
|
<div class="prose prose-slate max-w-none mt-6 dark:prose-dark">
|
||||||
{{zmpl.fmt.raw(zmpl.ref("blog.content"))}}
|
{{zmpl.fmt.raw(zmpl.ref("blog.content"))}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,13 +1,54 @@
|
|||||||
@args allowed: bool
|
@args allowed: bool
|
||||||
<div>
|
<div class="max-w-3xl mx-auto mt-10 space-y-6">
|
||||||
@for (.blogs) |blog| {
|
@for (.blogs) |blog| {
|
||||||
<a href="/blogs/{{blog.id}}">{{blog.title}}</a>
|
<div class="p-4 border border-gray-200 rounded-xl shadow-sm hover:shadow transition">
|
||||||
{{zmpl.fmt.datetime(blog.get("created_at"), "%Y-%m-%d %H:%M")}}
|
<div class="flex justify-between items-center">
|
||||||
<br/>
|
<a href="/blogs/{{blog.id}}" class="text-xl font-semibold text-blue-600 hover:underline">
|
||||||
}
|
{{blog.title}}
|
||||||
<hr/>
|
</a>
|
||||||
|
|
||||||
@if ($.allowed)
|
@if ($.allowed)
|
||||||
<a href="/blogs/new">New Blog</a>
|
<button
|
||||||
@end
|
id="delete-post-{{blog.id}}"
|
||||||
|
class="text-sm text-red-600 hover:text-red-800"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
@end
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-gray-500 text-sm mt-1">
|
||||||
|
{{zmpl.fmt.datetime(blog.get("created_at"), "%Y-%m-%d %H:%M")}}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@if ($.allowed)
|
||||||
|
<div class="pt-4">
|
||||||
|
<a href="/blogs/new" class="inline-block px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 transition">
|
||||||
|
+ New Blog
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
@end
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if ($.allowed)
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
document.querySelectorAll('[id^="delete-post-"]').forEach(button => {
|
||||||
|
button.addEventListener("click", () => {
|
||||||
|
const id = button.id.replace("delete-post-", "");
|
||||||
|
fetch(`/blogs/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => window.location.reload())
|
||||||
|
.catch(err => console.error("Failed to delete post:", err));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
@end
|
||||||
|
|
||||||
|
@ -1,11 +1,25 @@
|
|||||||
<div>
|
<div class="max-w-2xl mx-auto mt-10 p-6 bg-white shadow-md rounded-2xl">
|
||||||
<form action="/blogs" method="POST">
|
<form action="/blogs" method="POST" class="space-y-6">
|
||||||
<label>Title</label>
|
{{context.authenticityFormElement()}}
|
||||||
<input name="title" />
|
|
||||||
|
|
||||||
<label>Content</label>
|
<div>
|
||||||
<textarea name="content"></textarea>
|
<label for="title" class="block text-sm font-medium text-gray-700">Title</label>
|
||||||
|
<input name="title" id="title" class="mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<input type="submit" />
|
<div>
|
||||||
|
<label for="content" class="block text-sm font-medium text-gray-700">Content</label>
|
||||||
|
<textarea name="content" id="content" rows="8" class="mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="preview" class="block text-sm font-medium text-gray-700">Preview</label>
|
||||||
|
<textarea name="preview" id="preview" rows="3" class="mt-1 w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring focus:ring-blue-200 focus:ring-opacity-50"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<input type="submit" value="Publish" class="px-4 py-2 bg-blue-600 text-white font-semibold rounded-md shadow hover:bg-blue-700 transition-colors" />
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
30
src/app/views/layouts/_footer.zmpl
Normal file
30
src/app/views/layouts/_footer.zmpl
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
@zig {
|
||||||
|
const links = .{
|
||||||
|
.{ .href = "https://yuzucchii.xyz", .title = "Home" },
|
||||||
|
.{ .href = "/rss", .title = "RSS" },
|
||||||
|
.{ .href = "https://codeberg.org/yuzu", .title = "Codeberg" },
|
||||||
|
.{ .href = "https://bsky.app/", .title = "Bluesky" },
|
||||||
|
.{ .href = "https://discord.gg/pvraBkepUP", .title = "Discord" },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
<footer class="mt-12 border-t pt-6 text-center text-sm text-gray-600">
|
||||||
|
<div class="max-w-xl mx-auto px-4">
|
||||||
|
|
||||||
|
<div class="bg-yellow-100 text-yellow-800 border border-yellow-200 rounded-md px-4 py-3 mb-6">
|
||||||
|
<p class="text-sm md:text-base">
|
||||||
|
This website is not reliant on JavaScript. All scripts are optional and publicly available.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-center flex-wrap gap-4 mb-4 text-blue-500">
|
||||||
|
@zig {
|
||||||
|
inline for (links) |link| {
|
||||||
|
<a href="{{link.href}}" class="hover:underline">{{link.title}}</a>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-xs text-gray-500 padding-bottom-5">© 2025 yuzucchii.xyz</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
34
src/app/views/layouts/panel.zmpl
Normal file
34
src/app/views/layouts/panel.zmpl
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
@args allowed: bool
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta name="theme-color" content="#ffffff" />
|
||||||
|
<link rel="icon" href="/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="/main.css" />
|
||||||
|
<title>yuzucchii.xyz</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<div class="mx-auto max-w-3xl mt-sm">
|
||||||
|
<br/>
|
||||||
|
<h1 class="text-3xl font-bold mb-4">Hello, I like doing things.</h1>
|
||||||
|
|
||||||
|
<div class="mb-8">
|
||||||
|
<a href="/blogs" class="text-blue-500 hover:underline">Blog</a>
|
||||||
|
@if ($.allowed)
|
||||||
|
<a href="/logout" class="text-blue-500 hover:underline ml-4">Logout</a>
|
||||||
|
@else
|
||||||
|
<a href="/login" class="text-blue-500 hover:underline ml-4">Login</a>
|
||||||
|
@end
|
||||||
|
<a href="https://git.yuzucchii.xyz" class="text-blue-500 hover:underline ml-4" rel="noopener">Gitea</a>
|
||||||
|
<a href="/rss" class="text-blue-500 hover:underline ml-4">RSS feed</a>
|
||||||
|
<a href="https://searx.yuzucchii.xyz" class="text-blue-500 hover:underline ml-4">SearX</a>
|
||||||
|
<a href="/files" class="text-red-500 hover:underline ml-4">Files</a>
|
||||||
|
<a href="mailto:me@yuzucchii.xyz" class="text-blue-500 hover:underline ml-4">E-mail</a>
|
||||||
|
</div>
|
||||||
|
{{zmpl.content}}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
@partial layouts/footer
|
||||||
|
</html>
|
@ -2,9 +2,21 @@ const std = @import("std");
|
|||||||
const jetzig = @import("jetzig");
|
const jetzig = @import("jetzig");
|
||||||
const uuid4 = @import("uuid").v4;
|
const uuid4 = @import("uuid").v4;
|
||||||
|
|
||||||
pub fn index(request: *jetzig.Request) !jetzig.View {
|
pub const layout = "panel";
|
||||||
// check if logged in
|
|
||||||
|
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||||
const cookies = try request.cookies();
|
const cookies = try request.cookies();
|
||||||
|
const allowed = blk: {
|
||||||
|
const session = cookies.get("session") orelse break :blk false;
|
||||||
|
|
||||||
|
const session_query = jetzig.database.Query(.Session)
|
||||||
|
.findBy(.{ .session_id = session.value });
|
||||||
|
_ = request.repo.execute(session_query) catch break :blk false;
|
||||||
|
break :blk true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const root = try data.object();
|
||||||
|
try root.put("allowed", allowed);
|
||||||
|
|
||||||
if (cookies.get("session")) |session| if (session.value.len != 0)
|
if (cookies.get("session")) |session| if (session.value.len != 0)
|
||||||
return request.redirect("/blogs", .moved_permanently);
|
return request.redirect("/blogs", .moved_permanently);
|
||||||
@ -42,9 +54,12 @@ pub fn post(request: *jetzig.Request) !jetzig.View {
|
|||||||
.name = "session",
|
.name = "session",
|
||||||
.value = uuid,
|
.value = uuid,
|
||||||
.path = "/",
|
.path = "/",
|
||||||
|
.domain = @import("dev").DOMAIN,
|
||||||
|
.same_site = .lax,
|
||||||
.http_only = true,
|
.http_only = true,
|
||||||
.secure = false,
|
.secure = false,
|
||||||
.max_age = 60 * 60 * 24 * 7, // 1 week
|
.max_age = 60 * 60 * 24 * 7, // 1 week
|
||||||
|
.partitioned = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
// post to Session table
|
// post to Session table
|
||||||
|
@ -1,30 +1,45 @@
|
|||||||
<div>
|
<title>Login panel for admins</title>
|
||||||
<form id="login-form">
|
<body>
|
||||||
<label>Discrete password</label>
|
<div class="min-h-screen flex items-center justify-center bg-gray-100">
|
||||||
<input name="password" type="password"/>
|
<form id="login-form" class="bg-white p-8 rounded-2xl shadow-xl w-full max-w-sm space-y-6">
|
||||||
<button type="submit">Login</button>
|
<div>
|
||||||
</form>
|
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">Discrete password</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
class="w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="w-full bg-black text-white font-semibold py-2 rounded-md hover:bg-gray-800 transition-colors"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script>
|
|
||||||
document.querySelector('#login-form').addEventListener('submit', function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
const password = document.querySelector('input[name="password"]').value;
|
|
||||||
|
|
||||||
fetch('/login', {
|
<script>
|
||||||
method: 'POST',
|
document.querySelector('#login-form').addEventListener('submit', function(event) {
|
||||||
headers: {
|
event.preventDefault();
|
||||||
'Content-Type': 'application/json'
|
const password = document.querySelector('input[name="password"]').value;
|
||||||
},
|
|
||||||
body: JSON.stringify({ password })
|
|
||||||
})
|
|
||||||
.then(res => res.json())
|
|
||||||
.then(res => {
|
|
||||||
window.location.href = '/blogs';
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
console.error('Login error:', err);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
fetch('/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ password })
|
||||||
|
})
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(res => {
|
||||||
|
window.location.href = '/blogs';
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error('Login error:', err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
@ -1,7 +1,21 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const jetzig = @import("jetzig");
|
const jetzig = @import("jetzig");
|
||||||
|
|
||||||
pub fn index(request: *jetzig.Request) !jetzig.View {
|
pub const layout = "panel";
|
||||||
|
|
||||||
|
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||||
|
const cookies = try request.cookies();
|
||||||
|
const allowed = blk: {
|
||||||
|
const session = cookies.get("session") orelse break :blk false;
|
||||||
|
|
||||||
|
const session_query = jetzig.database.Query(.Session)
|
||||||
|
.findBy(.{ .session_id = session.value });
|
||||||
|
_ = request.repo.execute(session_query) catch break :blk false;
|
||||||
|
break :blk true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const root = try data.object();
|
||||||
|
try root.put("allowed", allowed);
|
||||||
return request.render(.ok);
|
return request.render(.ok);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,28 +1,39 @@
|
|||||||
<button id="logout-button">Logout</button>
|
|
||||||
|
|
||||||
<script>
|
<title>Login panel for admins</title>
|
||||||
document.getElementById('logout-button').addEventListener('click', function () {
|
<body>
|
||||||
fetch('/logout', {
|
<div>
|
||||||
method: 'POST',
|
<button
|
||||||
headers: {
|
id="logout-button"
|
||||||
'Content-Type': 'application/json'
|
class="px-4 py-2 bg-white border border-gray-300 text-gray-800 rounded-md shadow-sm hover:bg-gray-100 hover:border-gray-400 transition-all"
|
||||||
},
|
>
|
||||||
body: JSON.stringify({ action: 'logout' })
|
Logout
|
||||||
})
|
</button>
|
||||||
.then(response => {
|
</div>
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Logout failed: ${response.status}`);
|
|
||||||
}
|
|
||||||
return response.json(); // if you expect JSON back
|
|
||||||
})
|
|
||||||
.then(data => {
|
|
||||||
console.log('Logout successful:', data);
|
|
||||||
// window.location.href = '/login'; // redirect if needed
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error during logout:', error);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.getElementById('logout-button').addEventListener('click', function () {
|
||||||
|
fetch('/logout', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ action: 'logout' })
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Logout failed: ${response.status}`);
|
||||||
|
}
|
||||||
|
return response.json(); // if you expect JSON back
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
console.log('Logout successful:', data);
|
||||||
|
// window.location.href = '/login'; // redirect if needed
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error during logout:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
@ -1,50 +1,17 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const jetzig = @import("jetzig");
|
const jetzig = @import("jetzig");
|
||||||
|
|
||||||
|
pub const layout = "panel";
|
||||||
|
|
||||||
const Article = struct {
|
const Article = struct {
|
||||||
title: [255]u8,
|
title: [255]u8,
|
||||||
blob: [255]u8
|
blob: [255]u8
|
||||||
};
|
};
|
||||||
|
|
||||||
/// `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 {
|
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();
|
var root = try data.object();
|
||||||
|
|
||||||
// Add a string to the root object.
|
|
||||||
try root.put("title", data.string("yuzucchii.xyz"));
|
|
||||||
|
|
||||||
// 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();
|
|
||||||
const cookies = try request.cookies();
|
const cookies = try request.cookies();
|
||||||
|
|
||||||
var obj = try data.object();
|
|
||||||
try obj.put("title",data.string("The simplest article ever"));
|
|
||||||
try obj.put("description",data.string("Hello in this article I being to do things"));
|
|
||||||
|
|
||||||
var objj = try data.object();
|
|
||||||
try objj.put("title",data.string("The most complicated thing ever"));
|
|
||||||
try objj.put("description",data.string("Dude, that shit is like harder than a rock"));
|
|
||||||
|
|
||||||
var array = try root.put("articles", .array);
|
|
||||||
try array.append(obj);
|
|
||||||
try array.append(objj);
|
|
||||||
|
|
||||||
const query = jetzig.database.Query(.Blog).orderBy(.{.created_at = .desc});
|
const query = jetzig.database.Query(.Blog).orderBy(.{.created_at = .desc});
|
||||||
const blogs = try request.repo.all(query);
|
const blogs = try request.repo.all(query);
|
||||||
|
|
||||||
@ -61,8 +28,6 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
|||||||
|
|
||||||
try root.put("allowed", allowed);
|
try root.put("allowed", allowed);
|
||||||
|
|
||||||
try root.put("message_param", params.get("message"));
|
|
||||||
|
|
||||||
// Set arbitrary response headers as required. `content-type` is automatically assigned for
|
// Set arbitrary response headers as required. `content-type` is automatically assigned for
|
||||||
// HTML, JSON responses.
|
// HTML, JSON responses.
|
||||||
//
|
//
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
@args title: []const u8
|
@args title: []const u8, id: i32, blob: []const u8
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h2 class="text-2xl font-semibold mb-2">{{title}}</h2>
|
<h2 class="text-2xl font-semibold mb-2">{{title}}</h2>
|
||||||
|
<div class="text-gray-700">
|
||||||
|
{{blob}}
|
||||||
|
</div>
|
||||||
|
<a href="/blogs/{{id}}" class="text-blue-600 underline">Read more</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,37 +1,24 @@
|
|||||||
@args articles: Zmpl.Array, allowed: bool
|
@args articles: Zmpl.Array
|
||||||
|
<section class="max-w-3xl mx-auto px-4 py-8">
|
||||||
|
<!-- Banner -->
|
||||||
|
<div class="mb-8">
|
||||||
|
<img src="/banner.jpg" alt="Banner" class="w-full rounded-xl shadow-md">
|
||||||
|
</div>
|
||||||
|
|
||||||
<html>
|
<!-- Introduction -->
|
||||||
<head>
|
<p class="text-lg leading-relaxed mb-10 text-gray-700">
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
I created this website in order to store things I do. Don’t be scared of the simple layout —
|
||||||
|
this website is featureful and full of things to explore. Please enjoy your visit.
|
||||||
|
</p>
|
||||||
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<!-- Articles Section -->
|
||||||
<script src="https://cdn.tailwindcss.com"></script>
|
<h2 class="text-2xl font-semibold mb-4 border-b pb-2">Latest Articles</h2>
|
||||||
<link rel="stylesheet" href="/styles.css" />
|
|
||||||
<title> {{.title}} </title
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
<div class="space-y-6">
|
||||||
<div class="mx-auto max-w-3xl mt-sm">
|
@for (.articles) |article| {
|
||||||
<h1 class="text-3xl font-bold mb-4">Hello, I like doing things.</h1>
|
<div class="p-4 border rounded-xl bg-white shadow-sm hover:shadow transition">
|
||||||
<p class="text-lg mb-6">I created this website using Jetzig in order to store things I do.</p>
|
@partial root/article_blob(title: article.title, blob: article.blob, id: article.id)
|
||||||
|
</div>
|
||||||
<div class="mb-8">
|
}
|
||||||
<a href="/blogs" class="text-blue-500 hover:underline">Blog</a>
|
</div>
|
||||||
@if ($.allowed)
|
</section>
|
||||||
<a href="/logout" class="text-blue-500 hover:underline ml-4">Logout</a>
|
|
||||||
@else
|
|
||||||
<a href="/login" class="text-blue-500 hover:underline ml-4">Login</a>
|
|
||||||
@end
|
|
||||||
<a href="#" class="text-blue-500 hover:underline ml-4">Gitea instance</a>
|
|
||||||
<a href="#" class="text-blue-500 hover:underline ml-4">IRC</a>
|
|
||||||
<a href="#" class="text-blue-500 hover:underline ml-4">Tor url</a>
|
|
||||||
<a href="#" class="text-blue-500 hover:underline ml-4">RSS feed</a>
|
|
||||||
<a href="mailto:me@yuzucchii.xyz" class="text-blue-500 hover:underline ml-4">E-mail</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@for (.articles) |article| {
|
|
||||||
@partial root/article_blob(title: article.title, blob: article.content)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
76
src/app/views/rss.zig
Normal file
76
src/app/views/rss.zig
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
const std = @import("std");
|
||||||
|
const jetzig = @import("jetzig");
|
||||||
|
|
||||||
|
const RssItem = struct {
|
||||||
|
id: i32,
|
||||||
|
title: []const u8,
|
||||||
|
blob: []const u8,
|
||||||
|
content: []const u8,
|
||||||
|
created_at: jetzig.jetquery.DateTime,
|
||||||
|
updated_at: jetzig.jetquery.DateTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn generateRss(items: []const RssItem, allocator: std.mem.Allocator) ![]u8 {
|
||||||
|
var list = std.ArrayList(u8).init(allocator);
|
||||||
|
const writer = list.writer();
|
||||||
|
|
||||||
|
try writer.print(
|
||||||
|
\\<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
\\<rss version="2.0">
|
||||||
|
\\<channel>
|
||||||
|
\\<title>yuzucchii.xyz</title>
|
||||||
|
\\<link>https://yuzucchii.xyz</link>
|
||||||
|
\\<description>Personal blog of Yuzu with all kinds of different articles</description>
|
||||||
|
\\<atom:link xmlns:atom="http://www.w3.org/2005/Atom" href="https://yuzucchii.xyz/rss" rel="self" type="application/rss+xml"/>
|
||||||
|
, .{});
|
||||||
|
|
||||||
|
for (items) |item| {
|
||||||
|
try writer.print(
|
||||||
|
\\<item>
|
||||||
|
\\<title>{s}</title>
|
||||||
|
\\<link>yuzucchii.xyz/blogs/{d}</link>
|
||||||
|
\\<description>{s}</description>
|
||||||
|
, .{ item.title, item.id, item.blob });
|
||||||
|
|
||||||
|
try item.created_at.strftime(writer, "<pubDate>Day, DD Mon YYYY HH:MM:SS GMT</pubDate>");
|
||||||
|
|
||||||
|
try writer.print(
|
||||||
|
\\</item>
|
||||||
|
, .{});
|
||||||
|
}
|
||||||
|
|
||||||
|
try writer.writeAll("</channel></rss>");
|
||||||
|
return list.toOwnedSlice();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// we'll send xml instead
|
||||||
|
pub fn index(request: *jetzig.Request) !jetzig.View {
|
||||||
|
const query = jetzig.database.Query(.Blog).orderBy(.{.created_at = .desc});
|
||||||
|
|
||||||
|
const blogs = try request.repo.all(query);
|
||||||
|
|
||||||
|
var items: std.ArrayList(RssItem) = .init(request.allocator);
|
||||||
|
|
||||||
|
for (blogs) |blog| {
|
||||||
|
try items.append(RssItem{
|
||||||
|
.id = blog.id,
|
||||||
|
.title = blog.title,
|
||||||
|
.blob = blog.blob,
|
||||||
|
.content = blog.content orelse "<empty>",
|
||||||
|
.created_at = blog.created_at,
|
||||||
|
.updated_at = blog.updated_at,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rss = generateRss(items.toOwnedSlice() catch {
|
||||||
|
std.debug.print("lol", .{});
|
||||||
|
return request.fail(.bad_request);
|
||||||
|
}, request.allocator) catch {
|
||||||
|
return request.fail(.bad_request);
|
||||||
|
};
|
||||||
|
|
||||||
|
request.response.content_type = "application/rss+xml";
|
||||||
|
return request.renderText(rss, .ok);
|
||||||
|
}
|
||||||
|
|
1
src/app/views/rss/index.zmpl
Normal file
1
src/app/views/rss/index.zmpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
<div></div>
|
@ -13,6 +13,7 @@ pub const jetzig_options = struct {
|
|||||||
/// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`).
|
/// `jetzig.middleware` (e.g. `jetzig.middleware.HtmxMiddleware`).
|
||||||
pub const middleware: []const type = &.{
|
pub const middleware: []const type = &.{
|
||||||
jetzig.middleware.AntiCsrfMiddleware,
|
jetzig.middleware.AntiCsrfMiddleware,
|
||||||
|
//@import("app/middleware/BasicMiddleware.zig"),
|
||||||
//@import("app/middleware/AuthMiddleware.zig"),
|
//@import("app/middleware/AuthMiddleware.zig"),
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -214,9 +215,7 @@ pub const jetzig_options = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(app: *jetzig.App) !void {
|
pub fn init(app: *jetzig.App) !void {
|
||||||
_ = app;
|
app.route(.GET, "/rss.xml", @import("app/views/rss.zig"), .index);
|
||||||
// Example custom route:
|
|
||||||
// app.route(.GET, "/custom/:id/foo/bar", @import("app/views/custom/foo.zig"), .bar);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() !void {
|
pub fn main() !void {
|
||||||
|
73
src/tailwind.css
Normal file
73
src/tailwind.css
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
/* Base typography improvements */
|
||||||
|
@layer base {
|
||||||
|
body {
|
||||||
|
@apply bg-white text-gray-800 antialiased leading-relaxed;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h5, h6 {
|
||||||
|
@apply font-semibold text-gray-900;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
@apply text-blue-600 hover:underline transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
input, textarea, button {
|
||||||
|
@apply outline-none focus:ring focus:ring-blue-200 focus:ring-opacity-50;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
@apply resize-y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reusable components */
|
||||||
|
@layer components {
|
||||||
|
.btn {
|
||||||
|
@apply inline-block px-4 py-2 rounded-md font-medium shadow-sm transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
@apply btn bg-blue-600 text-white hover:bg-blue-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
@apply btn bg-red-600 text-white hover:bg-red-700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-outline {
|
||||||
|
@apply btn border border-gray-300 text-gray-800 hover:bg-gray-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
@apply p-4 border border-gray-200 rounded-xl bg-white shadow-sm hover:shadow transition;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
@apply text-2xl font-semibold mb-4 border-b pb-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-field {
|
||||||
|
@apply block w-full rounded-md border border-gray-300 shadow-sm mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
@apply block text-sm font-medium text-gray-700 mb-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utility overrides or extras */
|
||||||
|
@layer utilities {
|
||||||
|
.text-muted {
|
||||||
|
@apply text-gray-500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.max-w-read {
|
||||||
|
@apply max-w-prose mx-auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
22
tailwind.config.js
Normal file
22
tailwind.config.js
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: ["./src/app/views/**/*.{zmpl,html,js}", "./src/main.zig"],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
typography: ({ theme }) => ({
|
||||||
|
DEFAULT: {
|
||||||
|
css: {
|
||||||
|
color: theme('colors.gray.800'),
|
||||||
|
a: { color: theme('colors.blue.600') },
|
||||||
|
pre: { backgroundColor: theme('colors.gray.100') },
|
||||||
|
blockquote: {
|
||||||
|
borderLeftColor: theme('colors.yellow.400'),
|
||||||
|
fontStyle: 'italic',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
plugins: [require('@tailwindcss/typography')],
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user