minor changes
This commit is contained in:
parent
e9f81a8ad0
commit
e8171642ef
2
.gitignore
vendored
2
.gitignore
vendored
@ -15,4 +15,4 @@ bundle.tar.gz
|
||||
log/
|
||||
.env.*
|
||||
.env
|
||||
public/new-styles.css
|
||||
file.log
|
||||
|
2
build.sh
2
build.sh
@ -1,2 +0,0 @@
|
||||
npx tailwind --output public/new-styles.css
|
||||
zig build -Denvironment=production install
|
@ -3,13 +3,14 @@ const jetzig = @import("jetzig");
|
||||
|
||||
pub fn build(b: *std.Build) !void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = .ReleaseSafe;
|
||||
const optimize = .ReleaseFast;
|
||||
|
||||
const exe = b.addExecutable(.{
|
||||
.name = "yuzucchiidotxyz",
|
||||
.root_source_file = b.path("src/main.zig"),
|
||||
.target = target,
|
||||
.optimize = optimize
|
||||
.optimize = optimize,
|
||||
.use_llvm = false,
|
||||
});
|
||||
|
||||
const options = b.addOptions();
|
||||
@ -29,9 +30,7 @@ pub fn build(b: *std.Build) !void {
|
||||
exe.root_module.addImport("uuid", uuid.module("uuid"));
|
||||
// ^ Add all dependencies before `jetzig.jetzigInit()` ^
|
||||
|
||||
try jetzig.jetzigInit(b, exe, .{
|
||||
.zmpl_version = .v2
|
||||
});
|
||||
try jetzig.jetzigInit(b, exe, .{ .zmpl_version = .v2 });
|
||||
|
||||
b.installArtifact(exe);
|
||||
|
||||
|
@ -29,8 +29,8 @@
|
||||
.hash = "uuid-0.3.0-oOieIYF1AAA_BtE7FvVqqTn5uEYTvvz7ycuVnalCOf8C",
|
||||
},
|
||||
.jetzig = .{
|
||||
.url = "git+https://git.yuzucchii.xyz/yuzucchii/jetzig#6baa8c114ad739e5461584dfc79df5b7792557bf",
|
||||
.hash = "jetzig-0.0.0-IpAgLURbDwD5jrmRznyPbGcMGFusxX1xXU1_FVFozIBL",
|
||||
.url = "https://github.com/jetzig-framework/jetzig/archive/1cb27ffec8fb648a30a9aa65c1e6128cf967a2f8.tar.gz",
|
||||
.hash = "jetzig-0.0.0-IpAgLYuEDwAjm-yxXp2A6efoieUq3lSYgsFq2g9x-oB6",
|
||||
},
|
||||
},
|
||||
.paths = .{
|
||||
|
@ -2,12 +2,10 @@ pub const database = .{
|
||||
.development = .{
|
||||
.adapter = .postgresql,
|
||||
.hostname = "localhost",
|
||||
.port = 5432,
|
||||
},
|
||||
.testing = .{
|
||||
.adapter = .postgresql,
|
||||
},
|
||||
.production = .{
|
||||
.adapter = .postgresql,
|
||||
.hostname = "postgres"
|
||||
},
|
||||
.production = .{ .adapter = .postgresql, .hostname = "postgres" },
|
||||
};
|
||||
|
2409
package-lock.json
generated
2409
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
18
package.json
18
package.json
@ -1,18 +0,0 @@
|
||||
{
|
||||
"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/agrees.gif
Normal file
BIN
public/agrees.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 MiB |
BIN
public/cc.png
Normal file
BIN
public/cc.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 418 KiB |
BIN
public/happy.gif
Normal file
BIN
public/happy.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.5 MiB |
BIN
public/miku1.png
Normal file
BIN
public/miku1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
public/miku2.png
Normal file
BIN
public/miku2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 303 KiB |
BIN
public/miku3.png
Normal file
BIN
public/miku3.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 222 KiB |
280
public/styles.css
Normal file
280
public/styles.css
Normal file
@ -0,0 +1,280 @@
|
||||
html {
|
||||
font-size: 62.5%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
body .container {
|
||||
max-width: 38em;
|
||||
font-size: 1.8rem;
|
||||
line-height: 1.618;
|
||||
margin: auto;
|
||||
color: #4a4a4a;
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 684px) {
|
||||
body {
|
||||
font-size: 1.53rem;
|
||||
}
|
||||
}
|
||||
@media (max-width: 382px) {
|
||||
body {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
}
|
||||
|
||||
.page-content {
|
||||
}
|
||||
|
||||
.main-menu {
|
||||
display: flex;
|
||||
text-decoration: none;
|
||||
list-style: none;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.main-menu a {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main-menu p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.main-menu a:hover {
|
||||
text-decoration: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.main-menu li:hover {
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.main-menu img {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
margin-top: 2.5em;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
footer {
|
||||
max-width: 100%;
|
||||
height: 130px;
|
||||
width: 100%;
|
||||
font-size: 16px;
|
||||
overflow: hidden;
|
||||
max-width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
footer img {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
footer h5 {
|
||||
padding: 0 5px 5px;
|
||||
color: 656565;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
line-height: 1.1;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
|
||||
font-weight: 700;
|
||||
margin-top: 3rem;
|
||||
margin-bottom: 1.5rem;
|
||||
overflow-wrap: break-word;
|
||||
word-wrap: break-word;
|
||||
-ms-word-break: break-all;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2.35em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2em;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.75em;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
small, sub, sup {
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #1d7484;
|
||||
}
|
||||
|
||||
a:visited {
|
||||
color: #144f5a;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: #982c61;
|
||||
border-bottom: 2px solid #4a4a4a;
|
||||
}
|
||||
|
||||
ul {
|
||||
padding-left: 1.4em;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0.4em;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
margin-left: 0px;
|
||||
margin-right: 0px;
|
||||
padding-left: 1em;
|
||||
padding-top: 0.8em;
|
||||
padding-bottom: 0.8em;
|
||||
padding-right: 0.8em;
|
||||
border-left: 5px solid #1d7484;
|
||||
margin-bottom: 2.5rem;
|
||||
background-color: #f1f1f1;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
img, video {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
blockquote p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
img, video {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
/* Pre and Code */
|
||||
pre {
|
||||
background-color: #f1f1f1;
|
||||
display: block;
|
||||
padding: 1em;
|
||||
overflow-x: auto;
|
||||
margin-top: 0px;
|
||||
margin-bottom: 2.5rem;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
code, kbd, samp {
|
||||
font-size: 0.9em;
|
||||
padding: 0 0.5em;
|
||||
background-color: #f1f1f1;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
pre > code {
|
||||
padding: 0;
|
||||
background-color: transparent;
|
||||
white-space: pre;
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
/* Buttons, forms and input */
|
||||
input, textarea {
|
||||
border: 1px solid #4a4a4a;
|
||||
}
|
||||
input:focus, textarea:focus {
|
||||
border: 1px solid #1d7484;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button, button, input[type=submit], input[type=reset], input[type=button], input[type=file]::file-selector-button {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
background-color: #1d7484;
|
||||
color: #f9f9f9;
|
||||
border-radius: 1px;
|
||||
border: 1px solid #1d7484;
|
||||
cursor: pointer;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.button[disabled], button[disabled], input[type=submit][disabled], input[type=reset][disabled], input[type=button][disabled], input[type=file]::file-selector-button[disabled] {
|
||||
cursor: default;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.button:hover, button:hover, input[type=submit]:hover, input[type=reset]:hover, input[type=button]:hover, input[type=file]::file-selector-button:hover {
|
||||
background-color: #982c61;
|
||||
color: #f9f9f9;
|
||||
outline: 0;
|
||||
}
|
||||
.button:focus-visible, button:focus-visible, input[type=submit]:focus-visible, input[type=reset]:focus-visible, input[type=button]:focus-visible, input[type=file]::file-selector-button:focus-visible {
|
||||
outline-style: solid;
|
||||
outline-width: 2px;
|
||||
}
|
||||
|
||||
textarea, select, input {
|
||||
color: #4a4a4a;
|
||||
padding: 6px 10px; /* The 6px vertically centers text on FF, ignored by Webkit */
|
||||
margin-bottom: 10px;
|
||||
background-color: #f1f1f1;
|
||||
border: 1px solid #f1f1f1;
|
||||
border-radius: 4px;
|
||||
box-shadow: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
textarea:focus, select:focus, input:focus {
|
||||
border: 1px solid #1d7484;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
input[type=checkbox]:focus {
|
||||
outline: 1px dotted #1d7484;
|
||||
}
|
||||
|
||||
label, legend, fieldset {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
@ -6,8 +6,8 @@ pub const Blog = jetquery.Model(
|
||||
struct {
|
||||
id: i32,
|
||||
title: []const u8,
|
||||
blob: []const u8,
|
||||
content: ?[]const u8,
|
||||
blob: []const u8,
|
||||
created_at: jetquery.DateTime,
|
||||
updated_at: jetquery.DateTime,
|
||||
},
|
||||
@ -25,3 +25,38 @@ pub const Session = jetquery.Model(
|
||||
},
|
||||
.{},
|
||||
);
|
||||
|
||||
pub const BlogTag = jetquery.Model(
|
||||
@This(),
|
||||
"blog_tags",
|
||||
struct {
|
||||
id: i32,
|
||||
blog_id: i32,
|
||||
tag_id: i32,
|
||||
created_at: jetquery.DateTime,
|
||||
updated_at: jetquery.DateTime,
|
||||
},
|
||||
.{
|
||||
.relations = .{
|
||||
.blog = jetquery.belongsTo(.Blog, .{}),
|
||||
.tag = jetquery.belongsTo(.Tag, .{}),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
pub const Tag = jetquery.Model(
|
||||
@This(),
|
||||
"tags",
|
||||
struct {
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
emoji: ?[]const u8,
|
||||
created_at: jetquery.DateTime,
|
||||
updated_at: jetquery.DateTime,
|
||||
},
|
||||
.{
|
||||
.relations = .{
|
||||
.blog_tags = jetquery.hasMany(.BlogTag, .{}),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -0,0 +1,30 @@
|
||||
const std = @import("std");
|
||||
const jetquery = @import("jetquery");
|
||||
const t = jetquery.schema.table;
|
||||
|
||||
pub fn up(repo: anytype) !void {
|
||||
try repo.createTable(
|
||||
"tags",
|
||||
&.{
|
||||
t.primaryKey("id", .{}),
|
||||
t.column("name", .string, .{}),
|
||||
t.timestamps(.{}),
|
||||
},
|
||||
.{},
|
||||
);
|
||||
|
||||
try repo.createTable(
|
||||
"blog_tags",
|
||||
&.{
|
||||
t.primaryKey("id", .{}),
|
||||
t.column("blog_id", .integer, .{ .reference = .{ "blogs", "id" } }),
|
||||
t.column("tag_id", .integer, .{ .reference = .{ "tags", "id" } }),
|
||||
t.timestamps(.{}),
|
||||
},
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn down(repo: anytype) !void {
|
||||
try repo.dropTable("tags", .{});
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
const std = @import("std");
|
||||
const jetquery = @import("jetquery");
|
||||
const t = jetquery.schema.table;
|
||||
|
||||
pub fn up(repo: anytype) !void {
|
||||
try repo.createTable(
|
||||
"tags",
|
||||
&.{
|
||||
t.primaryKey("id", .{}),
|
||||
t.column("name", .string, .{}),
|
||||
t.timestamps(.{}),
|
||||
},
|
||||
.{},
|
||||
);
|
||||
|
||||
try repo.createTable(
|
||||
"blog_tags",
|
||||
&.{
|
||||
t.primaryKey("id", .{}),
|
||||
t.column("blog_id", .integer, .{ .reference = .{ "blogs", "id" } }),
|
||||
t.column("tag_id", .integer, .{ .reference = .{ "tags", "id" } }),
|
||||
t.timestamps(.{}),
|
||||
},
|
||||
.{},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn down(repo: anytype) !void {
|
||||
try repo.dropTable("tags", .{});
|
||||
try repo.dropTable("blog_tags", .{});
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
const std = @import("std");
|
||||
const jetquery = @import("jetquery");
|
||||
const t = jetquery.schema.table;
|
||||
|
||||
pub fn up(repo: anytype) !void {
|
||||
try repo.alterTable("tags", .{
|
||||
.columns = .{
|
||||
.add = &.{
|
||||
t.column("emoji", .string, .{ .optional = true }),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
pub fn down(_: anytype) !void {}
|
@ -1,43 +1,20 @@
|
||||
<div class="max-w-3xl mx-auto px-4 py-12 font-sans text-win7-text bg-win7-bg rounded-xl shadow border border-win7-border">
|
||||
<h2 class="text-3xl font-bold mb-6 pb-2 border-b border-win7-border">
|
||||
About Me
|
||||
</h2>
|
||||
<div>
|
||||
<h2>About Me</h2>
|
||||
|
||||
<p class="mb-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
I perform sorcery in the software craftmanship endeavours, I do things, quiet things — mostly in text editors, occasionally in thought (I am able to play Balatro in my brain,) and sometimes in places unseen. This site is my collection unbestowed experiments, side quests, and digital breadcrumbs.
|
||||
</p>
|
||||
|
||||
<p class="mb-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
I do not sleep — I suspend. I do not dream — I garbage collect. My working hours are loosely tied to the gravitational pull of niche programming languages and the smell of old documentation, I talk about documentation like a synesthetic metaphor, I am very into skeuomorphism, so you will watch me build awful UIs.
|
||||
</p>
|
||||
|
||||
<p class="mb-4 text-lg leading-relaxed">
|
||||
<p>
|
||||
You'll find posts here about software, personal projects, systems I’m poking at, and the occasional half-serious exploration into how things break and why that is interesting.
|
||||
</p>
|
||||
|
||||
<p class="text-lg leading-relaxed">
|
||||
<p>
|
||||
If you'd like to challenge me to a duel, discuss obscure software, or just say hello — I'm usually somewhere near Discord, or off refactoring something that didn't ask for it. May your stack traces be short.
|
||||
</p>
|
||||
|
||||
<div class="my-6">
|
||||
<button
|
||||
onclick="copyIRC()"
|
||||
class="bg-win7-link text-white px-4 py-2 rounded-md shadow hover:bg-win7-hover transition"
|
||||
>
|
||||
Meet me here on IRC.
|
||||
</button>
|
||||
<span id="irc-copied" class="ml-3 text-sm text-green-600 hidden">Copied!</span>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function copyIRC() {
|
||||
const ircInfo = "irc://sv.yuzucchii.xyz:6697/#the-graveyard";
|
||||
navigator.clipboard.writeText(ircInfo).then(() => {
|
||||
const msg = document.getElementById("irc-copied");
|
||||
msg.classList.remove("hidden");
|
||||
setTimeout(() => msg.classList.add("hidden"), 2000);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
const jetquery = jetzig.jetquery;
|
||||
|
||||
const Zmd = @import("zmd").Zmd;
|
||||
const tokens = @import("zmd").tokens;
|
||||
@ -7,75 +8,102 @@ const Node = @import("zmd").Node;
|
||||
|
||||
pub const layout = "panel";
|
||||
|
||||
/// Default fragments. Pass this to `Zmd.toHtml` or provide your own.
|
||||
/// 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.
|
||||
pub const Fragments = struct {
|
||||
pub fn root(allocator: std.mem.Allocator, node: Node) ![]const u8 {
|
||||
return try std.fmt.allocPrint(allocator, "{s}", .{node.content});
|
||||
}
|
||||
|
||||
pub fn block(allocator: std.mem.Allocator, node: Node) ![]const u8 {
|
||||
const style = "font-family: Monospace; background: #1e1e1e; color: #d4d4d4; padding: 12px; border-radius: 8px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);";
|
||||
|
||||
return if (node.meta) |meta|
|
||||
std.fmt.allocPrint(allocator,
|
||||
\\<pre class="language-{s}" style="{s}"><code>{s}</code></pre>
|
||||
, .{ meta, style, node.content })
|
||||
else
|
||||
std.fmt.allocPrint(allocator,
|
||||
\\<pre style="{s}"><code>{s}</code></pre>
|
||||
, .{ style, node.content });
|
||||
}
|
||||
|
||||
pub fn link(allocator: std.mem.Allocator, node: Node) ![]const u8 {
|
||||
return std.fmt.allocPrint(allocator,
|
||||
\\<a href="{s}" style="color: #1a73e8; text-decoration: none; font-weight: bold; transition: color 0.3s ease;">{s}</a>
|
||||
, .{ node.href.?, node.title.? });
|
||||
}
|
||||
|
||||
pub fn image(allocator: std.mem.Allocator, node: Node) ![]const u8 {
|
||||
return std.fmt.allocPrint(allocator,
|
||||
\\<img src="{s}" title="{s}" style="border: 4px solid #ccc; border-radius: 8px; max-width: 100%; height: auto;" />
|
||||
, .{ node.href.?, node.title.? });
|
||||
}
|
||||
|
||||
pub const h1 = .{ "<h1 style=\"font-family: 'Segoe UI', sans-serif; color: #333; border-bottom: 2px solid #ccc; padding-bottom: 10px;\">", "</h1>\n" };
|
||||
pub const h2 = .{ "<h2 style=\"font-family: 'Segoe UI', sans-serif; color: #444; border-bottom: 1px solid #ddd; padding-bottom: 8px;\">", "</h2>\n" };
|
||||
pub const h3 = .{ "<h3 style=\"font-family: 'Segoe UI', sans-serif; color: #555; padding-bottom: 6px;\">", "</h3>\n" };
|
||||
pub const h4 = .{ "<h4 style=\"font-family: 'Segoe UI', sans-serif; color: #666; padding-bottom: 4px;\">", "</h4>\n" };
|
||||
pub const h5 = .{ "<h5 style=\"font-family: 'Segoe UI', sans-serif; color: #777; padding-bottom: 2px;\">", "</h5>\n" };
|
||||
pub const h6 = .{ "<h6 style=\"font-family: 'Segoe UI', sans-serif; color: #888; padding-bottom: 2px;\">", "</h6>\n" };
|
||||
pub const bold = .{ "<b style=\"font-weight: 600;\">", "</b>" };
|
||||
pub const italic = .{ "<i style=\"font-style: italic;\">", "</i>" };
|
||||
pub const unordered_list = .{ "<ul style=\"list-style-type: disc; margin-left: 20px;\">", "</ul>" };
|
||||
pub const ordered_list = .{ "<ol style=\"list-style-type: decimal; margin-left: 20px;\">", "</ol>" };
|
||||
pub const list_item = .{ "<li style=\"margin-bottom: 5px;\">", "</li>" };
|
||||
pub const code = .{ "<span style=\"font-family: Monospace; background: #f4f4f4; padding: 2px 5px; border-radius: 4px;\">", "</span>" };
|
||||
pub const paragraph = .{ "\n<p style=\"line-height: 1.6; font-size: 1.1rem; color: #333;\">", "</p>\n" };
|
||||
pub const default = .{ "", "" };
|
||||
pub const Tag = struct {
|
||||
id: i32,
|
||||
name: []const u8,
|
||||
emoji: ?[]const u8,
|
||||
created_at: jetzig.jetquery.DateTime,
|
||||
updated_at: jetzig.jetquery.DateTime,
|
||||
};
|
||||
|
||||
/// Escape HTML entities.
|
||||
pub fn escape(allocator: std.mem.Allocator, input: []const u8) ![]const u8 {
|
||||
const replacements = .{
|
||||
.{ "&", "&" },
|
||||
.{ "<", "<" },
|
||||
.{ ">", ">" },
|
||||
};
|
||||
pub const Blog = struct {
|
||||
id: i32,
|
||||
title: []const u8,
|
||||
content: ?[]const u8,
|
||||
blob: []const u8,
|
||||
created_at: jetzig.jetquery.DateTime,
|
||||
updated_at: jetzig.jetquery.DateTime,
|
||||
tags: ?[]const Tag,
|
||||
};
|
||||
|
||||
var output = input;
|
||||
inline for (replacements) |replacement| {
|
||||
output = try std.mem.replaceOwned(u8, allocator, output, replacement[0], replacement[1]);
|
||||
pub fn fetchBlogs(allocator: std.mem.Allocator, req: *jetzig.Request) ![]Blog {
|
||||
const tag_query = jetzig.database.Query(.Tag)
|
||||
.select(.{})
|
||||
.include(.blog_tags, .{});
|
||||
|
||||
var blogs: std.AutoArrayHashMapUnmanaged(i32, Blog) = .empty;
|
||||
const tags = try req.repo.all(tag_query);
|
||||
|
||||
for (tags) |tag| {
|
||||
for (tag.blog_tags) |blog_tag| {
|
||||
// fetch blog
|
||||
const blog = try fetchBlog(allocator, req, blog_tag.blog_id);
|
||||
try blogs.put(allocator, blog.id, blog);
|
||||
}
|
||||
}
|
||||
return output;
|
||||
|
||||
return blogs.values();
|
||||
}
|
||||
|
||||
pub fn fetchBlog(allocator: std.mem.Allocator, req: *jetzig.Request, id: i32) !Blog {
|
||||
const query = jetzig.database.Query(.Blog).find(id);
|
||||
const blog = try req.repo.execute(query) orelse return error.BlogNotFound;
|
||||
|
||||
return .{
|
||||
.id = blog.id,
|
||||
.title = blog.title,
|
||||
.content = blog.content,
|
||||
.blob = blog.blob,
|
||||
.created_at = blog.created_at,
|
||||
.updated_at = blog.updated_at,
|
||||
.tags = blk: {
|
||||
const blog_tag_query = jetzig.database.Query(.BlogTag)
|
||||
.findBy(.{ .blog_id = blog.id });
|
||||
if (try req.repo.execute(blog_tag_query)) |blog_tag| {
|
||||
var found: std.ArrayList(Tag) = .init(allocator);
|
||||
|
||||
const tag_query = jetzig.database.Query(.Tag)
|
||||
.where(.{ .id = blog_tag.tag_id });
|
||||
|
||||
const tags = try req.repo.all(tag_query);
|
||||
|
||||
for (tags) |tag| {
|
||||
try found.append(.{
|
||||
.id = tag.id,
|
||||
.name = tag.name,
|
||||
.emoji = tag.emoji,
|
||||
.created_at = tag.created_at,
|
||||
.updated_at = tag.updated_at,
|
||||
});
|
||||
}
|
||||
|
||||
break :blk try found.toOwnedSlice();
|
||||
} else {
|
||||
break :blk null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fetchTag(id: i32, req: *jetzig.Request) !Tag {
|
||||
const query = jetzig.database.Query(.Tag)
|
||||
.find(id);
|
||||
const tag = try req.repo.execute(query) orelse return error.TagNotFound;
|
||||
|
||||
return .{
|
||||
.id = tag.id,
|
||||
.name = tag.name,
|
||||
.emoji = tag.emoji,
|
||||
.created_at = tag.created_at,
|
||||
.updated_at = tag.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
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 root = try request.data(.object);
|
||||
|
||||
const blogs = try fetchBlogs(request.allocator, request);
|
||||
|
||||
try root.put("blogs", blogs);
|
||||
|
||||
const cookies = try request.cookies();
|
||||
@ -94,36 +122,25 @@ pub fn index(request: *jetzig.Request) !jetzig.View {
|
||||
}
|
||||
|
||||
pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
|
||||
const query = jetzig.database.Query(.Blog).find(id);
|
||||
const blog = try request.repo.execute(query) orelse return request.fail(.not_found);
|
||||
|
||||
const Blog = struct {
|
||||
id: i32,
|
||||
title: []const u8,
|
||||
content: ?[]const u8,
|
||||
created_at: jetzig.jetquery.DateTime,
|
||||
updated_at: jetzig.jetquery.DateTime,
|
||||
};
|
||||
|
||||
var zmd = Zmd.init(request.allocator);
|
||||
defer zmd.deinit();
|
||||
|
||||
var blog = try fetchBlog(
|
||||
request.allocator,
|
||||
request,
|
||||
try std.fmt.parseInt(i32, id, 10),
|
||||
);
|
||||
|
||||
try zmd.parse(blog.content orelse {
|
||||
return request.fail(.unprocessable_entity);
|
||||
});
|
||||
|
||||
const blob = try zmd.toHtml(Fragments);
|
||||
const blob = try zmd.toHtml(@import("zmd").html.DefaultFragments);
|
||||
|
||||
const eb = Blog{
|
||||
.id = blog.id,
|
||||
.title = blog.title,
|
||||
.content = blob,
|
||||
.created_at = blog.created_at,
|
||||
.updated_at = blog.updated_at,
|
||||
};
|
||||
blog.content = blob;
|
||||
|
||||
var root = try request.data(.object);
|
||||
try root.put("blog", eb);
|
||||
try root.put("blog", blog);
|
||||
|
||||
const cookies = try request.cookies();
|
||||
const allowed = blk: {
|
||||
@ -184,17 +201,51 @@ pub fn post(request: *jetzig.Request) !jetzig.View {
|
||||
return request.fail(.unprocessable_entity);
|
||||
};
|
||||
|
||||
const tag_str = params.getT(.string, "tags") orelse {
|
||||
return request.fail(.unprocessable_entity);
|
||||
};
|
||||
|
||||
try request.repo.insert(.Blog, .{
|
||||
.title = title,
|
||||
.blob = preview,
|
||||
.content = content,
|
||||
});
|
||||
|
||||
var tag_names = std.mem.splitScalar(u8, tag_str, ',');
|
||||
while (tag_names.next()) |tag_name| {
|
||||
// check if tag exists
|
||||
const tag_query = jetzig.database.Query(.Tag)
|
||||
.findBy(.{ .name = tag_name });
|
||||
|
||||
const tag = blk: {
|
||||
break :blk try request.repo.execute(tag_query) orelse {
|
||||
// tag does not exist, create it
|
||||
try request.repo.insert(.Tag, .{ .name = tag_name });
|
||||
break :blk try request.repo.execute(tag_query) orelse {
|
||||
return request.fail(.unprocessable_entity);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
try request.repo.insert(.BlogTag, .{
|
||||
.tag_id = tag.id,
|
||||
.blog_id = blk: {
|
||||
const blog_query = jetzig.database.Query(.Blog)
|
||||
.findBy(.{ .title = title });
|
||||
if (try request.repo.execute(blog_query)) |blog| {
|
||||
break :blk blog.id;
|
||||
} else {
|
||||
return request.fail(.not_found);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return request.redirect("/blogs", .moved_permanently);
|
||||
} else {
|
||||
return request.fail(.not_found);
|
||||
}
|
||||
|
||||
// session is not valid
|
||||
return request.fail(.not_found);
|
||||
}
|
||||
|
||||
pub fn delete(id: []const u8, request: *jetzig.Request) !jetzig.View {
|
||||
@ -244,12 +295,36 @@ pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
|
||||
const blog_query = jetzig.database.Query(.Blog)
|
||||
.find(try std.fmt.parseInt(i32, id, 10));
|
||||
|
||||
const blog = request.repo.execute(blog_query) catch {
|
||||
const blog = try request.repo.execute(blog_query) orelse {
|
||||
return request.fail(.not_found);
|
||||
};
|
||||
|
||||
const root = try request.data(.object);
|
||||
try root.put("allowed", true);
|
||||
|
||||
try root.put("blog_tags", blk: {
|
||||
var tagstring: std.ArrayList(u8) = .init(request.allocator);
|
||||
errdefer tagstring.deinit();
|
||||
|
||||
const blog_tag_query = jetzig.database.Query(.BlogTag)
|
||||
.where(.{ .blog_id = blog.id });
|
||||
|
||||
const blog_tags = try request.repo.all(blog_tag_query);
|
||||
|
||||
for (blog_tags) |blog_tag| {
|
||||
const tag_query = jetzig.database.Query(.Tag)
|
||||
.find(blog_tag.tag_id);
|
||||
|
||||
const tag = try request.repo.execute(tag_query) orelse {
|
||||
return request.fail(.not_found);
|
||||
};
|
||||
|
||||
try tagstring.appendSlice(tag.name);
|
||||
try tagstring.append(',');
|
||||
}
|
||||
|
||||
break :blk tagstring.toOwnedSlice();
|
||||
});
|
||||
try root.put("blog", blog);
|
||||
|
||||
return request.render(.ok);
|
||||
@ -265,12 +340,41 @@ pub fn edit(id: []const u8, request: *jetzig.Request) !jetzig.View {
|
||||
const title = params.getT(.string, "title") orelse blog.title;
|
||||
const content = params.getT(.string, "content") orelse blog.content;
|
||||
const preview = params.getT(.string, "preview") orelse blog.blob;
|
||||
const tags = params.getT(.string, "tags") orelse "";
|
||||
|
||||
// query the blogpost first
|
||||
const blog_query = jetzig.database.Query(.Blog)
|
||||
.update(.{ .title = title, .blob = preview, .content = content })
|
||||
.where(.{ .id = try std.fmt.parseInt(i32, id, 10) });
|
||||
|
||||
const prune = jetzig.database.Query(.BlogTag)
|
||||
.delete()
|
||||
.where(.{ .blog_id = blog.id });
|
||||
|
||||
try request.repo.execute(prune);
|
||||
|
||||
var tag_names = std.mem.splitScalar(u8, tags, ',');
|
||||
while (tag_names.next()) |tag_name| {
|
||||
const tag_query = jetzig.database.Query(.Tag)
|
||||
.findBy(.{ .name = tag_name });
|
||||
|
||||
const tag = blk: {
|
||||
break :blk try request.repo.execute(tag_query) orelse {
|
||||
// tag does not exist, create it
|
||||
try request.repo.insert(.Tag, .{ .name = tag_name });
|
||||
break :blk try request.repo.execute(tag_query) orelse {
|
||||
return request.fail(.unprocessable_entity);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
// insert the tag
|
||||
try request.repo.insert(.BlogTag, .{
|
||||
.tag_id = tag.id,
|
||||
.blog_id = blog.id,
|
||||
});
|
||||
}
|
||||
|
||||
// update the blogpost
|
||||
try request.repo.execute(blog_query);
|
||||
|
||||
|
@ -1,36 +1,44 @@
|
||||
<div class="max-w-2xl mx-auto mt-10 p-6 bg-white border border-win7-border shadow-md rounded-2xl font-sans text-win7-text">
|
||||
<form action="/blogs/{{.blog.id}}/edit" method="POST" class="space-y-6">
|
||||
<div>
|
||||
<form action="/blogs/{{.blog.id}}/edit" method="POST">
|
||||
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium mb-1">Title</label>
|
||||
<label for="title">Title</label>
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
value="{{.blog.title}}"
|
||||
required
|
||||
class="mt-1 w-full px-4 py-2 bg-white border border-win7-border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-win7-hover focus:border-win7-hover transition"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="content" class="block text-sm font-medium mb-1">Content</label>
|
||||
<label for="tags">Tags (comma-separated)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="tags"
|
||||
id="tags"
|
||||
value="{{.blog_tags}}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="content">Content</label>
|
||||
<textarea
|
||||
name="content"
|
||||
id="content"
|
||||
rows="8"
|
||||
required
|
||||
class="mt-1 w-full px-4 py-2 bg-white border border-win7-border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-win7-hover focus:border-win7-hover transition"
|
||||
>{{.blog.content}}</textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="preview" class="block text-sm font-medium mb-1">Preview</label>
|
||||
<label for="preview">Preview</label>
|
||||
<textarea
|
||||
name="preview"
|
||||
id="preview"
|
||||
rows="3"
|
||||
class="mt-1 w-full px-4 py-2 bg-white border border-win7-border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-win7-hover focus:border-win7-hover transition"
|
||||
>{{.blog.blob}}</textarea>
|
||||
</div>
|
||||
|
||||
@ -38,7 +46,6 @@
|
||||
<input
|
||||
type="submit"
|
||||
value="Save Changes"
|
||||
class="px-4 py-2 bg-win7-link text-white font-semibold rounded-md shadow hover:bg-win7-hover transition"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
40
src/app/views/blogs/filter.zmpl
Normal file
40
src/app/views/blogs/filter.zmpl
Normal file
@ -0,0 +1,40 @@
|
||||
<div>
|
||||
@for ($.blogs) |blog| {
|
||||
<div>
|
||||
<a href="/blogs/{{blog.id}}">
|
||||
{{zmpl.fmt.datetime(blog.get("created_at"), "%Y-%m-%d | ")}} {{blog.title}}
|
||||
</a>
|
||||
@if ($.allowed)
|
||||
<div>
|
||||
<a href="/blogs/{{blog.id}}/edit">Edit</a>
|
||||
<button id="delete-post-{{blog.id}}">Delete</button>
|
||||
</div>
|
||||
@end
|
||||
</div>
|
||||
}
|
||||
|
||||
@if ($.allowed)
|
||||
<br>
|
||||
<div><a href="/blogs/new">+ New Blog</a></div>
|
||||
@end
|
||||
</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,15 +1,18 @@
|
||||
<div class="relative w-full px-6 py-12 bg-white shadow-xl shadow-slate-700/10 ring-1 ring-gray-900/5 md:max-w-3xl md:mx-auto lg:max-w-4xl lg:pt-16 lg:pb-28">
|
||||
<h1 class="text-4xl font-bold text-gray-900 mt-6 mb-4" style="text-shadow: 1px 1px 5px rgba(0,0,0,0.1);">{{.blog.title}}</h1>
|
||||
<article class="prose lg:prose-xl">
|
||||
<div>
|
||||
<h1>{{.blog.title}}</h1>
|
||||
<span>Tags:
|
||||
@zig {
|
||||
|
||||
@if ($.blog.tags) |tags|
|
||||
@for (tags) |tag| {
|
||||
<a href="/blogs/tags/{{tag.name}}">{{tag.name}}</a>
|
||||
}
|
||||
@else
|
||||
<a href="/blogs">None</a>
|
||||
@end
|
||||
}
|
||||
</span>
|
||||
<article>
|
||||
{{zmpl.fmt.raw(zmpl.ref("blog.content"))}}
|
||||
</article>
|
||||
|
||||
<button class="fixed bottom-6 right-6 bg-blue-500 text-white p-3 rounded-full shadow-lg hover:bg-blue-600" id="back-to-top">
|
||||
↑
|
||||
</button>
|
||||
|
||||
<script>
|
||||
document.getElementById('back-to-top').addEventListener('click', () => window.scrollTo({ top: 0, behavior: 'smooth' }));
|
||||
</script>
|
||||
|
||||
</article>
|
||||
</div>
|
||||
|
@ -1,58 +1,40 @@
|
||||
@args allowed: bool
|
||||
|
||||
<div class="max-w-3xl mx-auto mt-10 space-y-6 font-sans">
|
||||
@for (.blogs) |blog| {
|
||||
<div class="p4 bg-win7-bg border border-win7-border rounded-xl px-5 py-4 shadow-sm hover:shadow-md transition duration-150">
|
||||
<div class="flex justify-between items-center">
|
||||
<a href="/blogs/{{blog.id}}" class="text-xl font-semibold text-win7-link hover:underline transition">{{blog.title}}</a>
|
||||
|
||||
@if ($.allowed)
|
||||
<div class="flex items-center gap-4">
|
||||
<a href="/blogs/{{blog.id}}/edit"
|
||||
class="text-sm text-win7-link hover:underline transition"
|
||||
>
|
||||
Edit
|
||||
</a>
|
||||
<button
|
||||
id="delete-post-{{blog.id}}"
|
||||
class="text-sm text-win7-red hover:text-red-800 transition"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
@end
|
||||
</div>
|
||||
<p class="text-xs text-[#4a5c75] mt-2">{{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-win7-link text-white rounded-md hover:bg-win7-hover shadow transition duration-150">
|
||||
+ New Blog
|
||||
</a>
|
||||
<div>
|
||||
@for ($.blogs) |blog| {
|
||||
<div>
|
||||
<a href="/blogs/{{blog.id}}">
|
||||
{{zmpl.fmt.datetime(blog.get("created_at"), "%Y-%m-%d | ")}} {{blog.title}}
|
||||
</a>
|
||||
@if ($.allowed)
|
||||
<div>
|
||||
<a href="/blogs/{{blog.id}}/edit">Edit</a>
|
||||
<button id="delete-post-{{blog.id}}">Delete</button>
|
||||
</div>
|
||||
@end
|
||||
@end
|
||||
</div>
|
||||
}
|
||||
|
||||
@if ($.allowed)
|
||||
<br>
|
||||
<div><a href="/blogs/new">+ New Blog</a></div>
|
||||
@end
|
||||
</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>
|
||||
<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,34 +1,41 @@
|
||||
<div class="max-w-2xl mx-auto mt-10 p-6 bg-white border border-win7-border shadow-md rounded-2xl font-sans text-win7-text">
|
||||
<form action="/blogs" method="POST" class="space-y-6">
|
||||
<div>
|
||||
<form action="/blogs" method="POST">
|
||||
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium mb-1">Title</label>
|
||||
<label for="title">Title</label>
|
||||
<input
|
||||
name="title"
|
||||
id="title"
|
||||
required
|
||||
class="mt-1 w-full px-4 py-2 bg-white border border-win7-border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-win7-hover focus:border-win7-hover transition"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="content" class="block text-sm font-medium mb-1">Content</label>
|
||||
<label for="tags">Tags (comma-separated)</label>
|
||||
<input
|
||||
type="text"
|
||||
name="tags"
|
||||
id="tags"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="content">Content</label>
|
||||
<textarea
|
||||
name="content"
|
||||
id="content"
|
||||
rows="8"
|
||||
required
|
||||
class="mt-1 w-full px-4 py-2 bg-white border border-win7-border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-win7-hover focus:border-win7-hover transition"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="preview" class="block text-sm font-medium mb-1">Preview</label>
|
||||
<label for="preview">Preview</label>
|
||||
<textarea
|
||||
name="preview"
|
||||
id="preview"
|
||||
rows="3"
|
||||
class="mt-1 w-full px-4 py-2 bg-white border border-win7-border rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-win7-hover focus:border-win7-hover transition"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@ -36,7 +43,6 @@
|
||||
<input
|
||||
type="submit"
|
||||
value="Publish"
|
||||
class="px-4 py-2 bg-win7-link text-white font-semibold rounded-md shadow hover:bg-win7-hover transition"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -1,3 +1 @@
|
||||
<div>
|
||||
<span>Content goes here</span>
|
||||
</div>
|
||||
<div><span>Content goes here</span></div>
|
||||
|
@ -1,30 +1,21 @@
|
||||
@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" },
|
||||
};
|
||||
const links = .{
|
||||
.{ .href = "https://yuzucchii.xyz", .title = "Home" },
|
||||
.{ .href = "/rss", .title = "RSS" },
|
||||
.{ .href = "https://codeberg.org/yuzu", .title = "Codeberg" },
|
||||
.{ .href = "https://bsky.app/profile/yuzucchii.bsky.social", .title = "Bluesky" },
|
||||
.{ .href = "https://discord.gg/pvraBkepUP", .title = "Discord" },
|
||||
};
|
||||
}
|
||||
|
||||
<footer class="mt-12 bg-win7-bg border-t border-win7-border pt-6 text-center text-sm text-win7-text font-sans">
|
||||
<div class="max-w-xl mx-auto px-4">
|
||||
|
||||
<div class="bg-warnbox-bg text-warnbox-text border border-warnbox-border rounded-md px-4 py-3 mb-6 shadow-inner">
|
||||
<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-win7-link">
|
||||
@zig {
|
||||
<footer>
|
||||
<img src="/cc.png" alt="CC from Code Geass" width="100px" height="100px"/>
|
||||
<p>
|
||||
@zig {
|
||||
inline for (links) |link| {
|
||||
<a href="{{link.href}}" class="hover:underline">{{link.title}}</a>
|
||||
<a href="{{link.href}}">{{link.title}}</a>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-[#4a5c75] pb-5">© 2025 yuzucchii.xyz</p>
|
||||
</div>
|
||||
}
|
||||
© 2025 yuzucchii.xyz
|
||||
</p>
|
||||
</footer>
|
||||
|
@ -10,21 +10,21 @@
|
||||
<meta property="og:site_name" content="yuzucchii.xyz" />
|
||||
|
||||
@if ($.title) |t|
|
||||
<meta name="og:title" property="og:title" content="{{t}}" />
|
||||
<meta name="og:title" property="og:title" content="{{t}}" />
|
||||
@else
|
||||
<meta name="og:title" property="og:title" content="yuzucchii.xyz" />
|
||||
<meta name="og:title" property="og:title" content="yuzucchii.xyz" />
|
||||
@end
|
||||
|
||||
@if ($.image) |img|
|
||||
<meta name="og:image" content="{{img}}" />
|
||||
<meta property="twitter:card" content="{{img}}" />
|
||||
<meta name="twitter:image:src" property="twitter:image:src" content="{{img}}" />
|
||||
<meta name="twitter:image" property="twitter:image" content="{{img}}" />
|
||||
<meta name="og:image:alt" property="og:image:alt" content="alt text for your image" />
|
||||
<meta name="og:image" content="{{img}}" />
|
||||
<meta property="twitter:card" content="{{img}}" />
|
||||
<meta name="twitter:image:src" property="twitter:image:src" content="{{img}}" />
|
||||
<meta name="twitter:image" property="twitter:image" content="{{img}}" />
|
||||
<meta name="og:image:alt" property="og:image:alt" content="alt text for your image" />
|
||||
@end
|
||||
|
||||
@if ($.description) |desc|
|
||||
<meta name="description" content="{{desc}}" />
|
||||
<meta property="og:description" content="{{desc}}" />
|
||||
<meta name="twitter:description" property="twitter:description" content="{{desc}}" />
|
||||
<meta name="description" content="{{desc}}" />
|
||||
<meta property="og:description" content="{{desc}}" />
|
||||
<meta name="twitter:description" property="twitter:description" content="{{desc}}" />
|
||||
@end
|
||||
|
20
src/app/views/layouts/minimal.zmpl
Normal file
20
src/app/views/layouts/minimal.zmpl
Normal file
@ -0,0 +1,20 @@
|
||||
|
||||
@args allowed: bool, description: ?[]const u8, title: ?[]const u8, img: ?[]const u8
|
||||
|
||||
<html>
|
||||
<head>
|
||||
@partial layouts/metadata
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/styles.css" type="text/css">
|
||||
<title>yuzucchii.xyz</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<main>
|
||||
{{zmpl.content}}
|
||||
</main>
|
||||
</div>
|
||||
@partial layouts/footer
|
||||
</body>
|
||||
</html>
|
||||
|
@ -1,44 +1,36 @@
|
||||
@args allowed: bool, description: ?[]const u8, title: ?[]const u8, img: ?[]const u8
|
||||
|
||||
<html>
|
||||
<head>
|
||||
@partial layouts/metadata
|
||||
<head>
|
||||
@partial layouts/metadata
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="stylesheet" href="/new-styles.css" />
|
||||
<link rel="stylesheet" href="/styles.css" type="text/css">
|
||||
<title>yuzucchii.xyz</title>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="bg-win7-bg shadow-sm border-b border-win7-border py-4 font-sans">
|
||||
<div class="max-w-4xl mx-auto text-center">
|
||||
<div class="text-2xl font-bold text-win7-text mb-2">
|
||||
<a href="/" class="hover:text-win7-hover transition">yuzucchii.xyz</a>
|
||||
</div>
|
||||
<div class="flex flex-wrap justify-center gap-4 text-sm sm:text-base text-win7-link">
|
||||
<a href="/blogs" class="hover:underline">Blog</a>
|
||||
<a href="/about-me" class="hover:underline">About Me</a>
|
||||
@if ($.allowed)
|
||||
<a href="/logout" class="hover:underline">Logout</a>
|
||||
@else
|
||||
<a href="/login" class="hover:underline">Login</a>
|
||||
@end
|
||||
<a href="https://git.yuzucchii.xyz" class="hover:underline" rel="noopener">Gitea</a>
|
||||
<a href="https://watcharr.yuzucchii.xyz" class="hover:underline">Watcharr</a>
|
||||
<a href="/rss" class="hover:underline">RSS</a>
|
||||
<a href="https://searx.yuzucchii.xyz" class="hover:underline">SearX</a>
|
||||
<a href="/files" class="text-win7-red hover:underline">Files</a>
|
||||
<a href="mailto:me@yuzucchii.xyz" class="hover:underline">E-mail</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<nav>
|
||||
<div>
|
||||
<a href="/">Home</a>
|
||||
<a href="/blogs">Blog</a>
|
||||
<a href="/about-me">About Me</a>
|
||||
@if ($.allowed)
|
||||
<a href="/logout">Logout</a>
|
||||
@end
|
||||
<a href="https://git.yuzucchii.xyz">Gitea</a>
|
||||
<a href="/rss">RSS</a>
|
||||
<a href="mailto:me@yuzucchii.xyz">E-mail</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
<!-- Page Content -->
|
||||
<main class="max-w-3xl mx-auto px-4 mt-8">
|
||||
<h1 class="text-3xl font-bold mb-6">Hello, I like doing things.</h1>
|
||||
{{zmpl.content}}
|
||||
</main>
|
||||
<!-- Page Content -->
|
||||
<main>
|
||||
{{zmpl.content}}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@partial layouts/footer
|
||||
</body>
|
||||
@partial layouts/footer
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
@ -1,35 +1,28 @@
|
||||
<div class="min-h-screen flex items-center justify-center bg-win7-bg font-sans text-win7-text">
|
||||
|
||||
<form id="login-form" class="bg-white p-8 border border-win7-border rounded-2xl shadow-xl w-full max-w-sm space-y-6">
|
||||
|
||||
<h1 class="text-xl font-semibold text-center mb-2 text-win7-link">Admin Login</h1>
|
||||
<div>
|
||||
<form id="login-form">
|
||||
<h1>Admin Login</h1>
|
||||
|
||||
<div>
|
||||
<label for="username" class="block text-sm font-medium mb-1">Username</label>
|
||||
<label for="username">Username</label>
|
||||
<input
|
||||
id="username"
|
||||
name="username"
|
||||
type="text"
|
||||
required
|
||||
class="w-full px-4 py-2 border border-win7-border rounded-md shadow-sm bg-white focus:outline-none focus:ring-2 focus:ring-win7-hover focus:border-win7-hover transition"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium mb-1">Discrete password</label>
|
||||
<label for="password">Discrete password</label>
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
class="w-full px-4 py-2 border border-win7-border rounded-md shadow-sm bg-white focus:outline-none focus:ring-2 focus:ring-win7-hover focus:border-win7-hover transition"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-win7-link text-white font-semibold py-2 rounded-md hover:bg-win7-hover shadow transition"
|
||||
>
|
||||
<button type="submit">
|
||||
Login
|
||||
</button>
|
||||
</form>
|
||||
|
@ -1,39 +1,28 @@
|
||||
<div>
|
||||
<button id="logout-button">Logout</button>
|
||||
</div>
|
||||
|
||||
<title>Login panel for admins</title>
|
||||
<body>
|
||||
<div>
|
||||
<button
|
||||
id="logout-button"
|
||||
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"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
||||
<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>
|
||||
<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';
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error during logout:', error);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
@ -1,20 +1,22 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub const layout = "panel";
|
||||
pub const layout = "minimal";
|
||||
|
||||
const Article = struct {
|
||||
title: [255]u8,
|
||||
blob: [255]u8
|
||||
};
|
||||
const Article = struct { title: [255]u8, blob: [255]u8 };
|
||||
|
||||
pub fn index(request: *jetzig.Request, data: *jetzig.Data) !jetzig.View {
|
||||
var root = try data.object();
|
||||
const cookies = try request.cookies();
|
||||
|
||||
const query = jetzig.database.Query(.Blog).orderBy(.{.created_at = .desc});
|
||||
const query = jetzig.database.Query(.Blog).orderBy(.{ .created_at = .desc });
|
||||
|
||||
const tags = try request.repo.all(
|
||||
jetzig.database.Query(.Tag).orderBy(.{ .name = .asc }),
|
||||
);
|
||||
const blogs = try request.repo.all(query);
|
||||
|
||||
try root.put("tags", tags);
|
||||
try root.put("articles", blogs);
|
||||
|
||||
const allowed = blk: {
|
||||
|
@ -1,8 +1,8 @@
|
||||
@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}}">{{title}}</a>
|
||||
<div>
|
||||
{{blob}}
|
||||
</div>
|
||||
<a href="/blogs/{{id}}" class="text-blue-600 underline">Read more</a>
|
||||
<br>
|
||||
</div>
|
||||
|
6
src/app/views/root/_tag_blob.zmpl
Normal file
6
src/app/views/root/_tag_blob.zmpl
Normal file
@ -0,0 +1,6 @@
|
||||
@args name: []const u8, emoji: []const u8
|
||||
<div>
|
||||
<li>
|
||||
<a href="/blogs/tags/{{name}}">{{emoji}} {{name}}</a>
|
||||
</li>
|
||||
</div>
|
@ -1,24 +1,51 @@
|
||||
@args articles: Zmpl.Array
|
||||
<section class="max-w-3xl mx-auto px-4 py-8">
|
||||
<!-- Banner -->
|
||||
<div class="mb-8">
|
||||
<img src="/new-banner.webp" alt="Banner" class="w-full rounded-xl shadow-md">
|
||||
</div>
|
||||
<section>
|
||||
<!-- Links Section -->
|
||||
<ul class="main-menu">
|
||||
<li class="main-menu-item">
|
||||
<a href="/">
|
||||
<img src="/miku1.png" alt="Miku expressive" width="100px" height="100px"/>
|
||||
<p>Home</p>
|
||||
</a>
|
||||
</li>
|
||||
<li class="main-menu-item">
|
||||
<a href="/about-me">
|
||||
<img src="/miku2.png" alt="Miku cat" width="100px" height="100px"/>
|
||||
<p>About</p>
|
||||
</a>
|
||||
<li class="main-menu-item">
|
||||
<a href="/blogs">
|
||||
<img src="/miku3.png" alt="Miku blogger" width="100px" height="100px"/>
|
||||
<p>Blogs</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- Introduction -->
|
||||
<p class="text-lg leading-relaxed mb-10 text-gray-700">
|
||||
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>
|
||||
<!-- Introduction -->
|
||||
<p>
|
||||
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>
|
||||
|
||||
<!-- Articles Section -->
|
||||
<h2 class="text-2xl font-semibold mb-4 border-b pb-2">Latest Articles</h2>
|
||||
<!-- Articles Section -->
|
||||
<h2>Latest Articles</h2>
|
||||
|
||||
<div class="space-y-6">
|
||||
@for (.articles) |article| {
|
||||
<div class="p-4 border rounded-xl bg-white shadow-sm hover:shadow transition">
|
||||
@partial root/article_blob(title: article.title, blob: article.blob, id: article.id)
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@for (.tags) |tag| {
|
||||
@if ($.tag.emoji) |emoji|
|
||||
<div>
|
||||
@partial root/tag_blob(name: tag.name, emoji: emoji)
|
||||
</div>
|
||||
@else
|
||||
<div>
|
||||
@partial root/tag_blob(name: tag.name, emoji: "")
|
||||
</div>
|
||||
@end
|
||||
}
|
||||
|
||||
@for (.articles) |article| {
|
||||
@partial root/article_blob(title: article.title, blob: article.blob, id: article.id)
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
|
157
src/app/views/tags.zig
Normal file
157
src/app/views/tags.zig
Normal file
@ -0,0 +1,157 @@
|
||||
const std = @import("std");
|
||||
const jetzig = @import("jetzig");
|
||||
|
||||
pub const layout = "panel";
|
||||
|
||||
const Blog = @import("blogs.zig").Blog;
|
||||
const Tag = @import("blogs.zig").Tag;
|
||||
|
||||
pub fn filterBlogs(allocator: std.mem.Allocator, req: *jetzig.Request, tag_str: []const u8) ![]Blog {
|
||||
var blogs: std.AutoArrayHashMapUnmanaged(i32, Blog) = .empty;
|
||||
var tag_names = std.mem.splitScalar(u8, tag_str, ',');
|
||||
|
||||
while (tag_names.next()) |tag_name| {
|
||||
const query = jetzig.database.Query(.Tag)
|
||||
.findBy(.{ .name = tag_name })
|
||||
.include(.blog_tags, .{});
|
||||
|
||||
const tag = try req.repo.execute(query) orelse continue;
|
||||
for (tag.blog_tags) |blog_tag| {
|
||||
const blog = try @import("blogs.zig").fetchBlog(allocator, req, blog_tag.blog_id);
|
||||
try blogs.put(allocator, blog.id, blog);
|
||||
}
|
||||
}
|
||||
|
||||
std.debug.print("blogs: {any}\n", .{blogs.values()});
|
||||
return blogs.values();
|
||||
}
|
||||
|
||||
pub fn index(request: *jetzig.Request) !jetzig.View {
|
||||
var root = try request.data(.object);
|
||||
|
||||
const query = jetzig.database.Query(.Tag)
|
||||
.orderBy(.{ .name = .asc });
|
||||
const tags = try request.repo.all(query);
|
||||
|
||||
try root.put("tags", tags);
|
||||
|
||||
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);
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
pub fn get(id: []const u8, request: *jetzig.Request) !jetzig.View {
|
||||
var root = try request.data(.object);
|
||||
|
||||
const blogs =
|
||||
try filterBlogs(request.allocator, request, id);
|
||||
|
||||
try root.put("blogs", blogs);
|
||||
|
||||
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);
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
||||
|
||||
// tags/:id/edit
|
||||
pub fn edit(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);
|
||||
};
|
||||
|
||||
const session_query = jetzig.database.Query(.Session)
|
||||
.findBy(.{ .session_id = session.value });
|
||||
|
||||
_ = request.repo.execute(session_query) catch {
|
||||
return request.fail(.not_found);
|
||||
};
|
||||
|
||||
switch (request.method) {
|
||||
.GET => {
|
||||
// get tag
|
||||
const tag_query = jetzig.database.Query(.Tag)
|
||||
.findBy(.{ .name = id });
|
||||
|
||||
const tag = request.repo.execute(tag_query) catch {
|
||||
return request.fail(.not_found);
|
||||
};
|
||||
|
||||
const root = try request.data(.object);
|
||||
try root.put("allowed", true);
|
||||
try root.put("tag", tag);
|
||||
|
||||
return request.render(.ok);
|
||||
},
|
||||
.POST => {
|
||||
const params = try request.params();
|
||||
|
||||
const tag_query_original = jetzig.database.Query(.Tag)
|
||||
.findBy(.{ .name = id });
|
||||
|
||||
if (try request.repo.execute(tag_query_original)) |tag| {
|
||||
const name = params.getT(.string, "name") orelse tag.name;
|
||||
const emoji = params.getT(.string, "emoji") orelse tag.emoji;
|
||||
|
||||
const tag_query = jetzig.database.Query(.Tag)
|
||||
.update(.{ .name = name, .emoji = emoji })
|
||||
.where(.{ .id = tag.id });
|
||||
|
||||
try request.repo.execute(tag_query);
|
||||
|
||||
return request.redirect("/tags", .moved_permanently);
|
||||
} else {
|
||||
return request.fail(.not_found);
|
||||
}
|
||||
},
|
||||
else => return request.fail(.not_found),
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
};
|
||||
const session_query = jetzig.database.Query(.Session)
|
||||
.findBy(.{ .session_id = session.value });
|
||||
_ = request.repo.execute(session_query) catch {
|
||||
return request.fail(.not_found);
|
||||
};
|
||||
|
||||
const tag_query = jetzig.database.Query(.Tag)
|
||||
.findBy(.{ .name = id });
|
||||
|
||||
if (try request.repo.execute(tag_query)) |tag| {
|
||||
try request.repo.Query(.BlogTag)
|
||||
.deleteAll()
|
||||
//.where(.{ .tag_id = tag.id })
|
||||
.execute(request.repo);
|
||||
|
||||
try request.repo.delete(tag);
|
||||
}
|
||||
|
||||
return request.render(.ok);
|
||||
}
|
34
src/app/views/tags/edit.zmpl
Normal file
34
src/app/views/tags/edit.zmpl
Normal file
@ -0,0 +1,34 @@
|
||||
<div>
|
||||
<form action="/tags/{{.tag.name}}/edit" method="POST">
|
||||
|
||||
<div>
|
||||
<label for="name">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
value="{{.tag.name}}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="emoji">Emoji</label>
|
||||
<input
|
||||
type="text"
|
||||
name="emoji"
|
||||
id="emoji"
|
||||
value="{{.tag.emoji}}"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<input
|
||||
type="submit"
|
||||
value="Save Changes"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
40
src/app/views/tags/get.zmpl
Normal file
40
src/app/views/tags/get.zmpl
Normal file
@ -0,0 +1,40 @@
|
||||
<div>
|
||||
@for ($.blogs) |blog| {
|
||||
<div>
|
||||
<a href="/blogs/{{blog.id}}">
|
||||
{{zmpl.fmt.datetime(blog.get("created_at"), "%Y-%m-%d | ")}} {{blog.title}}
|
||||
</a>
|
||||
@if ($.allowed)
|
||||
<div>
|
||||
<a href="/blogs/{{blog.id}}/edit">Edit</a>
|
||||
<button id="delete-post-{{blog.id}}">Delete</button>
|
||||
</div>
|
||||
@end
|
||||
</div>
|
||||
}
|
||||
|
||||
@if ($.allowed)
|
||||
<br>
|
||||
<div><a href="/blogs/new">+ New Blog</a></div>
|
||||
@end
|
||||
</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
|
37
src/app/views/tags/index.zmpl
Normal file
37
src/app/views/tags/index.zmpl
Normal file
@ -0,0 +1,37 @@
|
||||
@for (.tags) |tag| {
|
||||
@if ($.tag.emoji) |emoji|
|
||||
<div>
|
||||
@partial root/tag_blob(name: tag.name, emoji: emoji)
|
||||
</div>
|
||||
@else
|
||||
<div>
|
||||
@partial root/tag_blob(name: tag.name, emoji: "")
|
||||
</div>
|
||||
@end
|
||||
@if ($.allowed)
|
||||
<div>
|
||||
<a href="/tags/{{tag.name}}/edit">Edit</a>
|
||||
<button id="delete-tag-{{tag.name}}">Delete</button>
|
||||
</div>
|
||||
@end
|
||||
}
|
||||
|
||||
@if ($.allowed)
|
||||
<script>
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.querySelectorAll('[id^="delete-tag-"]').forEach(button => {
|
||||
button.addEventListener("click", () => {
|
||||
const name = button.id.replace("delete-tag-", "");
|
||||
fetch(`/tags/${name}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
})
|
||||
.then(() => window.location.reload())
|
||||
.catch(err => console.error("Failed to delete tag:", err));
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@end
|
@ -105,8 +105,11 @@ pub const jetzig_options = struct {
|
||||
|
||||
pub fn init(app: *jetzig.App) !void {
|
||||
app.route(.GET, "/rss.xml", @import("app/views/rss.zig"), .index);
|
||||
app.route(.GET, "/blogs/:id/edit" , @import("app/views/blogs.zig"), .edit);
|
||||
app.route(.POST, "/blogs/:id/edit" , @import("app/views/blogs.zig"), .edit);
|
||||
app.route(.GET, "/blogs/:id/edit", @import("app/views/blogs.zig"), .edit);
|
||||
app.route(.POST, "/blogs/:id/edit", @import("app/views/blogs.zig"), .edit);
|
||||
app.route(.GET, "/tags/:id/edit", @import("app/views/tags.zig"), .edit);
|
||||
app.route(.POST, "/tags/:id/edit", @import("app/views/tags.zig"), .edit);
|
||||
app.route(.GET, "/blogs/tags/:id", @import("app/views/tags.zig"), .get);
|
||||
}
|
||||
|
||||
pub fn main() !void {
|
||||
|
@ -1,72 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@plugin "@tailwindcss/typography";
|
||||
@config "../tailwind.config.js";
|
||||
|
||||
/* Base typography improvements */
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-white text-gray-800 antialiased leading-relaxed;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
plugins: [require('@tailwindcss/typography')],
|
||||
content: ["./src/app/views/**/*.{zmpl,html,js}", "./src/main.zig"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
win7: {
|
||||
bg: '#eaf3ff', // Background base
|
||||
border: '#b5cde4', // Borders (like nav/footer lines)
|
||||
text: '#003c75', // Base text (e.g., headings)
|
||||
link: '#004f9f', // Link color
|
||||
red: '#cc0000', // Accent red (e.g., for "Files")
|
||||
hover: '#005dc1', // Link hover
|
||||
},
|
||||
warnbox: {
|
||||
bg: '#fff8d8', // Background of JS-free box
|
||||
text: '#7a5b00', // Text for warning
|
||||
border: '#f5e8a3', // Border for warning
|
||||
},
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['"Segoe UI"', 'Tahoma', 'Geneva', 'Verdana', 'sans-serif'],
|
||||
},
|
||||
},
|
||||
}
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user