summaryrefslogtreecommitdiff
path: root/web/src
diff options
context:
space:
mode:
Diffstat (limited to 'web/src')
-rw-r--r--web/src/api.tsx39
-rw-r--r--web/src/ascii-art.tsx148
-rw-r--r--web/src/blog.tsx10
-rw-r--r--web/src/components.tsx259
-rw-r--r--web/src/header.tsx10
-rw-r--r--web/src/home.tsx24
-rw-r--r--web/src/index-page.tsx14
-rw-r--r--web/src/index.html31
-rw-r--r--web/src/jsx.tsx267
-rw-r--r--web/src/pkg.tsx164
-rw-r--r--web/src/sidebar.tsx126
-rw-r--r--web/src/tsconfig.json121
-rw-r--r--web/src/user.tsx6
-rw-r--r--web/src/util.tsx42
14 files changed, 1261 insertions, 0 deletions
diff --git a/web/src/api.tsx b/web/src/api.tsx
new file mode 100644
index 0000000..7233d89
--- /dev/null
+++ b/web/src/api.tsx
@@ -0,0 +1,39 @@
+export const url = "https://networkheaven.net";
+
+export interface ReqParams {
+ method: string,
+ body?: string,
+}
+
+export async function post( endpoint: string, body: Object ) {
+ return await req( endpoint, {
+ method: "POST",
+ body: JSON.stringify( body ),
+ } );
+}
+
+export async function req( endpoint: string, params: ReqParams ) {
+ const res = await fetch( `${url}/${endpoint}`, {
+ method: params.method,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: params.body,
+ } );
+
+ if( !res.ok ) {
+ let json = null;
+ try {
+ json = await res.json();
+ } catch( e: any ) {
+ throw new Error( "error contacting server" );
+ }
+
+ throw new Error( json.msg );
+ }
+
+ const json = await res.json();
+ if( json.status != 'ok' )
+ throw new Error( json.msg );
+ return json;
+}
diff --git a/web/src/ascii-art.tsx b/web/src/ascii-art.tsx
new file mode 100644
index 0000000..a6caefe
--- /dev/null
+++ b/web/src/ascii-art.tsx
@@ -0,0 +1,148 @@
+import * as JSX from "./jsx";
+import $ from "jquery";
+
+export function AsciiArt() {
+ const ret = <div id="ascii-art">
+ { asciiArtStr }
+ </div>;
+
+ setTimeout( doAsciiArt );
+ return ret;
+}
+
+let startTime = 0.0;
+let anim = 0.0;
+
+function animFunc( a: number ) {
+ let ringRadius = anim;
+ let radius = a;
+
+ let dist = ringRadius - radius;
+
+ dist = Math.abs( dist );
+
+ let ret = Math.pow( dist, 0.1 + 0.8 * Math.pow( (anim + 0.5) / 2, 1.6 ) );
+ let remain = ret % 0.12;
+ return ret - remain;
+}
+
+function hexToRgb( hex: string ) {
+ let r = parseInt( hex.slice( 1, 3 ), 16 );
+ let g = parseInt( hex.slice( 3, 5 ), 16 );
+ let b = parseInt( hex.slice( 5, 7 ), 16 );
+ return [ r, g, b ];
+}
+
+function lerpColor( c1: number[], c2: number[], t: number ) {
+ let r1 = c1[0], g1 = c1[1], b1 = c1[2], a1 = c1[3];
+ let r2 = c2[0], g2 = c2[1], b2 = c2[2], a2 = c2[3];
+ let r = r1 + ( r2 - r1 ) * t;
+ let g = g1 + ( g2 - g1 ) * t;
+ let b = b1 + ( b2 - b1 ) * t;
+ let a = a1 + ( a2 - a1 ) * t;
+ return [ Math.round( r ), Math.round( g ), Math.round( b ), Math.round( a ) ];
+}
+
+function getDist( x: number, y: number ) {
+ const x1 = 0.75 - x / 71;
+ const y1 = 0.75 - y / 35;
+ return Math.sqrt( x1 * x1 + y1 * y1 );
+}
+
+let topY = 0;
+
+let isDoingAnim = false;
+function doAsciiArt() {
+ let div = $( "#ascii-art" );
+ if( !div.length ) { isDoingAnim = false; return; }
+ isDoingAnim = true;
+
+ let deltaTime = ( Date.now() - startTime ) * 0.001;
+ startTime = Date.now();
+ anim += .5 * deltaTime;
+
+ if( anim > 2 )
+ anim = -0.5;
+
+ let rootColor = [ 255, 0, 255 ];
+ let innerStr = "";
+ for( let i = 0; i < asciiArtStr.length; ++i ) {
+ let x = i % 71;
+ let y = Math.floor( i / 71 );
+ if( y > topY )
+ topY = y;
+
+
+ if( asciiArtStr[i] == ',' || asciiArtStr[i] == '\t' ) {
+ innerStr += asciiArtStr[i];
+ continue;
+ }
+ if( asciiArtStr[i] == ' ' ) {
+ innerStr += ' ';
+ continue;
+ }
+
+ let dist = getDist( x, y );
+
+ let color = [];
+ if( asciiArtStr[i] == '#' ) {
+ let a = animFunc( dist * 2 );
+ color = lerpColor( rootColor, [255, 255, 255, 1], a );
+ color[3] = Math.max( Math.min( 1, Math.pow( dist, 2 ) + (1.0 - a) ), 0.8 );
+ }
+ else {
+ const t = Math.max( 0.05, 1.0 - animFunc( (dist) * 0.75 ) );
+ if( t < 0.25 ) {
+ color = lerpColor( [255, 255, 255, 255], [255, 189, 255, 255], (t - 0.5) * 2 );
+ }
+ else if( t > 0.5 && t < 0.75 ) {
+ color = lerpColor( [255, 189, 255, 255], [0, 255, 255, 255], (t - 0.5) * 2 );
+ } else {
+ color = lerpColor( [255, 255, 255, 255], [0, 255, 255, 255], (t - 0.5) * 2 );
+ }
+ }
+ // appending string is faster than jsx
+ innerStr += `<span style='color: rgba(${color[0]}, ${color[1]}, ${color[2]}, ${color[3]})'>${asciiArtStr[i]}</span>`;
+ }
+
+ div.html( innerStr );
+ setTimeout( doAsciiArt, 85 );
+}
+
+const asciiArtStr = `
+⣿⣿⣿⣿⣿⣿⣿⣿⣶⣤⣄⡀⠀⢿⡿⣁⠀⢆⡘⠤⡘⠠⢰⠃⡄⠂⠄⠀⠀⠀⠀⡀⡆⠌⢄⠀⠀⢠⡇⠂⡌⠄⠀⠀⠀⠀⠀⠀⠡⣖⠀⢃⠸⠀⠸⣟⡄⠘⡽⡜⡄⠀⠀⠀⠈⠦⠀⠀⠀⠸
+⣿⣿⣿⣿⣿⣿⡿⠁⠈⢻⡝⣟⣷⡿⢁⠤⢀⠢⢌⠰⡀⠂⡟⠠⡄⠈⠀⠀⣀⠤⠐⣰⠌⡈⢄⠂⡡⣺⠄⡇⠰⠈⡄⢃⠰⢀⠰⢀⠂⢽⠂⢼⠐⡠⠄⣿⡼⡀⠱⢹⡰⠀⠀⠀⠀⠀⣆⠀⠀⠀
+⣿⣿⣿⣿⣿⣿⠁⢄⠠⢀⣹⣞⡿⢁⢊⠰⢀⠎⡐⢂⠁⢳⠋⠴⠁⠀⣠⠞⡠⢁⣺⡇⠤⢁⠢⠘⢠⡿⢰⡏⠄⢃⡐⢈⠰⢈⠰⢸⠌⣸⠌⢺⡐⠤⢈⣽⣧⢩⡄⢉⢧⠡⠉⡍⠒⠤⣸⡀⠀⠀
+⣿⣿⣿⣿⣿⣿⠩⡐⠌⢿⡯⣿⠇⡌⠢⢡⠈⡔⠈⠂⠃⢾⢈⠒⠀⡴⢁⡾⠑⣸⢻⢀⠒⢠⠂⡉⢼⡇⢸⡇⠌⠄⡄⢃⠰⢈⠰⠸⡂⢼⢈⢹⠆⠰⢸⢿⠸⡆⢷⢈⠸⣄⠃⠤⢉⡐⠨⣧⠀⡀
+⣿⣿⣿⣿⣿⣿⡟⣿⣻⢾⡷⣿⠐⡄⢃⠆⠈⠐⠁⠀⠀⣾⠸⠀⢀⡇⣼⠇⢡⡏⣇⠢⠘⡀⠆⢡⡎⡇⢺⣇⠘⢠⠐⠨⡐⢂⠢⢡⡇⠾⢈⢸⡃⡘⢼⢸⠀⢻⠈⣇⠌⣷⢈⡐⠂⠤⢱⢿⠀⠔
+⣿⣿⣿⣿⣿⣿⣯⡚⢧⡻⣽⡏⠰⡈⢄⠈⠀⡀⠀⠀⠀⣿⢸⢀⢂⢸⡟⡈⢼⢳⠁⢂⡁⠆⡑⢺⢱⡇⣼⢿⠈⠄⡈⠡⢐⠠⢁⢺⡄⡟⡀⢺⢁⡴⡿⢸⢄⠈⡇⢺⡆⢸⡆⠤⢉⠰⢐⣸⠈⠔
+⣿⣿⣿⣿⣿⣿⣿⣷⠁⢿⣹⡇⠡⠌⠀⠀⠀⠀⠀⠀⢀⡿⣸⠀⢂⣿⢃⠐⡞⡼⢈⣄⣐⣤⡴⡯⢼⠴⡿⢼⠾⠶⠶⠶⣤⠂⠌⣹⢰⡇⡐⡏⢸⢡⡇⡽⡀⠑⢳⠐⣷⠀⢿⡇⠌⡐⠂⡽⢈⡐
+⣿⣿⣿⣿⣿⣿⣿⡏⠠⣄⣿⠄⠀⠀⠀⠀⠀⡅⠀⠠⢠⡟⣧⠈⣰⣿⠀⣼⠴⡗⢉⠉⡐⢠⢷⠃⢸⢠⠃⢸⣿⠀⢀⠂⢸⠀⢂⡏⣼⠁⣸⠁⡏⡜⡇⡧⠬⣴⣼⣀⢿⢇⢸⣹⠀⠀⡁⠇⡃⠄
+⣿⣿⣿⣿⣿⣿⣿⣇⡷⠊⢸⠀⠀⠀⢠⣦⣄⠇⡈⢁⠆⣿⣧⠐⣽⡇⠐⣾⢰⡇⢂⠡⢈⡏⡞⠀⡼⣸⠀⠀⣿⠀⠀⠂⣏⠐⢸⢡⡟⢀⡏⣼⢱⠁⣧⠁⠀⠀⠈⡟⣻⠺⣤⣿⡆⠐⠀⡇⠄⡈
+⣿⣿⣿⣿⣿⣿⣿⣿⣄⠂⣿⠀⠀⢠⠃⢿⣣⠐⠠⠌⠠⣿⣿⠀⢿⠃⢌⣿⠸⡇⢀⢂⡼⡼⠀⢀⢧⡯⠄⡀⣿⠀⠌⢰⡇⠈⣼⡿⠁⡞⣸⢇⠇⢰⡟⠀⠀⠀⠈⡇⡝⠀⢇⡟⡧⣄⠂⡇⠀⢰
+⣿⣿⣿⣿⣿⡿⢭⢿⣿⣶⣻⡆⠀⡏⠠⠸⡽⣏⠐⡈⡐⢿⣻⡠⣿⠈⢸⡍⣆⣧⣾⣾⣿⣿⣶⣾⣾⣆⠄⠀⣿⡆⠀⣸⠁⣸⡿⢁⡞⡕⡹⡌⠀⣼⢁⠤⠤⢀⣀⡇⡇⠀⠸⣼⡇⠀⠑⣷⠈⣸
+⣿⣿⣿⣿⣿⡹⢎⣗⡻⣿⣷⣇⠘⣇⠄⡁⢻⣽⡆⠡⠠⢹⣿⡓⣿⣴⣿⣿⣿⣿⡿⣟⣿⢫⣟⣏⠉⠙⠛⠶⣿⢿⠀⡞⢀⡟⢡⣾⠊⣰⠟⠀⣼⠃⣐⣤⣴⣦⣤⣧⣇⣠⠀⣿⡇⠀⡁⡏⠀⡿
+⣿⣿⣿⡟⣧⣛⡭⢶⡙⣿⢻⣿⡄⢻⡔⠠⡈⢧⢿⡆⢡⠈⢿⣇⣷⣿⡿⠛⣯⢽⡳⢧⡞⠀⠀⣽⡀⠀⠀⠀⠛⠘⠻⢣⣞⣴⠟⠁⠠⠏⠀⠐⠁⢸⣿⢿⣟⠿⣿⡿⣿⣷⣤⣸⡂⠀⢸⠃⢰⠇
+⣿⣿⡿⣹⣧⡓⢾⢷⡟⢸⣯⢹⣷⡌⢻⣄⠱⣌⠳⣿⣦⠲⠾⣿⣿⣿⠁⠈⣟⠊⢿⡧⢿⠃⡜⣸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣏⣟⡑⢀⡻⡴⢷⠈⠻⣿⣿⣟⠋⡟⢀⡎⠄
+⣿⣿⠳⣽⠲⣝⡿⢸⡇⣸⠙⣷⢺⣿⣧⡻⣷⣌⢢⡙⢽⣷⣤⠘⢧⠙⢦⡀⢻⡌⠀⢠⢁⠒⢯⡀⠀⠀⠀⠀⠀⡀⠀⢀⠀⢀⠀⠀⠀⠀⠀⠀⠀⡏⣷⣹⡏⠵⡙⣺⠀⠀⠸⣿⣿⣾⢁⡾⠀⣲
+⣿⢫⡝⣾⢹⣾⢃⢹⡇⢼⠂⣿⣹⠋⢉⡿⣾⣿⣷⣌⠲⣽⢿⣿⣮⣷⣀⠈⠀⠙⠗⠒⠚⡋⠉⠀⠀⠀⠀⣺⠂⠀⠀⢀⠀⠠⠐⣀⡀⠂⠀⠀⠀⢷⢈⠩⠀⡡⠲⠏⠀⠀⣸⢟⣽⣫⡟⢁⣴⢏
+⣏⢷⣙⣮⠟⡐⠢⢼⡇⢸⡿⢧⣟⠀⡇⡄⣤⢩⢿⣛⢷⣤⣭⣛⢿⣮⠉⠳⠦⣤⠁⠁⡡⢈⢄⠡⠅⡆⠂⠀⠀⡈⠀⢈⡠⠆⠨⢀⠀⠀⠀⠄⠐⠈⠙⡲⢷⡼⡆⠀⠄⡐⠁⣿⣿⣋⣴⣾⢋⣾
+⣞⢺⣼⠏⡐⠌⡁⣿⡇⢸⡇⠼⣿⠀⢷⠀⢹⡆⣿⡉⠚⠶⣭⣻⣿⡟⠢⢄⢤⢇⡀⡅⠦⠠⡴⠀⠀⠐⢐⠀⠆⠃⠄⠖⠉⠂⠅⠊⠁⢒⠄⠐⣀⠀⠂⠉⣀⠁⡄⠁⠈⠐⢸⣿⢫⣿⡳⣵⡿⢿
+⣎⡿⢼⠂⡅⢊⠔⣿⣵⢺⡏⠄⣿⣧⡈⠓⢌⠷⢾⡇⢁⠢⢀⢹⠑⣷⠈⡠⠏⠤⠨⣁⢃⠕⠋⡫⢗⠉⠈⡂⠁⠀⢂⠐⢁⠣⠀⠁⠀⠘⠒⠥⠴⢄⢀⠐⠁⠀⠐⠀⠀⠈⡿⣬⡿⢣⡓⣸⣇⢻
+⣾⣃⡯⢼⣀⠣⣈⣿⡽⣺⠛⢠⠹⡻⣿⣦⡀⠑⠪⣇⠂⠔⡈⡾⠊⣿⠎⠵⠃⡓⠀⡀⠉⠄⠆⠁⠉⠀⠈⠡⠂⠀⠀⡀⢈⣀⠊⣡⠂⠀⡐⠛⠊⢁⠰⠖⠃⠀⠁⢀⡀⢰⠟⡞⠡⢠⢰⣿⠸⣸
+⣿⣿⣳⢯⣿⣿⣿⣷⣽⣹⡇⠌⣷⢳⢋⠻⢿⣦⣀⡟⡀⠊⢰⡇⠂⣿⡇⠠⠁⠀⠁⠀⡐⠀⠀⠀⠀⠀⠀⠀⠀⠀⡠⡔⠁⠀⠉⠢⢀⠀⠀⠀⠀⠈⠈⠁⢀⠠⠊⠀⠈⢺⡞⠠⢁⢂⣯⡏⠵⡘
+⣿⣿⡜⣿⣿⣿⣿⣿⢾⣻⣧⡂⢹⣏⣿⡄⠌⢻⣿⣧⠀⠡⣸⠠⠁⣾⡇⠀⠀⠀⠀⠀⠈⠀⠀⠀⠀⠀⢀⡠⠐⠁⠀⠀⠀⠀⠀⠀⠀⠉⠫⣦⠀⠀⢀⠔⠁⠀⠀⠀⡀⠀⠙⢦⢁⡞⣼⡙⢦⠑
+⣿⣿⡽⣹⣿⣿⣿⣿⡯⣷⣯⢿⣄⠻⣜⣷⡈⠄⢻⣷⠈⢠⡗⠠⠁⣿⢸⣄⠀⠀⠀⠀⠀⠀⢀⡔⠊⠉⠀⠀⠀⠀⠀⠀⡰⠂⠤⣀⠀⠀⢀⣃⠀⣰⠁⠀⠀⡴⢴⣧⠀⡀⠀⠀⠙⢦⡏⡝⢢⠌
+⡿⣿⣿⣽⣿⢿⣿⣿⡷⣣⢿⣯⣻⢷⣽⢞⣿⡄⠂⣿⠀⣸⠃⠠⢁⣷⠸⡇⠑⢤⡀⠀⣠⠞⠁⠀⠀⠀⠀⠀⠀⣀⠔⠀⠀⠀⠀⠈⢧⡀⢿⣿⣾⡿⠁⡠⢊⣴⣿⢸⡷⣄⣐⠀⢀⠄⠙⠓⠧⣌
+⣿⣷⣿⣿⣿⣿⣿⣿⡿⣵⢫⣷⣏⣟⣾⣻⣾⣿⣆⢿⠀⡿⠀⠀⠂⣼⠐⣿⡀⢀⡼⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠒⢄⡀⠀⠀⠀⠀⠑⠮⠿⠛⠒⢈⣴⣿⣿⡏⡾⢁⠈⣿⠖⠁⠀⠀⠀⠀⠀
+⣿⣿⣿⣿⣿⣿⣿⣿⣿⣳⣿⣿⣿⣯⣟⣷⢯⣿⣿⣿⢰⠃⠀⠀⠐⢸⠀⣻⠟⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠲⠄⡀⠀⠀⠀⢀⣠⣾⣿⣿⣿⣿⣃⠇⢂⢡⠏⠀⠀⠀⠀⢀⠀⠀
+⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣞⢾⡻⢷⣯⣿⣼⠀⠀⠀⢀⡼⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠲⣤⣀⡀⠀⠀⠀⠀⠈⠢⣤⣾⣿⣿⣿⣿⣿⣿⣿⢸⠠⡱⠃⠀⠀⠀⣠⣶⠋⠀⠀
+⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢾⣽⣯⣞⣽⡟⠀⣠⠔⠋⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠿⣷⣤⡀⠀⠀⠀⠀⠙⢿⣿⣿⣿⣿⣷⣿⢀⡜⠁⠀⠀⣠⡞⡱⠁⠀⠀⠀
+⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢯⢿⣿⣿⣿⣧⠞⠁⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠐⠠⢀⠀⠀⠀⠀⠀⠀⠙⢿⣷⣄⡀⠀⠀⠀⠹⢿⣿⣿⣿⣿⡼⠀⠀⢀⣾⣿⡟⠀⠀⠀⠀⠀
+⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡻⣞⢾⡹⡿⠀⠀⠀⢀⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠉⠂⢄⡀⠀⠀⣠⣿⣿⣿⣿⣦⣄⠀⠀⠀⠻⣿⣿⡟⢀⠀⣡⠂⢺⣿⡣⢀⢀⠄⠀⠀
+⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⣝⣾⣿⣧⢶⡶⣿⢿⣿⣦⣤⣀⣀⣀⣤⣀⣴⣶⣤⣤⣄⡀⠀⠀⢀⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣄⠀⢠⣾⣿⣾⡷⣰⠉⡆⣹⣿⣿⣶⣿⣆⡀⢀
+⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡞⣯⢷⣏⡿⣽⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣦⣽⣿⡿⠕⠉⢆⢱⢸⣿⣿⣿⣿⣿⣿⣿
+⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⡽⣾⣹⡞⣷⡽⣞⡽⣯⢿⣹⢯⣿⣿⣿⣿⣯⣿⡿⣿⣿⣿⣿⣿⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀⠀⠀⢪⢿⣿⣿⣿⣿⣿⣿⣿
+⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⡽⣶⢯⡽⣞⣳⢯⡽⣞⣯⣽⢫⣿⡟⣿⣻⢿⡽⣿⣿⣿⣿⣻⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡈⠀⠀⠹⣿⣿⣿⣿⣿⣿⣿
+⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⢷⣛⡾⣽⣹⡽⢾⣹⠷⣞⣞⠿⣼⣿⡘⣯⣾⣿⢿⣹⢯⣟⡿⣿⢿⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣄⠀⠀⠈⠻⣿⣿⣿⣿⣿
+`;
diff --git a/web/src/blog.tsx b/web/src/blog.tsx
new file mode 100644
index 0000000..9d86657
--- /dev/null
+++ b/web/src/blog.tsx
@@ -0,0 +1,10 @@
+import $ from "jquery";
+import * as JSX from "./jsx";
+import { Page } from "./components";
+import { AsciiArt } from "./ascii-art";
+
+export default function Blog() {
+ return <Page>
+ work in progress (:
+ </Page>
+}
diff --git a/web/src/components.tsx b/web/src/components.tsx
new file mode 100644
index 0000000..ccd8da8
--- /dev/null
+++ b/web/src/components.tsx
@@ -0,0 +1,259 @@
+import $ from "jquery";
+import * as JSX from "./jsx";
+import { Header } from "./header";
+import { Sidebar } from "./sidebar";
+
+const popup_stack = [];
+window.onclick = ( e: Event ) => {
+ if( !e.target ) return;
+
+ if( popup_stack.length > 0 ) {
+ const last = popup_stack[popup_stack.length - 1];
+ if( last.el[0] == e.target || last.el.find( e.target ).length != 0 ) return;
+ e.preventDefault();
+ e.stopPropagation();
+ if( last.fn )
+ last.fn();
+
+ popup_stack.pop();
+ last.el.remove();
+ }
+}
+
+/**
+ * appends an element to the DOM and saves it in the popup stack to be removed.
+**/
+export function addPopup( element: JQuery ) {
+ document.body.appendChild( element[0] );
+ setTimeout( () => popup_stack.push( { el: element, fn: null } ) );
+}
+
+/**
+ * closes the topmost popup
+**/
+export function closePopup() {
+ if( popup_stack.length > 0 ) {
+ const last = popup_stack[popup_stack.length - 1];
+ if( last.fn )
+ last.fn();
+
+ popup_stack.pop();
+ setTimeout( () => last.el.remove() );
+ }
+}
+
+/**
+ * sets a callback that will get executed once the current popup is closed
+ **/
+export function onPopupClosed( fn: Function ) {
+ setTimeout( () => {
+ if( popup_stack.length > 0 ) {
+ popup_stack[popup_stack.length - 1].fn = fn;
+ }
+ } );
+}
+
+/*
+ * accepts "folded" prop
+**/
+export function RolldownListItem( props: any ) {
+ const ret = <div class="rolldown">
+ <div class="rolldown-collapsed-container">
+ <div class="rolldown-icon-container">&gt;</div>
+ <div class="rolldown-collapsed">{props.folded}</div>
+ </div>
+ <div class="rolldown-expanded-container">
+ <div class="rolldown-expanded">{props.children}</div>
+ </div>
+ </div>;
+
+ ret.onclick = () => {
+ $( ret ).toggleClass( "active" );
+ $( ret ).find( ".rolldown-expanded-container" ).toggleClass( 'active' );
+ };
+
+ return ret;
+}
+
+export function Spinner( props: any ) {
+ let spinner_steps = [
+ '/',
+ '-',
+ '\\',
+ '|',
+ '/',
+ '-',
+ '\\',
+ '|'
+ ];
+
+ const id = props.id || '';
+
+ let el = $( <div class="spinner" id={ id } style={ props.style || '' } /> );
+ let i = 0;
+ let loop = () => {
+ el.text( spinner_steps[i++] );
+ if( i > spinner_steps.length )
+ i = 0;
+
+ if( el[0].isConnected )
+ setTimeout( loop, 100 );
+ };
+
+ setTimeout( loop, 100 );
+ return el[0];
+}
+
+/*
+ * accepts title prop
+**/
+export function RolldownList( props: any ) {
+ return <GroupBox title={props.title} style={props.style} innerStyle="margin: 0px; width: 100%">
+ <div class="rolldownlist">
+ { props.children }
+ </div>
+ </GroupBox>
+}
+
+/*
+ * accepts title prop and optional innerStyle
+**/
+export function GroupBox( props: any ) {
+ return <div class="groupbox" id={props.id || ''} style={props.style || ''}>
+ <span class="grouptitle">
+ {props.title}
+ </span>
+ <span class="groupbody" style={props.innerStyle || ''}>
+ {props.children}
+ </span>
+ </div>
+}
+
+export function Page( props: any ) {
+ return <>
+ <Header />
+ <div id="page-main">
+ <Sidebar />
+ <div id="page-main-content">
+ {props.children}
+ </div>
+ </div>
+
+ <BackgroundToggle />
+ </>
+}
+
+export function DropdownItem( props: any ) {
+ return <div class="dropdown-inner" style={ props.style || "" } onclick={ props.onclick || null }>
+ {props.children}
+ </div>
+}
+
+/**
+ * supports innerStyle for styling the actual dropdown picker
+ **/
+export function Dropdown( props: any ) {
+ const children = props.children;
+ let title = props.title;
+ let inline = props.inline;
+ let onchange = props.onchange;
+ let classes = props.class || '';
+ let style = props.style || "";
+ let id = props.id || '';
+ let innerStyle = props.innerStyle || "";
+
+ const showItems = ( e: Event ) => {
+ e.preventDefault();
+ const target = $( e.target as HTMLElement );
+
+ const newDropdown = $( <div class="dropdown-wrapper" style={ innerStyle }>
+ { children.map( ( child: HTMLElement ) => {
+ if( !child.onclick ) child.onclick = onchange; return child;
+ } ) }
+ </div> );
+ target.parent().append( newDropdown[0] );
+ setTimeout( () => popup_stack.push( { el: newDropdown, fn: null } ) );
+ }
+
+ if( inline ) {
+ return <button class={ 'dropdown' + ' ' + classes } style={ style } onclick={ showItems } id={ id }>
+ { title }
+ </button>
+ }
+ else {
+ return <>
+ <label>{ title }</label>
+ <button class={ 'dropdown' + ' ' + classes } style={ style } onclick={ showItems } id={ id }>
+ </button>
+ </>
+ }
+}
+
+export function OkPopup( props: any ) {
+ const children = props.children;
+ const classes = props.class || '';
+ const style = props.style || '';
+ const id = props.id || '';
+
+ const onclick = ( e: Event ) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if( props.onclick )
+ props.onclick();
+
+ closePopup();
+ }
+
+ return <div class={ "popup-msg " + classes } id={ id } style={ style }>
+ { children }
+ <div style="display: flex; justify-content: center">
+ <button onclick={ onclick }>Ok</button>
+ </div>
+ </div>
+}
+
+export function OkCancelPopup( props: any ) {
+ const children = props.children;
+ const classes = props.class || '';
+ const style = props.style || '';
+ const id = props.id || '';
+
+ const onclick = ( e: Event ) => {
+ e.preventDefault();
+ e.stopPropagation();
+ if( props.onclick )
+ props.onclick();
+
+ closePopup();
+ }
+
+ const oncancel = ( e: Event ) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ closePopup();
+ }
+
+ return <div class={ "popup-msg " + classes } id={ id } style={ style }>
+ { children }
+ <div style="display: flex; justify-content: center">
+ <button onclick={ onclick } style="margin-right: 10px">Ok</button>
+ <button onclick={ oncancel }>Cancel</button>
+ </div>
+ </div>
+}
+
+export function BackgroundToggle() {
+ const toggleBackground = () => {
+ const main = $( "#page-main" );
+ if( main.css( "display" ) == "none" ) {
+ main.css( "display", "" );
+ } else {
+ main.css( "display", "none" );
+ }
+ }
+
+ return <div id="background-toggle" onclick={ toggleBackground }>
+ show background
+ </div>
+}
diff --git a/web/src/header.tsx b/web/src/header.tsx
new file mode 100644
index 0000000..94906c0
--- /dev/null
+++ b/web/src/header.tsx
@@ -0,0 +1,10 @@
+import $ from 'jquery';
+import * as JSX from './jsx';
+
+export function Header( props: any ) {
+ return <div class="border-wrapper" style="z-index: 1">
+ <div class="header">
+ networkheaven.net
+ </div>
+ </div>
+}
diff --git a/web/src/home.tsx b/web/src/home.tsx
new file mode 100644
index 0000000..77404fd
--- /dev/null
+++ b/web/src/home.tsx
@@ -0,0 +1,24 @@
+import $ from "jquery";
+import * as JSX from "./jsx";
+import { Page } from "./components";
+import { AsciiArt } from "./ascii-art";
+
+export default function Home() {
+ return <Page>
+ <div class="page-title">
+ <h3 style="font-family: JPN24; font-size: 25px; width: min-content" class="gradient">NETWORKHEAVEN</h3>
+ </div>
+ <hr />
+ <div style="width: 100%; display: flex; justify-content: center">
+ <div style="text-align: left; width: 75%">
+ <h4 style="font-family: JPN16; font-size: 17px; font-weight: normal; margin-bottom: 0px;">
+ hi, im aura and this is my website.
+ <br />
+ i will add more stuff when i have time
+ </h4>
+ </div>
+ </div>
+ <AsciiArt />
+ <h3 style="font-family: JPN19; font-size: 20px"><a href="https://steamcommunity.com/groups/networkheaven">STEAM</a></h3>
+ </Page>;
+}
diff --git a/web/src/index-page.tsx b/web/src/index-page.tsx
new file mode 100644
index 0000000..4b7ce1c
--- /dev/null
+++ b/web/src/index-page.tsx
@@ -0,0 +1,14 @@
+import * as JSX from "./jsx";
+import Home from "./home";
+import Blog from "./blog";
+import Pkgs from "./pkg";
+
+JSX.addRoute( "/", () => <Home /> );
+JSX.addRoute( "/blog", () => <Blog /> );
+JSX.addRoute( "/pkg", () => <Pkgs /> );
+JSX.addRoute( "/pkg/*", () => <Pkgs /> );
+
+window.onpopstate = JSX.onPopState;
+
+const url = new URL( window.location.href );
+JSX.navigateParams( url.pathname, url.searchParams.entries() );
diff --git a/web/src/index.html b/web/src/index.html
new file mode 100644
index 0000000..a5500e4
--- /dev/null
+++ b/web/src/index.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="description" content=" ">
+ <meta name="viewport" content="width=device-width,user-scalable=no">
+ <title>networkheaven.net</title>
+ <link rel="stylesheet" href="/static/main.css">
+ <link href="/static/highlight.css" rel="preload" as="style" onload="this.rel='stylesheet'">
+ <link rel="apple-touch-icon" href="/static/nh.ico">
+ <link rel="icon" href="/static/nh.ico" type="image/png">
+ <link rel="shortcut icon" href="/static/nh.ico" type="image/png">
+ </head>
+ <body>
+ <div id="moneyjsx-root">
+ <div id="homepage" style="padding-top: 0px">
+ <noscript>
+ <!--- for browsers with noscript !--->
+ <div id="ascii-art">
+
+ </div>
+ <h3 style="margin-top: 5px;">networkheaven.net</h3>
+ <div>
+ you need javascript to use this website
+ </div>
+ </noscript>
+ </div>
+ </div>
+ </body>
+</html>
+
diff --git a/web/src/jsx.tsx b/web/src/jsx.tsx
new file mode 100644
index 0000000..98ca440
--- /dev/null
+++ b/web/src/jsx.tsx
@@ -0,0 +1,267 @@
+import $ from 'jquery';
+const assetAttributeNames = new Set( ['data', 'srcset', 'src', 'href'] );
+
+export interface Route {
+ path: string,
+ component: Function,
+ wildcard: boolean
+};
+
+const routes: Route[] = [];
+let err404page = "/";
+let rootId = "moneyjsx-root";
+let onprenavigate: Function = () => {};
+let onpostnavigate: Function = () => {};
+
+function routeForPath( route: string ) : Function | null {
+ if( !routes[route] ) {
+ for( let key of Object.keys( routes ) ) {
+ const r = routes[key];
+
+ if( r.wildcard ) {
+ if( route.slice( 0, r.path.length ) == r.path ) {
+ return r.component;
+ }
+ }
+ }
+
+ return null;
+ } else return routes[route].component;
+}
+
+/**
+ * sets the id of the element to be replaced by the navigator
+ **/
+export function setRootId( rootId: string ) {
+ rootId = rootId;
+}
+
+/**
+ * adds a route component to the routes list
+ * the component function must return either a jquery or a DOM element
+ **/
+export function addRoute( name: string, component: Function ) {
+ let path = name;
+ let wildcard = false;
+ if( path[path.length - 1] === "*" ) {
+ console.log( "name" );
+ path = path.substring( 0, path.length - 1 );
+ wildcard = true;
+ }
+
+ routes[path] = { path, component, wildcard };
+}
+
+/**
+ * sets the route for a 404 page
+ **/
+export function set404Route( name: string ) {
+ err404page = name;
+}
+
+/**
+ * sets the callback that will get called when a route changes
+ **/
+export function onPreNavigate( callback: Function ) {
+ onprenavigate = callback;
+}
+
+/**
+ * sets the callback that will get called when a route changes
+ **/
+export function onPostNavigate( callback: Function ) {
+ onpostnavigate = callback;
+}
+
+/**
+ * replaces the root element with the route component
+ **/
+export function navigate( route: string ) {
+ console.log( route );
+ let url = new URL( window.location.href );
+ let cb = routeForPath( route );
+ url.pathname = route;
+ if( !cb )
+ return navigate( err404page );
+
+ window.history.pushState( {}, null, url.href );
+
+ onprenavigate();
+ const el = $( cb() );
+
+ $( `#${rootId}` ).children().remove();
+ $( `#${rootId}` ).append( el );
+ onpostnavigate();
+}
+
+/**
+ * navigate with params. see: navigate
+ **/
+export function navigateParams( route: string, params: any ) {
+ let url = new URL( window.location.href );
+ let uparams = new URLSearchParams( params );
+ url.pathname = route;
+ url.search = uparams.toString();
+ let cb = routeForPath( route );
+ if( !cb )
+ return navigate( err404page );
+
+ window.history.pushState( {}, null, url.href );
+
+ onprenavigate();
+ const el = $( cb() );
+
+ $( `#${rootId}` ).children().remove();
+ $( `#${rootId}` ).append( el );
+ onpostnavigate();
+}
+
+/**
+ * wrapper for history.pushState
+ **/
+export function pushParams( params: any ) {
+ const url = new URL( window.location.href );
+ url.search = new URLSearchParams( params ).toString();
+
+ window.history.pushState( {}, null, url.href );
+}
+
+/**
+ * navigates without adding a history entry
+ * useful for e.g. re-rendering a page after waiting for a data callback
+**/
+export function navigateParamsSilent( route: string, params: any ) {
+ let url = new URL( window.location.href );
+ let uparams = new URLSearchParams( params );
+ url.pathname = route;
+ url.search = uparams.toString();
+ let cb = routeForPath( route );
+ if( !cb )
+ return navigateSilent( err404page );
+
+ onprenavigate();
+ const el = $( cb() );
+
+ $( `#${rootId}` ).children().remove();
+ $( `#${rootId}` ).append( el );
+ onpostnavigate();
+}
+
+/**
+ * see: navigateParamsSilent
+ **/
+export function navigateSilent( route: string ) {
+ let url = new URL( window.location.href );
+ url.pathname = route;
+ let cb = routeForPath( route );
+ if( !cb )
+ return navigateSilent( err404page );
+
+ onprenavigate();
+ const el = $( cb() );
+
+ $( `#${rootId}` ).children().remove();
+ $( `#${rootId}` ).append( el );
+ onpostnavigate();
+}
+
+/**
+ * action when the back button is pressed
+**/
+export function onPopState() {
+ let url = new URL( window.location.href );
+ let uparams = new URLSearchParams( url.searchParams );
+ url.search = uparams.toString();
+ let cb = routeForPath( url.pathname );
+ if( !cb )
+ return navigateSilent( err404page );
+
+ onprenavigate();
+ const el = $( cb() );
+
+ $( `#${rootId}` ).children().remove();
+ $( `#${rootId}` ).append( el );
+ onpostnavigate();
+}
+
+export function getRoutes() : Route[] {
+ return routes;
+}
+
+
+// internal stuff below
+
+const originalAppendChild = Element.prototype.appendChild;
+Element.prototype.appendChild = function( child: any ) {
+ if( Array.isArray( child ) ) {
+ for( const childArrayMember of child )
+ this.appendChild( childArrayMember );
+
+ return child;
+ }
+ else if( typeof child === 'string' ) {
+ return originalAppendChild.call( this, document.createTextNode( child ) );
+ }
+ else if( child ) {
+ return originalAppendChild.call( this, child );
+ }
+};
+
+export function createElement( tag: any, props: any, ...children: any ) {
+ props = props || {};
+
+ if( typeof tag === "function" ) {
+ props.children = children;
+ return tag( props );
+ }
+
+ if( tag === 'raw-content' ) {
+ const dummy = document.createElement( 'div' );
+ dummy.innerHTML = props.content;
+ return [...dummy.children];
+ }
+
+ const element = document.createElement( tag );
+
+ for( const [name, value] of Object.entries( props ) ) {
+ if( name.startsWith( 'on' ) ) {
+ const lowercaseName = name.toLowerCase();
+
+ if( lowercaseName in window ) {
+ element.addEventListener( lowercaseName.substring(2 ), value );
+ continue;
+ }
+ }
+
+ if( name == 'ref' ) {
+ ( value as any ).current = element;
+ continue;
+ }
+
+ if( value === false )
+ continue;
+
+ if( value === true ) {
+ element.setAttribute( name, '' );
+ continue;
+ }
+
+ if( assetAttributeNames.has( name ) ) {
+ if( typeof value === 'string' ) {
+ element.setAttribute( name, value );
+ }
+ continue;
+ }
+
+ element.setAttribute( name, value );
+ };
+
+ for( const child of children )
+ element.appendChild( child );
+
+ return element;
+}
+
+export function createFragment( props: any ) {
+ return props.children;
+}
diff --git a/web/src/pkg.tsx b/web/src/pkg.tsx
new file mode 100644
index 0000000..70e75c6
--- /dev/null
+++ b/web/src/pkg.tsx
@@ -0,0 +1,164 @@
+import $ from "jquery";
+import * as JSX from "./jsx";
+import { Page, Spinner } from "./components";
+import { sizeHumanReadable } from "./util";
+
+function downloadFile( file: string ) {
+ const a = document.createElement( "a" );
+ a.href = "https://networkheaven.net/pkgs/";
+ a.download = "string";
+ a.click();
+ a.remove();
+}
+
+function urlForHref( href: string, isdir: boolean ) {
+ const url = new URL( window.location.href );
+ if( isdir ) {
+ const path = url.pathname;
+ if( path.endsWith( '/' ) ) {
+ return path + href;
+ }
+ return path + "/" + href;
+ }
+
+ let searchParams = url.searchParams.toString();
+ searchParams = searchParams.slice( searchParams.indexOf( '/' ) );
+ searchParams = searchParams.slice( searchParams.indexOf( '/' ) );
+ return "https://networkheaven.net/pkgs" + searchParams + "/" + href;
+}
+
+interface PkgEntry {
+ name: string;
+ date: string;
+ time: string;
+ size: string;
+ isdir: boolean;
+}
+
+function entryFromLine( line: string ): PkgEntry | null {
+ const isdir = line[line.length - 1] == '-';
+ const name = line.slice( 0, line.indexOf( " " ) );
+ if( name == ".." ) return null;
+ if( !name )
+ return null;
+ let date = '';
+ let time = '';
+ let size = ''
+
+ if( !isdir ) {
+ let end = line.lastIndexOf( " " );
+ size = line.slice( end + 1 );
+ date = line.slice( line.indexOf( " " ) + 1, end );
+ end = date.indexOf( " " );
+ const datetime = date.slice( date.search( /[0-9]/ ) );
+ end = datetime.indexOf( ' ' );
+ date = datetime.slice( 0, end );
+ time = datetime.slice( end + 1 );
+ time = time.slice( 0, time.indexOf( " " ) );
+ } else {
+ let start = line.search( /[0-9]/ )
+ date = line.slice( start );
+ let end = date.indexOf( " " );
+ date = date.slice( 0, end );
+ time = line.slice( start + end + 1, line.length - 1 );
+ time = time.slice( 0, time.indexOf( " " ) );
+ }
+
+ return { name, date, time, size, isdir };
+}
+
+async function getEntries(): Promise<PkgEntry[]> {
+ const url = new URL( window.location.href );
+ const href = url.pathname.split( "/pkg" )[1];
+
+ const packages = await fetch( "https://networkheaven.net/pkgs/" + href );
+ const text = await packages.text();
+
+ const pkgBody = $( <div /> );
+ pkgBody.html( text );
+ pkgBody.html( pkgBody.find( "body" ).html() );
+ const pre = pkgBody.find( "pre" )[0].innerText;
+
+ const ret = [];
+ console.log( url.pathname );
+ if( !url.pathname.endsWith( "/pkg/" ) && !url.pathname.endsWith( "/pkg" ) ) {
+ ret.push({
+ name: '../',
+ date: ' ',
+ time: ' ',
+ size: '',
+ isdir: true
+ } );
+ }
+
+ const lines = pre.split( "\n" );
+ for( const line of lines ) {
+ if( !line.length )
+ continue;
+
+ const entry = entryFromLine( line );
+ console.log( entry );
+ if( entry )
+ ret.push( entry );
+ }
+
+ return ret;
+}
+
+function back() {
+ const url = new URL( window.location.href );
+ if( url.pathname.endsWith( "/" ) )
+ url.pathname = url.pathname.slice( 0, -1 );
+ url.pathname = url.pathname.slice( 0, url.pathname.lastIndexOf( "/" ) );
+ JSX.navigate( url.pathname );
+}
+
+function PackageEntry( props: any ) {
+ console.log( props );
+ const entry = props.entry as PkgEntry;
+ console.log( urlForHref( entry.name, entry.isdir ) );
+ return <tr>
+ <td>
+ { entry.name == "../" &&
+ <a href='#' onclick={ () => back() } class="package-entry-link">
+ ../yes
+ </a> }
+ { entry.name != "../" &&
+ <a href='#' onclick={ () => JSX.navigate( urlForHref( entry.name, entry.isdir ) ) } class="package-entry-link">
+ {entry.name}
+ </a> }
+ </td>
+ <td><span class="package-entry-date">{entry.date}</span></td>
+ <td><span class="package-entry-time">{entry.time}</span></td>
+ <td>{ !entry.isdir && <span>{ sizeHumanReadable( parseInt( entry.size ) ) }</span> }</td>
+ </tr>
+}
+
+export default function Pkgs() {
+ setTimeout( async () => {
+ try {
+ const entries = await getEntries();
+ const target = $( "#package-entries" ).find( "table" );
+ $( "#package-entries" ).find( ".spinner" ).remove();
+ for( const entry of entries ) {
+ target.append( <PackageEntry entry={entry} /> );
+ }
+
+ } catch( e ) {
+ console.log( e );
+ }
+
+ } );
+
+ return <Page>
+ <div style="display: flex; justify-content: center">
+ <h3 style="font-family: JPN24; font-size: 25px; width: min-content" class="gradient">PACKAGE&nbsp;REPOSITORY</h3>
+ </div>
+ <hr />
+
+ <div id="package-entries">
+ <table style="width: 90%;" />
+ <Spinner />
+ </div>
+ </Page>
+}
diff --git a/web/src/sidebar.tsx b/web/src/sidebar.tsx
new file mode 100644
index 0000000..a1b3ad7
--- /dev/null
+++ b/web/src/sidebar.tsx
@@ -0,0 +1,126 @@
+import $ from 'jquery';
+import * as JSX from './jsx';
+
+var interval = null;
+
+function cacheWeather( result: any ) {
+ const timestamp = new Date().getTime();
+
+ localStorage.setItem( "weather", JSON.stringify( result ) );
+ localStorage.setItem( "weather_timestamp", timestamp.toString() );
+}
+
+function getWeatherCache() {
+ const timestamp = localStorage.getItem( "weather_timestamp" );
+ if( !timestamp )
+ return null;
+
+ if( parseInt( timestamp ) + 60 * 60 * 1000 < new Date().getTime() ) {
+ localStorage.removeItem( "weather" );
+ localStorage.removeItem( "weather_timestamp" );
+ return null;
+ }
+
+ try {
+ const res = JSON.parse( localStorage.getItem( "weather" ) );
+ return res;
+ } catch( e ) {
+ console.log( e );
+ return null;
+ }
+}
+
+async function updateWeather() {
+ let weather = getWeatherCache();
+ if( !weather ) {
+ weather = await fetch(
+ "https://api.open-meteo.com/v1/forecast?latitude=35.0647937&longitude=137.1784597&current=temperature_2m,wind_speed_10m,relative_humidity_2m"
+ )
+
+ weather = await weather.json();
+ cacheWeather( weather );
+ }
+
+ $( "#temp" ).text( weather.current.temperature_2m );
+ $( "#wind" ).text( weather.current.wind_speed_10m );
+ $( "#humi" ).text( weather.current.relative_humidity_2m );
+
+ setTimeout( updateWeather, 60 * 60 * 1000 );
+}
+
+function Weather() {
+ setTimeout( updateWeather );
+ const weather = getWeatherCache();
+ if( weather && weather.current ) {
+ const temp = weather.current.temperature_2m;
+ const wind = weather.current.wind_speed_10m;
+ const humi = weather.current.relative_humidity_2m;
+ return <div id="weather">
+ <div class="sidebar-row"><h4>weather in </h4> <h4>愛知県、日本:</h4></div>
+ <div class="sidebar-row"><h4>temperature:</h4><h4><span id="temp">{temp.toString()} </span>°C</h4></div>
+ <div class="sidebar-row"><h4>wind speed:</h4><h4><span id="wind">{wind.toString()}</span>km/h</h4></div>
+ <div class="sidebar-row"><h4>humidity:</h4><h4><span id="humi">{humi.toString()}</span>%</h4></div>
+ </div>
+ }
+
+ return <div id="weather">
+ <div class="sidebar-row"><h4>weather in </h4> <h4>愛知県、日本:</h4></div>
+ <div class="sidebar-row"><h4>temperature:</h4><h4><span id="temp">--</span>°C</h4></div>
+ <div class="sidebar-row"><h4>wind speed:</h4><h4><span id="wind">--</span>km/h</h4></div>
+ <div class="sidebar-row"><h4>humidity:</h4><h4><span id="humi">--</span>%</h4></div>
+ </div>
+}
+
+export function Sidebar() {
+ if( interval == null ) {
+ interval = setInterval( () => {
+ $( "#time" ).text( new Date().toLocaleTimeString() );
+ $( "#date" ).text( new Date().toLocaleDateString() );
+ }, 1000 );
+ }
+
+ return <div id="sidebar">
+ <div><h3>information</h3></div>
+ <hr/>
+ <div class="sidebar-row"><h4>current time: </h4><h4><span id="time">{new Date().toLocaleTimeString()}</span></h4></div>
+ <div class="sidebar-row"><h4>today: </h4><h4><span id="date">{new Date().toLocaleDateString()}</span></h4></div>
+ <Weather />
+ <div style="margin-top: 15px"><h3>links</h3></div>
+ <hr/>
+ <div class="sidebar-row">
+ <h4>
+ <a class="nogradient" style="margin-bottom: 0" href="#" onclick={ () => JSX.navigate( "/" ) }>
+ homepage
+ </a>
+ </h4>
+ </div>
+ <div class="sidebar-row" style="margin-top: -18px">
+ <h4>
+ <a class="nogradient" href="#" onclick={ () => JSX.navigate( "/pkg" ) }>
+ slackware packages
+ </a>
+ </h4>
+ </div>
+ <div class="sidebar-row" style="margin-top: -18px">
+ <h4>
+ <a class="nogradient" href="#" onclick={ () => JSX.navigate( "/blog" ) }>
+ blog entries
+ </a>
+ </h4>
+ </div>
+ <div class="sidebar-row" style="margin-top: -18px">
+ <h4>
+ <a class="nogradient" href="https://steamcommunity.com/groups/networkheaven">
+ steam
+ </a>
+ </h4>
+ </div>
+ <div class="sidebar-row" style="margin-top: -18px">
+ <h4>
+ <a class="nogradient" href="https://git.networkheaven.net">
+ git
+ </a>
+ </h4>
+ </div>
+ </div>
+}
diff --git a/web/src/tsconfig.json b/web/src/tsconfig.json
new file mode 100644
index 0000000..b964594
--- /dev/null
+++ b/web/src/tsconfig.json
@@ -0,0 +1,121 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig to read more about this file */
+
+ /* Projects */
+ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
+ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
+ "moduleResolution": "node",
+ /* Language and Environment */
+ "target": "es2020", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+ "lib": [
+ "ES2017",
+ "DOM",
+ "DOM.Iterable",
+ "ScriptHost"
+ ],
+ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+ "jsx": "react", /* Specify what JSX code is generated. */
+ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
+ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
+ "jsxFactory": "JSX.createElement",
+ "jsxFragmentFactory": "JSX.createFragment",
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
+ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
+
+ /* Modules */
+ "module": "es2020", /* Specify what module code is generated. */
+ // "rootDir": "./", /* Specify the root folder within your source files. */
+ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
+ "baseUrl": ".",
+ "paths": {
+ "*": ["types/*"]
+ }, /* Specify a set of entries that re-map imports to additional lookup locations. */
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
+ "typeRoots": [
+ "./node_modules/@types",
+ "./types"
+ ], /* Specify multiple folders that act like './node_modules/@types'. */
+ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
+ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
+ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
+ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
+ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
+ // "noUncheckedSideEffectImports": true, /* Check side effect imports. */
+ // "resolveJsonModule": true, /* Enable importing .json files. */
+ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
+ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
+
+ /* JavaScript Support */
+ "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+ "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+
+ /* Emit */
+ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
+ "sourceMap": true, /* Create source map files for emitted JavaScript files. */
+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
+ // "noEmit": true, /* Disable emitting files from a compilation. */
+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+ // "outDir": "./", /* Specify an output folder for all emitted files. */
+ // "removeComments": true, /* Disable emitting comments. */
+ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+ // "newLine": "crlf", /* Set the newline character for emitting files. */
+ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
+ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
+ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
+
+ /* Interop Constraints */
+ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
+ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
+ // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
+ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
+ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
+
+ /* Type Checking */
+ "strict": true, /* Enable all strict type-checking options. */
+ "noImplicitAny": false, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+ "strictNullChecks": false, /* When type checking, take into account 'null' and 'undefined'. */
+ "strictFunctionTypes": false, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+ "strictBindCallApply": false, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+ "strictPropertyInitialization": false, /* Check for class properties that are declared but not set in the constructor. */
+ "strictBuiltinIteratorReturn": false, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
+ "noImplicitThis": false, /* Enable error reporting when 'this' is given the type 'any'. */
+ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
+ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
+ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
+ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
+ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
+ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
+ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
+ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
+ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
+ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
+ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
+ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
+
+ /* Completeness */
+ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
+ "skipLibCheck": true /* Skip type checking all .d.ts files. */
+ }
+}
diff --git a/web/src/user.tsx b/web/src/user.tsx
new file mode 100644
index 0000000..d909d9e
--- /dev/null
+++ b/web/src/user.tsx
@@ -0,0 +1,6 @@
+import $ from 'jquery';
+
+import * as JSX from './jsx';
+import * as api from './api';
+
+
diff --git a/web/src/util.tsx b/web/src/util.tsx
new file mode 100644
index 0000000..1b1ccc0
--- /dev/null
+++ b/web/src/util.tsx
@@ -0,0 +1,42 @@
+export function escapeHtml( html: string ) {
+ const entityMap = {
+ '&': '&amp;',
+ '<': '&lt;',
+ '>': '&gt;',
+ '"': '&quot;',
+ "'": '&#39;',
+ '/': '&#x2F;',
+ '`': '&#x60;',
+ '=': '&#x3D;'
+ };
+
+ return String( html ).replace( /[&<>"'`=\/]/g, ( s ) => {
+ return entityMap[s];
+ } );
+}
+
+export function parseJWT( token: string ) : any {
+ const parts = token.split( '.' );
+ let encoded = parts[1];
+ encoded = encoded.replace(/-/g, '+').replace(/_/g, '/');
+ const pad = encoded.length % 4;
+ if( pad === 1 )
+ throw new Error( 'what the fuck' );
+ if( pad > 1 )
+ encoded += new Array( 5 - pad ).join( '=' );
+
+ const payload = JSON.parse( atob( encoded ) );
+ return payload;
+}
+
+export function sizeHumanReadable( size: number ) {
+ if( size < 1024 )
+ return size + ' B';
+ else if( size < 1024 * 1024 )
+ return ( size / 1024 ).toFixed( 2 ) + ' KB';
+ else if( size < 1024 * 1024 * 1024 )
+ return ( size / 1024 / 1024 ).toFixed( 2 ) + ' MB';
+ else
+ return ( size / 1024 / 1024 / 1024 ).toFixed( 2 ) + ' GB';
+}
+