Compare commits
236 Commits
c4192cbbfd
...
main
Author | SHA1 | Date | |
---|---|---|---|
91a11e4611
|
|||
13a703352b
|
|||
2c5388bcce
|
|||
f053b93c24
|
|||
babbe50c00
|
|||
92f594f27f
|
|||
cb5f5628a3
|
|||
8adbc9d354
|
|||
8a084c4e95
|
|||
897d445cdc
|
|||
ee4b8a1d50
|
|||
53a0fc3927
|
|||
84763e7ef9
|
|||
8b0cf20bf2
|
|||
e3692f90b1
|
|||
bf95c575eb | |||
60087fc306 | |||
f20883cb26 | |||
924461b9a2 | |||
0b065172f5 | |||
c2c9893ab3 | |||
704430b91a | |||
9f443c66b2 | |||
065df723a9 | |||
557cea36d8 | |||
c287ffa086
|
|||
26d656896e | |||
734bbdb998 | |||
7e8a927bb3 | |||
f08b04e710 | |||
fa6c1d8b2c | |||
0c1e07b779 | |||
c615409a42 | |||
da50365380 | |||
68f5480ff1 | |||
12216ca0f7 | |||
7e407b181d | |||
4f0e911407 | |||
b447c8cf13 | |||
e212d43bf2 | |||
2bf94c0f3c | |||
3f49d8294a | |||
323b76fe98 | |||
f714bc6eaa | |||
8ad2778dd0 | |||
0d99a7da69 | |||
c049f6c44a | |||
9acbe212ad | |||
aa29cca4f4 | |||
afc0a30660 | |||
647634addc | |||
dba65911a6 | |||
1067d0a3fd | |||
1ffd51b641 | |||
cd97df5f21 | |||
12dedf3700 | |||
ade745cdc5 | |||
ab5977eaae | |||
39813d246f | |||
975626fc5c | |||
1f47d59760 | |||
38abe411a8 | |||
e7216d51e3 | |||
2b244645c6 | |||
43589f7ddc | |||
b43a219fb3 | |||
7bf92f9049 | |||
577195cb7f | |||
fc64b742bb | |||
922eaeb124 | |||
65ec5a51aa | |||
8036b0c1e1 | |||
c505698b0b
|
|||
4e08375748
|
|||
912bf616b6
|
|||
d1ddbf2605 | |||
66b01bd58d | |||
01468670e3 | |||
1f09aae11e | |||
ce1ac7b09e | |||
f9faddf660 | |||
945f1b84f2 | |||
a9d2633107
|
|||
506d16e0c9
|
|||
30c3b0c37b | |||
2da4f81a6b | |||
cc482fc0ad | |||
51ea22fbc5 | |||
58595d5eb3 | |||
671d44017f | |||
95d24b734b | |||
c2e31fb440 | |||
2d3dab5646 | |||
4b2e12b374 | |||
c6323cae38 | |||
e05d42a956 | |||
426c474016 | |||
91ca908980 | |||
5a61508359 | |||
86c0e3cc97 | |||
1afa33f28b | |||
a22b1e67c8 | |||
f477b4525b | |||
462d35ea0d | |||
693c94274a | |||
af2e5b49ae | |||
33a31dac32 | |||
00b7227d4d | |||
2f9d38cd45 | |||
e68dd1d317 | |||
fe834d2778 | |||
686f51dcde | |||
dc43bdb3af
|
|||
05185cb82a
|
|||
ee3fa10096 | |||
0e4457d5fb | |||
b95c51193e | |||
322d8d5c39 | |||
904e9f6d15 | |||
cc4abc85fe | |||
73cfcea923 | |||
90af3d0500
|
|||
8d16273540
|
|||
4fb78c0ab7
|
|||
a57a4955ba
|
|||
ccf0d8abf9
|
|||
b312e40204
|
|||
a67bd74ced
|
|||
9a72d3f95d
|
|||
66c96ae312
|
|||
f779a03ae5
|
|||
bcb48d789b
|
|||
aa69525912
|
|||
0134bd2ab1
|
|||
84e477b23e | |||
515dcf6a09 | |||
a50cc1ff7f | |||
f2ec9479b1 | |||
e1eae3c05b | |||
53ab201ab3 | |||
c9792fb6be | |||
68808a14f9 | |||
4343192a2c | |||
20fb3cd685 | |||
906ace188b | |||
f2a1a21a8c | |||
9353631931 | |||
4eb5ad6ef3
|
|||
bac570615e
|
|||
037a4405b9
|
|||
389a9d3df4 | |||
921fe4255d | |||
f7e4442de5 | |||
e4d1fc21fb | |||
fe6b3512fd | |||
3dbc2e0510
|
|||
26362cf89d
|
|||
16288c2bcb | |||
177b546fb9 | |||
311a37ae5c | |||
4380638982 | |||
32e0ddaa36 | |||
27c58b1174 | |||
d5f1325b79 | |||
9e27aabdb9 | |||
a1967141ae | |||
a172262dbd | |||
caee8c2a90 | |||
060c5f0c73 | |||
e2796b39b8 | |||
191c54cc9a | |||
87095a8c35 | |||
a28a678693 | |||
d3fd1e479c | |||
7f97f5330e | |||
30286710d1 | |||
8de018bf5c | |||
5a73910331 | |||
53236fab61 | |||
d5dfaaad96 | |||
c1e2974f1b | |||
40cdbfba36 | |||
be8777eb67 | |||
b75bd4f3b2 | |||
c127c62492 | |||
6a01631d22 | |||
1389233108 | |||
e2a4df8602 | |||
b4d8ab7606 | |||
478834fe2f | |||
ba441d5192
|
|||
4556452f69 | |||
6217103c5a | |||
eee6c2c2d2 | |||
db7d062662 | |||
c8a4067596
|
|||
437404dfca | |||
1b24596abf | |||
1759261332 | |||
3a9685511b | |||
7eac5d8bb3 | |||
68bc30d98b | |||
bc9b92cdd2 | |||
e49b30b2fb | |||
01a6cf34bc | |||
3ab85605c6 | |||
2bfc4886c8 | |||
e861bb4030 | |||
002044232f | |||
7facbbdeb2 | |||
55038a9745 | |||
665fb2e34c | |||
f0e168c684 | |||
e13ccd2e3e | |||
6e07adb59d | |||
7239df94dd | |||
ec7afca33d | |||
565ed66f70 | |||
12da87479c | |||
db3f0ed29a | |||
2b13b2e1ca | |||
914d1abf64 | |||
03564197a2 | |||
8e49322395
|
|||
af8c26511a
|
|||
5813338232
|
|||
29b8caab34
|
|||
3a78818e8d | |||
c53103352e | |||
442c1d2ca7 | |||
803506153f | |||
01205d6a1f
|
|||
ce40a002b7 | |||
7d5b649bd7 | |||
29937d3af2 | |||
b2cdd937b6 |
@@ -3,6 +3,7 @@ name: Build the docker image for the API
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
paths: ["api/**"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: git.ngn.tf
|
REGISTRY: git.ngn.tf
|
||||||
|
@@ -3,6 +3,7 @@ name: Build the docker image for the frontend application
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
paths: ["app/**"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: git.ngn.tf
|
REGISTRY: git.ngn.tf
|
||||||
@@ -27,8 +28,8 @@ jobs:
|
|||||||
cd app
|
cd app
|
||||||
docker build --build-arg WEBSITE_REPORT_URL=https://git.ngn.tf/ngn/website/issues/new \
|
docker build --build-arg WEBSITE_REPORT_URL=https://git.ngn.tf/ngn/website/issues/new \
|
||||||
--build-arg WEBSITE_SOURCE_URL=https://git.ngn.tf/ngn/website \
|
--build-arg WEBSITE_SOURCE_URL=https://git.ngn.tf/ngn/website \
|
||||||
--build-arg WEBSITE_APP_URL=https://ngn.tf \
|
|
||||||
--build-arg WEBSITE_API_URL=https://api.ngn.tf \
|
|
||||||
--build-arg WEBSITE_DOC_URL=http://doc:7003 \
|
--build-arg WEBSITE_DOC_URL=http://doc:7003 \
|
||||||
|
--build-arg WEBSITE_API_URL=http://api:7002 \
|
||||||
|
--build-arg WEBSITE_API_PATH=/api \
|
||||||
--tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest .
|
--tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest .
|
||||||
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest
|
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest
|
||||||
|
@@ -3,6 +3,7 @@ name: Build the docker image for the doc server
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: ["main"]
|
branches: ["main"]
|
||||||
|
paths: ["doc/**"]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: git.ngn.tf
|
REGISTRY: git.ngn.tf
|
||||||
|
21
.github/dependabot.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "npm"
|
|
||||||
directory: "/app"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
|
|
||||||
- package-ecosystem: "gomod"
|
|
||||||
directory: "/api"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
|
|
||||||
- package-ecosystem: "docker"
|
|
||||||
directory: "/app"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
|
|
||||||
- package-ecosystem: "docker"
|
|
||||||
directory: "/api"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
14
.gitignore
vendored
@@ -1,11 +1,7 @@
|
|||||||
|
# docker(-compose) stuff
|
||||||
data.db
|
data.db
|
||||||
*.yaml
|
docker-compose.yaml
|
||||||
*.yml
|
docker-compose.yml
|
||||||
|
compose.yaml
|
||||||
|
compose.yml
|
||||||
*.env
|
*.env
|
||||||
|
|
||||||
# don't ignore example deployment stuff
|
|
||||||
!deploy/*
|
|
||||||
!.github/*
|
|
||||||
!.gitea/*
|
|
||||||
!.github/*/*
|
|
||||||
!.gitea/*/*
|
|
||||||
|
147
README.md
@@ -1,86 +1,115 @@
|
|||||||
# website | my personal website
|
# website | my personal website
|
||||||

|
|
||||||

|
|
||||||

|
|
||||||
|
|
||||||
This repo contains all the source code for my personal website, [ngn.tf](https://ngn.tf)
|

|
||||||
All code is licensed under AGPL version 3 (see [LICENSE.txt](LICENSE.txt))
|

|
||||||
|

|
||||||
|
|
||||||
|
This repo contains all the source code for my personal website,
|
||||||
|
[ngn.tf](https://ngn.tf) All code is licensed under AGPL version 3 (see
|
||||||
|
[LICENSE.txt](LICENSE.txt))
|
||||||
|
|
||||||
## Directory structure
|
## Directory structure
|
||||||
|
|
||||||
### `app`
|
### `app`
|
||||||
|
|
||||||
Contains frontend application, written with SvelteKit. It supports full SSR.
|
Contains frontend application, written with SvelteKit. It supports full SSR.
|
||||||
Contains modified CSS from [github-markdown-css](https://github.com/sindresorhus/github-markdown-css)
|
Contains modified CSS from
|
||||||
and fonts from [NerdFonts](https://www.nerdfonts.com/)
|
[github-markdown-css](https://github.com/sindresorhus/github-markdown-css) and
|
||||||
|
fonts from [NerdFonts](https://www.nerdfonts.com/)
|
||||||
|
|
||||||
### `api`
|
### `api`
|
||||||
Contains the API server, written in Go. It uses the [Fiber](https://github.com/gofiber/fiber) web
|
|
||||||
framework which offers an [Express](https://expressjs.com/) like experience. I choose Fiber since I've used
|
|
||||||
worked with express a lot in the past. However previously the I was using [Gin](https://github.com/gin-gonic/gin)
|
|
||||||
(see history section).
|
|
||||||
|
|
||||||
API stores all the data in a local SQLite(3) database. Go doesn't support SQLite3 out of the box so
|
Contains the API server, written in Go. It uses the
|
||||||
I'm using [mattn's sqlite3 driver](https://github.com/mattn/go-sqlite3).
|
[Fiber](https://github.com/gofiber/fiber) web framework which offers an
|
||||||
|
[Express](https://expressjs.com/) like experience. I choose Fiber since I've
|
||||||
|
used worked with express a lot in the past. However previously the I was using
|
||||||
|
[Gin](https://github.com/gin-gonic/gin) (see history section).
|
||||||
|
|
||||||
|
API stores all the data in a local SQLite(3) database. Go doesn't support
|
||||||
|
SQLite3 out of the box so I'm using
|
||||||
|
[mattn's sqlite3 driver](https://github.com/mattn/go-sqlite3).
|
||||||
|
|
||||||
### `doc`
|
### `doc`
|
||||||
Contains the documentation server, written in C. It uses the [ctorm](https://github.com/ngn13/ctorm) web
|
|
||||||
framework, which is a framework that I myself wrote. Unlike the frontend application or the API server, it's not
|
Contains the documentation server, written in C. It uses the
|
||||||
accessable by public, the frontend application gets the documentation content from this server and renders it using
|
[ctorm](https://github.com/ngn13/ctorm) web framework, which is a framework that
|
||||||
SSR. The reason I don't use the API for hosting the documentation content is that I want a separate server for hosting
|
I myself wrote. Unlike the frontend application or the API server, it's not
|
||||||
|
accessable by public, the frontend application gets the documentation content
|
||||||
|
from this server and renders it using SSR. The reason I don't use the API for
|
||||||
|
hosting the documentation content is that I want a separate server for hosting
|
||||||
static content, API is only for hosting dynamic stuff.
|
static content, API is only for hosting dynamic stuff.
|
||||||
|
|
||||||
### `admin`
|
### `admin`
|
||||||
The frontend application does not contain an admin interface, I do the administration stuff (such as adding news posts,
|
|
||||||
adding services etc.) using the python script in this directory. This script can be installed on to the PATH by running
|
The frontend application does not contain an admin interface, I do the
|
||||||
the Makefile install script. After installation it can be used by running `admin_script`.
|
administration stuff (such as adding news posts, adding services etc.) using the
|
||||||
|
python script in this directory. This script can be installed on to the PATH by
|
||||||
|
running the Makefile install script. After installation it can be used by
|
||||||
|
running `admin_script`.
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
Easiest way to deploy is to use docker. There is `compose.yml` and a `run.sh` script in the [deploy](deploy/) directory
|
|
||||||
that can be used to startup all the docker containers. Configuration options are passed during build time for the frontend
|
Easiest way to deploy is to use docker. There is `compose.yml` and a `run.sh`
|
||||||
application, and for others it's passed with environment variables.
|
script in the [deploy](deploy/) directory that can be used to startup all the
|
||||||
|
docker containers. Configuration options are passed during build time for the
|
||||||
|
frontend application, and for others it's passed with environment variables.
|
||||||
|
|
||||||
## History
|
## History
|
||||||
|
|
||||||
Some nostalgic history/changelog stuff (just for the major version numbers):
|
Some nostalgic history/changelog stuff (just for the major version numbers):
|
||||||
|
|
||||||
- **v0.1 (late 2020 - early 2021)**: First ever version of my website, it was just a simple HTML/CSS page,
|
- **v0.1 (late 2020 - early 2021)**: First ever version of my website, it was
|
||||||
I never published any of the source code and I wiped the local copy on my USB drive in early 2022, I still
|
just a simple HTML/CSS page, I never published any of the source code and I
|
||||||
remember what it looked like though, it looked like I made entire website in microsoft paint... while blindfoled,
|
wiped the local copy on my USB drive in early 2022, I still remember what it
|
||||||
so yeah it was shit.
|
looked like though, it looked like I made entire website in microsoft paint...
|
||||||
|
while blindfoled, so yeah it was shit.
|
||||||
|
|
||||||
- **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my github.io page, and all the source code
|
- **v1.0 (early 2021 - late 2022)**: This version was actualy hosted on my
|
||||||
was (and still is) avaliable, it was just a simple static site, [here is a screenshot](assets/githubio.png).
|
github.io page, and all the source code was (and still is) avaliable, it was
|
||||||
|
just a simple static site, [here is a screenshot](assets/githubio.png).
|
||||||
|
|
||||||
- **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite (and rework) my website with one
|
- **vLOST (late 2022 - early 2023)**: As I learned more JS, I decided to rewrite
|
||||||
of the fancy JS frameworks. I decided to go with Svelte. Not the kit version, at the time svelte did not support SSR.
|
(and rework) my website with one of the fancy JS frameworks. I decided to go
|
||||||
I do not remember writting an API for it so I guess I just updated it everytime I wanted to add content? It was pretty
|
with Svelte. Not the kit version, at the time svelte did not support SSR. I do
|
||||||
much like a static website and was hosted on `ngn13.fun` as at this point I had my own hosting. The source code for
|
not remember writting an API for it so I guess I just updated it everytime I
|
||||||
this website was in a deleted github repository of mine, I looked for a local copy on my old hard drive but I wasn't able
|
wanted to add content? It was pretty much like a static website and was hosted
|
||||||
to find it. I also do not remember how it looked like, sooo this version is pretty much lost :(
|
on `ngn13.fun` as at this point I had my own hosting. The source code for this
|
||||||
|
website was in a deleted github repository of mine, I looked for a local copy
|
||||||
|
on my old hard drive but I wasn't able to find it. I also do not remember how
|
||||||
|
it looked like, sooo this version is pretty much lost :(
|
||||||
|
|
||||||
- **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided to rewrite and rework my website one more
|
- **v2.0 (early 2023 - late 2023)**: After I discovered what SSR is, I decided
|
||||||
time in NuxtJS. I had really "fun" time using vue stuff. As NuxtJS supported server-side code, this website had its own
|
to rewrite and rework my website one more time in NuxtJS. I had really "fun"
|
||||||
built in API. This website was also hosted on `ngn13.fun`. This also the first version that lives on this git repository.
|
time using vue stuff. As NuxtJS supported server-side code, this website had
|
||||||
|
its own built in API. This website was also hosted on `ngn13.fun`. This also
|
||||||
|
the first version that lives on this git repository.
|
||||||
|
|
||||||
- **v3.0 (2023 august - 2023 november)**: In agust of 2023, I decided to rewrite and rework my website again, this time
|
- **v3.0 (2023 august - 2023 november)**: In agust of 2023, I decided to rewrite
|
||||||
I was going with SvelteKit as I haven't had the greatest experience with NuxtJS. SvelteKit was really fun to work with
|
and rework my website again, this time I was going with SvelteKit as I haven't
|
||||||
and I got my new website done pretty quickly. (I don't wanna brag or something but I really imporeved the CSS/styling
|
had the greatest experience with NuxtJS. SvelteKit was really fun to work with
|
||||||
stuff ya know). I also wrote a new API with Go and Gin. I did not publish the source code for the API, the code lived
|
and I got my new website done pretty quickly. (I don't wanna brag or something
|
||||||
on my local git server until I deleted it when I was done with 6.0. This website was hosted on `ngn13.fun` as well.
|
but I really imporeved the CSS/styling stuff ya know). I also wrote a new API
|
||||||
|
with Go and Gin. I did not publish the source code for the API, the code lived
|
||||||
|
on my local git server until I deleted it when I was done with 6.0. This
|
||||||
|
website was hosted on `ngn13.fun` as well.
|
||||||
|
|
||||||
- **v4.0 (2023 november - 2024 october)**: In this version the frontend was still similar to 3.0, the big changes are in
|
- **v4.0 (2023 november - 2024 october)**: In this version the frontend was
|
||||||
the API. I rewrote the API with Fiber. This version was the first version hosted on `ngn.tf` which is my new domain name.
|
still similar to 3.0, the big changes are in the API. I rewrote the API with
|
||||||
|
Fiber. This version was the first version hosted on `ngn.tf` which is my new
|
||||||
|
domain name. Here is a [picture of the index](assets/4.0_index.png) and the
|
||||||
|
[blog page](assets/4.0_blog.png).
|
||||||
|
|
||||||
- **v5.0 (2024 october - 2025 january)**: This version just had simple frontend UI changes compared to 4.0, at this
|
- **v5.0 (2024 october - 2025 january)**: This version just had simple frontend
|
||||||
point I was thinking about doing a massive rework (which I did with 6.0), however I was working on some other shit at
|
UI changes compared to 4.0, at this point I was thinking about doing a massive
|
||||||
the time, so I just did some small changes with the limited time I had for this project.
|
rework (which I did with 6.0), however I was working on some other shit at the
|
||||||
|
time, so I just did some small changes with the limited time I had for this
|
||||||
|
project.
|
||||||
|
|
||||||
- **v6.0 (2025 january - ...)**: The current major version of my website, frontend had a massive rework, API has been
|
- **v6.0 (2025 january - ...)**: The current major version of my website,
|
||||||
cleaned up and extended to do status checking for the services I host. The `doc` server has been added to the mix
|
frontend had a massive rework, API has been cleaned up and extended to do
|
||||||
so I can host static documentation. The most important thing about this version is that it adds multi-language support,
|
status checking for the services I host. The `doc` server has been added to
|
||||||
so literally everything on the website (including the API and documentation content) is localized for both English
|
the mix so I can host static documentation. The most important thing about
|
||||||
and Turkish, which was something I wanted to do for the longest time ever.
|
this version is that it adds multi-language support, so literally everything
|
||||||
|
on the website (including the API and documentation content) is localized for
|
||||||
Damn it has been 4 years since I wrote that shit HTML page huh? Time flies...
|
both English and Turkish, which was something I wanted to do for the longest
|
||||||
|
time ever.
|
||||||
## Screenshots (from v4.0)
|
|
||||||

|
|
||||||

|
|
||||||
|
@@ -218,6 +218,7 @@ class AdminScript:
|
|||||||
"logs": self.get_logs,
|
"logs": self.get_logs,
|
||||||
}
|
}
|
||||||
self.api_url_env = "API_URL"
|
self.api_url_env = "API_URL"
|
||||||
|
self.password_env = "API_PASSWORD"
|
||||||
|
|
||||||
def __format_time(self, ts: int) -> str:
|
def __format_time(self, ts: int) -> str:
|
||||||
return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y")
|
return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y")
|
||||||
@@ -264,7 +265,10 @@ class AdminScript:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
password = getenv(self.password_env)
|
||||||
|
if password is None:
|
||||||
password = self.log.password("Please enter the admin password")
|
password = self.log.password("Please enter the admin password")
|
||||||
|
|
||||||
self.api = AdminAPI(url, password)
|
self.api = AdminAPI(url, password)
|
||||||
|
|
||||||
if len(argv) == 2:
|
if len(argv) == 2:
|
||||||
@@ -401,4 +405,4 @@ class AdminScript:
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
script = AdminScript()
|
script = AdminScript()
|
||||||
exit(script.run() if 1 else 0)
|
exit(0 if script.run() else 1)
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.23.4
|
FROM golang:1.24.5
|
||||||
|
|
||||||
WORKDIR /api
|
WORKDIR /api
|
||||||
|
|
||||||
|
@@ -11,4 +11,7 @@ run:
|
|||||||
format:
|
format:
|
||||||
gofmt -s -w .
|
gofmt -s -w .
|
||||||
|
|
||||||
.PHONY: test format
|
clean:
|
||||||
|
rm -f *.elf
|
||||||
|
|
||||||
|
.PHONY: test format clean
|
||||||
|
@@ -3,107 +3,47 @@ package config
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
|
"github.com/ngn13/ortam"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Type struct {
|
type Type struct {
|
||||||
Options []Option
|
Debug bool // should display debug messgaes?
|
||||||
Count int
|
AppUrl *url.URL // frontend application URL for the website
|
||||||
|
Password string // admin password
|
||||||
|
Host string // host the server should listen on
|
||||||
|
IPHeader string // header that should be checked for obtaining the client IP
|
||||||
|
Interval string // service status check interval
|
||||||
|
Timeout string // timeout for the service status check
|
||||||
|
Limit string // if the service responds slower than this limit, it will be marked as "slow"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Type) Find(name string, typ uint8) (*Option, error) {
|
func Load() (*Type, error) {
|
||||||
for i := 0; i < c.Count; i++ {
|
var conf = Type{
|
||||||
if c.Options[i].Name != name {
|
Debug: false,
|
||||||
continue
|
Password: "",
|
||||||
|
Host: "0.0.0.0:7002",
|
||||||
|
IPHeader: "X-Real-IP",
|
||||||
|
Interval: "1h",
|
||||||
|
Timeout: "15s",
|
||||||
|
Limit: "5s",
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Options[i].Type != typ {
|
if err := ortam.Load(&conf, "WEBSITE"); err != nil {
|
||||||
return nil, fmt.Errorf("bad option type")
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &c.Options[i], nil
|
if conf.AppUrl == nil {
|
||||||
|
conf.AppUrl, _ = url.Parse("http://localhost:7001/")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("option not found")
|
if conf.Password == "" {
|
||||||
|
return nil, fmt.Errorf("password is not specified")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Type) Load() (err error) {
|
if conf.Host == "" {
|
||||||
var (
|
return nil, fmt.Errorf("host address is not specified")
|
||||||
env_val string
|
|
||||||
env_name string
|
|
||||||
opt *Option
|
|
||||||
exists bool
|
|
||||||
)
|
|
||||||
|
|
||||||
// default options
|
|
||||||
c.Options = []Option{
|
|
||||||
{Name: "debug", Value: "false", Type: OPTION_TYPE_BOOL, Required: true}, // should display debug messgaes?
|
|
||||||
{Name: "app_url", Value: "http://localhost:7001/", Type: OPTION_TYPE_URL, Required: true}, // frontend application URL for the website
|
|
||||||
{Name: "password", Value: "", Type: OPTION_TYPE_STR, Required: true}, // admin password
|
|
||||||
{Name: "host", Value: "0.0.0.0:7002", Type: OPTION_TYPE_STR, Required: true}, // host the server should listen on
|
|
||||||
{Name: "ip_header", Value: "X-Real-IP", Type: OPTION_TYPE_STR, Required: false}, // header that should be checked for obtaining the client IP
|
|
||||||
{Name: "interval", Value: "1h", Type: OPTION_TYPE_STR, Required: false}, // service status check interval
|
|
||||||
{Name: "timeout", Value: "15s", Type: OPTION_TYPE_STR, Required: false}, // timeout for the service status check
|
|
||||||
{Name: "limit", Value: "5s", Type: OPTION_TYPE_STR, Required: false}, // if the service responds slower than this limit, it will be marked as "slow"
|
|
||||||
}
|
|
||||||
c.Count = len(c.Options)
|
|
||||||
|
|
||||||
for i := 0; i < c.Count; i++ {
|
|
||||||
opt = &c.Options[i]
|
|
||||||
|
|
||||||
env_name = opt.Env()
|
|
||||||
|
|
||||||
if env_val, exists = os.LookupEnv(env_name); exists {
|
|
||||||
opt.Value = env_val
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if opt.Value == "" && opt.Required {
|
return &conf, nil
|
||||||
return fmt.Errorf("please specify a value for the config option \"%s\" (\"%s\")", opt.Name, env_name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = opt.Load(); err != nil {
|
|
||||||
return fmt.Errorf("failed to load option \"%s\" (\"%s\"): %s", opt.Name, env_name, err.Error())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Type) GetStr(name string) string {
|
|
||||||
var (
|
|
||||||
opt *Option
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if opt, err = c.Find(name, OPTION_TYPE_STR); err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return opt.TypeValue.Str
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Type) GetBool(name string) bool {
|
|
||||||
var (
|
|
||||||
opt *Option
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if opt, err = c.Find(name, OPTION_TYPE_BOOL); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return opt.TypeValue.Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Type) GetURL(name string) *url.URL {
|
|
||||||
var (
|
|
||||||
opt *Option
|
|
||||||
err error
|
|
||||||
)
|
|
||||||
|
|
||||||
if opt, err = c.Find(name, OPTION_TYPE_URL); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return opt.TypeValue.URL
|
|
||||||
}
|
}
|
||||||
|
@@ -1,49 +0,0 @@
|
|||||||
package config
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
OPTION_TYPE_STR = 0
|
|
||||||
OPTION_TYPE_BOOL = 1
|
|
||||||
OPTION_TYPE_URL = 2
|
|
||||||
)
|
|
||||||
|
|
||||||
type Option struct {
|
|
||||||
Name string
|
|
||||||
Value string
|
|
||||||
Required bool
|
|
||||||
Type uint8
|
|
||||||
TypeValue struct {
|
|
||||||
URL *url.URL
|
|
||||||
Str string
|
|
||||||
Bool bool
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Option) Env() string {
|
|
||||||
return strings.ToUpper(fmt.Sprintf("WEBSITE_%s", o.Name))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *Option) Load() (err error) {
|
|
||||||
err = nil
|
|
||||||
|
|
||||||
switch o.Type {
|
|
||||||
case OPTION_TYPE_STR:
|
|
||||||
o.TypeValue.Str = o.Value
|
|
||||||
|
|
||||||
case OPTION_TYPE_BOOL:
|
|
||||||
o.TypeValue.Bool = "1" == o.Value || "true" == strings.ToLower(o.Value)
|
|
||||||
|
|
||||||
case OPTION_TYPE_URL:
|
|
||||||
o.TypeValue.URL, err = url.Parse(o.Value)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("invalid option type")
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
17
api/go.mod
@@ -1,22 +1,23 @@
|
|||||||
module github.com/ngn13/website/api
|
module github.com/ngn13/website/api
|
||||||
|
|
||||||
go 1.21.3
|
go 1.24.5
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gofiber/fiber/v2 v2.52.5
|
github.com/gofiber/fiber/v2 v2.52.8
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
github.com/mattn/go-sqlite3 v1.14.28
|
||||||
|
github.com/ngn13/ortam v0.0.0-20250421004351-8dea81680817
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
github.com/google/uuid v1.5.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/klauspost/compress v1.17.0 // indirect
|
github.com/klauspost/compress v1.17.9 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/rivo/uniseg v0.2.0 // indirect
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||||
golang.org/x/sys v0.15.0 // indirect
|
golang.org/x/sys v0.28.0 // indirect
|
||||||
)
|
)
|
||||||
|
30
api/go.sum
@@ -1,20 +1,22 @@
|
|||||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
|
github.com/gofiber/fiber/v2 v2.52.8 h1:xl4jJQ0BV5EJTA2aWiKw/VddRpHrKeZLF0QPUxqn0x4=
|
||||||
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
|
github.com/gofiber/fiber/v2 v2.52.8/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
|
||||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
|
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/ngn13/ortam v0.0.0-20250421004351-8dea81680817 h1:WkHM4w51N5jCsWcDVcPsXz3zhi/kCfNp/VGh2uPjwsk=
|
||||||
|
github.com/ngn13/ortam v0.0.0-20250421004351-8dea81680817/go.mod h1:MSJZ4ZstrLvVEvivbp9hhup+iL8rvtpgKcYaF3DSOKk=
|
||||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
@@ -25,5 +27,5 @@ github.com/valyala/tcplisten v1.0.0 h1:rBHj/Xf+E1tRGZyWIWwJDiRY0zc1Js+CV5DqwacVS
|
|||||||
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
14
api/main.go
@@ -36,18 +36,18 @@ func main() {
|
|||||||
app *fiber.App
|
app *fiber.App
|
||||||
stat status.Type
|
stat status.Type
|
||||||
|
|
||||||
conf config.Type
|
conf *config.Type
|
||||||
db database.Type
|
db database.Type
|
||||||
|
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
if err = conf.Load(); err != nil {
|
if conf, err = config.Load(); err != nil {
|
||||||
util.Fail("failed to load the configuration: %s", err.Error())
|
util.Fail("failed to load the configuration: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !conf.GetBool("debug") {
|
if !conf.Debug {
|
||||||
util.Debg = func(m string, v ...any) {}
|
util.Debg = func(m string, v ...any) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ func main() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = stat.Setup(&conf, &db); err != nil {
|
if err = stat.Setup(conf, &db); err != nil {
|
||||||
util.Fail("failed to setup the status checker: %s", err.Error())
|
util.Fail("failed to setup the status checker: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ func main() {
|
|||||||
c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET") // POST can be sent from HTML forms, so I prefer PUT for API endpoints
|
c.Set("Access-Control-Allow-Methods", "PUT, DELETE, GET") // POST can be sent from HTML forms, so I prefer PUT for API endpoints
|
||||||
|
|
||||||
c.Locals("status", &stat)
|
c.Locals("status", &stat)
|
||||||
c.Locals("config", &conf)
|
c.Locals("config", conf)
|
||||||
c.Locals("database", &db)
|
c.Locals("database", &db)
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
@@ -121,9 +121,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// start the app
|
// start the app
|
||||||
util.Info("starting web server on %s", conf.GetStr("host"))
|
util.Info("starting web server on %s", conf.Host)
|
||||||
|
|
||||||
if err = app.Listen(conf.GetStr("host")); err != nil {
|
if err = app.Listen(conf.Host); err != nil {
|
||||||
util.Fail("failed to start the web server: %s", err.Error())
|
util.Fail("failed to start the web server: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -21,7 +21,7 @@ func admin_log(c *fiber.Ctx, m string) error {
|
|||||||
func AuthMiddleware(c *fiber.Ctx) error {
|
func AuthMiddleware(c *fiber.Ctx) error {
|
||||||
conf := c.Locals("config").(*config.Type)
|
conf := c.Locals("config").(*config.Type)
|
||||||
|
|
||||||
if c.Get("Authorization") != conf.GetStr("password") {
|
if c.Get("Authorization") != conf.Password {
|
||||||
return util.ErrAuth(c)
|
return util.ErrAuth(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -7,8 +7,7 @@ import (
|
|||||||
|
|
||||||
func GET_Index(c *fiber.Ctx) error {
|
func GET_Index(c *fiber.Ctx) error {
|
||||||
conf := c.Locals("config").(*config.Type)
|
conf := c.Locals("config").(*config.Type)
|
||||||
app := conf.GetURL("app_url")
|
|
||||||
|
|
||||||
// redirect to the API documentation
|
// redirect to the API documentation
|
||||||
return c.Redirect(app.JoinPath("/doc/api").String())
|
return c.Redirect(conf.AppUrl.JoinPath("/doc/api").String())
|
||||||
}
|
}
|
||||||
|
@@ -40,7 +40,6 @@ func GET_News(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
db := c.Locals("database").(*database.Type)
|
db := c.Locals("database").(*database.Type)
|
||||||
conf := c.Locals("config").(*config.Type)
|
conf := c.Locals("config").(*config.Type)
|
||||||
app := conf.GetURL("app_url")
|
|
||||||
lang := c.Params("lang")
|
lang := c.Params("lang")
|
||||||
|
|
||||||
if lang == "" || len(lang) != 2 {
|
if lang == "" || len(lang) != 2 {
|
||||||
@@ -63,10 +62,10 @@ func GET_News(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if feed, err = util.Render("views/news.xml", fiber.Map{
|
if feed, err = util.Render("views/news.xml", fiber.Map{
|
||||||
|
"app_url": conf.AppUrl,
|
||||||
"updated": time.Now().Format(time.RFC3339),
|
"updated": time.Now().Format(time.RFC3339),
|
||||||
"entries": entries,
|
"entries": entries,
|
||||||
"lang": lang,
|
"lang": lang,
|
||||||
"app": app,
|
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return util.ErrInternal(c, err)
|
return util.ErrInternal(c, err)
|
||||||
}
|
}
|
||||||
|
@@ -67,28 +67,23 @@ func (s *Type) loop() {
|
|||||||
func (s *Type) Setup(conf *config.Type, db *database.Type) error {
|
func (s *Type) Setup(conf *config.Type, db *database.Type) error {
|
||||||
var (
|
var (
|
||||||
dur time.Duration
|
dur time.Duration
|
||||||
iv, to, lm string
|
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
|
|
||||||
iv = conf.GetStr("interval")
|
if conf.Interval == "" || conf.Timeout == "" || conf.Limit == "" {
|
||||||
to = conf.GetStr("timeout")
|
|
||||||
lm = conf.GetStr("limit")
|
|
||||||
|
|
||||||
if iv == "" || to == "" || lm == "" {
|
|
||||||
s.disabled = true
|
s.disabled = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if dur, err = util.GetDuration(iv); err != nil {
|
if dur, err = util.GetDuration(conf.Interval); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.timeout, err = util.GetDuration(iv); err != nil {
|
if s.timeout, err = util.GetDuration(conf.Timeout); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.limit, err = util.GetDuration(iv); err != nil {
|
if s.limit, err = util.GetDuration(conf.Limit); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,10 +10,9 @@ import (
|
|||||||
|
|
||||||
func IP(c *fiber.Ctx) string {
|
func IP(c *fiber.Ctx) string {
|
||||||
conf := c.Locals("config").(*config.Type)
|
conf := c.Locals("config").(*config.Type)
|
||||||
ip_header := conf.GetStr("ip_header")
|
|
||||||
|
|
||||||
if ip_header != "" && c.Get(ip_header) != "" {
|
if conf.IPHeader != "" && c.Get(conf.IPHeader) != "" {
|
||||||
return strings.Clone(c.Get(ip_header))
|
return strings.Clone(c.Get(conf.IPHeader))
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.IP()
|
return c.IP()
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
|
<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
|
||||||
<title>{{.app.Host}} news</title>
|
<title>{{.app_url.Host}} news</title>
|
||||||
<updated>{{.updated}}</updated>
|
<updated>{{.updated}}</updated>
|
||||||
<subtitle>News and updates about my projects and self-hosted services</subtitle>
|
<subtitle>News and updates about my projects and self-hosted services</subtitle>
|
||||||
<link href="{{.app.JoinPath "/news"}}"></link>
|
<link href="{{.app_url.JoinPath "/news"}}"></link>
|
||||||
{{ range .entries }}
|
{{ range .entries }}
|
||||||
<entry>
|
<entry>
|
||||||
<title>{{.Title}}</title>
|
<title>{{.Title}}</title>
|
||||||
|
11
app/.gitignore
vendored
@@ -1,10 +1,9 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
/build
|
package
|
||||||
/.svelte-kit
|
build
|
||||||
/package
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
vite.config.js.timestamp-*
|
vite.config.js.timestamp-*
|
||||||
vite.config.ts.timestamp-*
|
vite.config.ts.timestamp-*
|
||||||
|
@@ -1,2 +0,0 @@
|
|||||||
engine-strict=true
|
|
||||||
resolution-mode=highest
|
|
@@ -3,7 +3,8 @@
|
|||||||
"tabWidth": 2,
|
"tabWidth": 2,
|
||||||
"singleQuote": false,
|
"singleQuote": false,
|
||||||
"trailingComma": "es5",
|
"trailingComma": "es5",
|
||||||
"printWidth": 100,
|
"printWidth": 80,
|
||||||
|
"arrowParens": "avoid",
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
"plugins": ["prettier-plugin-svelte"],
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
}
|
}
|
||||||
|
@@ -1,22 +1,23 @@
|
|||||||
# build the application with node
|
# build the application with node
|
||||||
FROM node:23.5.0 AS build
|
FROM node:24.4.1 AS build
|
||||||
|
|
||||||
ARG WEBSITE_REPORT_URL
|
ARG WEBSITE_REPORT_URL
|
||||||
ARG WEBSITE_SOURCE_URL
|
ARG WEBSITE_SOURCE_URL
|
||||||
ARG WEBSITE_APP_URL
|
|
||||||
ARG WEBSITE_API_URL
|
|
||||||
ARG WEBSITE_DOC_URL
|
ARG WEBSITE_DOC_URL
|
||||||
|
ARG WEBSITE_API_URL
|
||||||
|
ARG WEBSITE_API_PATH
|
||||||
|
|
||||||
ENV WEBSITE_REPORT_URL=$WEBSITE_REPORT_URL
|
ENV WEBSITE_REPORT_URL=$WEBSITE_REPORT_URL
|
||||||
ENV WEBSITE_SOURCE_URL=$WEBSITE_SOURCE_URL
|
ENV WEBSITE_SOURCE_URL=$WEBSITE_SOURCE_URL
|
||||||
ENV WEBSITE_APP_URL=$WEBSITE_APP_URL
|
|
||||||
ENV WEBSITE_API_URL=$WEBSITE_API_URL
|
|
||||||
ENV WEBSITE_DOC_URL=$WEBSITE_DOC_URL
|
ENV WEBSITE_DOC_URL=$WEBSITE_DOC_URL
|
||||||
|
ENV WEBSITE_API_URL=$WEBSITE_API_URL
|
||||||
|
ENV WEBSITE_API_PATH=$WEBSITE_API_PATH
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . /app
|
COPY . /app
|
||||||
|
|
||||||
RUN npm install && npm run build
|
RUN npm install
|
||||||
|
RUN make
|
||||||
|
|
||||||
# run it with bun (a lot faster)
|
# run it with bun (a lot faster)
|
||||||
FROM oven/bun:latest AS main
|
FROM oven/bun:latest AS main
|
||||||
|
11
app/Makefile
@@ -1,10 +1,13 @@
|
|||||||
all:
|
all:
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
format:
|
|
||||||
npm run format
|
|
||||||
|
|
||||||
run:
|
run:
|
||||||
npm run dev
|
npm run dev
|
||||||
|
|
||||||
.PHONY: format
|
format:
|
||||||
|
npm run format
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf build
|
||||||
|
|
||||||
|
.PHONY: format run clean
|
||||||
|
2260
app/package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "website",
|
"name": "website",
|
||||||
"version": "6.0",
|
"version": "6.5",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
@@ -10,19 +10,18 @@
|
|||||||
"format": "prettier --write ."
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^3.3.1",
|
"@sveltejs/adapter-node": "^5.2.13",
|
||||||
"@sveltejs/adapter-node": "^5.2.11",
|
"@sveltejs/kit": "^2.25.1",
|
||||||
"@sveltejs/kit": "^2.15.1",
|
"@sveltejs/vite-plugin-svelte": "^6.1.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.3",
|
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prettier-plugin-svelte": "^3.3.2",
|
"prettier-plugin-svelte": "^3.4.0",
|
||||||
"svelte": "^5.16.0",
|
"svelte": "^5.36.10",
|
||||||
"vite": "^5.4.11"
|
"vite": "^7.0.0"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dompurify": "^3.2.3",
|
"dompurify": "^3.2.3",
|
||||||
"marked": "^15.0.6",
|
"js-yaml": "^4.1.0",
|
||||||
"svelte-i18n": "^4.0.1"
|
"marked": "^16.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
57
app/src/components/card.svelte
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<script>
|
||||||
|
let { title = "", id = "", children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<main {id}>
|
||||||
|
{#if title === ""}
|
||||||
|
<div>{@render children()}</div>
|
||||||
|
{:else}
|
||||||
|
<h1>{title}</h1>
|
||||||
|
<div class="padded">{@render children()}</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>
|
94
app/src/components/footer.svelte
Normal 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 = $state(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>
|
32
app/src/components/head.svelte
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<script>
|
||||||
|
import { locale } from "$lib/locale.js";
|
||||||
|
import api from "$lib/api.js";
|
||||||
|
|
||||||
|
let { desc, title } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>ngn.tf | {title}</title>
|
||||||
|
|
||||||
|
<!-- standart metadata tags -->
|
||||||
|
|
||||||
|
<meta name="description" content={desc} />
|
||||||
|
<meta name="author" content="ngn" />
|
||||||
|
<meta name="keywords" content="ngn,ngn13,ngn.tf" />
|
||||||
|
<meta name="color-scheme" content="only dark" />
|
||||||
|
<meta name="theme-color" content="#000000" />
|
||||||
|
|
||||||
|
<!-- 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.join("/news/" + $locale.code)}
|
||||||
|
title="Service news and updates"
|
||||||
|
/>
|
||||||
|
</svelte:head>
|
72
app/src/components/header.svelte
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<script>
|
||||||
|
import { _ } from "$lib/locale.js";
|
||||||
|
let { picture, title } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<div style="--length: {title.length + 1}">
|
||||||
|
<h1>{title}<span>_</span></h1>
|
||||||
|
</div>
|
||||||
|
<img src="/assets/{picture}.png" alt="" />
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: end;
|
||||||
|
background: var(--transparent);
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header div {
|
||||||
|
padding: 0 40px 10px 40px;
|
||||||
|
color: var(--color);
|
||||||
|
font-size: var(--size-6);
|
||||||
|
font-family:
|
||||||
|
Consolas,
|
||||||
|
Monaco,
|
||||||
|
Lucida Console,
|
||||||
|
Liberation Mono,
|
||||||
|
DejaVu Sans Mono,
|
||||||
|
Bitstream Vera Sans Mono,
|
||||||
|
Courier New,
|
||||||
|
monospace;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
header div h1 {
|
||||||
|
text-shadow: var(--text-shadow);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
animation: typing 0.8s steps(var(--length), end);
|
||||||
|
}
|
||||||
|
|
||||||
|
header div span {
|
||||||
|
content: "_";
|
||||||
|
display: inline-block;
|
||||||
|
animation: blink 1.5s steps(2) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
header img {
|
||||||
|
width: 220px;
|
||||||
|
padding: 0 50px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 900px) {
|
||||||
|
header {
|
||||||
|
height: 180px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
header div {
|
||||||
|
padding: 30px 20px 20px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
header img {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
31
app/src/components/language.svelte
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<script>
|
||||||
|
import { localizer, next } from "$lib/locale.js";
|
||||||
|
|
||||||
|
/* if we have javascript, no need to send the GET request to the server to
|
||||||
|
* change the language, we can just switch it on the browser */
|
||||||
|
function onsubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
localizer.switch();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<form method="get" {onsubmit}>
|
||||||
|
<input type="hidden" name="l" value={$next.code} />
|
||||||
|
<button type="submit">{$next.icon}</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
button {
|
||||||
|
cursor: pointer;
|
||||||
|
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>
|
46
app/src/components/navbar.svelte
Normal 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>
|
35
app/src/components/page.svelte
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<script>
|
||||||
|
import { click } from "$lib/util.js";
|
||||||
|
import { page } from "$app/stores";
|
||||||
|
|
||||||
|
let { link, children } = $props();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<a
|
||||||
|
class={$page.url.pathname === link ? "active" : "inactive"}
|
||||||
|
data-sveltekit-preload-data
|
||||||
|
onclick={click}
|
||||||
|
href={link}
|
||||||
|
>
|
||||||
|
{@render children()}
|
||||||
|
</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>
|
141
app/src/components/service.svelte
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
<script>
|
||||||
|
import { time_from_ts } from "$lib/util.js";
|
||||||
|
import { _, locale } from "$lib/locale.js";
|
||||||
|
|
||||||
|
let { service } = $props();
|
||||||
|
</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>
|
||||||
|
<span>|</span>
|
||||||
|
<li><a href={service.onion}>TOR</a></li>
|
||||||
|
<span>|</span>
|
||||||
|
<li><a href={service.i2p}> I2P</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="check">
|
||||||
|
<p>
|
||||||
|
{$_("services.last", {
|
||||||
|
time: time_from_ts(service.check_time),
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
{#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: 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 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 .info ul span {
|
||||||
|
color: var(--white-3);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 p {
|
||||||
|
font-size: var(--size-2);
|
||||||
|
font-weight: 100;
|
||||||
|
|
||||||
|
color: var(--white-2);
|
||||||
|
padding: 5px 18px;
|
||||||
|
|
||||||
|
word-break: keep-all;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
@@ -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}`,
|
||||||
|
};
|
||||||
|
}
|
@@ -1,37 +1,59 @@
|
|||||||
|
import { browser } from "$app/environment";
|
||||||
import { urljoin } from "$lib/util.js";
|
import { urljoin } from "$lib/util.js";
|
||||||
|
|
||||||
const api_version = "v1";
|
class API {
|
||||||
const api_url = urljoin(import.meta.env.WEBSITE_API_URL, api_version);
|
constructor() {
|
||||||
|
this.version = "v1";
|
||||||
function api_urljoin(path = null, query = {}) {
|
|
||||||
return urljoin(api_url, path, query);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function api_check_err(json) {
|
// join given path and queries into an API URL
|
||||||
if (!("error" in json)) throw new Error('API response is missing the "error" key');
|
join(path = null, query = {}) {
|
||||||
|
let base = "";
|
||||||
|
|
||||||
if (json["error"] != "") throw new Error(`API returned an error: ${json["error"]}`);
|
if (browser) {
|
||||||
|
base = urljoin(import.meta.env.WEBSITE_API_PATH, this.version);
|
||||||
if (!("result" in json)) throw new Error('API response is missing the "result" key');
|
} else {
|
||||||
|
base = urljoin(import.meta.env.WEBSITE_API_URL, this.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function api_http_get(fetch, url) {
|
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 res = await fetch(url);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
api_check_err(json);
|
this.check_err(json);
|
||||||
return json["result"];
|
return json["result"];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function api_get_metrics(fetch) {
|
// get visitor metrics
|
||||||
return await api_http_get(fetch, api_urljoin("/metrics"));
|
async metrics(fetch) {
|
||||||
|
return await this.GET(fetch, this.join("/metrics"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function api_get_services(fetch) {
|
// get service list
|
||||||
return await api_http_get(fetch, api_urljoin("/services"));
|
async services(fetch) {
|
||||||
|
return await this.GET(fetch, this.join("/services"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function api_get_projects(fetch) {
|
// get projects list
|
||||||
return await api_http_get(fetch, api_urljoin("/projects"));
|
async projects(fetch) {
|
||||||
|
return await this.GET(fetch, this.join("/projects"));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { api_version, api_urljoin, api_get_metrics, api_get_services, api_get_projects };
|
const api = new API();
|
||||||
|
export default api;
|
||||||
|
@@ -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>
|
|
@@ -1,28 +1,36 @@
|
|||||||
import { urljoin } from "$lib/util.js";
|
import { urljoin } from "$lib/util.js";
|
||||||
|
|
||||||
function doc_urljoin(path = null, query = {}) {
|
class Doc {
|
||||||
|
// join given path and queries with a document server URL
|
||||||
|
join(path = null, query = {}) {
|
||||||
return urljoin(import.meta.env.WEBSITE_DOC_URL, path, query);
|
return urljoin(import.meta.env.WEBSITE_DOC_URL, path, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
function doc_check_err(json) {
|
// check JSON response and throw an error if it contains one
|
||||||
if ("error" in json) throw new Error(`Documentation server returned an error: ${json["error"]}`);
|
check_err(json) {
|
||||||
|
if ("error" in json)
|
||||||
|
throw new Error(`Doc server returned an error: ${json["error"]}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doc_http_get(fetch, url) {
|
// send a HTTP request to the documentation server
|
||||||
|
async GET(fetch, url) {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const json = await res.json();
|
const json = await res.json();
|
||||||
doc_check_err(json);
|
this.check_err(json);
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doc_get_list(fetch) {
|
// get a list of all the documentations
|
||||||
return await doc_http_get(fetch, doc_urljoin("/list"));
|
async list(fetch) {
|
||||||
|
return await this.GET(fetch, this.join("/list"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function doc_get(fetch, name) {
|
// get a documentation
|
||||||
let url = doc_urljoin("/get");
|
async get(fetch, name) {
|
||||||
url = urljoin(url, name);
|
let url = this.join(`/get/${name}`);
|
||||||
return await doc_http_get(fetch, url);
|
return await this.GET(fetch, url);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export { doc_urljoin, doc_get, doc_get_list };
|
const doc = new Doc();
|
||||||
|
export default doc;
|
||||||
|
@@ -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>
|
|
@@ -1,89 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { urljoin, 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 data = {};
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
data = await api_get_metrics(fetch);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<footer style="border-top: solid 2px var(--{color()});">
|
|
||||||
<div class="info">
|
|
||||||
<div class="links">
|
|
||||||
<span>
|
|
||||||
<Link link={import.meta.env.WEBSITE_SOURCE_URL} bold={true}>{$_("footer.source")}</Link>
|
|
||||||
</span>
|
|
||||||
<span>/</span>
|
|
||||||
<span>
|
|
||||||
<Link link={urljoin(import.meta.env.WEBSITE_APP_URL, "doc/license")} bold={true}
|
|
||||||
>{$_("footer.license")}</Link
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
<span>/</span>
|
|
||||||
<span>
|
|
||||||
<Link link={urljoin(import.meta.env.WEBSITE_APP_URL, "doc/privacy")} bold={true}
|
|
||||||
>{$_("footer.privacy")}</Link
|
|
||||||
>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<span>
|
|
||||||
{$_("footer.powered")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="useless">
|
|
||||||
<span>
|
|
||||||
{$_("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>
|
|
||||||
<span>
|
|
||||||
{$_("footer.version", { values: { api_version: "v1", frontend_version: pkg.version } })}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
footer {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
background: var(--black-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
div {
|
|
||||||
display: flex;
|
|
||||||
color: var(--white-2);
|
|
||||||
font-size: var(--size-2);
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.useless {
|
|
||||||
margin: 25px 50px 25px 0;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info {
|
|
||||||
margin: 25px 0 25px 50px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.info .links {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,22 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { api_urljoin } from "$lib/api.js";
|
|
||||||
import { app_url } from "$lib/util.js";
|
|
||||||
|
|
||||||
export let desc, title;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>[ngn.tf] | {title}</title>
|
|
||||||
|
|
||||||
<meta content="[ngn.tf] | {title}" property="og:title" />
|
|
||||||
<meta content={desc} property="og:description" />
|
|
||||||
<meta content={app_url()} property="og:url" />
|
|
||||||
<meta content="#000000" data-react-helmet="true" name="theme-color" />
|
|
||||||
|
|
||||||
<link
|
|
||||||
rel="alternate"
|
|
||||||
type="application/atom+xml"
|
|
||||||
href={api_urljoin("/news/en")}
|
|
||||||
title="Atom Feed"
|
|
||||||
/>
|
|
||||||
</svelte:head>
|
|
@@ -1,80 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { color } from "$lib/util.js";
|
|
||||||
import { _ } from "svelte-i18n";
|
|
||||||
|
|
||||||
export let picture = "";
|
|
||||||
export let title = "";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<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: var(--background);
|
|
||||||
background-size: 50%;
|
|
||||||
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: end;
|
|
||||||
}
|
|
||||||
|
|
||||||
header div {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: end;
|
|
||||||
padding: 50px 50px 30px 50px;
|
|
||||||
font-size: var(--size-6);
|
|
||||||
font-family:
|
|
||||||
Consolas,
|
|
||||||
Monaco,
|
|
||||||
Lucida Console,
|
|
||||||
Liberation Mono,
|
|
||||||
DejaVu Sans Mono,
|
|
||||||
Bitstream Vera Sans Mono,
|
|
||||||
Courier New,
|
|
||||||
monospace;
|
|
||||||
white-space: nowrap;
|
|
||||||
justify-content: start;
|
|
||||||
width: min-content;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
header img {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
@@ -1,6 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { color } from "$lib/util.js";
|
|
||||||
export let icon = "";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<i style="color: var(--{color()});" class="nf {icon}"></i>
|
|
@@ -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>
|
|
@@ -1,66 +1,225 @@
|
|||||||
import { init, locale, register, waitLocale } from "svelte-i18n";
|
import { derived, get, writable } from "svelte/store";
|
||||||
import { browser } from "$app/environment";
|
import { browser } from "$app/environment";
|
||||||
import { get, writable } from "svelte/store";
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
const locale_default = "en";
|
// defines a single locale
|
||||||
let locale_index = writable(0);
|
class Locale {
|
||||||
let locale_list = [];
|
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() {
|
this.code = code; // BCP 47 language tag
|
||||||
// english
|
this.icon = icon; // icon for the locale
|
||||||
register("en", () => import("../locales/en.json"));
|
this.all = {}; // all the locales
|
||||||
locale_list.push({ code: "en", name: "English", icon: "🇬🇧" });
|
}
|
||||||
|
|
||||||
// turkish
|
// load the locale
|
||||||
register("tr", () => import("../locales/tr.json"));
|
async load() {
|
||||||
locale_list.push({ code: "tr", name: "Turkish", icon: "🇹🇷" });
|
const text = await import(`../locales/${this.code}.yaml?raw`);
|
||||||
|
this.all = yaml.load(text.default);
|
||||||
|
}
|
||||||
|
|
||||||
init({
|
// renders a given locale using given values and links
|
||||||
fallbackLocale: locale_default,
|
render(locale, values, links) {
|
||||||
initialLocale: get(locale),
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
function locale_from_browser() {
|
// if we actually found it, set it as the current locale
|
||||||
if (browser) return window.navigator.language.slice(0, 2).toLowerCase();
|
this.current.set(this.list[indx]);
|
||||||
else return locale_default;
|
|
||||||
|
// set the next locale
|
||||||
|
if (++indx >= this.list.length) {
|
||||||
|
this.next.set(this.list[0]);
|
||||||
|
} else {
|
||||||
|
this.next.set(this.list[indx]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function locale_select(l = null) {
|
// switch to the next locale
|
||||||
if (l === null) {
|
switch() {
|
||||||
if (browser && null !== (l = localStorage.getItem("locale"))) locale_select(l);
|
// find the next locale's index
|
||||||
else locale_select(locale_from_browser());
|
let indx = this.list.findIndex(locale => {
|
||||||
return;
|
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]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
l = l.slice(0, 2);
|
resolve(key, vars = {}) {
|
||||||
|
// attempt to resolve the given key
|
||||||
|
let res = get(this.current).resolve(key, vars);
|
||||||
|
|
||||||
for (let i = 0; i < locale_list.length; i++) {
|
if (res !== undefined) {
|
||||||
if (l !== locale_list[i].code) continue;
|
return res;
|
||||||
|
|
||||||
if (browser) localStorage.setItem("locale", l);
|
|
||||||
|
|
||||||
locale.set(l);
|
|
||||||
locale_index.set(i);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
locale.set(locale_default);
|
// if we fail to resolve the key, try to resolve it using the fallback
|
||||||
locale_index.set(0);
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function locale_wait() {
|
export const localizer = new Localizer(); // global localizer
|
||||||
await waitLocale();
|
export const locale = localizer.current; // current locale
|
||||||
}
|
export const next = localizer.next; // next locale
|
||||||
|
|
||||||
export {
|
// resolve a given locale by it's key
|
||||||
|
export const _ = derived(
|
||||||
locale,
|
locale,
|
||||||
locale_list,
|
() =>
|
||||||
locale_index,
|
(key, vars = {}) =>
|
||||||
locale_default,
|
localizer.resolve(key, vars)
|
||||||
locale_setup,
|
);
|
||||||
locale_wait,
|
|
||||||
locale_select,
|
export default localizer;
|
||||||
locale_from_browser,
|
|
||||||
};
|
|
||||||
|
@@ -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>
|
|
@@ -1,27 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { color, click } from "$lib/util.js";
|
|
||||||
import { page } from "$app/stores";
|
|
||||||
|
|
||||||
export let link;
|
|
||||||
|
|
||||||
function is_active() {
|
|
||||||
return $page.url.pathname == link;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<a
|
|
||||||
style="text-decoration-color: var(--{color()}); {is_active() ? `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>
|
|
@@ -1,37 +0,0 @@
|
|||||||
<script>
|
|
||||||
import { locale_list, locale_select, locale_index } from "$lib/locale.js";
|
|
||||||
|
|
||||||
let len = locale_list.length;
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button on:click={next}>
|
|
||||||
{get_next($locale_index).icon}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<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>
|
|
@@ -1,114 +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>
|
|
@@ -1,71 +1,60 @@
|
|||||||
import { locale_from_browser } from "$lib/locale.js";
|
import { localizer } from "$lib/locale.js";
|
||||||
|
|
||||||
const colors = [
|
// colors defined in static/css/global.css
|
||||||
"yellow",
|
const colors = ["yellow", "cyan", "green", "pinkish", "red", "blue"];
|
||||||
"cyan",
|
|
||||||
"green",
|
|
||||||
"pinkish",
|
|
||||||
"red",
|
|
||||||
// "blue" (looks kinda ass)
|
|
||||||
];
|
|
||||||
|
|
||||||
let colors_pos = -1;
|
// randomly select a color
|
||||||
|
export function color() {
|
||||||
function color() {
|
return colors[Math.floor(Math.random() * colors.length)];
|
||||||
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];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function click() {
|
// play a click sound
|
||||||
let audio = new Audio("/click.wav");
|
export function click() {
|
||||||
|
let audio = new Audio("/assets/click.wav");
|
||||||
audio.play();
|
audio.play();
|
||||||
}
|
}
|
||||||
|
|
||||||
function urljoin(url, path = null, query = {}) {
|
// join a given path to the URL
|
||||||
if (undefined === url || null === url) return;
|
export function urljoin(url, path = null) {
|
||||||
|
if (null === path || path.length === 0) {
|
||||||
let url_len = url.length;
|
return url;
|
||||||
|
|
||||||
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(k, query[k]);
|
|
||||||
|
|
||||||
return url.href;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function app_url(path = null, query = {}) {
|
if (url[url.length - 1] != "/") {
|
||||||
return urljoin(import.meta.env.WEBSITE_APP_URL, path, query);
|
url += "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
function time_from_ts(ts) {
|
if (path[0] === "/") {
|
||||||
if (ts === 0 || ts === undefined) return;
|
path = path.slice(1);
|
||||||
|
|
||||||
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) {
|
return url + path;
|
||||||
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",
|
month: "2-digit",
|
||||||
year: "2-digit",
|
year: "2-digit",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
}).format(new Date(ts * 1000));
|
}).format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { color, click, urljoin, app_url, 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));
|
||||||
|
}
|
||||||
|
@@ -1,83 +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 a 600 Mbit/s interface",
|
|
||||||
"security": "All use SSL encrypted connection and they are all privacy-respecting",
|
|
||||||
"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 Money!",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"doc": {
|
|
||||||
"title": "Documentation"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"title": "Something went wrong!",
|
|
||||||
"report": "Report this issue"
|
|
||||||
},
|
|
||||||
"footer": {
|
|
||||||
"source": "Source",
|
|
||||||
"license": "License",
|
|
||||||
"privacy": "Privacy",
|
|
||||||
"powered": "Powered by Svelte, Go, SQLite and donations",
|
|
||||||
"number": "Visited {total} times since {since}",
|
|
||||||
"wow": "wow!!",
|
|
||||||
"version": "Using API version {api_version}, frontend version {frontend_version}"
|
|
||||||
}
|
|
||||||
}
|
|
98
app/src/locales/en.yaml
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
navbar:
|
||||||
|
home: home
|
||||||
|
services: services
|
||||||
|
donate: donate
|
||||||
|
|
||||||
|
home:
|
||||||
|
title: hello world!
|
||||||
|
welcome:
|
||||||
|
title: about
|
||||||
|
desc: |
|
||||||
|
Welcome to my personal website, I'm ngn. You might also know me as ngn13.
|
||||||
|
I am a security, privacy and freedom advocate high-school student from
|
||||||
|
Turkey. I'm mainly interesed in software development and system security,
|
||||||
|
so I usually spend my free time writing [Free/Libre
|
||||||
|
software](https://www.gnu.org/philosophy/free-sw.html), solving
|
||||||
|
[CTF](https://en.wikipedia.org/wiki/Capture_the_flag_(cybersecurity))s and
|
||||||
|
learning new things.
|
||||||
|
thanks: |
|
||||||
|
I have few personal software projects and I host web based services. If
|
||||||
|
you would like to check them out, see the sections below. Thank you for
|
||||||
|
visiting my website and I hope you have a good rest of your day!
|
||||||
|
|
||||||
|
projects:
|
||||||
|
title: projects
|
||||||
|
desc: |
|
||||||
|
I develop and work on Free/Libre software. I don't work on massive
|
||||||
|
projects and I'm not a professional but I do enjoy it. Here are some of my
|
||||||
|
more popular projects that you might find interesting
|
||||||
|
|
||||||
|
services:
|
||||||
|
title: services
|
||||||
|
desc: |
|
||||||
|
I host web based services. Most of these services provide front-end or
|
||||||
|
alternatives to already existing popular web services. Here are all the
|
||||||
|
details
|
||||||
|
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
|
||||||
|
blocks: |
|
||||||
|
Accessible from clearnet, [TOR](https://www.torproject.org/) and
|
||||||
|
[I2P](https://geti2p.net/en/) - no region or network blocks
|
||||||
|
bullshit: No CDNs, no cloudflare, no CAPTCHA, no analytics, no bullshit
|
||||||
|
link: See all the services!
|
||||||
|
|
||||||
|
contact:
|
||||||
|
title: contact
|
||||||
|
desc: |
|
||||||
|
If you have question or a suggestion, or if you simply want to chat, feel
|
||||||
|
free to send me an email at **ngn [at] ngn.tf**. You can use my [PGP
|
||||||
|
key](https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D) to
|
||||||
|
send me an encrypted email, which I would highly prefer. My PGP key also
|
||||||
|
includes identity claims, which you can use to verify my public profiles.
|
||||||
|
mastodon: |
|
||||||
|
I'm also on [mastodon](https://defcon.social/@ngn) where I mainly post
|
||||||
|
Turkish stuff.
|
||||||
|
|
||||||
|
services:
|
||||||
|
title: service status
|
||||||
|
desc: |
|
||||||
|
Here you can find a list of all the publicly accessible services I host. I
|
||||||
|
have periodic status check for clearnet addresses: A request is sent to the
|
||||||
|
clearnet address the status of the service is reported based on the
|
||||||
|
response. The latest reported status is displayed next to the time of the
|
||||||
|
report. If you experience any issues with a service even though the status
|
||||||
|
is displayed as up, please [contact me](/#contact) to report this issue.
|
||||||
|
feed: |
|
||||||
|
I also have an [1:Atom feed] that you can follow to get the latest news and
|
||||||
|
updates about the changes made to the different services.
|
||||||
|
last: Last checked at {time}
|
||||||
|
status:
|
||||||
|
up: Up
|
||||||
|
down: Down
|
||||||
|
slow: Slow
|
||||||
|
|
||||||
|
donate:
|
||||||
|
title: donate!
|
||||||
|
desc: |
|
||||||
|
If you like my work or if you are feeling generous, you can financially
|
||||||
|
support me. I spend around $15 every month to maintain everything, so even a
|
||||||
|
small donation would be highly appreciated and it would help me keep
|
||||||
|
everything up and running.
|
||||||
|
info: |
|
||||||
|
Currently I only accept donations with [Monero
|
||||||
|
(XMR)](https://www.getmonero.org/). Here is my monero address
|
||||||
|
thanks: |
|
||||||
|
Also I would like to thank everyone who has donated so far. I really
|
||||||
|
appreciate it!
|
||||||
|
|
||||||
|
error:
|
||||||
|
title: something went wrong!
|
||||||
|
report: Report this issue
|
||||||
|
|
||||||
|
footer:
|
||||||
|
source: Source
|
||||||
|
license: License
|
||||||
|
privacy: Privacy
|
||||||
|
number: Visited {total} times since {since}
|
||||||
|
wow: wow!!
|
||||||
|
render: Rendered on {time} just for you
|
@@ -1,84 +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 600 Mbit/s ağ arayüzü üzerinden erişilebilir",
|
|
||||||
"security": "Hepsi SSL şifreli bağlantı kullanıyor ve hepsi gizliğinize önem veriyor",
|
|
||||||
"privacy": "Accessible from clearnet, TOR and I2P, no region or network blocks",
|
|
||||||
"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 yok, boktan 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": "Para Bağışla!",
|
|
||||||
"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ı"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"doc": {
|
|
||||||
"title": "Dökümantasyon"
|
|
||||||
},
|
|
||||||
"error": {
|
|
||||||
"title": "Birşeyler yanlış gitti!",
|
|
||||||
"report": "Bu sorunu raporlayın"
|
|
||||||
},
|
|
||||||
"footer": {
|
|
||||||
"source": "Kaynak",
|
|
||||||
"license": "Lisans",
|
|
||||||
"privacy": "Gizlilik",
|
|
||||||
"powered": "Svelte, Go, SQLite ve bağışlar tarafından destekleniyor",
|
|
||||||
"number": "{since} tarihinden beri {total} kez ziyaret edildi",
|
|
||||||
"wow": "vay be!!",
|
|
||||||
"version": "Kullan API versiyonu {api_version}, arayüz versiyonu {frontend_version}"
|
|
||||||
}
|
|
||||||
}
|
|
103
app/src/locales/tr.yaml
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
navbar:
|
||||||
|
home: anasayfa
|
||||||
|
services: servisler
|
||||||
|
donate: bağış
|
||||||
|
|
||||||
|
home:
|
||||||
|
title: merhaba dünya!
|
||||||
|
welcome:
|
||||||
|
title: hakkımda
|
||||||
|
desc: |
|
||||||
|
Kişisel websiteme hoşgeldiniz, ben ngn. Beni ngn13 olarak da
|
||||||
|
bilebilirsiniz. Türkiye'den güvenlik, gizlilik ve özgürlük savunucusu bir
|
||||||
|
liseliyim. Genel olarak yazılım geliştirme ve sistem güvenliği ile
|
||||||
|
ilgileniyorum, o yüzden boş zamanımın çoğunu
|
||||||
|
[Özgür yazılım](https://www.gnu.org/philosophy/free-sw.tr.html) yazarak,
|
||||||
|
[CTF](https://wikipedia.org/wiki/Capture_the_flag_(cybersecurity)) çözerek
|
||||||
|
ve yeni şeyler öğrenerek harcıyorum.
|
||||||
|
thanks: |
|
||||||
|
Birkaç kişisel yazılım projem var ve web tabanlı servisler barındırıyorum.
|
||||||
|
Eğer bunları incelemek isterseniz, aşağıdaki bölümlere bakın. Websitemi
|
||||||
|
ziyaret ettiğiniz için teşekkür ederim ve size iyi günler dilerim!
|
||||||
|
|
||||||
|
projects:
|
||||||
|
title: projeler
|
||||||
|
desc: |
|
||||||
|
Özgür yazılım geliştiriyorum ve üzerinde çalışıyorum. Devasa projelerde
|
||||||
|
çalışmıyorum ve profesyonel değilim ama hoşuma gidiyor. İşte biraz daha
|
||||||
|
popüler olan ve ilginç bulabileceğiniz bazı projelerim
|
||||||
|
|
||||||
|
services:
|
||||||
|
title: servisler
|
||||||
|
desc: |
|
||||||
|
Web tabanlı servisler host ediyorum. Bu servislerin çoğu zaten mevcut
|
||||||
|
popüler web servislerine arayüz ya da alternatifler sağlıyor. İşte tüm
|
||||||
|
detaylar
|
||||||
|
speed: Tüm servisler 1 Gbit ağ arayüzü üzerinden erişilebilir
|
||||||
|
security: |
|
||||||
|
Hepsi SSL şifreli bağlantı kullanıyor ve mahremiyetinize ve özgürlüğünüze
|
||||||
|
saygı gösteriyor
|
||||||
|
blocks: |
|
||||||
|
Açık ağdan, [TOR](https://www.torproject.org/tr/) ve
|
||||||
|
[I2P](https://geti2p.net/tr/)'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 görüntüle!
|
||||||
|
|
||||||
|
contact:
|
||||||
|
title: contact
|
||||||
|
desc: |
|
||||||
|
Eğer bir sorunuz ya da öneriniz varsa, ya da basitçe muhabbet etmek
|
||||||
|
istiyorsanız, bana **ngn [at] ngn.tf** adresinden bir e-posta göndermekten
|
||||||
|
çekinmeyin. [PGP anahtarımı
|
||||||
|
](https://keyoxide.org/F9E70878C2FB389AEC2BA34CA3654DF5AD9F641D) bana
|
||||||
|
tercihim olan şifreli e-postalar göndermek için kullanabilirsiniz. PGP
|
||||||
|
anahtarım aynı zamanda identity claim'leri içeriyor, bunları internete
|
||||||
|
açık profillerimi doğrulamak için kullanabilirisniz.
|
||||||
|
mastodon: |
|
||||||
|
Aynı zamanda [mastodon'dayım](https://defcon.social/@ngn),
|
||||||
|
çoğunlukla Türkçe olan paylaşımlar yapıyorum.
|
||||||
|
|
||||||
|
services:
|
||||||
|
title: servis durumu
|
||||||
|
desc: |
|
||||||
|
Burada barındırdığım internete açık tüm servislerin bir listesini
|
||||||
|
bulabilirsiniz. Açık ağ adresleri için periyodik bir durum kontrolü mevcut:
|
||||||
|
Açık ağ adresine bir istek gönderiliyor ve servisin durumu verilen cevaba
|
||||||
|
göre raporlanıyor. En son raporlanan durum raporun zamanının yanında
|
||||||
|
görüntüleniyor. Bir servisin durumu çalışıyor olarak görünmesine rağmen
|
||||||
|
servisle alakalı sorunlar yaşıyorsanız, lütfen bu sorunları raporlamak için
|
||||||
|
[bana ulaşın](/#contact).
|
||||||
|
feed: |
|
||||||
|
Farklı servislere yapılan değişiklikler hakkında güncellemeleri ve
|
||||||
|
haberleri görmek adına takip edebileceğiniz bir [1:Atom beslemesi] de
|
||||||
|
mevcut.
|
||||||
|
last: Son kontrol zamanı {time}
|
||||||
|
status:
|
||||||
|
up: Çalışıyor
|
||||||
|
down: Kapalı
|
||||||
|
slow: Yavaş
|
||||||
|
|
||||||
|
donate:
|
||||||
|
title: bağış yap!
|
||||||
|
desc: |
|
||||||
|
Eğer emeğimi seviyorsanız ya da kendinizi cömert hissediyorsanız, beni
|
||||||
|
finansal olarak destekleyebilirsiniz. Herşeyi yönetmek adına her yaklaşık
|
||||||
|
$15 harcıyorum, o yüzden küçük bir bağış bile oldukça faydalı olacaktır ve
|
||||||
|
herşeyi ayakta tutmama yardım eder.
|
||||||
|
info: |
|
||||||
|
Şuan sadece [Monero (XMR)](https://www.getmonero.org/tr/) ile bağış kabul
|
||||||
|
ediyorum. İşte monero adresim
|
||||||
|
thanks: Ayrıca şuana kadar bağış yapan herkese çok teşekkür ederim.
|
||||||
|
|
||||||
|
error:
|
||||||
|
title: birşeyler yanlış gitti!
|
||||||
|
report: Bu hatayı raporla
|
||||||
|
|
||||||
|
footer:
|
||||||
|
source: Kaynak
|
||||||
|
license: Lisans
|
||||||
|
privacy: Gizlilik
|
||||||
|
number: "{since} tarihinden beri {total} kez ziyaret edildi"
|
||||||
|
wow: vay be!
|
||||||
|
render: Sizin için {time} tarihinde işlendi
|
@@ -1,8 +1,51 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from "svelte";
|
import { page } from "$app/state";
|
||||||
import { goto } from "$app/navigation";
|
import { _ } from "$lib/locale.js";
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
goto("/");
|
|
||||||
});
|
|
||||||
</script>
|
</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>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { locale_setup, locale_wait } from "$lib/locale.js";
|
import localizer from "$lib/locale.js";
|
||||||
|
|
||||||
export async function load() {
|
export async function load({ data }) {
|
||||||
locale_setup();
|
await localizer.setup(data.locale);
|
||||||
await locale_wait();
|
return data;
|
||||||
}
|
}
|
||||||
|
36
app/src/routes/+layout.server.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { redirect } from "@sveltejs/kit";
|
||||||
|
import { color } from "../lib/util.js";
|
||||||
|
|
||||||
|
// set the locale cookie
|
||||||
|
function set_locale(cookies, locale) {
|
||||||
|
cookies.set("locale", locale, { path: "/", secure: false, httpOnly: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function load({ cookies, request, url }) {
|
||||||
|
// if the locale param is specified, use the specified locale
|
||||||
|
let locale = url.searchParams.get("l");
|
||||||
|
|
||||||
|
if (locale) {
|
||||||
|
set_locale(cookies, locale);
|
||||||
|
return redirect(307, url.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt get the preferred locale from cookies
|
||||||
|
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];
|
||||||
|
set_locale(cookies, locale);
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
};
|
||||||
|
}
|
@@ -1,36 +1,30 @@
|
|||||||
<script>
|
<script>
|
||||||
import Navbar from "$lib/navbar.svelte";
|
import { browser } from "$app/environment";
|
||||||
import Footer from "$lib/footer.svelte";
|
|
||||||
|
|
||||||
import { locale_select } from "$lib/locale.js";
|
import { color } from "$lib/util.js";
|
||||||
import { onMount } from "svelte";
|
import api from "$lib/api.js";
|
||||||
|
|
||||||
let { children } = $props();
|
let { data, children } = $props();
|
||||||
|
|
||||||
onMount(() => {
|
if (browser) {
|
||||||
locale_select();
|
// set the current app and the API version
|
||||||
});
|
window._version = {
|
||||||
|
app: pkg.version,
|
||||||
|
api: api.version,
|
||||||
|
};
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main>
|
<main style="--color: var(--{data.color})">
|
||||||
<Navbar />
|
|
||||||
<div class="content">
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
|
||||||
<Footer />
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@import "/global.css";
|
@import "/css/global.css";
|
||||||
|
|
||||||
main {
|
main {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
|
||||||
background: var(--black-1);
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,15 +1,9 @@
|
|||||||
import { api_get_projects } from "$lib/api.js";
|
import api from "$lib/api.js";
|
||||||
|
|
||||||
export async function load({ fetch }) {
|
export async function load({ fetch }) {
|
||||||
try {
|
let projects = await api.projects(fetch);
|
||||||
let projects = await api_get_projects(fetch);
|
|
||||||
return {
|
return {
|
||||||
projects: null === projects ? [] : projects,
|
projects: null === projects ? [] : projects,
|
||||||
error: "",
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
error: err.toString(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@@ -1,121 +1,94 @@
|
|||||||
<script>
|
<script>
|
||||||
import Header from "$lib/header.svelte";
|
import Navbar from "$components/navbar.svelte";
|
||||||
import Error from "$lib/error.svelte";
|
import Footer from "$components/footer.svelte";
|
||||||
import Head from "$lib/head.svelte";
|
import Header from "$components/header.svelte";
|
||||||
import Card from "$lib/card.svelte";
|
import Head from "$components/head.svelte";
|
||||||
import Link from "$lib/link.svelte";
|
import Card from "$components/card.svelte";
|
||||||
|
|
||||||
import { _, locale } from "svelte-i18n";
|
import { locale, _ } from "$lib/locale.js";
|
||||||
import { color } from "$lib/util.js";
|
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
|
|
||||||
|
// 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>
|
</script>
|
||||||
|
|
||||||
<Head title="home" desc="home page of my personal website" />
|
<Head title="home" desc="home page of my personal website" />
|
||||||
|
<Navbar />
|
||||||
<Header picture="tired" title={$_("home.title")} />
|
<Header picture="tired" title={$_("home.title")} />
|
||||||
|
|
||||||
{#if data.error.length !== 0}
|
|
||||||
<Error error={data.error} />
|
|
||||||
{:else}
|
|
||||||
<main>
|
<main>
|
||||||
<Card title={$_("home.welcome.title")}>
|
<!-- welcome -->
|
||||||
<span> 👋 {$_("home.welcome.desc")}</span>
|
<Card>
|
||||||
<ul>
|
<p>{@html $_("home.welcome.desc")}</p>
|
||||||
<li>🇹🇷 {$_("home.welcome.whoami")}</li>
|
<br />
|
||||||
<li>🖥️ {$_("home.welcome.interest")}</li>
|
<p>{$_("home.welcome.thanks")}</p>
|
||||||
<li>❤️ {$_("home.welcome.support")}</li>
|
|
||||||
</ul>
|
|
||||||
</Card>
|
</Card>
|
||||||
<Card title={$_("home.work.title")}>
|
|
||||||
<span>{$_("home.work.desc")}</span>
|
<!-- projects -->
|
||||||
|
<Card title={$_("home.projects.title")} id="projects">
|
||||||
|
<p>{$_("home.projects.desc")}:</p>
|
||||||
|
<br />
|
||||||
<ul>
|
<ul>
|
||||||
<li>⌨️ {$_("home.work.build")}</li>
|
{#each projects() as project}
|
||||||
<li>🤦 {$_("home.work.fix")}</li>
|
<li class="project">
|
||||||
<li>🚩 {$_("home.work.ctf")}</li>
|
<a href={project.url}>{project.name}</a>: {project.desc[$locale.code]}
|
||||||
<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>
|
|
||||||
</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>
|
</li>
|
||||||
{/each}
|
{/each}
|
||||||
</ul>
|
</ul>
|
||||||
{/if}
|
</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>
|
</Card>
|
||||||
</main>
|
</main>
|
||||||
{/if}
|
<Footer />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main {
|
main {
|
||||||
|
background: var(--black-1);
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-direction: column;
|
||||||
justify-content: center;
|
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
|
||||||
padding: 50px;
|
padding: 40px;
|
||||||
gap: 28px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 900px) {
|
.services {
|
||||||
main {
|
color: var(--color);
|
||||||
flex-direction: column;
|
text-decoration-color: var(--color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.project a {
|
||||||
|
color: var(--color);
|
||||||
|
text-decoration-color: var(--color);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,13 +1,7 @@
|
|||||||
import { doc_get_list, doc_get } from "$lib/doc";
|
import doc from "$lib/doc";
|
||||||
|
|
||||||
export async function load({ fetch, params }) {
|
export async function load({ fetch, params }) {
|
||||||
try {
|
|
||||||
return {
|
return {
|
||||||
docs: await doc_get_list(fetch),
|
doc: await doc.get(fetch, params.name),
|
||||||
doc: await doc_get(fetch, params.name),
|
|
||||||
error: "",
|
|
||||||
};
|
};
|
||||||
} catch (err) {
|
|
||||||
return { error: err.toString() };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -1,123 +1,41 @@
|
|||||||
<script>
|
<script>
|
||||||
import Header from "$lib/header.svelte";
|
import Navbar from "$components/navbar.svelte";
|
||||||
import Error from "$lib/error.svelte";
|
import Footer from "$components/footer.svelte";
|
||||||
import Head from "$lib/head.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 DOMPurify from "dompurify";
|
||||||
import { onMount } from "svelte";
|
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
marked.use({ breaks: true });
|
marked.use();
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(() => {
|
||||||
for (let key in data.doc)
|
for (let key in data.doc) {
|
||||||
data.doc[key]["content"] = DOMPurify.sanitize(data.doc[key]["content"]);
|
data.doc[key]["content"] = DOMPurify.sanitize(data.doc[key]["content"]);
|
||||||
|
}
|
||||||
if (undefined !== data.error && data.error.includes("not found")) goto("/");
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Head title="documentation" desc="website and API documentation" />
|
<Head title="documentation" desc="website and API documentation" />
|
||||||
<Header picture="reader" title={$_("doc.title")} />
|
<Navbar />
|
||||||
|
<Header picture="reader" title={data.doc[$locale.code].title} />
|
||||||
{#if data.error.length !== 0}
|
|
||||||
{#if !data.error.includes("not found")}
|
|
||||||
<Error error={data.error} />
|
|
||||||
{/if}
|
|
||||||
{:else}
|
|
||||||
<main>
|
<main>
|
||||||
{#if data.doc !== undefined}
|
<div class="markdown-body">
|
||||||
<div class="markdown-body" style="--link-color: var(--{color()})">
|
{@html marked.parse(data.doc[$locale.code].content)}
|
||||||
{@html marked.parse(data.doc[$locale].content)}
|
|
||||||
</div>
|
</div>
|
||||||
<div class="docs">
|
|
||||||
{#each data.docs[$locale] as doc}
|
|
||||||
{#if doc.title == data.doc[$locale].title}
|
|
||||||
<a href="/doc/{doc.name}" style="border-color: var(--{color()})">
|
|
||||||
<h1>{doc.title}</h1>
|
|
||||||
<h3>{doc.desc}</h3>
|
|
||||||
</a>
|
|
||||||
{:else}
|
|
||||||
<a href="/doc/{doc.name}" style="border-color: var(--white-3)">
|
|
||||||
<h1>{doc.title}</h1>
|
|
||||||
<h3>{doc.desc}</h3>
|
|
||||||
</a>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</main>
|
</main>
|
||||||
{/if}
|
<Footer />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
@import "/markdown.css";
|
@import "/css/markdown.css";
|
||||||
|
|
||||||
main {
|
main {
|
||||||
padding: 50px;
|
background: var(--black-1);
|
||||||
display: flex;
|
padding: 40px;
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: start;
|
|
||||||
gap: 30px;
|
gap: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
main .docs {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: end;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
main .docs a {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: var(--black-3);
|
|
||||||
text-decoration: none;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-right-style: solid;
|
|
||||||
padding: 15px;
|
|
||||||
width: 100%;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
main .docs a:hover {
|
|
||||||
box-shadow: var(--box-shadow-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
main .docs a h1 {
|
|
||||||
font-size: var(--size-3);
|
|
||||||
color: var(--white-1);
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
main .docs a h3 {
|
|
||||||
font-size: var(--size-2);
|
|
||||||
color: var(--white-3);
|
|
||||||
font-weight: 100;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
main .markdown-body :global(a) {
|
|
||||||
color: var(--link-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media only screen and (max-width: 900px) {
|
|
||||||
main {
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
main .docs {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
main .docs a {
|
|
||||||
border-right-style: none;
|
|
||||||
border-left-style: solid;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,93 +1,43 @@
|
|||||||
<script>
|
<script>
|
||||||
import Header from "$lib/header.svelte";
|
import Navbar from "$components/navbar.svelte";
|
||||||
import Head from "$lib/head.svelte";
|
import Footer from "$components/footer.svelte";
|
||||||
import Icon from "$lib/icon.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 "$lib/locale.js";
|
||||||
import { _ } from "svelte-i18n";
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Head title="donate" desc="give me all of your life savings" />
|
<Head title="donate" desc="give me all of your life savings" />
|
||||||
|
<Navbar />
|
||||||
<Header picture="money" title={$_("donate.title")} />
|
<Header picture="money" title={$_("donate.title")} />
|
||||||
|
|
||||||
<main>
|
<main>
|
||||||
<span> </span>
|
<Card>
|
||||||
<span>
|
<p>{@html $_("donate.desc")}</p>
|
||||||
{$_("donate.info")}
|
|
||||||
{$_("donate.price")}
|
|
||||||
</span>
|
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<p>{@html $_("donate.info")}:</p>
|
||||||
<span>
|
<div>
|
||||||
{$_("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>
|
<code>
|
||||||
46q7G7u7cmASvJm7AmrhmNg6ctS77mYMmDAy1QxpDn5w57xV3GUY5za4ZPZHAjqaXdfS5YRWm4AVj5UArLDA1retRkJp47F
|
46q7G7u7cmASvJm7AmrhmNg6ctS77mYMmDAy1QxpDn5w57xV3GUY5za4ZPZHAjqaXdfS5YRWm4AVj5UArLDA1retRkJp47F
|
||||||
</code>
|
</code>
|
||||||
</td>
|
</div>
|
||||||
</tr>
|
<p>{$_("donate.thanks")}</p>
|
||||||
</tbody>
|
</Card>
|
||||||
</table>
|
|
||||||
<span>
|
|
||||||
{$_("donate.thanks")}
|
|
||||||
</span>
|
|
||||||
</main>
|
</main>
|
||||||
|
<Footer />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main {
|
main {
|
||||||
padding: 50px;
|
background: var(--black-1);
|
||||||
|
flex: 1;
|
||||||
|
padding: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
main span {
|
main div {
|
||||||
font-size: var(--size-4);
|
background: var(--black-2);
|
||||||
color: var(--white-1);
|
border: solid 1px var(--black-3);
|
||||||
}
|
padding: 6px;
|
||||||
|
margin: 15px 0;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
@@ -1,15 +1,9 @@
|
|||||||
import { api_get_services } from "$lib/api.js";
|
import api from "$lib/api.js";
|
||||||
|
|
||||||
export async function load({ fetch }) {
|
export async function load({ fetch }) {
|
||||||
try {
|
let services = await api.services(fetch);
|
||||||
let services = await api_get_services(fetch);
|
|
||||||
return {
|
return {
|
||||||
services: null === services ? [] : services,
|
services: null === services ? [] : services,
|
||||||
error: "",
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
return {
|
|
||||||
error: err.toString(),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@@ -1,92 +1,68 @@
|
|||||||
<script>
|
<script>
|
||||||
import Service from "$lib/service.svelte";
|
import Navbar from "$components/navbar.svelte";
|
||||||
import Header from "$lib/header.svelte";
|
import Footer from "$components/footer.svelte";
|
||||||
import Error from "$lib/error.svelte";
|
import Header from "$components/header.svelte";
|
||||||
import Link from "$lib/link.svelte";
|
import Service from "$components/service.svelte";
|
||||||
import Head from "$lib/head.svelte";
|
import Head from "$components/head.svelte";
|
||||||
|
import Card from "$components/card.svelte";
|
||||||
import { api_urljoin } from "$lib/api.js";
|
|
||||||
import { locale, _ } from "svelte-i18n";
|
|
||||||
|
|
||||||
|
import { locale, _ } from "$lib/locale.js";
|
||||||
|
import api from "$lib/api.js";
|
||||||
let { data } = $props();
|
let { data } = $props();
|
||||||
let services = $state(data.services);
|
|
||||||
|
|
||||||
function change(input) {
|
// filtered list of services that have descriptions for the current locale
|
||||||
let value = input.target.value.toLowerCase();
|
let services = $derived(
|
||||||
services = [];
|
data.services.filter(
|
||||||
|
s =>
|
||||||
if (value === "") {
|
s.desc[$locale.code] !== "" &&
|
||||||
services = data.services;
|
s.desc[$locale.code] !== null &&
|
||||||
return;
|
s.desc[$locale.code] !== undefined
|
||||||
}
|
)
|
||||||
|
);
|
||||||
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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Head title="services" desc="my self-hosted services and projects" />
|
<Head title="services" desc="my self-hosted services and projects" />
|
||||||
|
<Navbar />
|
||||||
<Header picture="cool" title={$_("services.title")} />
|
<Header picture="cool" title={$_("services.title")} />
|
||||||
|
|
||||||
{#if data.error.length !== 0}
|
|
||||||
<Error error={data.error} />
|
|
||||||
{:else}
|
|
||||||
<main>
|
<main>
|
||||||
<div class="title">
|
<Card>
|
||||||
<input oninput={change} type="text" placeholder={$_("services.search")} />
|
<p>{@html $_("services.desc")}</p>
|
||||||
<div>
|
<br />
|
||||||
<Link icon="nf-fa-feed" link={api_urljoin("/news/" + $locale)}>{$_("services.feed")}</Link>
|
<p>
|
||||||
</div>
|
{@html $_("services.feed", {
|
||||||
</div>
|
links: [api.join("/news/" + $locale.code)],
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
<div class="services">
|
<div class="services">
|
||||||
{#if get_services().length == 0}
|
{#if services.length === 0}
|
||||||
<h3 class="none">{$_("services.none")}</h3>
|
<h3>{$_("services.none")}</h3>
|
||||||
{:else}
|
{:else}
|
||||||
{#each get_services() as service}
|
{#each services as service}
|
||||||
<Service {service} />
|
<Service {service} />
|
||||||
{/each}
|
{/each}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
{/if}
|
<Footer />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
main {
|
main {
|
||||||
padding: 50px;
|
background: var(--black-1);
|
||||||
text-align: right;
|
flex: 1;
|
||||||
}
|
padding: 40px;
|
||||||
|
|
||||||
main .title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
main .none {
|
|
||||||
color: var(--white-3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main .services {
|
main .services {
|
||||||
margin-top: 20px;
|
border-top: solid 1px var(--white-3);
|
||||||
|
padding-top: 20px;
|
||||||
|
margin-top: 15px;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
gap: 28px;
|
color: var(--white-3);
|
||||||
}
|
gap: 15px;
|
||||||
|
|
||||||
@media only screen and (max-width: 1200px) {
|
|
||||||
main .services {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -1,21 +0,0 @@
|
|||||||
@keyframes blink {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes cursor {
|
|
||||||
to {
|
|
||||||
border-color: transparent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes typing {
|
|
||||||
from {
|
|
||||||
width: 0%;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
Before Width: | Height: | Size: 8.9 KiB After Width: | Height: | Size: 8.9 KiB |
BIN
app/static/assets/cool.png
Normal file
After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
BIN
app/static/assets/sad.png
Normal file
After Width: | Height: | Size: 32 KiB |
BIN
app/static/assets/tired.png
Normal file
After Width: | Height: | Size: 22 KiB |
126
app/static/css/global.css
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
/* 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: #3768fc;
|
||||||
|
|
||||||
|
/* white tones */
|
||||||
|
--white-1: #ffffff;
|
||||||
|
--white-2: #bfbfbf;
|
||||||
|
--white-3: #5f5f5f;
|
||||||
|
|
||||||
|
/* black tones */
|
||||||
|
--black-1: #000000;
|
||||||
|
--black-2: #111;
|
||||||
|
--black-3: #3a3b3c;
|
||||||
|
|
||||||
|
/* different sizes */
|
||||||
|
--size-1: 8px;
|
||||||
|
--size-2: 16px;
|
||||||
|
--size-3: 18px;
|
||||||
|
--size-4: 20px;
|
||||||
|
--size-5: 24px;
|
||||||
|
--size-6: 30px;
|
||||||
|
|
||||||
|
/* shadows */
|
||||||
|
--box-shadow: rgb(38, 57, 77) 0px 20px 30px -10px;
|
||||||
|
--text-shadow: 0 1px 5px rgba(255, 255, 255, 0.15);
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* typing animation */
|
||||||
|
@keyframes typing {
|
||||||
|
from {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
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 {
|
||||||
|
background: rgba(100, 100, 100, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
border-radius: 5px;
|
||||||
|
width: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--black-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--black-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--white-2);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
text-decoration: underline;
|
||||||
|
text-decoration-color: var(--white-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: var(--white-1);
|
||||||
|
text-shadow: var(--text-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style: inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
br {
|
||||||
|
display: block;
|
||||||
|
margin: 5px 0;
|
||||||
|
}
|
@@ -1,15 +1,21 @@
|
|||||||
|
/* modified version of github-markdown-css, the dark version
|
||||||
|
* licensed under MIT, see: https://github.com/sindresorhus/github-markdown-css
|
||||||
|
*/
|
||||||
|
|
||||||
.markdown-body {
|
.markdown-body {
|
||||||
color-scheme: dark;
|
color-scheme: dark;
|
||||||
-ms-text-size-adjust: 100%;
|
-ms-text-size-adjust: 100%;
|
||||||
-webkit-text-size-adjust: 100%;
|
-webkit-text-size-adjust: 100%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: #c9d1d9;
|
|
||||||
background-color: #000;
|
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;
|
font-size: 16px;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
word-wrap: break-word;
|
word-wrap: break-word;
|
||||||
|
color: var(--white-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body br {
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .octicon {
|
.markdown-body .octicon {
|
||||||
@@ -60,10 +66,6 @@
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body abbr[title] {
|
.markdown-body abbr[title] {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
text-decoration: underline dotted;
|
text-decoration: underline dotted;
|
||||||
@@ -81,9 +83,9 @@
|
|||||||
.markdown-body h1 {
|
.markdown-body h1 {
|
||||||
margin: 0.67em 0;
|
margin: 0.67em 0;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding-bottom: 0.3em;
|
padding-bottom: 5px;
|
||||||
font-size: 2em;
|
font-size: 2em;
|
||||||
border-bottom: 1px solid #21262d;
|
border-bottom: 1px solid var(--black-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body mark {
|
.markdown-body mark {
|
||||||
@@ -286,17 +288,18 @@
|
|||||||
.markdown-body h4,
|
.markdown-body h4,
|
||||||
.markdown-body h5,
|
.markdown-body h5,
|
||||||
.markdown-body h6 {
|
.markdown-body h6 {
|
||||||
margin-top: 24px;
|
color: var(--white-1);
|
||||||
margin-bottom: 16px;
|
margin-top: 30px;
|
||||||
|
margin-bottom: 10px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h2 {
|
.markdown-body h2 {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding-bottom: 0.3em;
|
padding-bottom: 5px;
|
||||||
font-size: 1.5em;
|
font-size: 1.5em;
|
||||||
border-bottom: 1px solid #21262d;
|
border-bottom: 1px solid var(--black-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h3 {
|
.markdown-body h3 {
|
||||||
@@ -358,28 +361,14 @@
|
|||||||
.markdown-body tt,
|
.markdown-body tt,
|
||||||
.markdown-body code,
|
.markdown-body code,
|
||||||
.markdown-body samp {
|
.markdown-body samp {
|
||||||
font-family:
|
font-family: var(--monospace);
|
||||||
ui-monospace,
|
|
||||||
SFMono-Regular,
|
|
||||||
SF Mono,
|
|
||||||
Menlo,
|
|
||||||
Consolas,
|
|
||||||
Liberation Mono,
|
|
||||||
monospace;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body pre {
|
.markdown-body pre {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
font-family:
|
font-family: var(--monospace);
|
||||||
ui-monospace,
|
|
||||||
SFMono-Regular,
|
|
||||||
SF Mono,
|
|
||||||
Menlo,
|
|
||||||
Consolas,
|
|
||||||
Liberation Mono,
|
|
||||||
monospace;
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
word-wrap: normal;
|
word-wrap: normal;
|
||||||
}
|
}
|
||||||
@@ -720,7 +709,7 @@
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
white-space: break-spaces;
|
white-space: break-spaces;
|
||||||
background: var(--black-3);
|
background: var(--black-2);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -761,11 +750,13 @@
|
|||||||
|
|
||||||
.markdown-body .highlight pre,
|
.markdown-body .highlight pre,
|
||||||
.markdown-body pre {
|
.markdown-body pre {
|
||||||
padding: 16px;
|
padding: 10px;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
font-size: 85%;
|
font-size: 85%;
|
||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
background-color: var(--black-3);
|
|
||||||
|
background-color: var(--black-2);
|
||||||
|
border: solid 1px var(--black-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body pre code,
|
.markdown-body pre code,
|
||||||
@@ -979,7 +970,7 @@
|
|||||||
.markdown-body g-emoji {
|
.markdown-body g-emoji {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
min-width: 1ch;
|
min-width: 1ch;
|
||||||
font-family: "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
font-family: var(--emoji);
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
font-style: normal !important;
|
font-style: normal !important;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
@@ -1026,7 +1017,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body .contains-task-list:hover .task-list-item-convert-container,
|
.markdown-body .contains-task-list:hover .task-list-item-convert-container,
|
||||||
.markdown-body .contains-task-list:focus-within .task-list-item-convert-container {
|
.markdown-body
|
||||||
|
.contains-task-list:focus-within
|
||||||
|
.task-list-item-convert-container {
|
||||||
display: block;
|
display: block;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 24px;
|
height: 24px;
|
@@ -1,10 +0,0 @@
|
|||||||
/* im using nerd fonts btw */
|
|
||||||
@import "https://www.nerdfonts.com/assets/css/webfont.css";
|
|
||||||
|
|
||||||
@font-face {
|
|
||||||
font-family: "Ubuntu";
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 300;
|
|
||||||
font-display: swap;
|
|
||||||
src: url("/ubuntu.woff2") format("woff2");
|
|
||||||
}
|
|
@@ -1,107 +0,0 @@
|
|||||||
@import "./animations.css";
|
|
||||||
@import "./font.css";
|
|
||||||
|
|
||||||
:root {
|
|
||||||
--yellow: #d3b910;
|
|
||||||
--cyan: #0dd2e8;
|
|
||||||
--green: #06e00a;
|
|
||||||
--pinkish: #d506e0;
|
|
||||||
--red: #e8180d;
|
|
||||||
--blue: #2036f9;
|
|
||||||
|
|
||||||
--white-1: #ffffff;
|
|
||||||
--white-2: #bfbfbf;
|
|
||||||
--white-3: #5f5f5f;
|
|
||||||
--white-4: #0f0f0f;
|
|
||||||
|
|
||||||
--black-1: #000000;
|
|
||||||
--black-2: #050505;
|
|
||||||
--black-3: #111111;
|
|
||||||
--black-4: #3a3b3c;
|
|
||||||
|
|
||||||
--size-1: 8px;
|
|
||||||
--size-2: 16px;
|
|
||||||
--size-3: 18px;
|
|
||||||
--size-4: 20px;
|
|
||||||
--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;
|
|
||||||
|
|
||||||
--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("/banner.png");
|
|
||||||
--profile-size: 220px;
|
|
||||||
}
|
|
||||||
|
|
||||||
* {
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
html {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
background: var(--black-1);
|
|
||||||
font-family: "Ubuntu", sans-serif;
|
|
||||||
overflow-x: hidden;
|
|
||||||
height: 100vh;
|
|
||||||
width: 100vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
::selection {
|
|
||||||
background: rgba(100, 100, 100, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
border-radius: 5px;
|
|
||||||
width: 1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--black-1);
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--black-2);
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
font-weight: 900;
|
|
||||||
color: var(--white-1);
|
|
||||||
text-decoration: none;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
a:hover {
|
|
||||||
text-decoration: underline;
|
|
||||||
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);
|
|
||||||
}
|
|
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 25 KiB |
3
app/static/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
User-Agent: *
|
||||||
|
Disallow: /doc/
|
||||||
|
Disallow: /api/
|
@@ -1,14 +1,13 @@
|
|||||||
//import adapter from '@sveltejs/adapter-auto';
|
|
||||||
import adapter from "@sveltejs/adapter-node";
|
import adapter from "@sveltejs/adapter-node";
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
kit: {
|
kit: {
|
||||||
adapter: adapter(),
|
adapter: adapter(),
|
||||||
|
alias: { $components: "src/components" },
|
||||||
},
|
},
|
||||||
onwarn: (warning, handler) => {
|
compilerOptions: {
|
||||||
if (warning.code === "a11y-click-events-have-key-events") return;
|
runes: true,
|
||||||
handler(warning);
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -3,21 +3,38 @@ import { defineConfig } from "vite";
|
|||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
|
|
||||||
|
function env_from(prefix, object) {
|
||||||
|
for (const [key, value] of Object.entries(object)) {
|
||||||
|
let type = typeof value;
|
||||||
|
let name = prefix + "_" + key.toUpperCase();
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "object":
|
||||||
|
env_from(name, value);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "string":
|
||||||
|
if (process.env[name] === undefined) process.env[name] = value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const default_env = {
|
const default_env = {
|
||||||
REPORT_URL: "https://github.com/ngn13/website/issues",
|
source_url: "https://git.ngn.tf/ngn/website",
|
||||||
SOURCE_URL: "https://github.com/ngn13/website",
|
report_url: "mailto:ngn@ngn.tf",
|
||||||
APP_URL: "http://localhost:7001",
|
doc_url: "http://localhost:7003",
|
||||||
API_URL: "http://localhost:7002",
|
api: {
|
||||||
DOC_URL: "http://localhost:7003",
|
url: "http://localhost:7002",
|
||||||
|
path: "http://localhost:7002",
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const file = fileURLToPath(new URL("package.json", import.meta.url));
|
const package_file = fileURLToPath(new URL("package.json", import.meta.url));
|
||||||
const json = readFileSync(file, "utf8");
|
const package_json = readFileSync(package_file, "utf8");
|
||||||
const pkg = JSON.parse(json);
|
const package_data = JSON.parse(package_json);
|
||||||
|
|
||||||
for (let env in default_env) {
|
env_from("WEBSITE", default_env);
|
||||||
if (process.env["WEBSITE_" + env] === undefined) process.env["WEBSITE_" + env] = default_env[env];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [sveltekit()],
|
plugins: [sveltekit()],
|
||||||
@@ -31,6 +48,6 @@ export default defineConfig({
|
|||||||
strictPort: true,
|
strictPort: true,
|
||||||
},
|
},
|
||||||
define: {
|
define: {
|
||||||
pkg: pkg,
|
pkg: package_data,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@@ -7,19 +7,20 @@ services:
|
|||||||
args:
|
args:
|
||||||
WEBSITE_SOURCE_URL: "http://github.com/ngn13/website"
|
WEBSITE_SOURCE_URL: "http://github.com/ngn13/website"
|
||||||
WEBSITE_REPORT_URL: "http://github.com/ngn13/website/issues"
|
WEBSITE_REPORT_URL: "http://github.com/ngn13/website/issues"
|
||||||
WEBSITE_APP_URL: "http://localhost:7001"
|
|
||||||
WEBSITE_API_URL: "http://localhost:7002"
|
|
||||||
WEBSITE_DOC_URL: "http://doc:7003"
|
WEBSITE_DOC_URL: "http://doc:7003"
|
||||||
|
WEBSITE_API_URL: "http://api:7002"
|
||||||
|
WEBSITE_API_PATH: "http://localhost:7002"
|
||||||
security_opt:
|
security_opt:
|
||||||
- "no-new-privileges:true"
|
- "no-new-privileges:true"
|
||||||
cap_drop:
|
cap_drop:
|
||||||
- ALL
|
- ALL
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:7001:7001"
|
- "127.0.0.1:7001:7001"
|
||||||
restart: unless-stopped
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- api
|
- api
|
||||||
- doc
|
- doc
|
||||||
|
read_only: true
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
api:
|
api:
|
||||||
container_name: "website_api"
|
container_name: "website_api"
|
||||||
@@ -34,28 +35,28 @@ services:
|
|||||||
- "127.0.0.1:7002:7002"
|
- "127.0.0.1:7002:7002"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data.db:/api/data.db:rw
|
- ./data.db:/api/data.db:rw
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
environment:
|
||||||
WEBSITE_DEBUG: "false"
|
WEBSITE_DEBUG: "false"
|
||||||
WEBSITE_APP_URL: "http://localhost:7001/"
|
WEBSITE_APP_URL: "http://localhost:7001"
|
||||||
WEBSITE_PASSWORD: "change_me"
|
WEBSITE_PASSWORD: "change_me"
|
||||||
WEBSITE_HOST: "0.0.0.0:7002"
|
WEBSITE_HOST: "0.0.0.0:7002"
|
||||||
WEBSITE_IP_HEADER: "X-Real-IP"
|
WEBSITE_IP_HEADER: "X-Real-IP"
|
||||||
WEBSITE_INTERVAL: "1h"
|
WEBSITE_INTERVAL: "1h"
|
||||||
WEBSITE_TIMEOUT: "15s"
|
WEBSITE_TIMEOUT: "15s"
|
||||||
WEBSITE_LIMIT: "5s"
|
WEBSITE_LIMIT: "5s"
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
doc:
|
doc:
|
||||||
container_name: "website_doc"
|
container_name: "website_doc"
|
||||||
image: website_doc
|
image: website_doc
|
||||||
read_only: true
|
|
||||||
build:
|
build:
|
||||||
context: ./doc
|
context: ./doc
|
||||||
security_opt:
|
security_opt:
|
||||||
- "no-new-privileges:true"
|
- "no-new-privileges:true"
|
||||||
cap_drop:
|
cap_drop:
|
||||||
- ALL
|
- ALL
|
||||||
restart: unless-stopped
|
|
||||||
environment:
|
environment:
|
||||||
WEBSITE_HOST: "0.0.0.0:7003"
|
WEBSITE_HOST: "0.0.0.0:7003"
|
||||||
WEBSITE_DOCS_DIR: "./docs"
|
WEBSITE_DOCS_DIR: "./docs"
|
||||||
|
read_only: true
|
||||||
|
restart: unless-stopped
|
||||||
|
@@ -81,7 +81,7 @@ BreakBeforeTernaryOperators: true
|
|||||||
BreakConstructorInitializers: BeforeColon
|
BreakConstructorInitializers: BeforeColon
|
||||||
BreakInheritanceList: BeforeColon
|
BreakInheritanceList: BeforeColon
|
||||||
BreakStringLiterals: true
|
BreakStringLiterals: true
|
||||||
ColumnLimit: 120
|
ColumnLimit: 80
|
||||||
CommentPragmas: '^ IWYU pragma:'
|
CommentPragmas: '^ IWYU pragma:'
|
||||||
CompactNamespaces: false
|
CompactNamespaces: false
|
||||||
ConstructorInitializerIndentWidth: 4
|
ConstructorInitializerIndentWidth: 4
|
||||||
|
35
doc/.clang-tidy
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
# "gnu-zero-variadic-macro-arguments" ignored because we are using GNU99
|
||||||
|
# standart
|
||||||
|
|
||||||
|
# "clang-diagnostic-language-extension-token" is ignored because we need the
|
||||||
|
# asm() extension token
|
||||||
|
|
||||||
|
# "DeprecatedOrUnsafeBufferHandling" ignored because C11 "_s" functions are not
|
||||||
|
# secure either
|
||||||
|
Checks: >-
|
||||||
|
clang-diagnostic-*,
|
||||||
|
-clang-diagnostic-gnu-zero-variadic-macro-arguments,
|
||||||
|
-clang-diagnostic-language-extension-token,
|
||||||
|
clang-analyzer-*,
|
||||||
|
-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling,
|
||||||
|
portability-*,
|
||||||
|
performance-*,
|
||||||
|
WarningsAsErrors: '*'
|
||||||
|
HeaderFileExtensions:
|
||||||
|
- ''
|
||||||
|
- h
|
||||||
|
- hh
|
||||||
|
- hpp
|
||||||
|
- hxx
|
||||||
|
ImplementationFileExtensions:
|
||||||
|
- c
|
||||||
|
- cc
|
||||||
|
- cpp
|
||||||
|
- cxx
|
||||||
|
HeaderFilterRegex: '.*'
|
||||||
|
ExcludeHeaderFilterRegex: ''
|
||||||
|
FormatStyle: file
|
||||||
|
SystemHeaders: false
|
||||||
|
...
|
||||||
|
|
@@ -1,9 +1,9 @@
|
|||||||
FROM ghcr.io/ngn13/ctorm:1.7
|
FROM ghcr.io/ngn13/ctorm:1.8.1
|
||||||
|
|
||||||
WORKDIR /doc
|
WORKDIR /doc
|
||||||
|
|
||||||
COPY Makefile ./
|
COPY Makefile ./
|
||||||
COPY docs ./docs
|
COPY pages ./pages
|
||||||
COPY inc ./inc
|
COPY inc ./inc
|
||||||
COPY src ./src
|
COPY src ./src
|
||||||
|
|
||||||
|
@@ -26,10 +26,14 @@ $(DISTDIR)/%.o: src/%.c
|
|||||||
format:
|
format:
|
||||||
clang-format -i -style=file $(CSRCS) $(HSRCS)
|
clang-format -i -style=file $(CSRCS) $(HSRCS)
|
||||||
|
|
||||||
|
lint:
|
||||||
|
clang-tidy --warnings-as-errors --config= $(CSRCS) $(HSRCS)
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
|
rm -f *.elf
|
||||||
rm -rf $(DISTDIR)
|
rm -rf $(DISTDIR)
|
||||||
|
|
||||||
run:
|
run:
|
||||||
./doc.elf
|
./doc.elf
|
||||||
|
|
||||||
.PHONY: format clean run
|
.PHONY: format lint clean run
|
||||||
|
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Privacy",
|
|
||||||
"desc": "Learn how I respect your privacy"
|
|
||||||
}
|
|
@@ -1,41 +0,0 @@
|
|||||||
As a privacy advocate myself I do my best to respect your privacy while simultaneously keeping my server safe. I also think it's
|
|
||||||
important to be transparent about this kind of stuff so I wrote this document to tell you how exactly I process or store your
|
|
||||||
information.
|
|
||||||
|
|
||||||
## DNS & SSL
|
|
||||||
Currently I use cloudflare's name servers, however cloudflare doesn't own my domain (I didn't purchase the domain from cloudflare)
|
|
||||||
nor it proxies any of the traffic. All of my records use the *DNS only*" mode, meaning they are just DNS records and they point
|
|
||||||
to my server, not cloudflare. This also means cloudflare doesn't control any of the SSL certificates. All the certificates are
|
|
||||||
stored on my server and they are created with Let's Encrypt. So I own the certificates and it's not possible for cloudlfare to
|
|
||||||
suddenly switch DNS records (it would break SSL).
|
|
||||||
|
|
||||||
## Usage metrics
|
|
||||||
I don't have any kind of request or traffic monitoring on my server. So no, your HTTP(S) requests or other network
|
|
||||||
connections are not processed to generate colorful graphs, pie charts and stuff.
|
|
||||||
|
|
||||||
You may have realized that in the bottom of this page there is total visitor number tracker. It is the only
|
|
||||||
usage/visitor metric tracking I have, which is implemented by website's API, which is free (as in freedom) so you can
|
|
||||||
go audit it's code yourself.
|
|
||||||
|
|
||||||
I want to make it clear that this metric tracker does not store any information about you or your HTTP(S) requests to
|
|
||||||
a database. It temporarily stores your IP address' SHA1 hash, in memory, so it doesn't recount the same visitor again
|
|
||||||
when they refresh the website or visit it multiple times in a short period of time. After a certain amount of requests,
|
|
||||||
your IP address' SHA1 hash will be removed from the memory and it will be replaced with a new visitor's SHA1 hash instead.
|
|
||||||
|
|
||||||
## Logs
|
|
||||||
All the HTTP(S) services are proxied with nginx, and nginx logs all of them to a file on the disk. This file (`access.log`)
|
|
||||||
is only readable by the root user, and it is contents are deleted every 4 hours (with `shred` to make sure it doesn't leave
|
|
||||||
anything on the disk). The logs include *only* the following information:
|
|
||||||
|
|
||||||
- Request time
|
|
||||||
- Requested host
|
|
||||||
- Requested path
|
|
||||||
- HTTP request method
|
|
||||||
- HTTP response code
|
|
||||||
|
|
||||||
This is the minimal information I need to trace any issues if something goes wrong, which is the main reason why I use
|
|
||||||
logging at all, to make it easier to find any issues.
|
|
||||||
|
|
||||||
## Data removal
|
|
||||||
If you want to remove any of your data from my server, [you can send me an email](mailto:ngn@ngn.tf). And yes this includes
|
|
||||||
removing usage metrics and logs.
|
|
@@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Gizlilik",
|
|
||||||
"desc": "Gizliliğinize nasıl önem verdiğimi öğrenin"
|
|
||||||
}
|
|
@@ -1,40 +0,0 @@
|
|||||||
Kişisel olarak ben de bir gizlik savunucusu olduğumdan, bir yandan sunucumu güvende tutarken bir yandan da gizliliğinize önem
|
|
||||||
göstermek için elimden geleni yapıyorum. Aynı zamanda bu tarz şeyler hakkında şeffaf ve açık olmanın önemli olduğunu düşünüyorum,
|
|
||||||
o yüzden verilerinizi nasıl işlediğimi ya da depoladığımı anlamanız için bu dökümanı yazmaya karar verdim.
|
|
||||||
|
|
||||||
## DNS & SSL
|
|
||||||
Şuan cloudflare'in isim sunucularını kullanıyorum, ancak cloudflare alan adıma sahip değil (alan adımı cloudflare'den almadım)
|
|
||||||
ve aynı şekilde herhangi bir trafiğe vekillik etmiyor. Tüm DNS kayıtlarım *Sadece DNS* modunu kullanıyor, yani sadece
|
|
||||||
DNS kayıtlarından ibaretler ve benim sunucuma işaret ediyorlar, cloudflare'e değil. Bu aynı zamanda cloudflare SSL sertifikalarımı
|
|
||||||
kontrol etmiyor demek. Tüm sertifikalar benim sunucumda tutuluyor ve Let's Encrypt ile oluşturuldular. Yani sertifikalar bana ait
|
|
||||||
ve cloudflare'in aniden DNS kayıtlarını değiştirmesi mümkün değil (bu SSL'in bozulmasına sebep olur).
|
|
||||||
|
|
||||||
## Kullanım metrikleri
|
|
||||||
Sunucumda herhangi bir istek ya da trafik monitörlermesi yok. Yani hayır, HTTP(S) istekleriniz ya da diğer ağ
|
|
||||||
bağlantılarınız renki grafikler, pasta grafikleri gibi şeyler üretmek için işlenmiyor.
|
|
||||||
|
|
||||||
Bu sayfanın altında bir ziyaretçi sayısı takipçisi olduğunu farketmiş olabilirsiniz. Bu kullandığım tek kullanım/ziyaretçi
|
|
||||||
metrik takibi ve websitemin, özgür olan, bu yüzden kodunu kendiniz denetleyebileceğiniz API'ı ile implemente edildi.
|
|
||||||
|
|
||||||
Bu metrik takipçisinin, HTTP(S) istekleriniz hakkında herhangi bir veriyi bir veri tabanına kaydetmediğini belirtmek isterim.
|
|
||||||
Bu takipçi geçici olarak IP adresinizin SHA1 hash'ini bellekte tutuyor, bunun amacı aynı ziyaretçiyi sayfayı yenilediği zaman
|
|
||||||
ya da kısa bir süre için websitesini birden fazla kez ziyaret ettiği zaman tekrar saymayı önlemek. Belirli bir miktar istekten
|
|
||||||
sonra, IP adresinizin SHA1 hash'i bellekten kaldırılacaktır ve yeni bir ziyaretçinin SHA1'i onun yerine geçicektir.
|
|
||||||
|
|
||||||
## Kayıtlar
|
|
||||||
Tüm HTTP(S) servisleri nginx ile vekilleniyor, ve nginx hepsini disk üzerindeki bir dosyaya kaydediyor. Bu dosya (`access.log`)
|
|
||||||
sadece root kullanıcısı tarafından okunabilir, ve içerği her 4 saatde bir siliniyor (diskde veri kalmadığından emin olmak için
|
|
||||||
shred komutu ile). Kayıtlar *sadece* aşağıdaki bilgileri içeriyor:
|
|
||||||
|
|
||||||
- İstek zamanı
|
|
||||||
- İstenilen host
|
|
||||||
- İstenilen yol
|
|
||||||
- HTTP istek yöntemi
|
|
||||||
- HTTP cevap kodu
|
|
||||||
|
|
||||||
Bu birşeyler yanlış giderse sorunları bulmak için ihtiyacım olan en az bilgi, kayıt tutmamın ana sebeplerinden bir tanesi
|
|
||||||
zaten bu, sorunları bulmayı kolaylaştırmak.
|
|
||||||
|
|
||||||
## Veri silimi
|
|
||||||
Sunucumdan herhangi bir verinizi kaldırmak isterseniz, [bana bir email gönderebilirsiniz](mailto:ngn@ngn.tf). Ve evet buna
|
|
||||||
kullanım metrikleri ve kayıtlar dahil.
|
|
@@ -3,11 +3,11 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
|
|
||||||
#include "util.h"
|
#include "file.h"
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
DIR *dir;
|
DIR *dir;
|
||||||
util_file_t *file;
|
file_t *file;
|
||||||
char name[NAME_MAX + 1];
|
char name[NAME_MAX + 1];
|
||||||
char *lang;
|
char *lang;
|
||||||
} docs_t;
|
} docs_t;
|
||||||
|
10
doc/inc/file.h
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char *content;
|
||||||
|
int64_t size;
|
||||||
|
} file_t;
|
||||||
|
|
||||||
|
file_t *file_load(int dirfd, char *path);
|
||||||
|
void file_free(file_t *file);
|
@@ -4,16 +4,8 @@
|
|||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
char *content;
|
|
||||||
uint64_t size;
|
|
||||||
} util_file_t;
|
|
||||||
|
|
||||||
#define util_toupper(str) \
|
#define util_toupper(str) \
|
||||||
for (char *c = str; *c != 0; c++) \
|
for (char *c = str; *c != 0; c++) \
|
||||||
*c = toupper(*c)
|
*c = toupper(*c)
|
||||||
uint64_t util_endswith(char *str, char *suf);
|
uint64_t util_endswith(char *str, char *suf);
|
||||||
void util_send(ctorm_res_t *res, uint16_t code, cJSON *json);
|
void util_send(ctorm_res_t *res, uint16_t code, cJSON *json);
|
||||||
util_file_t *util_file_load(int dirfd, char *path);
|
|
||||||
void util_file_free(util_file_t *file);
|
|
||||||
bool util_parse_doc_name(char *name, char **lang, const char *ext);
|
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"title": "API documentation",
|
"title": "API",
|
||||||
"desc": "Website's API documentation"
|
"desc": "Website's API documentation"
|
||||||
}
|
}
|
@@ -1,51 +1,64 @@
|
|||||||
My website's API stores information about my self-hosted services, it also allows me to
|
My website's API, stores information about my self-hosted services, it also
|
||||||
publish news and updates about these services using an Atom feed and it keeps track of
|
allows me to publish news and updates about these services using an Atom feed
|
||||||
visitor metrics. The API itself is written in Go and uses SQLite for storage.
|
and it keeps track of visitor metrics.
|
||||||
|
|
||||||
This documentation contains information about all the available API endpoints.
|
This documentation contains information about all the available API endpoints.
|
||||||
|
All the endpoints can be accessed using the `/api` route.
|
||||||
|
|
||||||
## Version 1 Endpoints
|
## Version 1 Endpoints
|
||||||
|
|
||||||
Each version 1 endpoint, can be accessed using the `/v1` route.
|
Each version 1 endpoint, can be accessed using the `/v1` route.
|
||||||
|
|
||||||
All the endpoints return JSON formatted data.
|
All the endpoints return JSON formatted data.
|
||||||
|
|
||||||
### Errors
|
### Errors
|
||||||
If any error occurs, you will get a non-200 response. And the JSON data will have an
|
|
||||||
`error` key, which will contain information about the error that occured, in the
|
If any error occurs, you will get a non-200 response. And the JSON data will
|
||||||
string format. This is the only JSON key that will be set in non-200 responses.
|
have an `error` key, which will contain information about the error that
|
||||||
|
occured, in the string format. This is the only JSON key that will be set in
|
||||||
|
non-200 responses.
|
||||||
|
|
||||||
### Results
|
### Results
|
||||||
If no error occurs, `error` key will be set to an emtpy string (""). If the endpoint
|
|
||||||
returns any data, this will be stored using the `result` key. The `result` have a
|
If no error occurs, `error` key will be set to an emtpy string (""). If the
|
||||||
different expected type and a format for each endpoint.
|
endpoint returns any data, this will be stored using the `result` key. The
|
||||||
|
`result` have a different expected type and a format for each endpoint.
|
||||||
|
|
||||||
### Multilang
|
### Multilang
|
||||||
Some `result` formats may use a structure called "Multilang". This is a simple JSON
|
|
||||||
structure that includes one key for each supported language. The key is named after
|
Some `result` formats may use a structure called "Multilang". This is a simple
|
||||||
the language it represents. Currently only supported languages are:
|
JSON structure that includes one key for each supported language. The key is
|
||||||
|
named after the language it represents. Currently only supported languages are:
|
||||||
|
|
||||||
- English (`en`)
|
- English (`en`)
|
||||||
- Turkish (`tr`)
|
- Turkish (`tr`)
|
||||||
|
|
||||||
So each multilang structure, will have **at least** one of these keys.
|
So each multilang structure, will have **at least** one of these keys.
|
||||||
|
|
||||||
Here is an example multilang structure:
|
Here is an example multilang structure:
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"en": "Hello, world!",
|
"en": "Hello, world!",
|
||||||
"tr": "Merhaba, dünya!"
|
"tr": "Merhaba, dünya!"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
If a `result` field is using a multilang structure, it will be specified as "Multilang"
|
|
||||||
in the rest of the documentation.
|
If a `result` field is using a multilang structure, it will be specified as
|
||||||
|
"Multilang" in the rest of the documentation.
|
||||||
|
|
||||||
### Administrator routes
|
### Administrator routes
|
||||||
The endpoints under the `/v1/admin` route, are administrator-only routes. To access
|
|
||||||
these routes you'll need to specfiy a password using the `Authorization` header. If
|
The endpoints under the `/v1/admin` route, are administrator-only routes. To
|
||||||
the password you specify, matches with the password specified using the `API_PASSWORD`
|
access these routes you'll need to specfiy a password using the `Authorization`
|
||||||
environment variable, you will be able to access the route.
|
header. If the password you specify, matches with the password specified using
|
||||||
|
the `API_PASSWORD` environment variable, you will be able to access the route.
|
||||||
|
|
||||||
### GET /v1/services
|
### GET /v1/services
|
||||||
Returns a list of available services. Each service has the following JSON format:
|
|
||||||
|
Returns a list of available services. Each service has the following JSON
|
||||||
|
format:
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"name": "Test Service",
|
"name": "Test Service",
|
||||||
@@ -61,73 +74,91 @@ Returns a list of available services. Each service has the following JSON format
|
|||||||
"i2p": ""
|
"i2p": ""
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Where:
|
Where:
|
||||||
|
|
||||||
- `name`: Service name (string)
|
- `name`: Service name (string)
|
||||||
- `desc`: Service description (Multilang)
|
- `desc`: Service description (Multilang)
|
||||||
- `check_time`: Last time status check time for the service, set 0 if status checking is
|
- `check_time`: Last time status check time for the service, set 0 if status
|
||||||
not supported for this service/status checking is disabled (integer, UNIX timestamp)
|
checking is not supported for this service/status checking is disabled
|
||||||
|
(integer, UNIX timestamp)
|
||||||
- `check_res`: Last service status check result (integer)
|
- `check_res`: Last service status check result (integer)
|
||||||
* 0 if the service is down
|
- 0 if the service is down
|
||||||
* 1 if the service is up
|
- 1 if the service is up
|
||||||
* 2 if the service is up, but slow
|
- 2 if the service is up, but slow
|
||||||
* 3 if the service doesn't support status checking/status checking is disabled
|
- 3 if the service doesn't support status checking/status checking is disabled
|
||||||
- `check_url`: URL used for service's status check (string, empty if none)
|
- `check_url`: URL used for service's status check (string, empty if none)
|
||||||
- `clear`: Clearnet URL for the service (string, empty string if none)
|
- `clear`: Clearnet URL for the service (string, empty string if none)
|
||||||
- `onion`: Onion (TOR) URL for the service (string, empty string if none)
|
- `onion`: Onion (TOR) URL for the service (string, empty string if none)
|
||||||
- `i2p`: I2P URL for the service (string, empty string if none)
|
- `i2p`: I2P URL for the service (string, empty string if none)
|
||||||
|
|
||||||
You can also get information about a specific service by specifying it's name using
|
You can also get information about a specific service by specifying it's name
|
||||||
a URL query named `name`.
|
using a URL query named `name`.
|
||||||
|
|
||||||
### GET /v1/news/:language
|
### GET /v1/news/:language
|
||||||
Returns a Atom feed of news for the given language. Supports languages that are supported
|
|
||||||
by Multilang.
|
Returns a Atom feed of news for the given language. Supports languages that are
|
||||||
|
supported by Multilang.
|
||||||
|
|
||||||
### GET /v1/metrics
|
### GET /v1/metrics
|
||||||
|
|
||||||
Returns metrics about the API usage. The metric data has the following format:
|
Returns metrics about the API usage. The metric data has the following format:
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"since": 1736294400,
|
"since": 1736294400,
|
||||||
"total": 8
|
"total": 8
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Where:
|
Where:
|
||||||
|
|
||||||
- `since`: Metric collection start date (integer, UNIX timestamp)
|
- `since`: Metric collection start date (integer, UNIX timestamp)
|
||||||
- `total`: Total number of visitors (integer)
|
- `total`: Total number of visitors (integer)
|
||||||
|
|
||||||
### GET /v1/admin/logs
|
### GET /v1/admin/logs
|
||||||
|
|
||||||
Returns a list of administrator logs. Each log has the following JSON format:
|
Returns a list of administrator logs. Each log has the following JSON format:
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"action": "Added service \"Test Service\"",
|
"action": "Added service \"Test Service\"",
|
||||||
"time": 1735861794
|
"time": 1735861794
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Where:
|
Where:
|
||||||
|
|
||||||
- `action`: Action that the administrator performed (string)
|
- `action`: Action that the administrator performed (string)
|
||||||
- `time`: Time when the administrator action was performed (integer, UNIX timestamp)
|
- `time`: Time when the administrator action was performed (integer, UNIX
|
||||||
|
timestamp)
|
||||||
|
|
||||||
### PUT /v1/admin/service/add
|
### PUT /v1/admin/service/add
|
||||||
Creates a new service. The request body needs to contain JSON data, and it needs to
|
|
||||||
have the JSON format used to represent a service. See `/v1/services/all` route to
|
Creates a new service. The request body needs to contain JSON data, and it needs
|
||||||
see this format.
|
to have the JSON format used to represent a service. See `/v1/services/all`
|
||||||
|
route to see this format.
|
||||||
|
|
||||||
Returns no data on success.
|
Returns no data on success.
|
||||||
|
|
||||||
### DELETE /v1/admin/service/del
|
### DELETE /v1/admin/service/del
|
||||||
Deletes a service. The client needs to specify the name of the service to delete, by
|
|
||||||
setting the URL query `name`.
|
Deletes a service. The client needs to specify the name of the service to
|
||||||
|
delete, by setting the URL query `name`.
|
||||||
|
|
||||||
Returns no data on success.
|
Returns no data on success.
|
||||||
|
|
||||||
### GET /v1/admin/service/check
|
### GET /v1/admin/service/check
|
||||||
|
|
||||||
Forces a status check for all the services.
|
Forces a status check for all the services.
|
||||||
|
|
||||||
Returns no data on success.
|
Returns no data on success.
|
||||||
|
|
||||||
### PUT /v1/admin/news/add
|
### PUT /v1/admin/news/add
|
||||||
|
|
||||||
Creates a news post. The request body needs to contain JSON data, and it needs
|
Creates a news post. The request body needs to contain JSON data, and it needs
|
||||||
to use the following JSON format:
|
to use the following JSON format:
|
||||||
|
|
||||||
```
|
```
|
||||||
{
|
{
|
||||||
"id": "test_news",
|
"id": "test_news",
|
||||||
@@ -142,7 +173,9 @@ to use the following JSON format:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Where:
|
Where:
|
||||||
|
|
||||||
- `id`: Unique ID for the news post (string)
|
- `id`: Unique ID for the news post (string)
|
||||||
- `title`: Title for the news post (Multilang)
|
- `title`: Title for the news post (Multilang)
|
||||||
- `author`: Author of the news post (string)
|
- `author`: Author of the news post (string)
|
||||||
@@ -151,7 +184,8 @@ Where:
|
|||||||
Returns no data on success.
|
Returns no data on success.
|
||||||
|
|
||||||
### DELETE /v1/admin/news/del
|
### DELETE /v1/admin/news/del
|
||||||
Deletes a news post. The client needs to specify the ID of the news post to delete,
|
|
||||||
by setting the URL query `id`.
|
Deletes a news post. The client needs to specify the ID of the news post to
|
||||||
|
delete, by setting the URL query `id`.
|
||||||
|
|
||||||
Returns no data on success.
|
Returns no data on success.
|
@@ -1,4 +1,4 @@
|
|||||||
{
|
{
|
||||||
"title": "API dökümantasyonu",
|
"title": "API",
|
||||||
"desc": "Websitesinin API dökümantasyonu"
|
"desc": "Websitesinin API dökümantasyonu"
|
||||||
}
|
}
|