get a nice developement setup going and add main page and recipe page start

This commit is contained in:
Tuomas Katajisto 2025-11-21 23:30:07 +02:00
parent 172934e8a0
commit 0f625dae1f
11 changed files with 355 additions and 20 deletions

52
.air.toml Normal file
View File

@ -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

1
components/index.jai Normal file
View File

@ -0,0 +1 @@
#load "nav.jai";

16
components/nav.jai Normal file
View File

@ -0,0 +1,16 @@
add_navbar :: (builder: *String_Builder) {
print_to_builder(builder, navbar, "OMAKASE");
}
#scope_file
navbar : string = #string DONE
<nav>
<div class="brand">%</div>
<div class="nav-actions">
<a href="index.html" class="active">Collection</a>
<a href="cookbook.html">Cookbooks</a>
<a href="atelier.html">Atelier</a>
</div>
</nav>
DONE

37
data.jai Normal file
View File

@ -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);
}

View File

@ -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;
}
@ -30,14 +32,26 @@ Response :: struct {
parse_request :: (data: *string) -> Request {
req := New(Request);
req.method = consoom(data, " ");
req.route = 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);
}

View File

@ -1,3 +1,5 @@
#load "components/index.jai";
serve_page :: (res: *Response, content: string) {
res.body = sprint("%\n%\n%", header, content, footer);
}

60
pages/index.jai Normal file
View File

@ -0,0 +1,60 @@
get_main_page :: () -> string {
builder : String_Builder;
add_navbar(*builder);
print_to_builder(*builder, "<div class=\"archive-layout\">\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, "</div>\n");
return builder_to_string(*builder);
}
#scope_file
start_grid : string = #string DONE
<main class="main-content">
<header class="archive-header">
<h2 class="mono" style="font-size: 1rem; color: var(--ink);">// RECIPES</h2>
<div class="mono">Showing % of %</div>
</header>
<div class="grid-dense">
DONE
card : string = #string DONE
<a href="/recipe?id=%" class="asset-card">
<div class="asset-img"><img src="%" alt="Image of recipe"></div>
<h3 class="asset-title">%</h3>
<div class="asset-meta mono"><span>%</span> <span>%</span></div>
</a>
DONE
end_grid : string = #string DONE
</div>
</main>
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, "<aside class=\"sidebar\">\n");
print_to_builder(builder, "<div class=\"filter-group\">\n");
print_to_builder(builder, "<span class=\"filter-title\">Category</span>\n");
for count, category : categories {
print_to_builder(builder, "<div class=\"filter-option\">% <span class=\"count\">%</span></div>", category, count);
}
print_to_builder(builder, "</div>\n");
print_to_builder(builder, "</aside>\n");
}

1
pages/recipe Normal file
View File

@ -0,0 +1 @@

22
pages/recipe.jai Normal file
View File

@ -0,0 +1,22 @@
get_recipe_page :: (req: Request) -> string {
id := table_find_pointer(*req.query, "id");
if !id then return "<h1>Error: invalid recipe id</h1>";
curRecipe : Recipe;
for state.recipes if it.id == id.* curRecipe = it;
if curRecipe.id == "" then return "<h1>Can't find recipe!</h1>";
builder : String_Builder;
add_navbar(*builder);
print_to_builder(*builder, recipeHero, curRecipe.image);
return builder_to_string(*builder);
}
recipeHero : string = #string DONE
<div class="recipe-hero">
<img id="parallax-img" src="%" alt="Steak">
</div>
DONE

View File

@ -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, "<h1>Hello from Jai!</h1>");
serve_page(res, get_main_page());
return;
case "/recipe";
serve_page(res, get_recipe_page(req));
return;
case "/style.css";
serve_stylesheet(res);

121
style.jai
View File

@ -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