remove bloat fonts, get rid of svelte-i18n

Signed-off-by: ngn <ngn@ngn.tf>
This commit is contained in:
ngn
2025-07-24 06:15:19 +03:00
parent bf95c575eb
commit e3692f90b1
53 changed files with 1965 additions and 2616 deletions

12
app/.gitignore vendored
View File

@@ -1,11 +1,9 @@
.DS_Store
.svelte-kit
node_modules
/build
/.svelte-kit
/package
.env
.env.*
!.env.example
package
build
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
nerdfonts.*

View File

@@ -1 +0,0 @@
engine-strict=true

View File

@@ -4,6 +4,7 @@
"singleQuote": false,
"trailingComma": "es5",
"printWidth": 80,
"arrowParens": "avoid",
"plugins": ["prettier-plugin-svelte"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@@ -1,17 +1,7 @@
NF_CSS = static/css/nerdfonts.css
NF_WOFF = static/assets/nerdfonts.woff2
all: $(NF_CSS)
all:
npm run build
$(NF_CSS): $(NF_WOFF)
wget "https://www.nerdfonts.com/assets/css/webfont.css" -O $@
sed 's/\.\.\/fonts\/Symbols-2048-em Nerd Font Complete\.woff2/\/assets\/nerdfonts\.woff2/g' -i $@
$(NF_WOFF):
wget "https://www.nerdfonts.com/assets/fonts/Symbols-2048-em%20Nerd%20Font%20Complete.woff2" -O $@
run: $(NF_CSS)
run:
npm run dev
format:

2157
app/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "website",
"version": "6.3",
"version": "6.5",
"private": true,
"scripts": {
"dev": "vite dev",
@@ -10,19 +10,18 @@
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.15.1",
"@sveltejs/vite-plugin-svelte": "^4.0.3",
"@sveltejs/adapter-node": "^5.2.13",
"@sveltejs/kit": "^2.25.1",
"@sveltejs/vite-plugin-svelte": "^6.1.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2",
"svelte": "^5.16.0",
"vite": "^5.4.11"
"prettier-plugin-svelte": "^3.4.0",
"svelte": "^5.36.10",
"vite": "^7.0.0"
},
"type": "module",
"dependencies": {
"dompurify": "^3.2.3",
"marked": "^16.0.0",
"svelte-i18n": "^4.0.1"
"js-yaml": "^4.1.0",
"marked": "^16.0.0"
}
}

View File

