This commit is contained in:
yuzu 2025-05-05 01:09:52 -05:00
parent 056b8918d3
commit d0a8d8d969
12 changed files with 145 additions and 14 deletions

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

View File

@ -6,6 +6,7 @@ pub const Blog = jetquery.Model(
struct {
id: i32,
title: []const u8,
blob: []const u8,
content: ?[]const u8,
created_at: jetquery.DateTime,
updated_at: jetquery.DateTime,

View File

@ -9,6 +9,7 @@ pub fn up(repo: anytype) !void {
t.primaryKey("id", .{}),
t.column("title", .string, .{}),
t.column("content", .text, .{ .optional = true }),
t.column("blob", .text, .{}),
t.timestamps(.{}),
},
.{},

72
src/app/rss.zig Normal file
View File

@ -0,0 +1,72 @@
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>yuzucchii.xyz</link>
\\<description>Personal blog of Yuzu with all kinds of different articles</description>
, .{});
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) !void {
try request.headers.append("Content-Type", "application/rss+xml");
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,
});
}
// TODO: wait until jetzig adds xml support
_ = try generateRss(try items.toOwnedSlice(), request.allocator);
return request.render(.ok);
}

View File

@ -153,7 +153,15 @@ pub fn post(request: *jetzig.Request) !jetzig.View {
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);
} else {

View File

@ -1,6 +1,24 @@
@args allowed: bool
<div>
@for (.blogs) |blog| {
@if ($.allowed)
<button id="delete-post">
Delete post
</button>
<script>
document.getElementById('delete-post').addEventListener('click', function () {
fetch(`/blogs/${blog.id}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
},
})
.catch(error => {
console.error('Error during logout:', error);
});
}
</script>
@end
<a href="/blogs/{{blog.id}}">{{blog.title}}</a>
{{zmpl.fmt.datetime(blog.get("created_at"), "%Y-%m-%d %H:%M")}}
<br/>

View File

@ -1,11 +1,12 @@
<div>
<form action="/blogs" method="POST">
{{context.authenticityFormElement()}}
<label>Title</label>
<input name="title" />
<label>Content</label>
<textarea name="content"></textarea>
<label>Preview</label>
<textarea name="preview"></textarea>
<input type="submit" />
</form>
</div>

View File

@ -61,6 +61,10 @@ pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
try root.put("allowed", allowed);
try root.put("bsky_link", "empty for now");
try root.put("discord_link", "https://discord.gg/pvraBkepUP");
try root.put("codeberg_link", "https://codeberg.org/yuzu");
try root.put("message_param", params.get("message"));
// Set arbitrary response headers as required. `content-type` is automatically assigned for

View File

@ -1,4 +1,8 @@
@args title: []const u8
@args title: []const u8, id: i32, blob: []const u8
<div class="mb-4">
<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>

View File

@ -11,8 +11,8 @@
<body>
<div class="mx-auto max-w-3xl mt-sm">
<br/>
<h1 class="text-3xl font-bold mb-4">Hello, I like doing things.</h1>
<p class="text-lg mb-6">I created this website using Jetzig in order to store things I do.</p>
<div class="mb-8">
<a href="/blogs" class="text-blue-500 hover:underline">Blog</a>
@ -21,10 +21,11 @@
@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="git.yuzucchii.xyz" class="text-blue-500 hover:underline ml-4">Gitea instance</a>
<a href="chat.yuzucchii.xyz" class="text-blue-500 hover:underline ml-4">IRC</a>
<a href="/rss" class="text-blue-500 hover:underline ml-4">RSS feed</a>
<a href="searx.yuzucchii.xyz" class="text-blue-500 hover:underline ml-4">SearX</a>
<a href="yuzucchii.xyz/files" class="text-blue-500 hover:underline ml-4">Files</a>
<a href="mailto:me@yuzucchii.xyz" class="text-blue-500 hover:underline ml-4">E-mail</a>
<button
id="copy-button"
@ -49,11 +50,34 @@
</noscript>
</div>
<!-- Banner Image -->
<div class="mb-6">
<img src="/banner.jpg" alt="Banner" class="w-full rounded-xl shadow-md">
</div>
<p class="text-lg mb-6">I created this website in order to store things I do, but don't be scared of the simple layout, this website is featureful and full of things to do, please enjoy your visit.</p>
@for (.articles) |article| {
@partial root/article_blob(title: article.title, blob: article.content)
@partial root/article_blob(title: article.title, blob: article.content, id: article.id)
}
</div>
<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, and all scripts are optional and publicly available.
</p>
</div>
<div class="flex justify-center flex-wrap gap-4 mb-4 text-blue-500">
<a href="{{.bsky_link}}" class="hover:underline" target="_blank" rel="noopener">Bluesky</a>
<a href="{{.discord_link}}" class="hover:underline" target="_blank" rel="noopener">Discord</a>
<a href="{{.codeberg_link}}" class="hover:underline" target="_blank" rel="noopener">Codeberg</a>
<a href="https://yuzucchii.xyz" class="hover:underline" target="_blank" rel="noopener">yuzucchii.xyz</a>
</div>
<p class="text-xs text-gray-500">&copy; 2025 yuzucchii.xyz</p>
</div>
</footer>
</body>
<script>

View File

@ -215,8 +215,6 @@ pub const jetzig_options = struct {
pub fn init(app: *jetzig.App) !void {
_ = app;
// Example custom route:
// app.route(.GET, "/custom/:id/foo/bar", @import("app/views/custom/foo.zig"), .bar);
}
pub fn main() !void {