This commit is contained in:
Flummi 2020-04-05 18:47:09 +02:00
parent a202ae2e69
commit 29329702da
10 changed files with 188 additions and 234 deletions

34
debug/trigger.mjs Normal file
View File

@ -0,0 +1,34 @@
import { promises as fs } from "fs";
(async () => {
const _args = process.argv.slice(2);
const _e = {
network: "console",
message: _args.join(" "),
args: _args.slice(1),
channel: "console",
user: {
prefix: "console!console@console",
nick: "console",
username: "console",
account: "console"
},
reply: (...args) => console.log(args),
replyAction: (...args) => console.log(args),
replyNotice: (...args) => console.log(args)
};
const trigger = (await Promise.all((await fs.readdir("./src/inc/trigger"))
.filter(f => f.endsWith(".mjs"))
.map(async t => await (await import(`../src/inc/trigger/${t}`)).default())
)).filter(t => t[0].call.test(_e.message)).map(t => ({ name: t[0].name, f: t[0].f }));
try {
if(trigger.length === 0)
return console.error("no matches");
console.log(`triggered > ${trigger[0].name} (${_e.message})`);
await trigger[0].f(_e);
} catch(err) {
console.error(err);
}
})();

107
package-lock.json generated
View File

@ -10,38 +10,18 @@
"integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ==" "integrity": "sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ=="
}, },
"@types/node": { "@types/node": {
"version": "13.9.8", "version": "13.11.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-13.9.8.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-13.11.0.tgz",
"integrity": "sha512-1WgO8hsyHynlx7nhP1kr0OFzsgKz5XDQL+Lfc3b1Q3qIln/n8cKD4m09NJ0+P1Rq7Zgnc7N0+SsMnoD1rEb0kA==" "integrity": "sha512-uM4mnmsIIPK/yeO+42F2RQhGUIs39K2RFmugcJANppXe6J1nvH87PvzPZYpza7Xhhs8Yn9yIAVdLZ84z61+0xQ=="
},
"amdefine": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz",
"integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU="
},
"async": {
"version": "0.2.10",
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"integrity": "sha1-trvgsGdLnXGXCMo43owjfLUmw9E="
},
"camelcase": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz",
"integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk="
}, },
"cuffeo": { "cuffeo": {
"version": "1.0.6", "version": "1.0.6-1",
"resolved": "https://registry.npmjs.org/cuffeo/-/cuffeo-1.0.6.tgz", "resolved": "https://registry.npmjs.org/cuffeo/-/cuffeo-1.0.6-1.tgz",
"integrity": "sha512-TdrkkDM4uR/iTEtazfxbmLUXoNB3NLC/RMgpWVUQC8j2uOiaLl7U7agdLdxfJq4uEt56cJUftqH9Tb3BofslNg==", "integrity": "sha512-r7MCG7rIuG86leo4aB73YEkl7UWAGprd2A/9xy4iZmrbMepIFaCgHOn7SC6wemPbS4LRXQAgqvV9a6PCyHaCfg==",
"requires": { "requires": {
"flumm-fetch-cookies": "^1.3.5" "flumm-fetch-cookies": "^1.3.5"
} }
}, },
"decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA="
},
"denque": { "denque": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz", "resolved": "https://registry.npmjs.org/denque/-/denque-1.4.1.tgz",
@ -86,11 +66,6 @@
"moment-timezone": "^0.5.27" "moment-timezone": "^0.5.27"
} }
}, },
"minimist": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
"integrity": "sha1-3j+YVD2/lggr5IrRoMfNqDYwHc8="
},
"moment": { "moment": {
"version": "2.24.0", "version": "2.24.0",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
@ -104,80 +79,10 @@
"moment": ">= 2.9.0" "moment": ">= 2.9.0"
} }
}, },
"optimist": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
"integrity": "sha1-2j6nRob6IaGaERwybpDrFaAZZoY=",
"requires": {
"minimist": "~0.0.1",
"wordwrap": "~0.0.2"
}
},
"safer-buffer": { "safer-buffer": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"source-map": {
"version": "0.1.34",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.34.tgz",
"integrity": "sha1-p8/omux7FoLDsZjQrPtH19CQVms=",
"requires": {
"amdefine": ">=0.0.4"
}
},
"swig": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/swig/-/swig-1.4.2.tgz",
"integrity": "sha1-QIXKBFM2kQS11IPihBs5t64aq6U=",
"requires": {
"optimist": "~0.6",
"uglify-js": "~2.4"
}
},
"uglify-js": {
"version": "2.4.24",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.4.24.tgz",
"integrity": "sha1-+tV1XB4Vd2WLsG/5q25UjJW+vW4=",
"requires": {
"async": "~0.2.6",
"source-map": "0.1.34",
"uglify-to-browserify": "~1.0.0",
"yargs": "~3.5.4"
}
},
"uglify-to-browserify": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz",
"integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc="
},
"window-size": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz",
"integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0="
},
"wordwrap": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
"integrity": "sha1-o9XabNXAvAAI03I0u68b7WMFkQc="
},
"yargs": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-3.5.4.tgz",
"integrity": "sha1-2K/49mXpTDS9JZvevRv68N3TU2E=",
"requires": {
"camelcase": "^1.0.2",
"decamelize": "^1.0.0",
"window-size": "0.1.0",
"wordwrap": "0.0.2"
},
"dependencies": {
"wordwrap": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
"integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8="
}
}
} }
} }
} }

