MaCH repo
This commit is contained in:
11
12_rails_15_min_blog_external/400.mustache.html
Normal file
11
12_rails_15_min_blog_external/400.mustache.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{{< layout}}
|
||||
{{$body}}
|
||||
<div class="text-center py-20">
|
||||
<h1 class="text-4xl font-bold text-gray-200">Validation Error</h1>
|
||||
<p class="text-red-500 mt-4">Please check your input and try again.</p>
|
||||
<a href="{{url:posts}}" class="text-blue-600 hover:underline mt-4 inline-block">
|
||||
← Back to all posts
|
||||
</a>
|
||||
</div>
|
||||
{{/body}}
|
||||
{{/layout}}
|
||||
11
12_rails_15_min_blog_external/404.mustache.html
Normal file
11
12_rails_15_min_blog_external/404.mustache.html
Normal file
@@ -0,0 +1,11 @@
|
||||
{{< layout}}
|
||||
{{$body}}
|
||||
<div class="text-center py-20">
|
||||
<h1 class="text-6xl font-bold text-gray-200">404</h1>
|
||||
<p class="text-gray-500 mt-4">Post not found.</p>
|
||||
<a href="{{url:posts}}" class="text-blue-600 hover:underline mt-4 inline-block">
|
||||
← Back to all posts
|
||||
</a>
|
||||
</div>
|
||||
{{/body}}
|
||||
{{/layout}}
|
||||
6
12_rails_15_min_blog_external/create_posts.sql
Normal file
6
12_rails_15_min_blog_external/create_posts.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
CREATE TABLE posts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
title TEXT NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%d %H:%M', 'now'))
|
||||
);
|
||||
2
12_rails_15_min_blog_external/delete_post.sql
Normal file
2
12_rails_15_min_blog_external/delete_post.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
DELETE FROM posts
|
||||
WHERE id = {{id}};
|
||||
16
12_rails_15_min_blog_external/edit_post.mustache.html
Normal file
16
12_rails_15_min_blog_external/edit_post.mustache.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{{< layout}}
|
||||
{{$body}}
|
||||
{{#post}}
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-8">Edit Post</h1>
|
||||
|
||||
<form method="post" action="{{url:post:id}}">
|
||||
<input type="hidden" name="http_method" value="patch">
|
||||
{{> post_form}}
|
||||
</form>
|
||||
|
||||
<a href="{{url:post:id}}" class="text-gray-500 hover:text-gray-700 mt-4 inline-block">
|
||||
← Back to post
|
||||
</a>
|
||||
{{/post}}
|
||||
{{/body}}
|
||||
{{/layout}}
|
||||
3
12_rails_15_min_blog_external/get_post.sql
Normal file
3
12_rails_15_min_blog_external/get_post.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
SELECT id, title, body, created_at
|
||||
FROM posts
|
||||
WHERE id = {{id}};
|
||||
3
12_rails_15_min_blog_external/get_posts.sql
Normal file
3
12_rails_15_min_blog_external/get_posts.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
SELECT id, title, body, created_at
|
||||
FROM posts
|
||||
ORDER BY created_at DESC;
|
||||
2
12_rails_15_min_blog_external/insert_post.sql
Normal file
2
12_rails_15_min_blog_external/insert_post.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
INSERT INTO posts (title, body)
|
||||
VALUES ({{title}}, {{body}});
|
||||
25
12_rails_15_min_blog_external/layout.mustache.html
Normal file
25
12_rails_15_min_blog_external/layout.mustache.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>MACH Blog</title>
|
||||
{{> tailwind_script}}
|
||||
</head>
|
||||
<body class="bg-gray-50 min-h-screen">
|
||||
<nav class="bg-white border-b border-gray-200">
|
||||
<div class="max-w-3xl mx-auto px-6 py-4 flex items-center justify-between">
|
||||
<a href="{{url:posts}}" class="text-xl font-bold text-gray-900 hover:text-blue-600 transition">
|
||||
MACH Blog
|
||||
</a>
|
||||
<span class="text-xs text-gray-400">built in 15 minutes</span>
|
||||
</div>
|
||||
</nav>
|
||||
<main class="max-w-3xl mx-auto px-6 py-10">
|
||||
{{$body}}{{/body}}
|
||||
</main>
|
||||
<footer class="max-w-3xl mx-auto px-6 py-6 text-center text-xs text-gray-300">
|
||||
Powered by MACH
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
205
12_rails_15_min_blog_external/main.c
Normal file
205
12_rails_15_min_blog_external/main.c
Normal file
@@ -0,0 +1,205 @@
|
||||
#include <mach.h>
|
||||
#include <sqlite.h>
|
||||
#include <tailwind.h>
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// 15 Minute Blog in MACH
|
||||
// ─────────────────────────────────────────────
|
||||
// Resources:
|
||||
// GET / → redirect to posts
|
||||
// GET /posts → list all posts
|
||||
// POST /posts → create a post
|
||||
// GET /posts/new → new post form
|
||||
// GET /posts/:id → show a post
|
||||
// PATCH /posts/:id → update a post
|
||||
// DEL /posts/:id → delete a post
|
||||
// GET /posts/:id/edit→ edit post form
|
||||
// ─────────────────────────────────────────────
|
||||
|
||||
config mach() {
|
||||
return (config) {
|
||||
|
||||
// ── Resources ──────────────────────────────
|
||||
|
||||
.resources = {{
|
||||
|
||||
{"home", "/",
|
||||
.get = {{
|
||||
redirect((u){"posts"})
|
||||
}}
|
||||
},
|
||||
|
||||
{"posts", "/posts",
|
||||
.get = {{
|
||||
query((d){
|
||||
.set = "posts",
|
||||
.db = "blog_db",
|
||||
(asset){
|
||||
#embed "get_posts.sql"
|
||||
}
|
||||
}),
|
||||
render((r){(asset){
|
||||
#embed "posts.mustache.html"
|
||||
}})
|
||||
}},
|
||||
|
||||
.post = {{
|
||||
validate((v){
|
||||
.name = "title",
|
||||
.validation = "^.{1,200}$",
|
||||
.message = "Title is required (max 200 characters)"
|
||||
}),
|
||||
validate((v){
|
||||
.name = "body",
|
||||
.validation = "^[\\s\\S]{1,50000}$",
|
||||
.message = "Body is required"
|
||||
}),
|
||||
query((d){
|
||||
.db = "blog_db",
|
||||
(asset){
|
||||
#embed "insert_post.sql"
|
||||
}
|
||||
}),
|
||||
redirect((u){"posts"})
|
||||
}}
|
||||
},
|
||||
|
||||
{"new_post", "/posts/new",
|
||||
.get = {{
|
||||
render((r){(asset){
|
||||
#embed "new_post.mustache.html"
|
||||
}})
|
||||
}}
|
||||
},
|
||||
|
||||
{"post", "/posts/:id",
|
||||
.get = {{
|
||||
find((d){
|
||||
.set = "post",
|
||||
.db = "blog_db",
|
||||
(asset){
|
||||
#embed "get_post.sql"
|
||||
}
|
||||
}),
|
||||
render((r){(asset){
|
||||
#embed "post_show.mustache.html"
|
||||
}})
|
||||
}},
|
||||
|
||||
.patch = {{
|
||||
validate((v){
|
||||
.name = "title",
|
||||
.validation = "^.{1,200}$",
|
||||
.message = "Title is required (max 200 characters)"
|
||||
}),
|
||||
validate((v){
|
||||
.name = "body",
|
||||
.validation = "^[\\s\\S]{1,50000}$",
|
||||
.message = "Body is required"
|
||||
}),
|
||||
query((d){
|
||||
.db = "blog_db",
|
||||
(asset){
|
||||
#embed "update_post.sql"
|
||||
}
|
||||
}),
|
||||
redirect((u){"post"})
|
||||
}},
|
||||
|
||||
.delete = {{
|
||||
query((d){
|
||||
.db = "blog_db",
|
||||
(asset){
|
||||
#embed "delete_post.sql"
|
||||
}
|
||||
}),
|
||||
redirect((u){"posts"})
|
||||
}},
|
||||
|
||||
.before = {
|
||||
validate((v){
|
||||
.name = "id",
|
||||
.validation = "^\\d{1,10}$",
|
||||
.message = "Invalid post ID"
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
{"edit_post", "/posts/:id/edit",
|
||||
.get = {{
|
||||
find((d){
|
||||
.set = "post",
|
||||
.db = "blog_db",
|
||||
(asset){
|
||||
#embed "get_post.sql"
|
||||
}
|
||||
}),
|
||||
render((r){(asset){
|
||||
#embed "edit_post.mustache.html"
|
||||
}})
|
||||
}},
|
||||
|
||||
.before = {
|
||||
validate((v){
|
||||
.name = "id",
|
||||
.validation = "^\\d{1,10}$",
|
||||
.message = "Invalid post ID"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
// ── Shared Layout & Partials ───────────────
|
||||
|
||||
.context = {
|
||||
{"layout", (asset){
|
||||
#embed "layout.mustache.html"
|
||||
}},
|
||||
{"post_card", (asset){
|
||||
#embed "post_card.mustache.html"
|
||||
}},
|
||||
{"post_form", (asset){
|
||||
#embed "post_form.mustache.html"
|
||||
}}
|
||||
},
|
||||
|
||||
// ── Error Handling ─────────────────────────
|
||||
|
||||
.errors = {
|
||||
{http_not_found, {
|
||||
render((r){(asset){
|
||||
#embed "404.mustache.html"
|
||||
}})
|
||||
}},
|
||||
{http_bad_request, {
|
||||
render((r){
|
||||
.status = http_bad_request,
|
||||
(asset){
|
||||
#embed "400.mustache.html"
|
||||
}
|
||||
})
|
||||
}}
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
// ── Database ───────────────────────────────
|
||||
|
||||
.databases = {{
|
||||
.engine = sqlite_db,
|
||||
.name = "blog_db",
|
||||
.connect = "file:blog.db?mode=rwc",
|
||||
.migrations = {(asset){
|
||||
#embed "create_posts.sql"
|
||||
}},
|
||||
.seeds = {(asset){
|
||||
#embed "seed_posts.sql"
|
||||
}}
|
||||
}},
|
||||
|
||||
// ── Modules ────────────────────────────────
|
||||
|
||||
.modules = {sqlite, tailwind}
|
||||
};
|
||||
}
|
||||
13
12_rails_15_min_blog_external/new_post.mustache.html
Normal file
13
12_rails_15_min_blog_external/new_post.mustache.html
Normal file
@@ -0,0 +1,13 @@
|
||||
{{< layout}}
|
||||
{{$body}}
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-8">New Post</h1>
|
||||
|
||||
<form method="post" action="{{url:posts}}">
|
||||
{{> post_form}}
|
||||
</form>
|
||||
|
||||
<a href="{{url:posts}}" class="text-gray-500 hover:text-gray-700 mt-4 inline-block">
|
||||
← Back to posts
|
||||
</a>
|
||||
{{/body}}
|
||||
{{/layout}}
|
||||
9
12_rails_15_min_blog_external/post_card.mustache.html
Normal file
9
12_rails_15_min_blog_external/post_card.mustache.html
Normal file
@@ -0,0 +1,9 @@
|
||||
<article class="bg-white rounded-xl shadow-sm border border-gray-100 p-6 mb-4 hover:shadow-md transition">
|
||||
<a href="{{url:post:id}}" class="block">
|
||||
<h2 class="text-xl font-semibold text-gray-900 hover:text-blue-600 transition">
|
||||
{{title}}
|
||||
</h2>
|
||||
<p class="text-sm text-gray-400 mt-1">{{created_at}}</p>
|
||||
<p class="text-gray-600 mt-3 line-clamp-2">{{body}}</p>
|
||||
</a>
|
||||
</article>
|
||||
19
12_rails_15_min_blog_external/post_form.mustache.html
Normal file
19
12_rails_15_min_blog_external/post_form.mustache.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<div class="space-y-6">
|
||||
<div>
|
||||
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">Title</label>
|
||||
<input type="text" id="title" name="title"
|
||||
value="{{title}}"
|
||||
placeholder="Your post title"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition">
|
||||
</div>
|
||||
<div>
|
||||
<label for="body" class="block text-sm font-medium text-gray-700 mb-1">Body</label>
|
||||
<textarea id="body" name="body" rows="12"
|
||||
placeholder="Write your post..."
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition resize-y">{{body}}</textarea>
|
||||
</div>
|
||||
<button type="submit"
|
||||
class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 transition font-medium">
|
||||
Save Post
|
||||
</button>
|
||||
</div>
|
||||
31
12_rails_15_min_blog_external/post_show.mustache.html
Normal file
31
12_rails_15_min_blog_external/post_show.mustache.html
Normal file
@@ -0,0 +1,31 @@
|
||||
{{< layout}}
|
||||
{{$body}}
|
||||
{{#post}}
|
||||
<article class="max-w-none">
|
||||
<h1 class="text-4xl font-bold text-gray-900 mb-2">{{title}}</h1>
|
||||
<p class="text-sm text-gray-400 mb-8">Published {{created_at}}</p>
|
||||
<div class="prose prose-lg text-gray-700 whitespace-pre-line leading-relaxed">
|
||||
{{body}}
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<div class="flex gap-4 mt-10 pt-6 border-t border-gray-200">
|
||||
<a href="{{url:edit_post:id}}"
|
||||
class="bg-gray-100 text-gray-700 px-4 py-2 rounded-lg hover:bg-gray-200 transition">
|
||||
Edit
|
||||
</a>
|
||||
<form method="post" action="{{url:post:id}}" class="inline">
|
||||
<input type="hidden" name="http_method" value="delete">
|
||||
<button type="submit"
|
||||
class="bg-red-50 text-red-600 px-4 py-2 rounded-lg hover:bg-red-100 transition"
|
||||
onclick="return confirm('Delete this post?')">
|
||||
Delete
|
||||
</button>
|
||||
</form>
|
||||
<a href="{{url:posts}}" class="text-gray-500 hover:text-gray-700 px-4 py-2">
|
||||
← Back
|
||||
</a>
|
||||
</div>
|
||||
{{/post}}
|
||||
{{/body}}
|
||||
{{/layout}}
|
||||
24
12_rails_15_min_blog_external/posts.mustache.html
Normal file
24
12_rails_15_min_blog_external/posts.mustache.html
Normal file
@@ -0,0 +1,24 @@
|
||||
{{< layout}}
|
||||
{{$body}}
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<h1 class="text-3xl font-bold text-gray-900">All Posts</h1>
|
||||
<a href="{{url:new_post}}"
|
||||
class="bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition">
|
||||
New Post
|
||||
</a>
|
||||
</div>
|
||||
|
||||
{{#posts}}
|
||||
{{> post_card}}
|
||||
{{/posts}}
|
||||
|
||||
{{^posts}}
|
||||
<div class="text-center py-16 text-gray-400">
|
||||
<p class="text-xl">No posts yet.</p>
|
||||
<a href="{{url:new_post}}" class="text-blue-600 hover:underline mt-2 inline-block">
|
||||
Write your first post
|
||||
</a>
|
||||
</div>
|
||||
{{/posts}}
|
||||
{{/body}}
|
||||
{{/layout}}
|
||||
5
12_rails_15_min_blog_external/seed_posts.sql
Normal file
5
12_rails_15_min_blog_external/seed_posts.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
INSERT INTO posts (title, body) VALUES
|
||||
('Hello World',
|
||||
'Welcome to the MACH blog! This post was created by the database seed. MACH is a high-performance, declarative web framework for C that lets you build full CRUD apps in a single file with zero boilerplate.'),
|
||||
('Why Declarative C?',
|
||||
'Traditional C web development means manual memory management, string wrangling, and security pitfalls at every turn. MACH flips the script: you declare resources, pipelines, and templates. The runtime handles allocation, prepared statements, XSS escaping, and async I/O. You get the speed of C with the ergonomics of a modern framework.');
|
||||
3
12_rails_15_min_blog_external/update_post.sql
Normal file
3
12_rails_15_min_blog_external/update_post.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
UPDATE posts
|
||||
SET title = {{title}}, body = {{body}}
|
||||
WHERE id = {{id}};
|
||||
Reference in New Issue
Block a user