get a nice developement setup going and add main page and recipe page start
This commit is contained in:
parent
172934e8a0
commit
0f625dae1f
52
.air.toml
Normal file
52
.air.toml
Normal 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
1
components/index.jai
Normal file
@ -0,0 +1 @@
|
||||
#load "nav.jai";
|
||||
16
components/nav.jai
Normal file
16
components/nav.jai
Normal 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
37
data.jai
Normal 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);
|
||||
}
|
||||
48
main.jai
48
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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
2
page.jai
2
page.jai
@ -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
60
pages/index.jai
Normal 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
1
pages/recipe
Normal file
@ -0,0 +1 @@
|
||||
|
||||
22
pages/recipe.jai
Normal file
22
pages/recipe.jai
Normal 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
|
||||
13
server.jai
13
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, "<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
121
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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user