Skip to content

Commit

Permalink
feat: added a tiny router to handle pags and auth state
Browse files Browse the repository at this point in the history
  • Loading branch information
soulsam480 committed Nov 3, 2024
1 parent e659e3a commit a02ea1c
Show file tree
Hide file tree
Showing 15 changed files with 169 additions and 37 deletions.
Binary file modified bun.lockb
Binary file not shown.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@
"name": "okane",
"type": "module",
"devDependencies": {
"daisyui": "^4.12.14",
"@preact/signals-core": "^1.8.0",
"@types/bun": "latest",
"tailwindcss": "^3.4.14"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"daisyui": "^4.12.14"
}
}
8 changes: 2 additions & 6 deletions priv/ui/js/boot.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
import { hydrate } from "./hydrate.js";
import { Component, html, render } from "htm";
import login_form from "./pages/login_form.js";
import home from "./pages/home.js";
import { $authState } from "./store.js";
import { Router } from "./router.js";

hydrate();

class App extends Component {
render() {
return html`<div class="p-2">
${$authState.value.user === null
? html`<${login_form}></${login_form}>`
: html`<${home} />`}
<${Router} />
</div>`;
}
}
Expand Down
12 changes: 8 additions & 4 deletions priv/ui/js/hydrate.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { $authState } from "./store.js";
import { $auth_state } from "./store.js";

export function hydrate() {
const hydrate_el = document.querySelector("#APP_DATA");

$authState.value.user = JSON.parse(
hydrate_el.textContent ?? '{"user": null}',
).user;
if (hydrate_el === null) {
throw new Error("[UI]: hydration failure.");
}

const parsed = JSON.parse(hydrate_el.textContent);

$auth_state.value = parsed.user;

hydrate_el.remove();
}
8 changes: 3 additions & 5 deletions priv/ui/js/pages/home.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { $authState } from "../store.js";
import { $auth_state } from "../store.js";
import { html } from "htm";