@@ -0,0 +1,58 @@
<script>
export let title = "";
export let id = "";
</script>
<main {id}>
{#if title === ""}
<div><slot></slot></div>
{:else}
<h1>{title}</h1>
<div class="padded"><slot></slot></div>
{/if}
</main>
<style>
main {
display: flex;
flex-direction: column;
height: min-content;
}
main h1 {
font-family: var(--monospace);
font-size: var(--size-6);
color: var(--white-1);
background: var(--black-1);
position: relative;
top: 20px;
right: 7px;
width: min-content;
padding-right: 10px;
}
main h1::before {
color: var(--white-3);
content: "#";
margin: 0 10px 0 0;
}
main div {
color: var(--white-2);
font-size: var(--size-3);
line-height: 1.5em;
word-wrap: break-word;
flex: 1;
align-items: center;
}
main .padded {
padding: 25px 20px 18px 20px;
border: solid 1px var(--black-3);
}
</style>

View File

@@ -0,0 +1,94 @@
<script>
import { _ } from "$lib/locale.js";
import { date, date_from_ts } from "$lib/util.js";
import api from "$lib/api.js";
import { onMount } from "svelte";
let data = null;
onMount(async () => {
data = await api.metrics(fetch);
});
</script>
<footer>
<ul>
<li>
<a href={import.meta.env.WEBSITE_SOURCE_URL}>{$_("footer.source")}</a>
</li>
<li>
<a href="/doc/license">{$_("footer.license")}</a>
</li>
<li>
<a href="/doc/privacy">{$_("footer.privacy")}</a>
</li>
</ul>
{#if data === null}
<span>
{$_("footer.render", {
time: date(new Date()),
})}
</span>
{:else}
<span>
{$_("footer.number", {
total: data.total,
since: date_from_ts(data.since),
})}
{#if data.total % 1000 === 0}
<span class="wow">({$_("footer.wow")})</span>
{/if}
</span>
{/if}
</footer>
<style>
footer {
background: var(--glass);
border-top: solid 2px var(--color);
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
box-sizing: border-box;
padding: 15px 40px;
}
footer ul {
list-style: none;
display: flex;
flex-direction: row;
gap: 5px;
font-size: var(--size-2);
text-align: left;
}
footer ul li a {
text-decoration: none;
font-weight: 200;
}
footer ul li a:hover {
color: var(--color);
}
footer ul li:not(:last-of-type)::after {
content: " / ";
color: var(--white-3);
font-weight: 900;
}
footer span {
color: var(--white-2);
font-size: 15px;
}
footer .wow {
color: var(--color);
}
</style>

View File

@@ -1,24 +1,32 @@
<script>
import { api_urljoin } from "$lib/api.js";
import { locale } from "$lib/locale.js";
import api from "$lib/api.js";
export let desc, title;
</script>
<svelte:head>
<title>[ngn.tf] | {title}</title>
<title>ngn.tf | {title}</title>
<!-- standart metadata tags -->
<meta name="description" content={desc} />
<meta name="author" content="ngn" />
<meta name="keywords" content="ngn,ngn13,ngn1,ngn.tf" />
<meta name="keywords" content="ngn,ngn13,ngn.tf" />
<meta name="color-scheme" content="only dark" />
<meta name="theme-color" content="#000000" />
<meta property="og:title" content="[ngn.tf] | {title}" />
<!-- open graph meta tags -->
<meta property="og:title" content="ngn.tf | {title}" />
<meta property="og:description" content={desc} />
<!-- atom feed for the service updates -->
<link
rel="alternate"
type="application/atom+xml"
href={api_urljoin("/news/en")}
href={api.join("/news/" + $locale.code)}
title="Service news and updates"
/>
</svelte:head>

View File

@@ -1,22 +1,32 @@
<script>
import { browser } from "$app/environment";
import { color } from "$lib/util.js";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
import { browser } from "$app/environment";
import { _ } from "$lib/locale.js";
export let picture = "";
export let title = "";
let title_cur = title;
let show_animation = false;
let javascript = false;
// TODO: make the animation first delete and then type
// do the typing animation
function animate(title) {
if (!browser) return;
// animation is displayed in the browser obv
if (!browser) {
return;
}
// clear any previous timeouts
let id = window.setTimeout(function () {}, 0);
while (id--) {
clearTimeout(id);
}
while (id--) clearTimeout(id);
// reset the current title and add each letter with a timeout to give the
// epic typing effect
title_cur = "";
for (let i = 0; i < title.length; i++) {
@@ -27,7 +37,7 @@
}
onMount(() => {
show_animation = true;
javascript = true;
});
$: animate(title);
@@ -35,20 +45,19 @@
<header>
<div>
{#if show_animation}
<h1 class="title" style="color: var(--{color()})">{title_cur}</h1>
<h1 class="cursor" style="color: var(--{color()})">_</h1>
{#if javascript}
<h1 class="title">{title_cur}</h1>
<h1 class="cursor">_</h1>
{:else}
<h1 class="title" style="color: var(--{color()})">{title}</h1>
<h1 class="title">{title}</h1>
{/if}
</div>
<img src="/profile/{picture}.png" alt="" />
<img src="/assets/{picture}.png" alt="" />
</header>
<style>
header {
background: var(--background);
background-size: 50%;
background: var(--transparent);
width: 100%;
height: 100%;
@@ -62,7 +71,7 @@
display: flex;
flex-direction: row;
align-items: end;
padding: 50px 50px 30px 50px;
padding: 40px 40px 10px 40px;
font-size: var(--size-6);
font-family:
Consolas,
@@ -78,6 +87,10 @@
width: min-content;
}
header h1 {
color: var(--color);
}
header div .title {
text-shadow: var(--text-shadow);
overflow: hidden;
@@ -90,10 +103,8 @@
}
header img {
width: 220px;
padding: 50px 50px 0 50px;
width: var(--profile-size);
bottom: 0;
left: 0;
}
@media only screen and (max-width: 900px) {

View File

@@ -0,0 +1,34 @@
<script>
import { localizer, next } from "$lib/locale.js";
import { onMount } from "svelte";
// "show" is simply used to not display the locale button when javascript is
// not enabled, bc it does not function without javascript
let show = false;
onMount(() => {
show = true;
});
</script>
{#if show}
<!-- TODO: make this work without javascript -->
<button on:click={() => localizer.switch()}>
{$next.icon}
</button>
{/if}
<style>
button {
background: none;
color: var(--white-1);
font-size: var(--size-4);
outline: none;
border: none;
transition: 0.4s;
}
button:hover {
background: var(--black-1);
}
</style>

View File

@@ -0,0 +1,46 @@
<script>
import Page from "$components/page.svelte";
import Language from "$components/language.svelte";
import { _ } from "$lib/locale.js";
</script>
<nav>
<h3>ngn.tf</h3>
<div>
<Page link="/">{$_("navbar.home")}</Page>
<Page link="/services">{$_("navbar.services")}</Page>
<Page link="/donate">{$_("navbar.donate")}</Page>
<Language />
</div>
</nav>
<style>
nav {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
background: var(--glass);
box-shadow: var(--box-shadow);
border-bottom: solid 2px var(--color);
padding: 15px 40px;
}
nav div {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: right;
gap: 15px;
}
nav h3 {
color: var(--color);
font-family: var(--monospace);
font-size: var(--size-4);
font-weight: 900;
}
</style>

View File

@@ -0,0 +1,35 @@
<script>
import { click } from "$lib/util.js";
import { page } from "$app/stores";
export let link;
</script>
<a
class={$page.url.pathname === link ? "active" : "inactive"}
data-sveltekit-preload-data
on:click={click}
href={link}
>
<slot></slot>
</a>
<style>
a {
font-weight: 600;
font-size: var(--size-3);
color: var(--white-1);
text-decoration: none;
text-decoration-color: var(--color);
}
a:hover {
color: var(--color);
text-decoration: underline;
}
.active {
color: var(--color);
}
</style>

View File

@@ -0,0 +1,138 @@
<script>
import { time_from_ts } from "$lib/util.js";
import { _, locale } from "$lib/locale.js";
export let service = {};
</script>
<main>
<div class="info">
<div>
<h1>{service.name}</h1>
<p>{service.desc[$locale.code]}</p>
</div>
<ul>
<li><a href={service.clear}> Clear</a></li>
<li><a href={service.onion}>TOR</a></li>
<li><a href={service.i2p}> I2P</a></li>
</ul>
</div>
<div class="check">
<h1>
{$_("services.last", {
time: time_from_ts(service.check_time),
})}
</h1>
{#if service.check_res == 0}
<span style="background: var(--white-2)">
{$_("services.status.down")}
</span>
{:else if service.check_res == 1}
<span style="background: var(--color)">
{$_("services.status.up")}
</span>
{:else if service.check_res == 2}
<span style="background: var(--color); filter: brightness(50%);">
{$_("services.status.slow")}
</span>
{/if}
</div>
</main>
<style>
main {
display: flex;
flex-direction: column;
background: var(--black-2);
border: solid 1px var(--black-3);
text-align: left;
flex-grow: 1;
}
main .info {
display: flex;
flex-direction: row;
align-items: start;
justify-content: space-between;
flex: 1;
color: var(--white-1);
padding: 15px 18px;
}
main .info div h1 {
font-size: var(--size-4);
font-weight: 900;
}
main .info div p {
font-size: var(--size-2);
color: var(--white-2);
font-weight: 100;
}
main .info ul {
display: flex;
flex-direction: row;
gap: 5px;
text-align: right;
list-style: none;
}
main .info ul li:not(:last-of-type)::after {
content: " / ";
color: var(--white-3);
font-weight: 900;
}
main .info li a {
color: var(--color);
text-decoration: none;
font-size: var(--size-2);
font-weight: 100;
}
main .info li a:hover {
text-decoration: underline;
}
main .info li a[href=""] {
color: var(--white-3);
cursor: not-allowed;
}
main .info li a[href=""]:hover {
text-decoration: none;
text-shadow: none;
}
main .check {
border-top: solid 1px var(--black-3);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
color: var(--white-1);
}
main .check h1 {
font-size: var(--size-2);
font-weight: 100;
color: var(--white-2);
padding: 5px 18px;
}
main .check span {
font-size: var(--size-4);
font-weight: 1000;
color: var(--black-1);
padding: 5px 18px;
text-transform: uppercase;
}
</style>

14
app/src/hooks.server.js Normal file
View File

@@ -0,0 +1,14 @@
import { redirect } from "@sveltejs/kit";
/** @type {import('@sveltejs/kit').HandleServerError} */
export async function handleError({ error, status }) {
// for unknown routes, just redirect to /
if (status === 404) {
return redirect(303, "/");
}
// for other errors, pass the message which will be used by the error page
return {
message: `${error}`,
};
}

View File

@@ -1,51 +1,59 @@
import { browser } from "$app/environment";
import { urljoin } from "$lib/util.js";
const api_version = "v1";
class API {
constructor() {
this.version = "v1";
}
function api_urljoin(path = null, query = {}) {
let api_url = "";
// join given path and queries into an API URL
join(path = null, query = {}) {
let base = "";
if (browser) api_url = urljoin(import.meta.env.WEBSITE_API_PATH, api_version);
else api_url = urljoin(import.meta.env.WEBSITE_API_URL, api_version);
if (browser) {
base = urljoin(import.meta.env.WEBSITE_API_PATH, this.version);
} else {
base = urljoin(import.meta.env.WEBSITE_API_URL, this.version);
}
return urljoin(api_url, path, query);
return urljoin(base, path, query);
}
// check given JSON body for errors
check_err(json) {
if (!("error" in json))
throw new Error('API response is missing the "error" key');
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');
}
// make a HTTP GET request to the given URL
async GET(fetch, url) {
const res = await fetch(url);
const json = await res.json();
this.check_err(json);
return json["result"];
}
// get visitor metrics
async metrics(fetch) {
return await this.GET(fetch, this.join("/metrics"));
}
// get service list
async services(fetch) {
return await this.GET(fetch, this.join("/services"));
}
// get projects list
async projects(fetch) {
return await this.GET(fetch, this.join("/projects"));
}
}
function api_check_err(json) {
if (!("error" in json))
throw new Error('API response is missing the "error" key');
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 api_http_get(fetch, url) {
const res = await fetch(url);
const json = await res.json();
api_check_err(json);
return json["result"];
}
async function api_get_metrics(fetch) {
return await api_http_get(fetch, api_urljoin("/metrics"));
}
async function api_get_services(fetch) {
return await api_http_get(fetch, api_urljoin("/services"));
}
async function api_get_projects(fetch) {
return await api_http_get(fetch, api_urljoin("/projects"));
}
export {
api_version,
api_urljoin,
api_get_metrics,
api_get_services,
api_get_projects,
};
const api = new API();
export default api;

View File

@@ -1,50 +0,0 @@
<script>
export let title;
</script>
<main>
<h1 class="title">{title}</h1>
<div>
<slot></slot>
</div>
</main>
<style>
main {
flex: 1;
flex-basis: 30%;
display: flex;
flex-direction: column;
}
main .title {
font-family:
Consolas,
Monaco,
Lucida Console,
Liberation Mono,
DejaVu Sans Mono,
Bitstream Vera Sans Mono,
Courier New,
monospace;
color: var(--white-1);
}
main .title::before {
content: "#";
margin: 0 10px 0 0;
color: var(--white-3);
}
main div {
border-left: solid 1px var(--black-4);
padding: 25px 25px 10px 25px;
font-size: var(--size-4);
color: var(--white-1);
word-wrap: break-word;
align-items: center;
margin-left: 7px;
flex: 1;
}
</style>

View File

@@ -1,3 +1,5 @@
// TODO: clean this up like api.js
import { urljoin } from "$lib/util.js";
function doc_urljoin(path = null, query = {}) {

View File

@@ -1,62 +0,0 @@
<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.WEBSITE_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;
align-items: flex-start;
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,85 +0,0 @@
<script>
import { color, date_from_ts } from "$lib/util.js";
import { api_get_metrics } from "$lib/api.js";
import Link from "$lib/link.svelte";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
let show_counter = false,
data = {};
onMount(async () => {
show_counter = true;
data = await api_get_metrics(fetch);
});
</script>
<footer style="border-top: solid 2px var(--{color()});">
<div class="links">
<span>
<Link link={import.meta.env.WEBSITE_SOURCE_URL} bold={true}
>{$_("footer.source")}</Link
>
</span>
<span>/</span>
<span>
<Link link="/doc/license" bold={true}>{$_("footer.license")}</Link>
</span>
<span>/</span>
<span>
<Link link="/doc/privacy" bold={true}>{$_("footer.privacy")}</Link>
</span>
</div>
{#if show_counter}
<span class="counter">
{$_("footer.number", {
values: {
total: data.total,
since: date_from_ts(data.since),
},
})}
{#if data.number % 1000 == 0}
<span style="color: var(--{color()})">({$_("footer.wow")})</span>
{/if}
</span>
{:else}
<span class="counter">{$_("footer.js")}</span>
{/if}
</footer>
<style>
footer {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
background: var(--black-1);
box-sizing: border-box;
padding: 20px 50px 20px 50px;
}
div {
display: flex;
font-size: var(--size-2);
flex-direction: column;
gap: 5px;
}
span {
color: var(--white-2);
font-size: 15px;
}
.counter {
text-align: right;
}
.links {
text-align: left;
display: flex;
flex-direction: row;
gap: 5px;
}
</style>

View File

@@ -1,6 +0,0 @@
<script>
import { color } from "$lib/util.js";
export let icon = "";
</script>
<i style="color: var(--{color()});" class="nf {icon}"></i>

View File

@@ -1,37 +0,0 @@
<script>
import Icon from "$lib/icon.svelte";
import { color } from "$lib/util.js";
const default_color = "white-1";
export let active = false;
export let highlight = true;
export let link = "";
export let icon = "";
let style = "";
if (highlight) style = `text-decoration-color: var(--${color()});`;
if (active) style += `color: var(--${color()});`;
else style += `color: var(--${default_color});`;
</script>
{#if icon != ""}
<Icon {icon} />
{/if}
{#if highlight}
<a data-sveltekit-preload-data {style} href={link}>
<slot></slot>
</a>
{:else}
<a data-sveltekit-preload-data {style} class="no-highlight" href={link}>
<slot></slot>
</a>
{/if}
<style>
.no-highlight:hover {
text-decoration: none;
}
</style>

View File

@@ -1,67 +1,225 @@
import { init, locale, register, waitLocale } from "svelte-i18n";
import { derived, get, writable } from "svelte/store";
import { browser } from "$app/environment";
import { get, writable } from "svelte/store";
import yaml from "js-yaml";
const locale_default = "en";
let locale_index = writable(0);
let locale_list = [];
// defines a single locale
class Locale {
constructor(code, icon) {
// regex for "render"ing the locale
this.ref_regex = /\[[0-9]*:.*?\]/gm;
this.link_regex = /\[[^\]]*]\([^ ]*\)/gm;
this.bold_regex = /\*\*.*?\*\*/gm;
function locale_setup() {
// english
register("en", () => import("../locales/en.json"));
locale_list.push({ code: "en", name: "English", icon: "🇬🇧" });
// turkish
register("tr", () => import("../locales/tr.json"));
locale_list.push({ code: "tr", name: "Turkish", icon: "🇹🇷" });
init({
fallbackLocale: locale_default,
initialLocale: get(locale),
});
}
function locale_from_browser() {
if (browser) return window.navigator.language.slice(0, 2).toLowerCase();
else return locale_default;
}
function locale_select(l = null) {
if (l === null) {
if (browser && null !== (l = localStorage.getItem("locale")))
locale_select(l);
else locale_select(locale_from_browser());
return;
this.code = code; // BCP 47 language tag
this.icon = icon; // icon for the locale
this.all = {}; // all the locales
}
l = l.slice(0, 2);
for (let i = 0; i < locale_list.length; i++) {
if (l !== locale_list[i].code) continue;
if (browser) localStorage.setItem("locale", l);
locale.set(l);
locale_index.set(i);
return;
// load the locale
async load() {
const text = await import(`../locales/${this.code}.yaml?raw`);
this.all = yaml.load(text.default);
}
locale.set(locale_default);
locale_index.set(0);
// renders a given locale using given values and links
render(locale, values, links) {
// get rid of newlines and remove trailing/repeating spaces and stuff
locale = locale.replaceAll("\r", "").replaceAll("\n", " ");
locale = locale.trim();
// find and replace all the values
for (let name in values) {
// values cannot be objects
if (typeof values[name] === "object") {
continue;
}
// "{name}" will be replaced by the values
locale = locale.replaceAll(`{${name}}`, values[name]);
}
// find all the references
const refs = [...locale.matchAll(this.ref_regex)];
// and repalce them
for (let i = 0; i < refs.length; i++) {
let ref = refs[i][0];
let name = ref.replaceAll(/(^\[|\]$)/g, "");
let indx = parseInt(name.charAt(0)) - 1;
name = name.substring(2);
// check the index
if (indx >= links.length) {
continue;
}
// replace the reference with a link
locale = locale.replaceAll(ref, `<a href="${links[indx]}">${name}</a>`);
}
// look for []() patterns, which are used adding links, kinda like markdown
// but this is shittier
links = [...locale.matchAll(this.link_regex)];
// replace the found links
for (let i = 0; i < links.length; i++) {
let link = links[i][0];
let name = link.match(/(?<=\[).*?(?=])/g);
let url = link.match(/(?<=\()[^ ]*(?=\))/g);
// if we fail to extract the link name and/or URL, skip this match
if (null === name || null === url) continue;
locale = locale.replaceAll(link, `<a href="${url[0]}">${name[0]}</a>`);
}
// look for double stars which is used for bold text
const bolds = [...locale.matchAll(this.bold_regex)];
// replace bold text with actual bold text
for (let i = 0; i < bolds.length; i++) {
let bold = bolds[i][0];
let text = bold.match(/(?<=\*\*).*?(?=\*\*)/g);
// if we fail to extract the text content, skip this match
if (null === text) continue;
locale = locale.replaceAll(bold, `<b>${text}</b>`);
}
return locale;
}
// resolve the given locale
resolve(key, vars = {}) {
let cur = this.all;
let keys = key.split(".");
for (let i = 0; i < keys.length; i++) {
cur = cur[keys[i]];
if (cur === undefined) {
return cur;
}
}
// locale needs to be a string
if (typeof cur !== "string") {
return undefined;
}
// extract links from the vars
let links = vars.links;
delete vars.links;
return this.render(cur, vars, links);
}
}
async function locale_wait() {
await waitLocale();
// localizer stores and defines all the locales
class Localizer {
constructor() {
// list of supported locales
this.list = [
new Locale("en", "🇬🇧󠁧󠁢󠁥󠁮󠁧󠁿"), // English
new Locale("tr", "🇹🇷"), // Turkish
];
this.current = writable(this.list[0]); // current locale
this.next = writable(this.list[1]); // next locale
this.fallback = this.list[0]; // fallback locale
}
// get the name of a language using the current locale
name(code) {
return get(this.current).name(code);
}
// get the current browser locale tag
browser() {
if (browser) {
window.navigator.language.slice(0, 2).toLowerCase();
} else {
return this.fallback.code;
}
}
/* load all the locales, attempt to set the provided locale as the current
* locale, if the provided locale is not available just use the default */
async setup(code) {
for (let i = 0; i < this.list.length; i++) {
await this.list[i].load();
}
// find the locale index by it's code
let indx = this.list.findIndex(locale => {
return locale.code === code;
});
// check the index to see if we found the locale
if (indx < 0) {
return false;
}
// if we actually found it, set it as the current locale
this.current.set(this.list[indx]);
// set the next locale
if (++indx >= this.list.length) {
this.next.set(this.list[0]);
} else {
this.next.set(this.list[indx]);
}
}
// switch to the next locale
switch() {
// find the next locale's index
let indx = this.list.findIndex(locale => {
return locale === get(this.next);
});
// set next locale as the new current locale
this.current.set(get(this.next));
document.cookie = `locale=${get(this.next).code}`;
// get the next locale based on the index
if (indx === this.list.length - 1) {
this.next.set(this.list[(indx = 0)]);
} else {
this.next.set(this.list[++indx]);
}
}
resolve(key, vars = {}) {
// attempt to resolve the given key
let res = get(this.current).resolve(key, vars);
if (res !== undefined) {
return res;
}
// if we fail to resolve the key, try to resolve it using the fallback
// locale, if that fails too then we are kinda fucked so yeah we just throw
// an error
if (get(this.current) === this.fallback) {
throw new Error(`missing key: ${key}`);
} else {
return this.fallback.resolve(key, vars);
}
}
}
export {
export const localizer = new Localizer(); // global localizer
export const locale = localizer.current; // current locale
export const next = localizer.next; // next locale
// resolve a given locale by it's key
export const _ = derived(
locale,
locale_list,
locale_index,
locale_default,
locale_setup,
locale_wait,
locale_select,
locale_from_browser,
};
() =>
(key, vars = {}) =>
localizer.resolve(key, vars)
);
export default localizer;

View File

@@ -1,42 +0,0 @@
<script>
import NavbarLink from "./navbar_link.svelte";
import NavbarSwitch from "./navbar_switch.svelte";
import { color } from "$lib/util.js";
import { _ } from "svelte-i18n";
</script>
<nav style="border-bottom: solid 2px var(--{color()});">
<h3 style="color: var(--{color()})">[ngn.tf]</h3>
<div>
<NavbarLink link="/">{$_("navbar.home")}</NavbarLink>
<NavbarLink link="/services">{$_("navbar.services")}</NavbarLink>
<NavbarLink link="/donate">{$_("navbar.donate")}</NavbarLink>
<NavbarSwitch />
</div>
</nav>
<style>
nav {
box-shadow: var(--box-shadow-1);
background: var(--black-1);
padding: 20px 30px 20px 20px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
div {
display: flex;
align-items: center;
flex-wrap: wrap;
justify-content: right;
gap: 15px;
}
h3 {
font-weight: 900;
font-size: var(--size-4);
}
</style>

View File

@@ -1,26 +0,0 @@
<script>
import { color, click } from "$lib/util.js";
import { page } from "$app/stores";
export let link;
</script>
<a
style="
text-decoration-color: var(--{color()});
{$page.url.pathname === link ? `color: var(--${color()});` : ''}
"
data-sveltekit-preload-data
on:click={click}
href={link}
>
<slot></slot>
</a>
<style>
a {
font-weight: 900;
font-size: var(--size-4);
color: var(--white-1);
}
</style>

View File

@@ -1,45 +0,0 @@
<script>
import { locale_list, locale_select, locale_index } from "$lib/locale.js";
import { onMount } from "svelte";
let len = locale_list.length;
let show = false;
function get_next(indx) {
let new_indx = 0;
if (indx + 1 >= len) indx = 0;
else new_indx = indx + 1;
return locale_list[new_indx];
}
function next() {
locale_select(get_next($locale_index).code);
}
onMount(() => {
show = true;
});
</script>
{#if show}
<button on:click={next}>
{get_next($locale_index).icon}
</button>
{/if}
<style>
button {
background: var(--black-2);
color: var(--white-1);
font-size: var(--size-4);
outline: none;
border: none;
transition: 0.4s;
}
button:hover {
background: var(--black-1);
}
</style>

View File

@@ -1,118 +0,0 @@
<script>
import Icon from "$lib/icon.svelte";
import Link from "$lib/link.svelte";
import { color, time_from_ts } from "$lib/util.js";
import { locale, _ } from "svelte-i18n";
export let service = {};
</script>
<main>
<div class="info">
<div class="title">
<h1>{service.name}</h1>
<p>{service.desc[$locale]}</p>
</div>
<div class="links">
<Link highlight={false} link={service.clear}
><Icon icon="nf-oct-link" /></Link
>
{#if service.onion != ""}
<Link highlight={false} link={service.onion}
><Icon icon="nf-linux-tor" /></Link
>
{/if}
{#if service.i2p != ""}
<Link highlight={false} link={service.i2p}
><span style="color: var(--{color()})">I2P</span></Link
>
{/if}
</div>
</div>
<div class="check">
<h1>
{$_("services.last", {
values: { time: time_from_ts(service.check_time) },
})}
</h1>
{#if service.check_res == 0}
<span style="background: var(--white-2)">
{$_("services.status.down")}
</span>
{:else if service.check_res == 1}
<span style="background: var(--{color()})">
{$_("services.status.up")}
</span>
{:else if service.check_res == 2}
<span style="background: var(--{color()}); filter: brightness(50%);">
{$_("services.status.slow")}
</span>
{/if}
</div>
</main>
<style>
main {
display: flex;
flex-direction: column;
background: var(--black-3);
border: solid 1px var(--black-4);
text-align: left;
flex: 1;
flex-basis: 40%;
}
main .info {
padding: 25px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
color: var(--white-1);
flex: 1;
}
main .info .title h1 {
font-size: var(--size-5);
margin-bottom: 8px;
font-weight: 900;
}
main .info .title p {
font-size: var(--size-4);
color: var(--white-2);
font-weight: 100;
}
main .info .links {
display: flex;
flex-direction: row;
gap: 10px;
font-size: var(--size-6);
}
main .check {
border-top: solid 1px var(--black-4);
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
color: var(--white-1);
}
main .check h1 {
padding: 15px 25px 15px 25px;
font-size: var(--size-4);
font-weight: 100;
}
main .check span {
padding: 15px 25px 15px 25px;
font-size: var(--size-5);
text-transform: uppercase;
color: var(--black-1);
font-weight: 1000;
}
</style>

View File

@@ -1,61 +1,60 @@
import { locale_from_browser } from "$lib/locale.js";
import { localizer } from "$lib/locale.js";
const colors = [
"yellow",
"cyan",
"green",
"pinkish",
"red",
// "blue" (looks kinda ass)
];
// colors defined in static/css/global.css
const colors = ["yellow", "cyan", "green", "pinkish", "red", "blue"];
let colors_pos = -1;
function color() {
if (colors_pos < 0) colors_pos = Math.floor(Math.random() * colors.length);
else if (colors_pos >= colors.length) colors_pos = 0;
return colors[colors_pos];
// randomly select a color
export function color() {
return colors[Math.floor(Math.random() * colors.length)];
}
function click() {
// play a click sound
export function click() {
let audio = new Audio("/assets/click.wav");
audio.play();
}
function urljoin(url, path = null) {
if (undefined === url || null === url) return;
// join a given path to the URL
export function urljoin(url, path = null) {
if (null === path || path.length === 0) {
return url;
}
if (url[url.length - 1] != "/") url += "/";
if (url[url.length - 1] != "/") {
url += "/";
}
if (path[0] === "/") {
path = path.slice(1);
}
if (null === path || "" === path) return url;
if (path[0] === "/") return url + path.slice(1);
return url + path;
}
function time_from_ts(ts) {
if (ts === 0 || ts === undefined) return;
let ts_date = new Date(ts * 1000);
let ts_zone = ts_date.toString().match(/([A-Z]+[\+-][0-9]+)/)[1];
return (
new Intl.DateTimeFormat(locale_from_browser(), {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).format(ts_date) + ` (${ts_zone})`
);
}
function date_from_ts(ts) {
if (ts === 0 || ts === undefined) return;
return new Intl.DateTimeFormat(locale_from_browser(), {
// convert Date() to readable date
export function date(date) {
return new Intl.DateTimeFormat(localizer.browser(), {
month: "2-digit",
year: "2-digit",
day: "2-digit",
}).format(new Date(ts * 1000));
}).format(date);
}
export { color, click, urljoin, time_from_ts, date_from_ts };
// convert timestamp to readable time
export function time_from_ts(ts) {
const date = new Date(ts * 1000);
const zone = date.toString().match(/([A-Z]+[\+-][0-9]+)/)[1];
return (
new Intl.DateTimeFormat(localizer.browser(), {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
}).format(date) + ` (${zone})`
);
}
// convert timestamp to readable date
export function date_from_ts(ts) {
return date(new Date(ts * 1000));
}

View File

@@ -1,79 +0,0 @@
{
"navbar": {
"home": "home",
"services": "services",
"donate": "donate"
},
"home": {
"title": "hello world!",
"welcome": {
"title": "about",
"desc": "Welcome to my website, I'm ngn",
"whoami": "I'm a security, privacy and freedom advocate high-schooler from Turkey",
"interest": "I'm interested in system security and software development",
"support": "I love and support Free/Libre and Open Source Software (FLOSS)"
},
"work": {
"title": "work",
"desc": "I don't currently have a job, so I spend most of my time...",
"build": "building stupid shit",
"fix": "fixing stupid shit",
"ctf": "solving CTF challenges",
"contribute": "contributing to random projects",
"wiki": "expanding my wiki"
},
"links": {
"title": "contact",
"desc": "Here are some useful links if you want to get in contact with me",
"prefer": "I highly prefer email, you can send encrypted emails using my PGP key"
},
"services": {
"title": "services",
"desc": "A part from working on stupid shit, I host free (as in freedom and price) services available for all",
"speed": "All of these services are available over an 1 Gbit interface",
"security": "All use SSL encrypted connection and they respect your privacy and freedom",
"privacy": "Accessible from clearnet, TOR and I2P, no region or network blocks",
"bullshit": "No CDNs, no cloudflare, no CAPTCHA, no analytics, no bullshit",
"link": "See all the services!"
},
"projects": {
"title": "projects",
"desc": "I mostly work on free software projects, here are some of projects that you might find interesting"
}
},
"services": {
"title": "service status",
"none": "No services found",
"search": "Search for a service",
"feed": "News and updates",
"last": "Last checked at {time}",
"status": {
"up": "Up",
"down": "Down",
"slow": "Slow"
}
},
"donate": {
"title": "donate!",
"info": "I spend a lot of time and money on different projects and maintaining different services.",
"price": "I mostly pay for hosting and electricity. Which when added up costs around 550₺ per month (~$15 at the time of writing).",
"details": "So even a small donation would be useful. And it would help me keep everything up and running.",
"thanks": "Also huge thanks to all of you who have donated so far!",
"table": {
"platform": "Platform",
"address": "Adress/Link"
}
},
"error": {
"title": "something went wrong!",
"report": "Report this issue"
},
"footer": {
"source": "Source",
"license": "License",
"privacy": "Privacy",
"number": "Visited {total} times since {since}",
"wow": "wow!!",
"js": "Enable javascript to display all the elements"
}
}

View File

@@ -1,79 +0,0 @@
{
"navbar": {
"home": "anasayfa",
"services": "servisler",
"donate": "bağış"
},
"home": {
"title": "merhaba dünya!",
"welcome": {
"title": "hakkımda",
"desc": "Websiteme hoşgeldiniz, ben ngn",
"whoami": "Türkiye'den, güvenlik, gizlik ve özgürlük savunucusu bir liseliyim",
"interest": "Sistem güvenliği ve yazılım geliştirmek ile ilgileniyorum",
"support": "Özgür/Libre ve Açık Kaynaklı Yazılımı (FLOSS) seviyorum ve destekliyorum"
},
"work": {
"title": "iş",
"desc": "Şuan bir işim yok, o yüzden zamanımın çoğunu şunlarla geçiriyorum:",
"build": "salak şeyler inşa etmek",
"fix": "salak şeyleri düzeltmek",
"ctf": "CTF challenge'ları çözmek",
"contribute": "rastgele projelere katkıda bulunmak",
"wiki": "wikimi genişletmek"
},
"links": {
"title": "iletişim",
"desc": "Eğer benim ile iletişime geçmek istiyorsanız, işte bazı faydalı linkler",
"prefer": "Email'i fazlasıyla tercih ediyorum, PGP anahtarım ile şifreli email'ler gönderebilirsiniz"
},
"services": {
"title": "servisler",
"desc": "Salak şeyler inşa etmenin yanı sıra, herkes için kullanıma açık özgür ve ücretsiz servisler host ediyorum",
"speed": "Tüm servisler 1 Gbit ağ arayüzü üzerinden erişilebilir",
"security": "Hepsi SSL şifreli bağlantı kullanıyor ve hepsi gizliliğinize ve özgürlüğünüze saygı gösteriyor",
"privacy": "Açık ağdan, TOR ve I2P'den erişilebilirler, bölge ya da ağ blokları yok",
"bullshit": "CDN yok, cloudflare yok, CAPTCHA yok, analitikler ve diğer saçmalıklar yok",
"link": "Tüm servisleri incele!"
},
"projects": {
"title": "projeler",
"desc": "Çoğunlukla özgür yazılım projeleri üzerinde çalışıyorum, işte ilginç bulabileceğiniz bazı projelerim"
}
},
"services": {
"title": "servis durumu",
"none": "Servis bulunamadı",
"search": "Bir servisi ara",
"feed": "Yenilikler ve güncellemeler",
"last": "Son kontrol zamanı {time}",
"status": {
"up": "Çalışıyor",
"down": "Kapalı",
"slow": "Yavaş"
}
},
"donate": {
"title": "bağış yap!",
"info": "Farklı projeler ve farklı servisleri yönetmek için oldukça zaman ve para harcıyorum.",
"price": "Çoğunlukla hosting ve elektrik için ödeme yapıyorum. Bunlar eklendiği zaman aylık 550₺ civarı bir miktar oluyor (yazdığım sırada ~15$).",
"details": "Bu sebepten küçük bir bağış bile oldukça faydalı olacaktır. Ve herşeyi açık ve çalışmakta tutmama yardımcı olacaktır.",
"thanks": "Ayrıca şuana kadar bağışta bulunan herkese çok teşekkür ederim!",
"table": {
"platform": "Platform",
"address": "Adres/Bağlantı"
}
},
"error": {
"title": "birşeyler yanlış gitti!",
"report": "Bu sorunu raporlayın"
},
"footer": {
"source": "Kaynak",
"license": "Lisans",
"privacy": "Gizlilik",
"number": "{since} tarihinden beri {total} kez ziyaret edildi",
"wow": "vay be!!",
"js": "Tüm elementleri görüntelemek için javascript'i açın"
}
}

View File

@@ -1,8 +1,51 @@
<script>
import { onMount } from "svelte";
import { goto } from "$app/navigation";
onMount(() => {
goto("/");
});
import { page } from "$app/state";
import { _ } from "$lib/locale.js";
</script>
<main>
<h1>{$_("error.title")}</h1>
<code>{page.error.message}</code>
<a href={import.meta.env.WEBSITE_REPORT_URL}>{$_("error.report")}</a>
<img src="/assets/sad.png" alt="" />
</main>
<style>
main {
background: var(--transparent);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1;
display: flex;
flex-direction: column;
justify-content: end;
align-items: flex-start;
gap: 10px;
padding: 50px;
font-size: var(--size-4);
}
main h1 {
font-size: var(--size-6);
text-shadow: var(--text-shadow);
color: var(--color);
}
main code {
font-size: var(--size-4);
color: var(--white-2);
}
main img {
width: 250px;
position: absolute;
right: 50px;
bottom: 0;
}
</style>

View File

@@ -1,6 +1,6 @@
import { locale_setup, locale_wait } from "$lib/locale.js";
import localizer from "$lib/locale.js";
export async function load() {
locale_setup();
await locale_wait();
export async function load({ data }) {
await localizer.setup(data.locale);
return data;
}

View File

@@ -0,0 +1,22 @@
import { color } from "../lib/util.js";
export async function load({ cookies, request }) {
// attempt get the preferred locale from cookies
let locale = cookies.get("locale");
/* if that doesn't work, try the accept-language header, and update the
* cookies respectively */
if (!locale) {
locale = request.headers.get("accept-language")?.split(",")[0];
cookies.set("locale", locale, { path: "/" });
}
return {
// the locale we detected
locale: locale,
/* color is randomly picked on the server and passed via data to the client
* so both the client and the server will have the same color */
color: color(),
};
}

View File

@@ -1,23 +1,22 @@
<script>
import Navbar from "$lib/navbar.svelte";
import Footer from "$lib/footer.svelte";
import { browser } from "$app/environment";
import { locale_select } from "$lib/locale.js";
import { onMount } from "svelte";
import { color } from "$lib/util.js";
import api from "$lib/api.js";
let { children } = $props();
let { data, children } = $props();
onMount(() => {
locale_select();
});
if (browser) {
// set the current app and the API version
window._version = {
app: pkg.version,
api: api.version,
};
}
</script>
<main>
<Navbar />
<div class="content">
{@render children()}
</div>
<Footer />
<main style="--color: var(--{data.color})">
{@render children()}
</main>
<style>
@@ -28,9 +27,4 @@
flex-direction: column;
min-height: 100vh;
}
.content {
background: var(--black-1);
flex-grow: 1;
}
</style>

View File

@@ -1,15 +1,9 @@
import { api_get_projects } from "$lib/api.js";
import api from "$lib/api.js";
export async function load({ fetch }) {
try {
let projects = await api_get_projects(fetch);
return {
projects: null === projects ? [] : projects,
error: "",
};
} catch (err) {
return {
error: err.toString(),
};
}
let projects = await api.projects(fetch);
return {
projects: null === projects ? [] : projects,
error: "",
};
}

View File

@@ -1,133 +1,94 @@
<script>
import { api_version } from "$lib/api.js";
import Header from "$lib/header.svelte";
import Error from "$lib/error.svelte";
import Head from "$lib/head.svelte";
import Card from "$lib/card.svelte";
import Link from "$lib/link.svelte";
import Navbar from "$components/navbar.svelte";
import Footer from "$components/footer.svelte";
import Header from "$components/header.svelte";
import Head from "$components/head.svelte";
import Card from "$components/card.svelte";
import { browser } from "$app/environment";
import { _, locale } from "svelte-i18n";
import { color } from "$lib/util.js";
import { locale, _ } from "$lib/locale.js";
let { data } = $props();
if (browser) {
window._version = {};
window._version.app = pkg.version;
window._version.api = api_version;
// return list of projects that have decriptions for the given locale
function projects() {
return data.projects.filter(p => {
return (
p.desc[$locale.code] !== "" &&
p.desc[$locale.code] !== null &&
p.desc[$locale.code] !== undefined
);
});
}
</script>
<Head title="home" desc="home page of my personal website" />
<Navbar />
<Header picture="tired" title={$_("home.title")} />
<main>
<!-- welcome -->
<Card>
<p>{@html $_("home.welcome.desc")}</p>
<br />
<p>{$_("home.welcome.thanks")}</p>
</Card>
{#if data.error.length !== 0}
<Error error={data.error} />
{:else}
<main>
<Card title={$_("home.welcome.title")}>
<span> 👋 {$_("home.welcome.desc")}</span>
<ul>
<li>🇹🇷 {$_("home.welcome.whoami")}</li>
<li>🖥️ {$_("home.welcome.interest")}</li>
<li>❤️ {$_("home.welcome.support")}</li>
</ul>
</Card>
<Card title={$_("home.work.title")}>
<span>{$_("home.work.desc")}</span>
<ul>
<li>⌨️ {$_("home.work.build")}</li>
<li>🤦 {$_("home.work.fix")}</li>
<li>🚩 {$_("home.work.ctf")}</li>
<li>👥 {$_("home.work.contribute")}</li>
<li>📑 {$_("home.work.wiki")}</li>
</ul>
</Card>
<Card title={$_("home.links.title")}>
<span>{$_("home.links.desc")}:</span>
<ul>
<li>
<Link
icon="nf-fa-key"
link="https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D"
>
PGP
</Link>
<!-- projects -->
<Card title={$_("home.projects.title")} id="projects">
<p>{$_("home.projects.desc")}:</p>
<br />
<ul>
{#each projects() as project}
<li class="project">
<a href={project.url}>{project.name}</a>: {project.desc[$locale.code]}
</li>
<li>
<Link icon="nf-md-email" link="mailto:ngn@ngn.tf">Email</Link>
</li>
<li>
<Link icon="nf-md-mastodon" link="https://defcon.social/@ngn"
>Mastodon</Link
>
</li>
</ul>
<span>
{$_("home.links.prefer")}
</span>
</Card>
<Card title={$_("home.services.title")}>
<span>
{$_("home.services.desc")}:
</span>
<ul>
<li>
<i style="color: var(--{color()});" class="nf nf-md-speedometer_slow"
></i>
{$_("home.services.speed")}
</li>
<li>
<i style="color: var(--{color()});" class="nf nf-fa-lock"></i>
{$_("home.services.security")}
</li>
<li>
<i style="color: var(--{color()});" class="nf nf-fa-network_wired"
></i>
{$_("home.services.privacy")}
</li>
<li>
<i style="color: var(--{color()});" class="nf nf-md-eye_off"></i>
{$_("home.services.bullshit")}
</li>
</ul>
<Link link="/services">{$_("home.services.link")}</Link>
</Card>
<Card title={$_("home.projects.title")}>
<span>
{$_("home.projects.desc")}:
</span>
{#if data.error.length === 0}
<ul>
{#each data.projects.filter((p) => {
return p.desc[$locale] !== "" && p.desc[$locale] !== null && p.desc[$locale] !== undefined;
}) as project}
<li>
<Link active={true} link={project.url}>{project.name}</Link>:
{project.desc[$locale]}
</li>
{/each}
</ul>
{/if}
</Card>
</main>
{/if}
{/each}
</ul>
</Card>
<!-- services -->
<Card title={$_("home.services.title")} id="services">
<p>{$_("home.services.desc")}:</p>
<br />
<ul>
<li>{@html $_("home.services.speed")}</li>
<li>{@html $_("home.services.security")}</li>
<li>{@html $_("home.services.blocks")}</li>
<li>{@html $_("home.services.bullshit")}</li>
</ul>
<br />
<a class="services" href="/services" data-sveltekit-preload-data>
{$_("home.services.link")}
</a>
</Card>
<!-- contact -->
<Card title={$_("home.contact.title")} id="contact">
<p>{@html $_("home.contact.desc")}</p>
<br />
<p>{@html $_("home.contact.mastodon")}</p>
</Card>
</main>
<Footer />
<style>
main {
background: var(--black-1);
flex: 1;
display: flex;
flex-wrap: wrap;
justify-content: center;
flex-direction: column;
align-items: stretch;
padding: 50px;
gap: 28px;
padding: 40px;
}
@media only screen and (max-width: 900px) {
main {
flex-direction: column;
}
.services {
color: var(--color);
text-decoration-color: var(--color);
}
.project a {
color: var(--color);
text-decoration-color: var(--color);
}
</style>

View File

@@ -1,12 +1,8 @@
import { doc_get } from "$lib/doc";
export async function load({ fetch, params }) {
try {
return {
doc: await doc_get(fetch, params.name),
error: "",
};
} catch (err) {
return { error: err.toString() };
}
return {
doc: await doc_get(fetch, params.name),
error: "",
};
}

View File

@@ -1,50 +1,41 @@
<script>
import Header from "$lib/header.svelte";
import Error from "$lib/error.svelte";
import Head from "$lib/head.svelte";
import Navbar from "$components/navbar.svelte";
import Footer from "$components/footer.svelte";
import Header from "$components/header.svelte";
import Head from "$components/head.svelte";
import { locale, _ } from "$lib/locale.js";
import { locale, _ } from "svelte-i18n";
import { goto } from "$app/navigation";
import { color } from "$lib/util.js";
import DOMPurify from "dompurify";
import { onMount } from "svelte";
import { marked } from "marked";
import { onMount } from "svelte";
let { data } = $props();
marked.use({ breaks: true });
marked.use();
onMount(async () => {
for (let key in data.doc)
onMount(() => {
for (let key in data.doc) {
data.doc[key]["content"] = DOMPurify.sanitize(data.doc[key]["content"]);
if (undefined !== data.error && data.error.includes("not found")) goto("/");
}
});
</script>
<Head title="documentation" desc="website and API documentation" />
<Header picture="reader" title={data.doc[$locale].title} />
{#if data.error.length !== 0}
{#if !data.error.includes("not found")}
<Error error={data.error} />
{/if}
{:else}
<main>
<div class="markdown-body" style="--link-color: var(--{color()})">
{@html marked.parse(data.doc[$locale].content)}
</div>
</main>
{/if}
<Navbar />
<Header picture="reader" title={data.doc[$locale.code].title} />
<main>
<div class="markdown-body">
{@html marked.parse(data.doc[$locale.code].content)}
</div>
</main>
<Footer />
<style>
@import "/css/markdown.css";
main {
padding: 50px;
background: var(--black-1);
padding: 40px;
gap: 30px;
}
main .markdown-body :global(a) {
color: var(--link-color);
}
</style>

View File

@@ -1,93 +1,43 @@
<script>
import Header from "$lib/header.svelte";
import Head from "$lib/head.svelte";
import Icon from "$lib/icon.svelte";
import Navbar from "$components/navbar.svelte";
import Footer from "$components/footer.svelte";
import Header from "$components/header.svelte";
import Head from "$components/head.svelte";
import Card from "$components/card.svelte";
import { color } from "$lib/util.js";
import { _ } from "svelte-i18n";
import { _ } from "$lib/locale.js";
</script>
<Head title="donate" desc="give me all of your life savings" />
<Navbar />
<Header picture="money" title={$_("donate.title")} />
<main>
<span> </span>
<span>
{$_("donate.info")}
{$_("donate.price")}
</span>
<br />
<br />
<span>
{$_("donate.details")}
</span>
<table>
<thead>
<tr>
<th style="color: var(--{color()})">{$_("donate.table.platform")}</th>
<th style="color: var(--{color()})">{$_("donate.table.address")}</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<Icon icon="nf-fa-monero" />
Monero (XMR)
</td>
<td>
<code>
46q7G7u7cmASvJm7AmrhmNg6ctS77mYMmDAy1QxpDn5w57xV3GUY5za4ZPZHAjqaXdfS5YRWm4AVj5UArLDA1retRkJp47F
</code>
</td>
</tr>
</tbody>
</table>
<span>
{$_("donate.thanks")}
</span>
<Card>
<p>{@html $_("donate.desc")}</p>
<br />
<p>{@html $_("donate.info")}:</p>
<div>
<code>
46q7G7u7cmASvJm7AmrhmNg6ctS77mYMmDAy1QxpDn5w57xV3GUY5za4ZPZHAjqaXdfS5YRWm4AVj5UArLDA1retRkJp47F
</code>
</div>
<p>{$_("donate.thanks")}</p>
</Card>
</main>
<Footer />
<style>
main {
padding: 50px;
background: var(--black-1);
flex: 1;
padding: 40px;
}
main span {
font-size: var(--size-4);
color: var(--white-1);
}
table {
box-shadow: var(--box-shadow);
background: var(--black-3);
border-collapse: collapse;
font-size: var(--size-3);
margin: 30px 0 30px 0;
width: 100%;
}
tr,
th,
td {
color: white;
background: var(--dark-two);
text-align: left;
}
td,
th {
font-size: var(--size-4);
border: solid 1px var(--black-4);
padding: 16px;
}
th {
font-weight: 1000;
}
td {
color: var(--white-2);
font-weight: 400;
main div {
background: var(--black-2);
border: solid 1px var(--black-3);
padding: 6px;
margin: 15px 0;
}
code {

View File

@@ -1,15 +1,9 @@
import { api_get_services } from "$lib/api.js";
import api from "$lib/api.js";
export async function load({ fetch }) {
try {
let services = await api_get_services(fetch);
return {
services: null === services ? [] : services,
error: "",
};
} catch (err) {
return {
error: err.toString(),
};
}
let services = await api.services(fetch);
return {
services: null === services ? [] : services,
error: "",
};
}

View File

@@ -1,110 +1,65 @@
<script>
import Service from "$lib/service.svelte";
import Header from "$lib/header.svelte";
import Error from "$lib/error.svelte";
import Link from "$lib/link.svelte";
import Head from "$lib/head.svelte";
import { api_urljoin } from "$lib/api.js";
import { locale, _ } from "svelte-i18n";
import { onMount } from "svelte";
import Navbar from "$components/navbar.svelte";
import Footer from "$components/footer.svelte";
import Header from "$components/header.svelte";
import Service from "$components/service.svelte";
import Head from "$components/head.svelte";
import Card from "$components/card.svelte";
import { locale, _ } from "$lib/locale.js";
import api from "$lib/api.js";
let { data } = $props();
let services = $state(data.services);
let show_input = $state(false);
function change(input) {
let value = input.target.value.toLowerCase();
services = [];
if (value === "") {
services = data.services;
return;
}
data.services.forEach((s) => {
if (s.name.toLowerCase().includes(value)) services.push(s);
else if (s.desc[$locale].toLowerCase().includes(value)) services.push(s);
});
}
function get_services() {
return services.filter((s) => {
return (
s.desc[$locale] !== "" &&
s.desc[$locale] !== null &&
s.desc[$locale] !== undefined
);
});
}
onMount(() => {
show_input = true;
});
// filtered list of services that have descriptions for the current locale
let services = $derived(
data.services.filter(
s =>
s.desc[$locale.code] !== "" &&
s.desc[$locale.code] !== null &&
s.desc[$locale.code] !== undefined
)
);
</script>
<Head title="services" desc="my self-hosted services and projects" />
<Navbar />
<Header picture="cool" title={$_("services.title")} />
{#if data.error.length !== 0}
<Error error={data.error} />
{:else}
<main>
<div class="title">
{#if show_input}
<input
oninput={change}
type="text"
placeholder={$_("services.search")}
/>
{/if}
<div>
<Link icon="nf-fa-feed" link={api_urljoin("/news/" + $locale)}
>{$_("services.feed")}</Link
>
</div>
</div>
<div class="services">
{#if get_services().length == 0}
<h3 class="none">{$_("services.none")}</h3>
{:else}
{#each get_services() as service}
<Service {service} />
{/each}
{/if}
</div>
</main>
{/if}
<main>
<Card>
<p>{@html $_("services.desc")}</p>
<br />
<p>
{@html $_("services.feed", {
links: [api.join("/news/" + $locale.code)],
})}
</p>
</Card>
<div class="services">
{#if services.length === 0}
<h3>{$_("services.none")}</h3>
{:else}
{#each services as service}
<Service {service} />
{/each}
{/if}
</div>
</main>
<Footer />
<style>
main {
padding: 50px;
text-align: right;
}
main .title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
main .none {
color: var(--white-3);
background: var(--black-1);
flex: 1;
padding: 40px;
}
main .services {
margin-top: 20px;
margin-top: 15px;
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: stretch;
gap: 28px;
}
@media only screen and (max-width: 1200px) {
main .services {
flex-direction: column;
}
color: var(--white-3);
}
</style>

View File

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

View File

@@ -1,11 +0,0 @@
@keyframes blink {
0% {
opacity: 0;
}
}
@keyframes cursor {
to {
border-color: transparent;
}
}

View File

@@ -1,7 +0,0 @@
@font-face {
font-family: "Ubuntu";
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("/assets/ubuntu.woff2") format("woff2");
}

View File

@@ -1,32 +1,26 @@
/*
* animations.css: stuff like the cursor animation
* webfont.css: webfont dumped from https://www.nerdfonts.com/assets/css/webfont.css
* font.css: the main font (Ubuntu)
*/
@import "./animations.css";
@import "./nerdfonts.css";
@import "./font.css";
/* global CSS file, imported in every page, defines commonly used color, effects
* etc. also defines styles for commonly used elements */
:root {
/* randomly selected colors */
--yellow: #d3b910;
--cyan: #0dd2e8;
--green: #06e00a;
--pinkish: #d506e0;
--red: #e8180d;
--blue: #2036f9;
--blue: #3768fc;
/* white tones */
--white-1: #ffffff;
--white-2: #bfbfbf;
--white-3: #5f5f5f;
--white-4: #0f0f0f;
/* black tones */
--black-1: #000000;
--black-2: #050505;
--black-3: #111111;
--black-4: #3a3b3c;
--black-2: #111;
--black-3: #3a3b3c;
/* different sizes */
--size-1: 8px;
--size-2: 16px;
--size-3: 18px;
@@ -34,17 +28,29 @@
--size-5: 24px;
--size-6: 30px;
--box-shadow-1:
rgba(20, 20, 20, 0.19) 0px 10px 20px, rgba(30, 30, 30, 0.23) 0px 6px 6px;
--box-shadow-2:
rgba(0, 0, 0, 0.35) 0px 30px 60px -12px inset,
rgba(20, 20, 20, 0.3) 0px 18px 36px -18px inset;
/* shadows */
--box-shadow: rgb(38, 57, 77) 0px 20px 30px -10px;
--text-shadow: 0 1px 5px rgba(255, 255, 255, 0.15);
--text-shadow: 3px 2px 8px rgba(50, 50, 50, 0.8);
--background:
linear-gradient(rgba(11, 11, 11, 0.808), rgba(1, 1, 1, 0.96)),
url("/assets/banner.png");
--profile-size: 220px;
/* backgrounds */
--transparent: linear-gradient(rgba(11, 11, 11, 0.808), rgba(1, 1, 1, 0.96));
--glass: rgba(1, 1, 1, 0.93);
/* fonts */
--monospace:
ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono,
monospace;
--sans-serif:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
--emoji: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
/* cursor blink animation */
@keyframes blink {
0% {
opacity: 0;
}
}
* {
@@ -57,11 +63,14 @@ html {
}
body {
background: var(--black-1);
font-family: "Ubuntu", sans-serif;
font-family: var(--sans-serif);
background-image: url("/assets/banner.png");
overflow-x: hidden;
height: 100vh;
width: 100vw;
line-height: 1.5;
font-size: var(--size-3);
}
::selection {
@@ -84,36 +93,24 @@ body {
}
a {
font-weight: 900;
color: var(--white-1);
text-decoration: none;
color: var(--white-2);
font-weight: 600;
cursor: pointer;
text-decoration: underline;
text-decoration-color: var(--white-2);
}
a:hover {
text-decoration: underline;
color: var(--white-1);
text-shadow: var(--text-shadow);
}
i .nf {
font-weight: 900;
}
ul {
list-style: inside;
margin: 12px 0 12px 0;
}
li + li {
margin-top: 10px;
}
input {
background: var(--black-3);
border: none;
outline: none;
font-size: var(--size-4);
padding: 10px;
border: solid 1px var(--black-4);
color: var(--white-1);
br {
display: block;
margin: 5px 0;
}

View File

@@ -1,16 +1,21 @@
/* modified version of github-markdown-css, the dark version
* licensed under MIT, see: https://github.com/sindresorhus/github-markdown-css
*/
.markdown-body {
color-scheme: dark;
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
margin: 0;
color: #c9d1d9;
background-color: #000;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica,
Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
color: var(--white-2);
}
.markdown-body br {
margin: 0;
}
.markdown-body .octicon {
@@ -61,10 +66,6 @@
display: none !important;
}
.markdown-body a {
text-decoration: none;
}
.markdown-body abbr[title] {
border-bottom: none;
text-decoration: underline dotted;
@@ -82,9 +83,9 @@
.markdown-body h1 {
margin: 0.67em 0;
font-weight: 600;
padding-bottom: 0.3em;
padding-bottom: 5px;
font-size: 2em;
border-bottom: 1px solid #21262d;
border-bottom: 1px solid var(--black-3);
}
.markdown-body mark {
@@ -287,17 +288,18 @@
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
color: var(--white-1);
margin-top: 30px;
margin-bottom: 10px;
font-weight: 600;
line-height: 1.25;
}
.markdown-body h2 {
font-weight: 600;
padding-bottom: 0.3em;
padding-bottom: 5px;
font-size: 1.5em;
border-bottom: 1px solid #21262d;
border-bottom: 1px solid var(--black-3);
}
.markdown-body h3 {
@@ -359,28 +361,14 @@
.markdown-body tt,
.markdown-body code,
.markdown-body samp {
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-family: var(--monospace);
font-size: 12px;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 0;
font-family:
ui-monospace,
SFMono-Regular,
SF Mono,
Menlo,
Consolas,
Liberation Mono,
monospace;
font-family: var(--monospace);
font-size: 12px;
word-wrap: normal;
}
@@ -721,7 +709,7 @@
margin: 0;
font-size: 85%;
white-space: break-spaces;
background: var(--black-3);
background: var(--black-2);
border-radius: 6px;
}
@@ -762,11 +750,13 @@
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
padding: 10px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: var(--black-3);
background-color: var(--black-2);
border: solid 1px var(--black-3);
}
.markdown-body pre code,
@@ -980,7 +970,7 @@
.markdown-body g-emoji {
display: inline-block;
min-width: 1ch;
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-family: var(--emoji);
font-size: 1em;
font-style: normal !important;
font-weight: 400;

View File

@@ -1,14 +1,10 @@
//import adapter from '@sveltejs/adapter-auto';
import adapter from "@sveltejs/adapter-node";
/** @type {import('@sveltejs/kit').Config} */
const config = {
kit: {
adapter: adapter(),
},
onwarn: (warning, handler) => {
if (warning.code === "a11y-click-events-have-key-events") return;
handler(warning);
alias: { $components: "src/components" },
},
};

View File

@@ -22,7 +22,7 @@ function env_from(prefix, object) {
const default_env = {
source_url: "https://git.ngn.tf/ngn/website",
report_url: "https://git.ngn.tf/ngn/website/issues",
report_url: "mailto:ngn@ngn.tf",
doc_url: "http://localhost:7003",
api: {
url: "http://localhost:7002",