View File

@ -4,14 +4,14 @@
"description": "f0ck, kennste?", "description": "f0ck, kennste?",
"main": "index.mjs", "main": "index.mjs",
"scripts": { "scripts": {
"start": "node --experimental-json-modules --harmony-optional-chaining src/index.mjs" "start": "node --experimental-json-modules --harmony-optional-chaining src/index.mjs",
"trigger": "node --experimental-json-modules --harmony-optional-chaining debug/trigger.mjs"
}, },
"author": "Flummi", "author": "Flummi",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"cuffeo": "^1.0.6-1", "cuffeo": "^1.0.6-1",
"flumm-fetch-cookies": "^1.3.5", "flumm-fetch-cookies": "^1.3.5",
"mariadb": "^2.3.1", "mariadb": "^2.3.1"
"swig": "^1.4.2"
} }
} }

55
public/s/js/item.js Normal file
View File

@ -0,0 +1,55 @@
const epochs = [
["year", 31536000],
["month", 2592000],
["day", 86400],
["hour", 3600],
["minute", 60],
["second", 1]
];
const getDuration = timeAgoInSeconds => {
for(let [name, seconds] of epochs) {
const interval = ~~(timeAgoInSeconds / seconds);
if(interval >= 1) return {
interval: interval,
epoch: name
};
}
};
const timeAgo = date => {
const { interval, epoch } = getDuration(~~((new Date() - new Date(date)) / 1000));
return `${interval} ${epoch}${interval === 1 ? "" : "s"} ago`;
};
const clickOnElementBinding = selector => () => (elem = document.querySelector(selector))?elem.click():null;
const keybindings = {
"ArrowLeft": clickOnElementBinding("#next"),
"ArrowRight": clickOnElementBinding("#prev"),
"r": clickOnElementBinding("#random")
};
(() => {
if(video = document.querySelector(".video-js")) {
const vid1 = videojs(video);
vid1.persistvolume({
namespace: "f0ck"
});
if(vid1.autoplay() && !vid1.paused() && vid1.hasClass("vjs-paused")) {
vid1.pause();
vid1.play();
}
}
document.querySelectorAll("time.timeago").forEach(e => e.innerHTML = timeAgo(e.title));
document.addEventListener("keydown", e => {
if(e.key in keybindings) {
e.preventDefault();
keybindings[e.key]();
}
});
if(f0ckimage = document.querySelector("#f0ck-image"))
f0ckimage.addEventListener("click", e => {
e.preventDefault();
f0ckimage.hasAttribute("style")?f0ckimage.removeAttribute("style"):f0ckimage.setAttribute("style", "max-height: unset;");
});
})();

View File

@ -1,25 +0,0 @@
const clickOnElementBinding = selector => () => (elem = document.querySelector(selector))?elem.click():null;
const keybindings = {
"ArrowLeft": clickOnElementBinding("#next"),
"ArrowRight": clickOnElementBinding("#prev"),
"r": clickOnElementBinding("#random")
};
(() => {
document.addEventListener("keydown", e => {
if(e.key in keybindings) {
e.preventDefault();
keybindings[e.key]();
}
});
const f0ckimage = document.querySelector("#f0ck-image");
if(f0ckimage) {
f0ckimage.addEventListener("click", e => {
e.preventDefault();
f0ckimage.hasAttribute("style")?f0ckimage.removeAttribute("style"):f0ckimage.setAttribute("style", "max-height: unset;");
});
}
})();
//sorry, jQuery ist dumm :--D sorry sirx, dass ich wonnes Kot auskommentiert habe

View File