export default function () {
return html`
<div>Hello ${$authState.value.user.name}! Welcome to Okane.</div>
`;
export function home() {
return html`<div>Hello ${$auth_state.value.name}! Welcome to Okane.</div>`;
}
2 changes: 1 addition & 1 deletion priv/ui/js/pages/login_form.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { html } from "htm";

export default function () {
export function login_form() {
/**
* @param {SubmitEvent} event
*/
Expand Down
61 changes: 61 additions & 0 deletions priv/ui/js/router.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { effect, html, signal } from "htm";
import { $current_route } from "./store.js";

/**
* @type {import('types/router.d.ts').RouteRecord}
*/
const PATH_TO_ROUTE = {
login: async () => {
const mod = await import("./pages/login_form.js");
return mod.login_form;
},
home: async () => {
const mod = await import("./pages/home.js");
return mod.home;
},
};

/**
* @type Map<string, import('types/router.d.ts').TPage>
*/
const ROUTE_CACHE = new Map();

const current_component = signal(null);

effect(() => {
const curr_route = $current_route.value;

if (curr_route === null) {
current_component.value = null;
return;
}

const from_cache = ROUTE_CACHE.get(curr_route);

if (from_cache) {
current_component.value = from_cache;
return;
}

const component_loader = PATH_TO_ROUTE[curr_route];

if (!component_loader) {
throw new Error(`${curr_route} doesn't have a route record`);
}

component_loader().then((func) => {
current_component.value = func;
ROUTE_CACHE.set(curr_route, func);
});
});

/**
* 60 loc router with lazy loading of pages
*/
export function Router() {
return html`
${current_component.value === null
? null
: html`<${current_component.value}></${current_component.value}>`}
`;
}
31 changes: 24 additions & 7 deletions priv/ui/js/store.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
import { signal } from "htm";
import { signal, effect } from "htm";

export const $hashState = signal(null);
/**
* @type {import('htm').Signal<string|null>}
*/
export const $current_route = signal(null);

window.addEventListener("hashchange", () => {
let hash = window.location.hash.replace(/^#/, "");
const hash = window.location.hash.replace(/^#/, "");

if (!$auth_state.peek()) {
$current_route.value = "login";
return;
}

if (hash.length === 0) {
$hashState.value = null;
$current_route.value = null;
} else {
$hashState.value = hash;
$current_route.value = hash;
}
});

export function goto(path) {
window.location.hash = path;
}

export const $authState = signal({
user: null,
/**
* @type {import('htm').Signal<import('types/models.d.ts').User>}
*/
export const $auth_state = signal(null);

effect(() => {
if ($auth_state.value) {
goto("home");
} else {
goto("login");
}
});
16 changes: 14 additions & 2 deletions src/app/hooks/auth.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,19 @@ fn get_cookie(
}
}

/// re-use user from ui hook if present
/// else move to auth hook
fn reuse_ui_auth_user(
ctx: config.Context,
handle: fn(config.Context) -> wisp.Response,
next: fn() -> wisp.Response,
) -> wisp.Response {
case ctx.user {
option.None -> next()
option.Some(_) -> handle(ctx)
}
}

/// session/auth hook
/// 1. check if cookie is present
/// 2. find user if there and put it inside context
Expand All @@ -31,8 +44,7 @@ pub fn hook(
ctx: config.Context,
handle: fn(config.Context) -> wisp.Response,
) -> wisp.Response {
// TODO: re-use user inside ctx

use <- reuse_ui_auth_user(ctx, handle)
use user_email <- get_cookie(req)

user.find_by_email(user_email, ctx.db)
Expand Down
1 change: 1 addition & 0 deletions src/app/hooks/hook.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import app/hooks/auth
import app/hooks/ui
import wisp

/// base hook that applies all hooks to a request
pub fn hook_on(
req: wisp.Request,
ctx: Context,
Expand Down
16 changes: 9 additions & 7 deletions src/app/hooks/ui.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ fn make_ssr_data(user: option.Option(user.User)) {
|> json.to_string_builder
}

fn app_shell(user: option.Option(user.User)) {
/// 1. render app shell html
/// 2. put session user info inside document
fn render_app_shell(user: option.Option(user.User)) {
string_builder.new()
|> string_builder.append(
"<!doctype html>
Expand Down Expand Up @@ -66,24 +68,24 @@ pub fn hook(

let user_email = auth_cookie.get_cookie(req)

// TODO: find ways to improve this
let user =
user_email
|> option.to_result(Nil)
|> result.map(fn(email) {
user.find_by_email(email, ctx.db) |> result.replace_error(Nil)
user.find_by_email(email, ctx.db) |> result.nil_error
})
|> result.flatten
|> option.from_result

// 1. render app shell on base. this way we don't need to redirect random URLs to base.
case wisp.path_segments(req) {
[] -> {
wisp.ok() |> wisp.html_body(app_shell(user))
}
_ ->
[] -> wisp.ok() |> wisp.html_body(render_app_shell(user))
// 2. for rest, just set user to content and proceed
_ -> {
handle(case user {
option.Some(u) -> config.set_user(ctx, u)
_ -> ctx
})
}
}
}
16 changes: 14 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@
// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
"noPropertyAccessFromIndexSignature": false,
"paths": {
"htm": [
"./types/htm.d.ts"
],
"types/*": [
"./types/*"
]
}
},
"include": [
"./priv/ui/js/*",
"./types/*"
]
}
10 changes: 10 additions & 0 deletions types/htm.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { Signal, ReadonlySignal } from "@preact/signals-core";

declare module "htm" {
export * from "@preact/signals-core";

export function html(
strings: TemplateStringsArray,
...values: Array<string | number | Signal<any> | ReadonlySignal<any>>
): any;
}
14 changes: 14 additions & 0 deletions types/models.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export interface User {
id: number;
name: string;
email: string;
created_at: string;
}

export interface ResourceData<R> {
data: R;
}

export interface APIError {
error: string;
}
6 changes: 6 additions & 0 deletions types/router.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/**
* render function of a page
*/
export type TPage = () => any;

export type RouteRecord = Record<string, () => Promise<TPage>>;

0 comments on commit a02ea1c

Please sign in to comment.