diff --git a/.air.toml b/.air.toml
new file mode 100644
index 0000000..1ee68ee
--- /dev/null
+++ b/.air.toml
@@ -0,0 +1,52 @@
+#:schema https://json.schemastore.org/any.json
+
+root = "."
+testdata_dir = "testdata"
+tmp_dir = "tmp"
+
+[build]
+ args_bin = []
+ bin = "./first"
+ cmd = "$HOME/bin/jai/bin/jai-linux first.jai"
+ delay = 1000
+ exclude_file = []
+ exclude_unchanged = false
+ follow_symlink = false
+ full_bin = ""
+ include_dir = []
+ include_ext = ["jai"]
+ include_file = []
+ kill_delay = "0s"
+ log = "build-errors.log"
+ poll = false
+ poll_interval = 0
+ post_cmd = []
+ pre_cmd = []
+ rerun = false
+ rerun_delay = 500
+ send_interrupt = false
+ stop_on_error = false
+
+[color]
+ app = ""
+ build = "yellow"
+ main = "magenta"
+ runner = "green"
+ watcher = "cyan"
+
+[log]
+ main_only = false
+ silent = false
+ time = false
+
+[misc]
+ clean_on_exit = false
+
+[proxy]
+ app_port = 0
+ enabled = false
+ proxy_port = 0
+
+[screen]
+ clear_on_rebuild = false
+ keep_scroll = true
diff --git a/components/index.jai b/components/index.jai
new file mode 100644
index 0000000..c96d79d
--- /dev/null
+++ b/components/index.jai
@@ -0,0 +1 @@
+#load "nav.jai";
diff --git a/components/nav.jai b/components/nav.jai
new file mode 100644
index 0000000..8308ef2
--- /dev/null
+++ b/components/nav.jai
@@ -0,0 +1,16 @@
+add_navbar :: (builder: *String_Builder) {
+ print_to_builder(builder, navbar, "OMAKASE");
+}
+
+#scope_file
+
+navbar : string = #string DONE
+
+DONE
diff --git a/data.jai b/data.jai
new file mode 100644
index 0000000..9a9ff8b
--- /dev/null
+++ b/data.jai
@@ -0,0 +1,37 @@
+State :: struct {
+ idTicker : int = 0;
+ recipes : [..]Recipe;
+};
+
+get_id :: () -> string {
+ state.idTicker += 1;
+ return sprint("REF-%", state.idTicker);
+}
+
+Component :: struct {
+
+}
+
+Recipe :: struct {
+ id : string;
+ name : string = "New recipe!";
+ category : string = "Breakfast";
+ image : string = "https://images.unsplash.com/photo-1546069901-ba9599a7e63c?q=80&w=500&auto=format&fit=crop";
+ duration : int = 5;
+}
+
+state : State;
+
+init_data :: () {
+ recipe := Recipe.{};
+ recipe.id = get_id();
+ array_add(*state.recipes, recipe);
+ recipe.id = get_id();
+ array_add(*state.recipes, recipe);
+ recipe.id = get_id();
+ array_add(*state.recipes, recipe);
+ recipe.id = get_id();
+ array_add(*state.recipes, recipe);
+ recipe.id = get_id();
+ array_add(*state.recipes, recipe);
+}
diff --git a/main.jai b/main.jai
index 0adb952..6195e39 100644
--- a/main.jai
+++ b/main.jai
@@ -1,12 +1,13 @@
#import "Basic";
#import "Hash_Table";
#import "Socket";
+#import "Pool";
String :: #import "String";
#load "server.jai";
consoom :: (input: *string, delimiter: string) -> string {
- if input.count < 1 then return "INVALID READ";
+ if input.count < 1 then return "";
found, left, right := String.split_from_left(input.*, delimiter);
input.* = right;
return left;
@@ -17,6 +18,7 @@ Request :: struct {
method : string;
protocol : string;
headers : Table(string, string);
+ query : Table(string, string);
body : string;
}
@@ -29,15 +31,27 @@ Response :: struct {
parse_request :: (data: *string) -> Request {
req := New(Request);
- req.method = consoom(data, " ");
- req.route = consoom(data, " ");
+ req.method = consoom(data, " ");
+ full_route := consoom(data, " ");
+
+ ok, route, query := String.split_from_left(full_route, "?");
+ req.route = route;
+
+ curQuery := consoom(*query, "&");
+ while curQuery.count > 0 {
+ key := consoom(*curQuery, "=");
+ value := curQuery;
+ table_set(*req.query, key, value);
+ curQuery = consoom(*query, "&");
+ }
+
req.protocol = consoom(data, "\n");
headers_string := consoom(data, "\r\n\r\n");
while headers_string.count > 0 {
key := consoom(*headers_string, ": ");
value := consoom(*headers_string, "\n");
- table_add(*req.headers, key, value);
+ table_set(*req.headers, key, value);
}
req.body = data.*;
@@ -52,7 +66,6 @@ serialize_response :: (res: *Response) -> string {
builder : String_Builder;
print_to_builder(*builder, "% % OK", res.protocol, res.status);
for v, k : res.headers {
- print("%: %\n", k,v);
print_to_builder(*builder, "\n%: %", k, v);
}
print_to_builder(*builder, "\r\n\r\n");
@@ -85,14 +98,17 @@ siginfo_t :: struct { // This is not the correct size, but we don't instantiate
si_addr: *void;
}
+is_killed : bool = false;
+
handle_signal :: (sig: s32, info: *siginfo_t, secret: *void) #c_call {
c : #Context;
- push_context(c) {
- print("Graceful exit!!!\n");
- close_and_reset(*main_socket);
+ push_context c {
+ print("Exiting gracefully.....\n");
}
+ is_killed = true;
}
main :: () {
+ init_server();
socket_init();
main_socket = socket(AF_INET, .SOCK_STREAM, .TCP);
reuse : s32 = 1;
@@ -112,9 +128,20 @@ main :: () {
sa.sa_sigaction = handle_signal;
sigaction(2, *sa, null);
+ // Creating a pool and setting it as the allocator in the context.
+ // This means that we can do whatever the fuck after this, and we
+ // can release all the memory by releasing memory in the pool.
+ //
+ // We can also just reset the pool, meaning that we just reset pointer
+ // to the pool's start, but keep the pool's size. So allocating inside
+ // a request is basically free and we never actually free any memory or
+ // allocate any new memory (unless we go over the previous amount of max used memory).
+ reqpool : Pool;
+ set_allocators(*reqpool);
+ context.allocator = .{pool_allocator_proc, *reqpool};
- while true {
- rbuf : [2048 * 1000]u8;
+ while !is_killed {
+ rbuf : [2048 * 1000]u8; // @ToDo: This sucks, we should have some better thing here.
addr : sockaddr;
addr_len : socklen_t;
reqs := accept(main_socket, *addr, *addr_len);
@@ -129,5 +156,8 @@ main :: () {
res_string := serialize_response(*res);
send(reqs, res_string.data, xx res_string.count, 0);
close_and_reset(*reqs);
+ // Here we reset the pool, obviously you should not be keeping pointers between requests.
+ reset(*reqpool);
}
+ close_and_reset(*main_socket);
}
diff --git a/page.jai b/page.jai
index 2aab128..11f0309 100644
--- a/page.jai
+++ b/page.jai
@@ -1,3 +1,5 @@
+#load "components/index.jai";
+
serve_page :: (res: *Response, content: string) {
res.body = sprint("%\n%\n%", header, content, footer);
}
diff --git a/pages/index.jai b/pages/index.jai
new file mode 100644
index 0000000..883a5f7
--- /dev/null
+++ b/pages/index.jai
@@ -0,0 +1,60 @@
+get_main_page :: () -> string {
+ builder : String_Builder;
+ add_navbar(*builder);
+ print_to_builder(*builder, "
\n");
+ add_sidebar(*builder);
+ print_to_builder(*builder, start_grid, state.recipes.count, state.recipes.count);
+ for recipe : state.recipes {
+ print_to_builder(*builder, card, recipe.id, recipe.image, recipe.name, recipe.category, recipe.duration);
+ }
+ print_to_builder(*builder, "%", end_grid);
+ print_to_builder(*builder, "
\n");
+ return builder_to_string(*builder);
+}
+
+#scope_file
+
+start_grid : string = #string DONE
+
+
+
+
+DONE
+
+card : string = #string DONE
+
+
+ %
+ % %
+
+DONE
+
+end_grid : string = #string DONE
+
+
+DONE
+
+add_sidebar :: (builder: *String_Builder) {
+
+ categories : Table(string, int);
+ for recipe : state.recipes {
+ categoryptr := table_find_pointer(*categories, recipe.category);
+ if categoryptr {
+ table_set(*categories, recipe.category, categoryptr.* + 1);
+ } else {
+ table_set(*categories, recipe.category, 1);
+ }
+ }
+
+ print_to_builder(builder, "\n");
+}
diff --git a/pages/recipe b/pages/recipe
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/pages/recipe
@@ -0,0 +1 @@
+
diff --git a/pages/recipe.jai b/pages/recipe.jai
new file mode 100644
index 0000000..ff8b72b
--- /dev/null
+++ b/pages/recipe.jai
@@ -0,0 +1,22 @@
+get_recipe_page :: (req: Request) -> string {
+ id := table_find_pointer(*req.query, "id");
+ if !id then return "Error: invalid recipe id
";
+
+ curRecipe : Recipe;
+
+ for state.recipes if it.id == id.* curRecipe = it;
+ if curRecipe.id == "" then return "Can't find recipe!
";
+
+ builder : String_Builder;
+ add_navbar(*builder);
+
+ print_to_builder(*builder, recipeHero, curRecipe.image);
+
+ return builder_to_string(*builder);
+}
+
+recipeHero : string = #string DONE
+
+

+
+DONE
diff --git a/server.jai b/server.jai
index 14f93f4..d3304b4 100644
--- a/server.jai
+++ b/server.jai
@@ -1,10 +1,21 @@
#load "style.jai";
#load "page.jai";
+#load "data.jai";
+
+#load "pages/index.jai";
+#load "pages/recipe.jai";
+
+init_server :: () {
+ init_data();
+}
handler :: (req: Request, res: *Response) {
if req.route == {
case "/";
- serve_page(res, "Hello from Jai!
");
+ serve_page(res, get_main_page());
+ return;
+ case "/recipe";
+ serve_page(res, get_recipe_page(req));
return;
case "/style.css";
serve_stylesheet(res);
diff --git a/style.jai b/style.jai
index f996c0f..a9a7e17 100644
--- a/style.jai
+++ b/style.jai
@@ -8,17 +8,120 @@ stylesheet : string = #string DONE
--ink: #050505;
--paper: #FAFAFA;
--gold: #C5A059;
- --gold-dim: #E0D0B0;
+ --gold-dim: #E6DCC8;
+ --grey: #888888;
+ --space: 2rem;
+}
+@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500&family=JetBrains+Mono:wght@400&family=Playfair+Display:ital,wght@0,300;0,400;0,600;1,400&display=swap');
+
+* { box-sizing: border-box; margin: 0; padding: 0; }
+body { background-color: var(--paper); color: var(--ink); font-family: 'Inter', sans-serif; line-height: 1.6; -webkit-font-smoothing: antialiased; }
+a { text-decoration: none; color: inherit; }
+h1, h2, h3 { font-family: 'Playfair Display', serif; font-weight: 400; letter-spacing: -0.02em; }
+.mono { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--grey); }
+
+/* --- NAV --- */
+nav { padding: 1rem var(--space); display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--gold-dim); position: sticky; top: 0; background: rgba(250, 250, 250, 0.98); backdrop-filter: blur(10px); z-index: 100; }
+.brand { font-family: 'Playfair Display', serif; font-size: 1.2rem; font-weight: 600; letter-spacing: 0.15em; display: flex; align-items: center; }
+.brand::before { content: ''; display: block; width: 6px; height: 6px; background: var(--gold); margin-right: 16px; transform: rotate(45deg); box-shadow: 0 0 0 2px var(--paper), 0 0 0 3px var(--gold-dim); }
+.nav-actions a { font-family: 'JetBrains Mono', monospace; text-transform: uppercase; letter-spacing: 0.1em; margin-left: var(--space); font-size: 0.8rem; transition: color 0.2s; }
+.nav-actions a:hover { color: var(--gold); }
+.nav-actions a.active { color: var(--gold); border-bottom: 1px solid var(--gold); }
+
+/* --- LAYOUT: SIDEBAR + GRID --- */
+.archive-layout {
+ display: grid;
+ grid-template-columns: 240px 1fr; /* Sidebar Fixed Width */
+ min-height: 100vh;
}
-@import url('https://fonts.googleapis.com/css2?family=EB+Garamond:wght@452&family=Prata&display=swap');
-
-body {
- font-family: "Prata", serif;
- font-weight: 400;
- font-style: normal;
- background-color: var(--paper);
- color: var(--gold);
+/* Sidebar Styling */
+.sidebar {
+ padding: 2rem var(--space) 2rem 2rem;
+ border-right: 1px solid var(--gold-dim);
+ position: sticky;
+ top: 60px; /* Below Nav */
+ height: calc(100vh - 60px);
+ overflow-y: auto;
}
+.filter-group { margin-bottom: 2.5rem; }
+.filter-title { font-family: 'JetBrains Mono', monospace; font-size: 0.8rem; font-weight: bold; margin-bottom: 1rem; display: block; color: var(--ink); }
+
+.filter-option {
+ display: flex; justify-content: space-between; align-items: center;
+ padding: 0.4rem 0; cursor: pointer; font-size: 0.9rem; color: #555; transition: color 0.2s;
+}
+.filter-option:hover { color: var(--gold); }
+.filter-option.selected { color: var(--ink); font-weight: 500; }
+.filter-option.selected::before { content: '●'; color: var(--gold); font-size: 0.6rem; margin-right: 0.5rem; }
+
+.count { font-family: 'JetBrains Mono', monospace; font-size: 0.7rem; color: #ccc; }
+
+/* Dense Grid Styling */
+.main-content { padding: 2rem; }
+
+.archive-header {
+ display: flex; justify-content: space-between; align-items: baseline;
+ margin-bottom: 2rem; padding-bottom: 1rem; border-bottom: 1px solid #eee;
+}
+
+.grid-dense {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); /* Smaller cards */
+ gap: 2rem;
+}
+
+.asset-card { cursor: pointer; display: block; }
+.asset-card:hover .asset-img img { transform: scale(1.05); filter: grayscale(0%); }
+
+.asset-img {
+ width: 100%; aspect-ratio: 1/1; /* Square for dense packing */
+ background: #eee; overflow: hidden; margin-bottom: 0.8rem;
+}
+.asset-img img { width: 100%; height: 100%; object-fit: cover; filter: grayscale(100%); transition: all 0.3s ease; }
+
+.asset-title { font-size: 1rem; font-weight: 500; line-height: 1.2; margin-bottom: 0.2rem; }
+.asset-meta { display: flex; gap: 1rem; }
+
+.recipe-hero { width: 100%; height: 80vh; position: relative; overflow: hidden; z-index: 1; }
+.recipe-hero img { width: 100%; height: 120%; object-fit: cover; position: absolute; top: 0; left: 0; will-change: transform; }
+
+.content-wrapper { position: relative; z-index: 10; margin-top: -150px; padding: 0 var(--space); }
+
+.header-card {
+ background: var(--paper); padding: 3rem var(--space); border: 1px solid var(--gold-dim);
+ max-width: 1200px; margin: 0 auto var(--space) auto; box-shadow: 0 20px 40px rgba(0,0,0,0.05);
+}
+.recipe-title { font-size: 4.5rem; line-height: 1; margin-bottom: 1.5rem; }
+
+.specs { display: flex; gap: 3rem; margin-top: 2rem; padding-top: 2rem; border-top: 1px solid var(--gold-dim); }
+.spec-val { font-family: 'Playfair Display', serif; font-size: 1.5rem; }
+
+.layout { display: grid; grid-template-columns: 1fr 1.5fr; gap: 5rem; max-width: 1200px; margin: 0 auto; padding-bottom: 100px; }
+
+.component-row { display: flex; justify-content: space-between; align-items: center; padding: 1.5rem 0; border-bottom: 1px solid var(--gold-dim); }
+.component-name { font-weight: 500; font-size: 1.1rem; }
+
+/* Dynamic Slots */
+.slot-dynamic { background: var(--gold-transparent); padding: 1rem; margin: 1rem -1rem; border-left: 3px solid var(--gold); }
+.slot-head { display: flex; justify-content: space-between; margin-bottom: 0.5rem; }
+.slot-tag { color: var(--gold); font-weight: bold; }
+
+/* HTMX Indicator styles */
+.htmx-indicator { display: none; }
+.htmx-request .htmx-indicator { display: inline; }
+.htmx-request.reroll { animation: spin 0.5s linear infinite; }
+@keyframes spin { 100% { transform: rotate(360deg); } }
+
+.reroll { background: none; border: 1px solid var(--gold); color: var(--gold); border-radius: 50%; width: 24px; height: 24px; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: transform 0.3s; }
+.reroll:hover { background: var(--gold); color: var(--paper); }
+
+.step { margin-bottom: 3rem; display: grid; grid-template-columns: 60px 1fr; }
+.step-num { font-family: 'Playfair Display', serif; font-size: 2.5rem; color: var(--gold); font-style: italic; line-height: 1; }
+.step-text { font-size: 1.15rem; font-weight: 300; max-width: 60ch; }
+
+.edit-fab { position: fixed; bottom: 2rem; right: 2rem; width: 60px; height: 60px; border-radius: 50%; background: var(--ink); color: var(--gold); display: flex; align-items: center; justify-content: center; cursor: pointer; box-shadow: 0 10px 30px rgba(0,0,0,0.2); transition: transform 0.2s; z-index: 99; }
+.edit-fab:hover { transform: scale(1.1); }
+
DONE