@ -1,9 +1,10 @@
import router from "../router.mjs"; import router from "../router.mjs";
import cfg from "../../../config.json"; import cfg from "../../../config.json";
import fs from "fs"; import fs from "fs";
import sql from "../sql.mjs";
import swig from "swig";
import url from "url"; import url from "url";
import sql from "../sql.mjs";
import lib from "../lib.mjs";
import tpl from "../tpl.mjs";
const templates = { const templates = {
contact: fs.readFileSync("./views/contact.html", "utf-8"), contact: fs.readFileSync("./views/contact.html", "utf-8"),
@ -21,7 +22,7 @@ router.get("/", async (req, res) => {
}; };
res.reply({ res.reply({
body: swig.compile(templates.index)(data) body: tpl.render(templates.index, data)
}); });
}); });
@ -74,7 +75,7 @@ router.get(/^\/([0-9]+)$/, async (req, res) => {
data.thumb = `${cfg.websrv.paths.thumbnails}/${e.id}.png`; data.thumb = `${cfg.websrv.paths.thumbnails}/${e.id}.png`;
data.dest = `${cfg.websrv.paths.images}/${e.dest}`; data.dest = `${cfg.websrv.paths.images}/${e.dest}`;
data.mime = e.mime; data.mime = e.mime;
data.size = e.size;//lib.formatSize(e.size); data.size = lib.formatSize(e.size);
data.userchannel = e.userchannel; data.userchannel = e.userchannel;
data.usernetwork = e.usernetwork; data.usernetwork = e.usernetwork;
data.timestamp = new Date(e.stamp * 1000).toISOString(); data.timestamp = new Date(e.stamp * 1000).toISOString();
@ -84,7 +85,7 @@ router.get(/^\/([0-9]+)$/, async (req, res) => {
data.prev = query[2][0].id; data.prev = query[2][0].id;
} }
res.reply({ res.reply({
body: swig.compile(templates.item)(data) body: tpl.render(templates.item, data)
}); });
}); });

30
src/inc/tpl.mjs Normal file
View File

@ -0,0 +1,30 @@
export default new class {
syntax = [
[ "each", t => `util.forEach(${t.slice(4).trim()},($value,$key)=>{` ],
[ "/each", () => "});" ],
[ "if", t => `if(${t.slice(2).trim()}){` ],
[ "elseif", t => `}else if(${t.slice(6).trim()}){` ],
[ "else", () => "}else{" ],
[ "/if", () => "}" ],
[ "=", t => `html+=${t.slice(1).trim()};` ]
];
forEach(o, f) {
if(Array.isArray(o))
o.forEach(f);
else if(typeof o === "object")
Object.keys(o).forEach(k => f.call(null, o[k], k));
else
throw new Error(`${o} is not a iterable object`);
}
render(tpl, data) {
return new Function("util", "data", "let html = \"\";with(data){"
+ tpl.trim().replace(/[\n\r]/g, "").split(/{{\s*([^}]+)\s*}}/).filter(Boolean).map(t => {
for(let i = 0; i < this.syntax.length; i++)
if(t.indexOf(this.syntax[i][0]) === 0)
return this.syntax[i][1](t);
return `html+='${t}';`;
})
.join`` + "}return html.trim().replace(/>[\\n\\r\\s]*?</g, '><')")
.bind(null, { forEach: this.forEach })(data);
}
};

View File

@ -18,12 +18,22 @@ export default async bot => {
t: await fs.readdir("./public/t") t: await fs.readdir("./public/t")
}; };
const sizes = { const sizes = {
b: (await Promise.all(dirs.b.map(async file => (await fs.stat(`./public/b/${file}`)).size))).reduce((a, b) => b + a), b: lib.formatSize((await Promise.all(dirs.b.map(async file => (await fs.stat(`./public/b/${file}`)).size))).reduce((a, b) => b + a)),
t: (await Promise.all(dirs.t.map(async file => (await fs.stat(`./public/t/${file}`)).size))).reduce((a, b) => b + a), t: lib.formatSize((await Promise.all(dirs.t.map(async file => (await fs.stat(`./public/t/${file}`)).size))).reduce((a, b) => b + a)),
}; };
return e.reply(`${dirs.b.length} f0cks: ${sizes.b}, ${dirs.t.length} thumbnails: ${sizes.t}`); return e.reply(`${dirs.b.length} f0cks: ${sizes.b}, ${dirs.t.length} thumbnails: ${sizes.t}`);
case "limit": case "limit":
return e.reply(`up to ${lib.formatSize(cfg.main.maxfilesize)} (${lib.formatSize(cfg.main.maxfilesize * 2.5)} for admins)`); return e.reply(`up to ${lib.formatSize(cfg.main.maxfilesize)} (${lib.formatSize(cfg.main.maxfilesize * 2.5)} for admins)`);
case "thumb":
const rows = await sql.query("select id from items");
const dir = (await fs.readdir("./public/t")).filter(d => d.endsWith(".png")).map(e => +e.split(".")[0]);
const tmp = [];
for(let row of rows) {
!dir.includes(row.id) ? tmp.push(row.id) : null;
}
e.reply(`${tmp.length}, ${rows.length}, ${dir.length}`);
break;
default: default:
return e.reply("lul"); return e.reply("lul");
} }

