commit 9c3bfe3aa34da9d401f90c10a95d6c3e66e5fa88 Author: zutto Date: Sat May 25 17:07:17 2024 +0300 init diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..bc0462d --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +SRC_DIR := ./dist +DEST_DIR := ../web + +all: install build copy + +install: + @npm install + +build: + @npm run build + +copy: + @rsync -av --exclude='index.html' $(SRC_DIR)/ $(DEST_DIR) diff --git a/README.md b/README.md new file mode 100644 index 0000000..768fa4a --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +vjfw - something something.. diff --git a/index.test.html b/index.test.html new file mode 100644 index 0000000..5ee2957 --- /dev/null +++ b/index.test.html @@ -0,0 +1,48 @@ + + + Zutto.fi + + + + + + + + + + + +
+ https://fedi.zutto.fi (Directory) +
+ + https://wiki.zutto.fi +
+ + https://git.zutto.fi +
+ +
+ + https://ip.zutto.fi + -
(Your IP address is %s)
+
+
+
+
+ +
+ + + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..c1ce282 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "vjfw", + "version": "1.0.0", + "description": "vjfw - Not a VanillaJS Framework", + "main": "main.js", + "scripts": { + "build": "webpack --mode=production", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "css-loader": "^7.1.2", + "mini-css-extract-plugin": "^2.9.0", + "style-loader": "^4.0.0", + "webpack": "^5.91.0", + "webpack-cli": "^5.1.4" + } +} diff --git a/src/css/main.css b/src/css/main.css new file mode 100644 index 0000000..6881a75 --- /dev/null +++ b/src/css/main.css @@ -0,0 +1,25 @@ + +.bottom { + position: fixed; + bottom: 0; + width: 100%; + text-align: center; + padding: 10px; +} + + + + + + + + + +/* core overrides */ +a { white-space: nowrap; } + + + +div[content] { + display: inline; +} diff --git a/src/css/shimmer.css b/src/css/shimmer.css new file mode 100644 index 0000000..bcc3360 --- /dev/null +++ b/src/css/shimmer.css @@ -0,0 +1,22 @@ +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + 100% { + background-position: 468px 0; + } +} + +.shimmer { + animation-duration: .5s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; + background: #f6f7f8; + background: linear-gradient(to right, #eeeeee 8%, #dddddd 18%, #eeeeee 33%); + background-size: 800px 104px; + height: 96px; + position: relative; +} + diff --git a/src/css/tripledot.css b/src/css/tripledot.css new file mode 100644 index 0000000..28316b8 --- /dev/null +++ b/src/css/tripledot.css @@ -0,0 +1,13 @@ +@keyframes dot-blink { + 0% { content: ''; } + 33% { content: '.'; } + 66% { content: '..'; } + 100% { content: '...'; } +} + +.loading::after { + content: ''; + display: inline-block; + animation: dot-blink 0.80s infinite steps(1, end); +} + diff --git a/src/html.js b/src/html.js new file mode 100644 index 0000000..c6742f8 --- /dev/null +++ b/src/html.js @@ -0,0 +1,24 @@ +class html { + constructor() { + this.html = ''; + } + + + async loadingStart(element) {} + + async loadingEnd(element) {} + + async render(element, content) { + var template = '%s'; + if (element.hasAttribute('html-template')) + template = element.getAttribute('html-template'); + else if (element.hasAttribute("template")) { + template = element.innerHTML; + element.setAttribute('html-template', template); + } + + element.innerHTML = template.replace('%s', content); + } +} + + diff --git a/src/js/app.js b/src/js/app.js new file mode 100644 index 0000000..f9b6296 --- /dev/null +++ b/src/js/app.js @@ -0,0 +1,9 @@ +import { MyIP } from './plugins/myip.js'; +import { Time } from './plugins/time.js'; + + + +(function() { + var time = new Time(); + var myip = new MyIP(); +})(); diff --git a/src/js/components/loader.js b/src/js/components/loader.js new file mode 100644 index 0000000..8daf3bc --- /dev/null +++ b/src/js/components/loader.js @@ -0,0 +1,22 @@ +import { html } from '../html.js'; +export class loader extends html { + constructor() { + super(); + return this; + } + + async loadingStart(element) { + element.classList.add('shimmer'); + return this.render(element); + } + + async loadingEnd(element) { + element.classList.remove('shimmer'); + } + + async render(element) { + return super.render(element, ` + + `); + } +} diff --git a/src/js/components/vhtml.js b/src/js/components/vhtml.js new file mode 100644 index 0000000..277c5b6 --- /dev/null +++ b/src/js/components/vhtml.js @@ -0,0 +1,29 @@ +import { html } from "../html.js"; +import { loader } from "./loader.js"; +export class vhtml extends html { + constructor() { + super(); + this.loader = undefined; + + return this; + } + + async loadingStart(element){ + if (this.loader !== undefined) this.loader.loadingEnd(element); + this.loader = new loader(); + this.loader.loadingStart(element); + return super.loadingStart(element); + } + + + async loadingEnd(element){ + if (this.loader !== undefined) + this.loader.loadingEnd(element); + + return super.loadingEnd(element); + } + + async render(element, content){ + return super.render(element, content); + } +} diff --git a/src/js/fetch.js b/src/js/fetch.js new file mode 100644 index 0000000..090746c --- /dev/null +++ b/src/js/fetch.js @@ -0,0 +1,40 @@ +export class fetcher { + constructor() { + } + + // get html content from a url + // opts - object with options + // opts.url - url to fetch + // opts.method - http method + // opts.headers - http headers + // opts.body - http body + // opts.mode - cors, no-cors, same-origin + // opts.cache - default, no-store, reload, no-cache, force-cache, only-if-cached + // opts.credentials - omit, same-origin, include + // opts.redirect - follow, error, manual + // opts.referrer - no-referrer, client + async go(opts) { + try { + const response = await fetch(opts.url, { + method: opts.method || 'GET', + headers: opts.headers || {}, + body: opts.body || null, + mode: opts.mode || 'cors', + cache: opts.cache || 'default', + credentials: opts.credentials || 'same-origin', + redirect: opts.redirect || 'follow', + referrer: opts.referrer || 'client' + }); + + if (!response.ok) { + throw new Error('Network response was not ok'); + } + + const html = await response.text(); + return html; + } catch(error) { + console.error("error at fetch.get:", error); + } + } +} + diff --git a/src/js/html.js b/src/js/html.js new file mode 100644 index 0000000..5b6a627 --- /dev/null +++ b/src/js/html.js @@ -0,0 +1,24 @@ +export class html { + constructor() { + this.html = ''; + } + + + async loadingStart(element) {} + + async loadingEnd(element) { + console.log("super stop", element); + } + + async render(element, content) { + var template = '%s'; + if (element.hasAttribute('html-template')) + template = element.getAttribute('html-template'); + else if (element.hasAttribute("template")) { + template = element.innerHTML; + element.setAttribute('html-template', template); + } + + element.innerHTML = template.replace('%s', content); + } +} diff --git a/src/js/plugins/myip.js b/src/js/plugins/myip.js new file mode 100644 index 0000000..00ff140 --- /dev/null +++ b/src/js/plugins/myip.js @@ -0,0 +1,61 @@ +import { vj } from "../vjfw.js"; +import { vhtml } from "../components/vhtml.js"; +import { fetcher } from "../fetch.js"; + +export class MyIP extends vhtml { + constructor() { + super(); + + this._ip = ""; + this._source = "https://ip.zutto.fi/" + + vj.watcher.watch('[content="myip"]', (element) => { + this.loadingStart(element); + this.ip().then((data) => { + try { + this._ip = data.trim(); + vj.ps.publish("ip", {"ip": this._ip}); + this.render(element); + this.loadingEnd(element); + } catch (e) { + console.log("error here", e) + throw e; + } + }); + }) + } + + async ip() { + try { + const fetch = new fetcher(); + console.log("ok"); + return fetch.go({"url": `${this._source}/ip` }); + } catch (e) { + throw e; + } + } + + + async loadingStart(element) { + vj.ps.publish("ip.load.start", {"element": element}); + return super.loadingStart(element); + } + + async loadingEnd(element) { + vj.ps.publish("ip.load.end", {"element": element}); + return super.loadingEnd(element); + } + + async render(element) { + try { + const rendered = await super.render(element, this._ip); + vj.ps.publish("ip.rendered", { "element": element, "rendered": rendered }); + } catch (e) { + throw e; + } + + } + +} + + diff --git a/src/js/plugins/time.js b/src/js/plugins/time.js new file mode 100644 index 0000000..82bfb5f --- /dev/null +++ b/src/js/plugins/time.js @@ -0,0 +1,20 @@ +import { html } from '../html.js'; +import { vj } from '../vjfw.js'; + +export class Time extends html { + constructor() { + super(); + + vj.watcher.watch('[content="time"]', (element) => { + this.render(element); + }); + } + + + async render(element) { + // update every second + setInterval(() => { + super.render(element, new Date().toLocaleTimeString('en-GB', { hour12: false }).toString()); + }); + } +} diff --git a/src/js/ps.js b/src/js/ps.js new file mode 100644 index 0000000..06befef --- /dev/null +++ b/src/js/ps.js @@ -0,0 +1,42 @@ +// pubsub library +export class ps { + constructor() { + this.subscribers = {}; + } + + // subscribe to event + // event: string + // callback: function + // return: void + subscribe(event, callback) { + if (!this.subscribers[event]) { + this.subscribers[event] = []; + } + this.subscribers[event].push(callback); + } + + // publish event + // event: string + // data: any + // return: void + // note: data is passed to all subscribers + publish(event, data) { + if (this.subscribers[event]) { + this.subscribers[event].forEach(callback => { + callback(data); + }); + } + } + + + // unsubscribe from event + // event: string + // callback: function + // return: void + unsubscribe(event, callback) { + if (this.subscribers[event]) { + this.subscribers[event] = this.subscribers[event].filter(cb => cb !== callback); + } + } + +} diff --git a/src/js/vjfw.js b/src/js/vjfw.js new file mode 100644 index 0000000..cb9a0cc --- /dev/null +++ b/src/js/vjfw.js @@ -0,0 +1,16 @@ +import { ps } from './ps.js'; +import { watcher } from './watcher.js'; + + +export class vjfw { + constructor() { + this.version = '0.0.1'; + this.compiled = false; + this.watcher = new watcher(); + this.ps = new ps(); + + return this; + } +} + +export const vj = new vjfw(); diff --git a/src/js/watcher.js b/src/js/watcher.js new file mode 100644 index 0000000..6c9335e --- /dev/null +++ b/src/js/watcher.js @@ -0,0 +1,57 @@ +export class watcher { + constructor() { + this.observers = new Map(); + this.initObserver(); + } + + initObserver() { + const observer = new MutationObserver((mutations) => { + mutations.forEach(mutation => { + mutation.addedNodes.forEach(node => { + if (node.nodeType === 1) { + this.checkNode(node); + } + }); + }); + }); + + observer.observe(document.body, { childList: true, subtree: true }); + } + + checkNode(node) { + this.observers.forEach((callback, selector) => { + if (node.matches(selector)) { + if (node.hasAttribute('lazyload')) { + this.observeLazyLoad(node, callback); + } else { + callback(node); + } + } + }); + } + + observeLazyLoad(element, callback) { + const intersectionObserver = new IntersectionObserver((entries, observer) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + callback(entry.target); + observer.unobserve(entry.target); + } + }); + }); + + intersectionObserver.observe(element); + } + + watch(selector, callback) { + document.querySelectorAll(selector).forEach(element => { + if (element.hasAttribute('lazyload')) { + this.observeLazyLoad(element, callback); + } else { + callback(element); + } + }); + + this.observers.set(selector, callback); + } +} diff --git a/web/css/main.css b/web/css/main.css new file mode 100644 index 0000000..6881a75 --- /dev/null +++ b/web/css/main.css @@ -0,0 +1,25 @@ + +.bottom { + position: fixed; + bottom: 0; + width: 100%; + text-align: center; + padding: 10px; +} + + + + + + + + + +/* core overrides */ +a { white-space: nowrap; } + + + +div[content] { + display: inline; +} diff --git a/web/css/shimmer.css b/web/css/shimmer.css new file mode 100644 index 0000000..bcc3360 --- /dev/null +++ b/web/css/shimmer.css @@ -0,0 +1,22 @@ +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + 100% { + background-position: 468px 0; + } +} + +.shimmer { + animation-duration: .5s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; + background: #f6f7f8; + background: linear-gradient(to right, #eeeeee 8%, #dddddd 18%, #eeeeee 33%); + background-size: 800px 104px; + height: 96px; + position: relative; +} + diff --git a/web/css/tripledot.css b/web/css/tripledot.css new file mode 100644 index 0000000..28316b8 --- /dev/null +++ b/web/css/tripledot.css @@ -0,0 +1,13 @@ +@keyframes dot-blink { + 0% { content: ''; } + 33% { content: '.'; } + 66% { content: '..'; } + 100% { content: '...'; } +} + +.loading::after { + content: ''; + display: inline-block; + animation: dot-blink 0.80s infinite steps(1, end); +} + diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..f5f7ee1 --- /dev/null +++ b/web/index.html @@ -0,0 +1,48 @@ + + + Zutto.fi + + + + + + + + + + + +
+ https://fedi.zutto.fi (Directory) +
+ + https://wiki.zutto.fi +
+ + https://git.zutto.fi +
+ +
+ + https://ip.zutto.fi + -
(Your IP address is %s)
+
+
+
+
+ +
+ + + + + + + + diff --git a/web/main.js b/web/main.js new file mode 100644 index 0000000..eca8ff5 --- /dev/null +++ b/web/main.js @@ -0,0 +1 @@ +(()=>{"use strict";class e{constructor(){this.subscribers={}}subscribe(e,r){this.subscribers[e]||(this.subscribers[e]=[]),this.subscribers[e].push(r)}publish(e,r){this.subscribers[e]&&this.subscribers[e].forEach((e=>{e(r)}))}unsubscribe(e,r){this.subscribers[e]&&(this.subscribers[e]=this.subscribers[e].filter((e=>e!==r)))}}class r{constructor(){this.observers=new Map,this.initObserver()}initObserver(){new MutationObserver((e=>{e.forEach((e=>{e.addedNodes.forEach((e=>{1===e.nodeType&&this.checkNode(e)}))}))})).observe(document.body,{childList:!0,subtree:!0})}checkNode(e){this.observers.forEach(((r,t)=>{e.matches(t)&&(e.hasAttribute("lazyload")?this.observeLazyLoad(e,r):r(e))}))}observeLazyLoad(e,r){new IntersectionObserver(((e,t)=>{e.forEach((e=>{e.isIntersecting&&(r(e.target),t.unobserve(e.target))}))})).observe(e)}watch(e,r){document.querySelectorAll(e).forEach((e=>{e.hasAttribute("lazyload")?this.observeLazyLoad(e,r):r(e)})),this.observers.set(e,r)}}const t=new class{constructor(){return this.version="0.0.1",this.compiled=!1,this.watcher=new r,this.ps=new e,this}};class s{constructor(){this.html=""}async loadingStart(e){}async loadingEnd(e){console.log("super stop",e)}async render(e,r){var t="%s";e.hasAttribute("html-template")?t=e.getAttribute("html-template"):e.hasAttribute("template")&&(t=e.innerHTML,e.setAttribute("html-template",t)),e.innerHTML=t.replace("%s",r)}}class n extends s{constructor(){return super(),this}async loadingStart(e){return e.classList.add("shimmer"),this.render(e)}async loadingEnd(e){e.classList.remove("shimmer")}async render(e){return super.render(e,'\n \n ')}}class i extends s{constructor(){return super(),this.loader=void 0,this}async loadingStart(e){return void 0!==this.loader&&this.loader.loadingEnd(e),this.loader=new n,this.loader.loadingStart(e),super.loadingStart(e)}async loadingEnd(e){return void 0!==this.loader&&this.loader.loadingEnd(e),super.loadingEnd(e)}async render(e,r){return super.render(e,r)}}class o{constructor(){}async go(e){try{const r=await fetch(e.url,{method:e.method||"GET",headers:e.headers||{},body:e.body||null,mode:e.mode||"cors",cache:e.cache||"default",credentials:e.credentials||"same-origin",redirect:e.redirect||"follow",referrer:e.referrer||"client"});if(!r.ok)throw new Error("Network response was not ok");return await r.text()}catch(e){console.error("error at fetch.get:",e)}}}new class extends s{constructor(){super(),t.watcher.watch('[content="time"]',(e=>{this.render(e)}))}async render(e){setInterval((()=>{super.render(e,(new Date).toLocaleTimeString("en-GB",{hour12:!1}).toString())}))}},new class extends i{constructor(){super(),this._ip="",this._source="https://ip.zutto.fi/",t.watcher.watch('[content="myip"]',(e=>{this.loadingStart(e),this.ip().then((r=>{try{this._ip=r.trim(),t.ps.publish("ip",{ip:this._ip}),this.render(e),this.loadingEnd(e)}catch(e){throw console.log("error here",e),e}}))}))}async ip(){try{const e=new o;return console.log("ok"),e.go({url:`${this._source}/ip`})}catch(e){throw e}}async loadingStart(e){return t.ps.publish("ip.load.start",{element:e}),super.loadingStart(e)}async loadingEnd(e){return t.ps.publish("ip.load.end",{element:e}),super.loadingEnd(e)}async render(e){try{const r=await super.render(e,this._ip);t.ps.publish("ip.rendered",{element:e,rendered:r})}catch(e){throw e}}}})(); \ No newline at end of file diff --git a/web/styles.css b/web/styles.css new file mode 100644 index 0000000..60f5b05 --- /dev/null +++ b/web/styles.css @@ -0,0 +1,63 @@ + +.bottom { + position: fixed; + bottom: 0; + width: 100%; + text-align: center; + padding: 10px; +} + + + + + + + + + +/* core overrides */ +a { white-space: nowrap; } + + + +div[content] { + display: inline; +} + +@keyframes shimmer { + 0% { + background-position: -468px 0; + } + 100% { + background-position: 468px 0; + } +} + +.shimmer { + animation-duration: .5s; + animation-fill-mode: forwards; + animation-iteration-count: infinite; + animation-name: shimmer; + animation-timing-function: linear; + background: #f6f7f8; + background: linear-gradient(to right, #eeeeee 8%, #dddddd 18%, #eeeeee 33%); + background-size: 800px 104px; + height: 96px; + position: relative; +} + + +@keyframes dot-blink { + 0% { content: ''; } + 33% { content: '.'; } + 66% { content: '..'; } + 100% { content: '...'; } +} + +.loading::after { + content: ''; + display: inline-block; + animation: dot-blink 0.80s infinite steps(1, end); +} + + diff --git a/webpack.config.js b/webpack.config.js new file mode 100644 index 0000000..c6d3672 --- /dev/null +++ b/webpack.config.js @@ -0,0 +1,25 @@ +const path = require('path'); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); + +module.exports = { + entry: './src/js/app.js', + output: { + filename: 'main.js', + path: path.resolve(__dirname, 'dist'), + }, + module: { + rules: [ + { + test: /\.css$/i, + use: [MiniCssExtractPlugin.loader, 'css-loader'], + }, + ], + }, + plugins: [ + new MiniCssExtractPlugin({ + filename: 'styles.css', + }), + ], + mode: 'development', // or 'production' +}; +