add the visitor API endpoint

This commit is contained in:
ngn
2025-01-08 00:20:11 +03:00
parent fc11748e57
commit dee3ef4d85
26 changed files with 444 additions and 163 deletions

View File

@ -1,21 +1,33 @@
import { urljoin } from "$lib/util.js";
const version = "v1";
const url = new URL(version + "/", import.meta.env.VITE_API_URL).href;
const url = urljoin(import.meta.env.VITE_API_URL, version);
function join(path) {
if (null === path || path === "") return url;
function api_url(path = null, query = {}) {
return urljoin(url, path, query);
}
if (path[0] === "/") path = path.slice(1);
function check_err(json) {
if (!("error" in json)) throw new Error('API response is missing the "error" key');
return new URL(path, url).href;
if (json["error"] != "") throw new Error(`API returned an error: ${json["error"]}`);
if (!("result" in json)) throw new Error('API response is missing the "result" key');
}
async function GET(fetch, url) {
const res = await fetch(url);
const json = await res.json();
check_err(json);
return json["result"];
}
async function visitor(fetch) {
return GET(fetch, api_url("/visitor"));
}
async function services(fetch) {
const res = await fetch(join("/services"));
const json = await res.json();
if (!("result" in json)) return [];
return json.result;
return GET(fetch, api_url("/services"));
}
export { version, join, services };
export { version, api_url, visitor, services };

View File

@ -1,25 +0,0 @@
<script>
export let class_name;
</script>
<main class={class_name}>
<slot></slot>
</main>
<style>
main {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: stretch;
padding: 50px;
gap: 28px;
}
@media only screen and (max-width: 900px) {
main {
flex-direction: column;
}
}
</style>

61
app/src/lib/error.svelte Normal file
View File

@ -0,0 +1,61 @@
<script>
import Link from "$lib/link.svelte";
import { color } from "$lib/util.js";
import { _ } from "svelte-i18n";
export let error = "";
</script>
<main>
<h1 style="color: var(--{color()})">{$_("error.title")}</h1>
<code>
{#if error === ""}
Unknown error
{:else}
{error}
{/if}
</code>
<Link link={import.meta.env.VITE_BUG_REPORT_URL}>
{$_("error.report")}
</Link>
<img src="/profile/sad.png" alt="" />
</main>
<style>
main {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: end;
gap: 10px;
padding: 50px;
font-size: var(--size-4);
background: var(--background);
background-size: 50%;
}
main h1 {
font-size: var(--size-6);
}
main code {
font-size: var(--size-4);
color: var(--white-2);
}
main img {
width: var(--profile-size);
position: absolute;
right: 0;
bottom: 0;
}
</style>

View File

@ -1,13 +1,15 @@
<script>
import Link from "$lib/link.svelte";
import { onMount } from "svelte";
import { visitor } from "$lib/api.js";
import { color } from "$lib/util.js";
import { _ } from "svelte-i18n";
let visitor_count = 1001;
let visitor_count = 0;
function should_congrat() {
return visitor_count % 1000 == 0;
}
onMount(async () => {
visitor_count = await visitor(fetch);
});
</script>
<footer style="border-top: solid 2px var(--{color()});">
@ -32,8 +34,8 @@
<div class="useless">
<span>
{$_("footer.number", { values: { count: visitor_count } })}
{#if should_congrat()}
<span style="color: var(--{color()})">({$_("footer.congrats")})</span>
{#if visitor_count % 1000 == 0}
<span style="color: var(--{color()})">({$_("footer.congrat")})</span>
{/if}
</span>
<span>

View File

@ -1,5 +1,7 @@
<script>
import { frontend_url, api_url } from "$lib/util.js";
import { frontend_url } from "$lib/util.js";
import { api_url } from "$lib/api.js";
export let desc, title;
</script>

View File

@ -4,29 +4,23 @@
export let picture = "";
export let title = "";
let current = "";
for (let i = 0; i < title.length; i++) {
setTimeout(
() => {
current += title[i];
},
100 * (i + 1)
);
}
</script>
<header>
<h1 style="color: var(--{color()})">{current}</h1>
<div>
<h1 class="title" style="color: var(--{color()})">
{title.toLowerCase()}
</h1>
<h1 class="cursor" style="color: var(--{color()})">_</h1>
</div>
<img src="/profile/{picture}.png" alt="" />
</header>
<style>
header {
background: linear-gradient(rgba(11, 11, 11, 0.808), rgba(1, 1, 1, 0.96)),
url("https://files.ngn.tf/banner.png");
background: var(--background);
background-size: 50%;
width: 100%;
height: 100%;
display: flex;
@ -35,15 +29,12 @@
align-items: end;
}
img {
padding: 50px 50px 0 50px;
width: 220px;
bottom: 0;
left: 0;
}
h1 {
font-size: var(--size-7);
header div {
display: flex;
flex-direction: row;
align-items: end;
padding: 50px 50px 30px 50px;
font-size: var(--size-6);
font-family:
Consolas,
Monaco,
@ -53,29 +44,38 @@
Bitstream Vera Sans Mono,
Courier New,
monospace;
padding: 50px 50px 30px 50px;
white-space: nowrap;
text-align: center;
color: white;
text-shadow: var(--text-shadow);
justify-content: start;
width: min-content;
}
h1::after {
header div .title {
text-shadow: var(--text-shadow);
overflow: hidden;
width: 0;
animation: typing 1s steps(20, end) forwards;
animation-delay: 0.3s;
}
header div .cursor {
content: "_";
display: inline-block;
animation: blink 1.5s steps(2) infinite;
}
header img {
padding: 50px 50px 0 50px;
width: var(--profile-size);
bottom: 0;
left: 0;
}
@media only screen and (max-width: 900px) {
header {
display: block;
}
h1 {
padding: 80px;
}
img {
header img {
display: none;
}
}

View File

@ -5,10 +5,13 @@
const default_color = "white-1";
export let active = false;
export let highlight = true;
export let link = "";
export let icon = "";
let style = `text-decoration-color: var(--${color()});`;
let style = "";
if (highlight) style = `text-decoration-color: var(--${color()});`;
if (active) style += `color: var(--${color()});`;
else style += `color: var(--${default_color});`;
@ -17,6 +20,18 @@
{#if icon != ""}
<Icon {icon} />
{/if}
<a {style} href={link}>
<slot></slot>
</a>
{#if highlight}
<a {style} href={link}>
<slot></slot>
</a>
{:else}
<a {style} class="no-highlight" href={link}>
<slot></slot>
</a>
{/if}
<style>
.no-highlight:hover {
text-decoration: none;
}
</style>

View File

@ -3,7 +3,7 @@
import Link from "$lib/link.svelte";
import { color, time_from_ts } from "$lib/util.js";
import { locale } from "svelte-i18n";
import { _, locale } from "svelte-i18n";
export let service = {};
let style = "";
@ -18,23 +18,35 @@
<p>{service.desc[$locale.slice(0, 2)]}</p>
</div>
<div class="links">
<Link link={service.clear}><Icon icon="nf-oct-link" /></Link>
<Link highlight={false} link={service.clear}><Icon icon="nf-oct-link" /></Link>
{#if service.onion != ""}
<Link link={service.onion}><Icon icon="nf-linux-tor" /></Link>
<Link highlight={false} link={service.onion}><Icon icon="nf-linux-tor" /></Link>
{/if}
{#if service.i2p != ""}
<Link link={service.i2p}><span style="color: var(--{color()})">I2P</span></Link>
<Link highlight={false} link={service.i2p}
><span style="color: var(--{color()})">I2P</span></Link
>
{/if}
</div>
</div>
<div class="check">
<h1>Last checked at {time_from_ts(service.check_time)}</h1>
<h1>
{$_("services.last", {
values: { time: time_from_ts(service.check_time) },
})}
</h1>
{#if service.check_res == 0}
<span style="background: var(--white-2)">Down</span>
<span style="background: var(--white-2)">
{$_("services.status.down")}
</span>
{:else if service.check_res == 1}
<span style="background: var(--{color()})">Up</span>
<span style="background: var(--{color()})">
{$_("services.status.up")}
</span>
{:else if service.check_res == 2}
<span style="background: var(--white-2)">Slow</span>
<span style="background: var(--white-2)">
{$_("services.status.slow")}
</span>
{/if}
</div>
</main>

View File

@ -1,5 +1,6 @@
import { join } from "$lib/api.js";
import { browser } from "$app/environment";
const default_lang = "en";
const colors = [
"yellow",
"cyan",
@ -8,8 +9,26 @@ const colors = [
"red",
// "blue" (looks kinda ass)
];
let colors_pos = -1;
let api_url = join;
function urljoin(url, path = null, query = {}) {
let url_len = url.length;
if (url[url_len - 1] != "/") url += "/";
if (null === path || "" === path) url = new URL(url);
else if (path[0] === "/") url = new URL(path.slice(1), url);
else url = new URL(path, url);
for (let k in query) url.searchParams.append(query[k]);
return url.href;
}
function frontend_url(path = null, query = {}) {
return urljoin(import.meta.env.VITE_FRONTEND_URL, path, query);
}
function color() {
if (colors_pos < 0) colors_pos = Math.floor(Math.random() * colors.length);
@ -23,13 +42,13 @@ function click() {
audio.play();
}
function frontend_url(path) {
if (null !== path && path !== "") return new URL(path, import.meta.env.VITE_FRONTEND_URL).href;
else return new URL(import.meta.env.VITE_FRONTEND_URL).href;
function browser_lang() {
if (browser) return window.navigator.language.slice(0, 2).toLowerCase();
return default_lang;
}
function time_from_ts(ts) {
return new Date(ts * 1000).toLocaleTimeString();
}
export { api_url, frontend_url, click, color, time_from_ts };
export { urljoin, frontend_url, browser_lang, click, color, time_from_ts };