View File

@ -1,4 +1,4 @@
<!DOCTYPE f0ck> <!doctype f0ck>
<html> <html>
<head> <head>
<title>f0ck!</title> <title>f0ck!</title>
@ -29,15 +29,15 @@
</nav> </nav>
<div class="container-fluid"> <div class="container-fluid">
<ul id="posts" data-last="{{ last }}"> <ul id="posts" data-last="{{=last}}">
{% for item in items %} {{each items}}
<li class="post"><a href="/{{ item.id }}" title="{{ item.mime }}"> <li class="post"><a href="/{{=$value.id}}" title="{{=$value.mime}}">
<img class="thumb" src="/t/{{ item.id }}.png" /> <img class="thumb" src="/t/{{=$value.id}}.png" />
<span class="item-mime">{{ item.mime }}</span> <span class="item-mime">{{=$value.mime}}</span>
</a> </a>
</li> </li>
{% endfor %} {{/each}}
</ul> </ul>
</div> </div>
<script src="./s/js/scroller.js"></script> <script src="./s/js/scroller.js"></script>
<script src="./s/js/theme.js"></script> <script src="./s/js/theme.js"></script>

View File

@ -1,7 +1,7 @@
<!doctype f0ck> <!doctype f0ck>
<html> <html>
<head> <head>
<title>{{ id }} - f0ck.me</title> <title>{{=id}} - f0ck.me</title>
<link rel="stylesheet" type="text/css" href="./s/css/video-js.min.css" /> <link rel="stylesheet" type="text/css" href="./s/css/video-js.min.css" />
<link rel="stylesheet" type="text/css" href="./s/css/vsg-skin.css" /> <link rel="stylesheet" type="text/css" href="./s/css/vsg-skin.css" />
<link rel="stylesheet" type="text/css" href="./s/css/bootstrap.css"> <link rel="stylesheet" type="text/css" href="./s/css/bootstrap.css">
@ -9,9 +9,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/png" href="./s/img/favicon.png" /> <link rel="icon" type="image/png" href="./s/img/favicon.png" />
<meta property="og:site_name" content="f0ck.me" /> <meta property="og:site_name" content="f0ck.me" />
<meta property="og:description" content="f0cked by {{ username }}" /> <meta property="og:description" content="f0cked by {{=username}}" />
<meta name="Description" content="f0cked by {{ username }}" /> <meta name="Description" content="f0cked by {{=username}}" />
<meta property="og:image" content="{{ thumbnail }}" /> <meta property="og:image" content="{{=thumbnail}}" />
<meta charset="utf-8" /> <meta charset="utf-8" />
</head> </head>
<body> <body>
@ -40,105 +40,49 @@
<div class="content"> <div class="content">
<div class="next-post"> <div class="next-post">
{% if next != null %} {{if next}}
<a id="next" href="/{{ next }}">«</a> <a id="next" href="/{{=next}}">«</a>
{% else %} {{else}}
<a id="next" href="#" style="color: #ccc !important;">«</a> <a id="next" href="#" style="color: #ccc !important;">«</a>
{% endif %} {{/if}}
</div> </div>
<div class="media-object"> <div class="media-object">
{% if item == "video" %} {{if item === "video"}}
<div class="embed-responsive embed-responsive-16by9"> <div class="embed-responsive embed-responsive-16by9">
<video id="my-video" class="video-js embed-responsive-item" width="640" height="360" src="{{ dest }}" preload="auto" autoplay controls loop data-setup="{}"></video> <video id="my-video" class="video-js embed-responsive-item" width="640" height="360" src="{{=dest}}" preload="auto" autoplay controls loop data-setup="{}"></video>
</div> </div>
{% elseif item == "audio" %} {{elseif item === "audio"}}
<div class="embed-responsive embed-responsive-16by9">
{% if thumb != null %} <audio id="my-video" class="embed-responsive-item video-js audiojs" autoplay controls loop src="{{=dest}}" data-setup="{}" poster="{{if thumb}}{{=thumb}}{{else}}/s/200.gif{{/if}}" type="audio/mp3" ></audio>
<div> </div>
{{elseif item === "image"}}
<!-- <img src="{{ thumb }}" /><br /> --> <a href="{{=dest}}" id="elfe" target="_blank"><img id="f0ck-image" src="{{=dest}}" /></a>
{{else}}
{% endif %} <h1>404 - Not f0cked</h1>
<div class="embed-responsive embed-responsive-16by9"> {{/if}}
<audio id="my-video" class="embed-responsive-item video-js audiojs" autoplay controls loop src="{{ dest }}" data-setup="{}" poster="{% if thumb !== null %}{{ thumb }}{% else %}/s/200.gif{% endif %}" type="audio/mp3" ></audio>
</div>
{% if thumb != null %}
</div> </div>
<div class="previous-post">
{% endif %} {{if prev}}
{% elseif item == "image" %} <a id="prev" href="/{{=prev}}">»</a>
<a href="{{ dest }}" id="elfe" target="_blank"><img id="f0ck-image" src="{{ dest }}" /></a> {{else}}
{% else %}
<h1>404 - Not f0cked</h1>
{% endif %}
</div>
<div class="previous-post">
{% if prev != null %}
<a id="prev" href="/{{ prev }}">»</a>
{% else %}
<a id="prev" href="#" style="color: #ccc !important;">»</a> <a id="prev" href="#" style="color: #ccc !important;">»</a>
{% endif %} {{/if}}
</div> </div>
</div> </div>
<div class="metadata"> <div class="metadata">
<span class="badge badge-dark"><a href="/{{ id }}" class="id-link">{{ id }} </a> by {{ username }}</span> <span class="badge badge-dark"><a href="/{{=id}}" class="id-link">{{=id}} </a> by {{=username}}</span>
<span class="badge badge-dark">{{ usernetwork }} / {{ userchannel }}</span> <span class="badge badge-dark">{{=usernetwork}} / {{=userchannel}}</span>
<span class="badge badge-dark"><a id="post_source" href="{{ srcurl }}" target="_blank">{{ src }}</a></span> <span class="badge badge-dark"><a id="post_source" href="{{=srcurl}}" target="_blank">{{=src}}</a></span>
<span class="badge badge-dark">{{ size }}</span> <span class="badge badge-dark">{{=size}}</span>
<span class="badge badge-dark"><time class="timeago" title="{{ timestamp }}" datetime="{{ timestamp }}"> </time></span> <span class="badge badge-dark"><time class="timeago" title="{{=timestamp}}" datetime="{{=timestamp}}"> </time></span>
<span class="badge badge-dark" id="themes"></span> <span class="badge badge-dark" id="themes"></span>
</div> </div>
</div> </div>
<script src="./s/js/shit.js"></script>
<script src="./s/js/theme.js"></script> <script src="./s/js/theme.js"></script>
<script src="./s/js/video.min.js"></script> <script src="./s/js/video.min.js"></script>
<script src="./s/js/videojs.persistvolume.js"></script> <script src="./s/js/videojs.persistvolume.js"></script>
<script> <script src="./s/js/item.js"></script>
(function() {
let video = document.querySelector(".video-js");
if(!video)
return;
var vid1 = videojs(video);
vid1.persistvolume({
namespace: "f0ck"
});
if(vid1.autoplay() && !vid1.paused() && vid1.hasClass('vjs-paused')) {
vid1.pause();
vid1.play();
}
})();
const epochs = [
["year", 31536000],
["month", 2592000],
["day", 86400],
["hour", 3600],
["minute", 60],
["second", 1]
];
const getDuration = timeAgoInSeconds => {
for(let [name, seconds] of epochs) {
const interval = ~~(timeAgoInSeconds / seconds);
if (interval >= 1) {
return {
interval: interval,
epoch: name
};
}
}
};
const timeAgo = date => {
const timeAgoInSeconds = ~~((new Date() - new Date(date)) / 1000);
const {interval, epoch} = getDuration(timeAgoInSeconds);
const suffix = interval === 1 ? "" : "s";
return `${interval} ${epoch}${suffix} ago`;
};
(() => {
document.querySelectorAll("time.timeago").forEach(e => e.innerHTML = timeAgo(e.title));
})();
</script>
</body> </body>
</html> </html>