1 Commits

Author SHA1 Message Date
b5bae3e3e8 Update dependency @sveltejs/vite-plugin-svelte to v5
Some checks failed
renovate/artifacts Artifact file update failure
2025-01-20 07:02:31 +00:00
62 changed files with 537 additions and 410 deletions

View File

@ -3,7 +3,6 @@ name: Build the docker image for the API
on:
push:
branches: ["main"]
paths: ["api/**"]
env:
REGISTRY: git.ngn.tf

View File

@ -3,7 +3,6 @@ name: Build the docker image for the frontend application
on:
push:
branches: ["main"]
paths: ["app/**"]
env:
REGISTRY: git.ngn.tf
@ -28,8 +27,8 @@ jobs:
cd app
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_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_API_URL=http://api:7002 \
--build-arg WEBSITE_API_PATH=/api \
--tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest .
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest

View File

@ -3,7 +3,6 @@ name: Build the docker image for the doc server
on:
push:
branches: ["main"]
paths: ["doc/**"]
env:
REGISTRY: git.ngn.tf

21
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,21 @@
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"

View File

@ -1,8 +1,7 @@
# website | my personal website
![](https://git.ngn.tf/ngn/website/actions/workflows/build-app.yml/badge.svg)
![](https://git.ngn.tf/ngn/website/actions/workflows/build-api.yml/badge.svg)
![](https://git.ngn.tf/ngn/website/actions/workflows/build-doc.yml/badge.svg)
![Version](https://img.shields.io/github/package-json/v/ngn13/ngn13.fun?filename=app%2Fpackage.json&color=blue)
![Website Status](https://img.shields.io/website?up_color=green&down_color=red&url=https%3A%2F%2Fngn.tf&label=Website%20Status)
![API Status](https://img.shields.io/website?up_color=green&down_color=red&url=https%3A%2F%2Fapi.ngn.tf&label=API%20Status)
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))
@ -69,7 +68,6 @@ on my local git server until I deleted it when I was done with 6.0. This website
- **v4.0 (2023 november - 2024 october)**: In this version the frontend was 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
point I was thinking about doing a massive rework (which I did with 6.0), however I was working on some other shit at
@ -83,6 +81,6 @@ and Turkish, which was something I wanted to do for the longest time ever.
Damn it has been 4 years since I wrote that shit HTML page huh? Time flies...
## Screenshots (from v6.0)
![](assets/6.0_index.png)
![](assets/6.0_services.png)
## Screenshots (from v4.0)
![](assets/4.0_index.png)
![](assets/4.0_blog.png)

View File

@ -218,7 +218,6 @@ class AdminScript:
"logs": self.get_logs,
}
self.api_url_env = "API_URL"
self.password_env = "API_PASSWORD"
def __format_time(self, ts: int) -> str:
return datetime.fromtimestamp(ts, UTC).strftime("%H:%M:%S %d/%m/%Y")
@ -265,10 +264,7 @@ class AdminScript:
return False
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)
if len(argv) == 2:
@ -405,4 +401,4 @@ class AdminScript:
if __name__ == "__main__":
script = AdminScript()
exit(0 if script.run() else 1)
exit(script.run() if 1 else 0)

View File

@ -1,4 +1,4 @@
FROM golang:1.24.2
FROM golang:1.23.4
WORKDIR /api

View File

@ -3,47 +3,107 @@ package config
import (
"fmt"
"net/url"
"github.com/ngn13/ortam"
"os"
)
type Type struct {
Debug bool // should display debug messgaes?
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"
Options []Option
Count int
}
func Load() (*Type, error) {
var conf = Type{
Debug: false,
Password: "",
Host: "0.0.0.0:7002",
IPHeader: "X-Real-IP",
Interval: "1h",
Timeout: "15s",
Limit: "5s",
func (c *Type) Find(name string, typ uint8) (*Option, error) {
for i := 0; i < c.Count; i++ {
if c.Options[i].Name != name {
continue
}
if c.Options[i].Type != typ {
return nil, fmt.Errorf("bad option type")
}
return &c.Options[i], nil
}
if err := ortam.Load(&conf, "WEBSITE"); err != nil {
return nil, err
}
if conf.AppUrl == nil {
conf.AppUrl, _ = url.Parse("http://localhost:7001/")
}
if conf.Password == "" {
return nil, fmt.Errorf("password is not specified")
}
if conf.Host == "" {
return nil, fmt.Errorf("host address is not specified")
}
return &conf, nil
return nil, fmt.Errorf("option not found")
}
func (c *Type) Load() (err error) {
var (
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 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
}

49
api/config/option.go Normal file
View File

@ -0,0 +1,49 @@
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
}

View File

@ -1,23 +1,22 @@
module github.com/ngn13/website/api
go 1.24.0
go 1.21.3
require (
github.com/gofiber/fiber/v2 v2.52.6
github.com/mattn/go-sqlite3 v1.14.27
github.com/gofiber/fiber/v2 v2.52.5
github.com/mattn/go-sqlite3 v1.14.24
)
require (
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.17.9 // indirect
github.com/andybalholm/brotli v1.0.5 // indirect
github.com/google/uuid v1.5.0 // indirect
github.com/klauspost/compress v1.17.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/ngn13/ortam v0.0.0-20250412195317-e76e62a7a305 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/sys v0.15.0 // indirect
)

View File

@ -1,22 +1,20 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/gofiber/fiber/v2 v2.52.5 h1:tWoP1MJQjGEe4GB5TUGOi7P2E0ZMMRx5ZTG4rT+yGMo=
github.com/gofiber/fiber/v2 v2.52.5/go.mod h1:KEOE+cXMhXG0zHc9d8+E38hoX+ZN7bhOtgeF2oT6jrQ=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
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-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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/ngn13/ortam v0.0.0-20250412195317-e76e62a7a305 h1:1YxtSMwR14PklXNlZxIqcmfpiq2+G98YNmhSuz7GKCQ=
github.com/ngn13/ortam v0.0.0-20250412195317-e76e62a7a305/go.mod h1:MSJZ4ZstrLvVEvivbp9hhup+iL8rvtpgKcYaF3DSOKk=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
@ -27,5 +25,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=
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.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=

View File

@ -36,18 +36,18 @@ func main() {
app *fiber.App
stat status.Type
conf *config.Type
conf config.Type
db database.Type
err error
)
if conf, err = config.Load(); err != nil {
if err = conf.Load(); err != nil {
util.Fail("failed to load the configuration: %s", err.Error())
return
}
if !conf.Debug {
if !conf.GetBool("debug") {
util.Debg = func(m string, v ...any) {}
}
@ -56,7 +56,7 @@ func main() {
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())
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.Locals("status", &stat)
c.Locals("config", conf)
c.Locals("config", &conf)
c.Locals("database", &db)
return c.Next()
@ -121,9 +121,9 @@ func main() {
}
// start the app
util.Info("starting web server on %s", conf.Host)
util.Info("starting web server on %s", conf.GetStr("host"))
if err = app.Listen(conf.Host); err != nil {
if err = app.Listen(conf.GetStr("host")); err != nil {
util.Fail("failed to start the web server: %s", err.Error())
}

View File

@ -21,7 +21,7 @@ func admin_log(c *fiber.Ctx, m string) error {
func AuthMiddleware(c *fiber.Ctx) error {
conf := c.Locals("config").(*config.Type)
if c.Get("Authorization") != conf.Password {
if c.Get("Authorization") != conf.GetStr("password") {
return util.ErrAuth(c)
}

View File

@ -7,7 +7,8 @@ import (
func GET_Index(c *fiber.Ctx) error {
conf := c.Locals("config").(*config.Type)
app := conf.GetURL("app_url")
// redirect to the API documentation
return c.Redirect(conf.AppUrl.JoinPath("/doc/api").String())
return c.Redirect(app.JoinPath("/doc/api").String())
}

View File

@ -40,6 +40,7 @@ func GET_News(c *fiber.Ctx) error {
db := c.Locals("database").(*database.Type)
conf := c.Locals("config").(*config.Type)
app := conf.GetURL("app_url")
lang := c.Params("lang")
if lang == "" || len(lang) != 2 {
@ -62,10 +63,10 @@ func GET_News(c *fiber.Ctx) error {
})
if feed, err = util.Render("views/news.xml", fiber.Map{
"app_url": conf.AppUrl,
"updated": time.Now().Format(time.RFC3339),
"entries": entries,
"lang": lang,
"app": app,
}); err != nil {
return util.ErrInternal(c, err)
}

View File

@ -66,24 +66,29 @@ func (s *Type) loop() {
func (s *Type) Setup(conf *config.Type, db *database.Type) error {
var (
dur time.Duration
err error
dur time.Duration
iv, to, lm string
err error
)
if conf.Interval == "" || conf.Timeout == "" || conf.Limit == "" {
iv = conf.GetStr("interval")
to = conf.GetStr("timeout")
lm = conf.GetStr("limit")
if iv == "" || to == "" || lm == "" {
s.disabled = true
return nil
}
if dur, err = util.GetDuration(conf.Interval); err != nil {
if dur, err = util.GetDuration(iv); err != nil {
return err
}
if s.timeout, err = util.GetDuration(conf.Timeout); err != nil {
if s.timeout, err = util.GetDuration(iv); err != nil {
return err
}
if s.limit, err = util.GetDuration(conf.Limit); err != nil {
if s.limit, err = util.GetDuration(iv); err != nil {
return err
}

View File

@ -10,9 +10,10 @@ import (
func IP(c *fiber.Ctx) string {
conf := c.Locals("config").(*config.Type)
ip_header := conf.GetStr("ip_header")
if conf.IPHeader != "" && c.Get(conf.IPHeader) != "" {
return strings.Clone(c.Get(conf.IPHeader))
if ip_header != "" && c.Get(ip_header) != "" {
return strings.Clone(c.Get(ip_header))
}
return c.IP()

View File

@ -1,8 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?><feed xmlns="http://www.w3.org/2005/Atom">
<title>{{.app_url.Host}} news</title>
<title>{{.app.Host}} news</title>
<updated>{{.updated}}</updated>
<subtitle>News and updates about my projects and self-hosted services</subtitle>
<link href="{{.app_url.JoinPath "/news"}}"></link>
<link href="{{.app.JoinPath "/news"}}"></link>
{{ range .entries }}
<entry>
<title>{{.Title}}</title>

1
app/.gitignore vendored
View File

@ -8,4 +8,3 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
nerdfonts.*

View File

@ -1,24 +1,22 @@
# build the application with node
FROM node:23.11.0 AS build
FROM node:23.5.0 AS build
ARG WEBSITE_REPORT_URL
ARG WEBSITE_SOURCE_URL
ARG WEBSITE_DOC_URL
ARG WEBSITE_APP_URL
ARG WEBSITE_API_URL
ARG WEBSITE_API_PATH
ARG WEBSITE_DOC_URL
ENV WEBSITE_REPORT_URL=$WEBSITE_REPORT_URL
ENV WEBSITE_SOURCE_URL=$WEBSITE_SOURCE_URL
ENV WEBSITE_DOC_URL=$WEBSITE_DOC_URL
ENV WEBSITE_APP_URL=$WEBSITE_APP_URL
ENV WEBSITE_API_URL=$WEBSITE_API_URL
ENV WEBSITE_API_PATH=$WEBSITE_API_PATH
ENV WEBSITE_DOC_URL=$WEBSITE_DOC_URL
WORKDIR /app
COPY . /app
RUN apt install -y make sed wget
RUN npm install
RUN make
RUN npm install && npm run build
# run it with bun (a lot faster)
FROM oven/bun:latest AS main

View File

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

132
app/package-lock.json generated
View File

@ -1,19 +1,20 @@
{
"name": "website",
"version": "6.3",
"version": "6.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "website",
"version": "6.3",
"version": "6.0",
"dependencies": {
"@types/dompurify": "^3.2.0",
"dompurify": "^3.2.3",
"marked": "^15.0.6",
"svelte-i18n": "^4.0.1"
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.15.1",
"@sveltejs/vite-plugin-svelte": "^4.0.3",
@ -831,21 +832,11 @@
"win32"
]
},
"node_modules/@sveltejs/acorn-typescript": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
"license": "MIT",
"peerDependencies": {
"acorn": "^8.9.0"
}
},
"node_modules/@sveltejs/adapter-auto": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-6.0.0.tgz",
"integrity": "sha512-7mR2/G7vlXakaOj6QBSG9dwBfTgWjV+UnEMB5Z6Xu0ZbdXda6c0su1fNkg0ab0zlilSkloMA2NjCna02/DR7sA==",
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz",
"integrity": "sha512-5Sc7WAxYdL6q9j/+D0jJKjGREGlfIevDyHSQ2eNETHcB1TKlQWHcAo8AS8H1QdjNvSXpvOwNjykDUHPEAyGgdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"import-meta-resolve": "^4.1.0"
},
@ -854,11 +845,10 @@
}
},
"node_modules/@sveltejs/adapter-node": {
"version": "5.2.12",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.12.tgz",
"integrity": "sha512-0bp4Yb3jKIEcZWVcJC/L1xXp9zzJS4hDwfb4VITAkfT4OVdkspSHsx7YhqJDbb2hgLl6R9Vs7VQR+fqIVOxPUQ==",
"version": "5.2.11",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.11.tgz",
"integrity": "sha512-lR7/dfUaKFf3aI408KRDy/BVDYoqUws7zNOJz2Hl4JoshlTnMgdha3brXBRFXB+cWtYvJjjPhvmq3xqpbioi4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@rollup/plugin-commonjs": "^28.0.1",
"@rollup/plugin-json": "^6.1.0",
@ -870,23 +860,24 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.20.5",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.20.5.tgz",
"integrity": "sha512-zT/97KvVUo19jEGZa972ls7KICjPCB53j54TVxnEFT5VEwL16G+YFqRVwJbfxh7AmS7/Ptr1rKF7Qt4FBMDNlw==",
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.15.1.tgz",
"integrity": "sha512-8t7D3hQHbUDMiaQ2RVnjJJ/+Ur4Fn/tkeySJCsHtX346Q9cp3LAnav8xXdfuqYNJwpUGX0x3BqF1uvbmXQw93A==",
"dev": true,
"license": "MIT",
"hasInstallScript": true,
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^0.6.0",
"devalue": "^5.1.0",
"esm-env": "^1.2.2",
"esm-env": "^1.2.1",
"import-meta-resolve": "^4.1.0",
"kleur": "^4.1.5",
"magic-string": "^0.30.5",
"mrmime": "^2.0.0",
"sade": "^1.8.1",
"set-cookie-parser": "^2.6.0",
"sirv": "^3.0.0"
"sirv": "^3.0.0",
"tiny-glob": "^0.2.9"
},
"bin": {
"svelte-kit": "svelte-kit.js"
@ -901,11 +892,10 @@
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.4.tgz",
"integrity": "sha512-0ba1RQ/PHen5FGpdSrW7Y3fAMQjrXantECALeOiOdBdzR5+5vPP6HVZRLmZaQL+W8m++o+haIAKq5qT+MiZ7VA==",
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.3.tgz",
"integrity": "sha512-J7nC5gT5qpmvyD2pmzPUntLUgoinyEaNy9sTpGGE6N7pblggO0A1NyneJJvR2ELlzK6ti28aF2SLXG1yJdnJeA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0",
"debug": "^4.3.7",
@ -922,12 +912,11 @@
"vite": "^5.0.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz",
"integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==",
"node_modules/@sveltejs/vite-plugin-svelte/node_modules/@sveltejs/vite-plugin-svelte-inspector": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.0.tgz",
"integrity": "sha512-hBxSYW/66989cq9dN248omD/ziskSdIV1NqfuueuAI1z6jGcg14k9Zd98pDIEnoA6wC9kWUGuQ6adzBbWwQyRg==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.7"
},
@ -947,6 +936,15 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/dompurify": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.2.0.tgz",
"integrity": "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg==",
"deprecated": "This is a stub types definition. dompurify provides its own type definitions, so you do not need this installed.",
"dependencies": {
"dompurify": "*"
}
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
@ -976,6 +974,14 @@
"node": ">=0.4.0"
}
},
"node_modules/acorn-typescript": {
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
"integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
"peerDependencies": {
"acorn": ">=8.9.0"
}
},
"node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
@ -1084,9 +1090,9 @@
"dev": true
},
"node_modules/dompurify": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.5.tgz",
"integrity": "sha512-mLPd29uoRe9HpvwP2TxClGQBzGXeEC/we/q+bFlmPPmj2p2Ugl3r6ATu/UU1v77DXNcehiBg9zsr1dREyA/dJQ==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.3.tgz",
"integrity": "sha512-U1U5Hzc2MO0oW3DF+G9qYN0aT7atAou4AgI0XjWz061nyBPbdxkfdhfy5uMgGn6+oLFCfn44ZGbdDqCzVmlOWA==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@ -1183,10 +1189,9 @@
}
},
"node_modules/esm-env": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
"license": "MIT"
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
"integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng=="
},
"node_modules/esniff": {
"version": "2.0.1",
@ -1204,10 +1209,9 @@
}
},
"node_modules/esrap": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz",
"integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==",
"license": "MIT",
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.3.2.tgz",
"integrity": "sha512-C4PXusxYhFT98GjLSmb20k9PREuUdporer50dhzGuJu9IJXktbMddVCMLAERl5dAHyAi73GWWCE4FVHGP1794g==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15"
}
@ -1396,9 +1400,9 @@
}
},
"node_modules/marked": {
"version": "15.0.10",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.10.tgz",
"integrity": "sha512-BXzsfFiR2UqXFKRwpugWuCYi9mWd1aX/Yns/X52xWfvfen9lnGEDbJw9ZEjjvLZVqntqT2gX45eYvqb2dIokDw==",
"version": "15.0.6",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.6.tgz",
"integrity": "sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
@ -1527,9 +1531,9 @@
}
},
"node_modules/prettier": {
"version": "3.5.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz",
"integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==",
"version": "3.4.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
"dev": true,
"license": "MIT",
"bin": {
@ -1543,9 +1547,9 @@
}
},
"node_modules/prettier-plugin-svelte": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.3.tgz",
"integrity": "sha512-yViK9zqQ+H2qZD1w/bH7W8i+bVfKrD8GIFjkFe4Thl6kCT9SlAsXVNmt3jCvQOCsnOhcvYgsoVlRV/Eu6x5nNw==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.3.2.tgz",
"integrity": "sha512-kRPjH8wSj2iu+dO+XaUv4vD8qr5mdDmlak3IT/7AOgGIMRG86z/EHOLauFcClKEnOUf4A4nOA7sre5KrJD4Raw==",
"dev": true,
"license": "MIT",
"peerDependencies": {
@ -1661,21 +1665,20 @@
}
},
"node_modules/svelte": {
"version": "5.26.2",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.26.2.tgz",
"integrity": "sha512-e2TEcGK2YKVwDWYy5OsptVclYgDvfY1E/8IzPiOq63uG/GDo/j5VUYTC9EinQNraoZalbMWN+5f5TYC1QlAqOw==",
"license": "MIT",
"version": "5.16.0",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.16.0.tgz",
"integrity": "sha512-Ygqsiac6UogVED2ruKclU+pOeMThxWtp9LG+li7BXeDKC2paVIsRTMkNmcON4Zejerd1s5sZHWx6ZtU85xklVg==",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@sveltejs/acorn-typescript": "^1.0.5",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"clsx": "^2.1.1",
"esm-env": "^1.2.1",
"esrap": "^1.4.6",
"esrap": "^1.3.2",
"is-reference": "^3.0.3",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
@ -2160,11 +2163,10 @@
"license": "ISC"
},
"node_modules/vite": {
"version": "5.4.18",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz",
"integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==",
"version": "5.4.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",

View File

@ -1,6 +1,6 @@
{
"name": "website",
"version": "6.3",
"version": "6.0",
"private": true,
"scripts": {
"dev": "vite dev",
@ -10,10 +10,10 @@
"format": "prettier --write ."
},
"devDependencies": {
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/adapter-auto": "^3.3.1",
"@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.15.1",
"@sveltejs/vite-plugin-svelte": "^4.0.3",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.2",
"svelte": "^5.16.0",

View File

@ -1,14 +1,9 @@
import { browser } from "$app/environment";
import { urljoin } from "$lib/util.js";
const api_version = "v1";
const api_url = urljoin(import.meta.env.WEBSITE_API_URL, api_version);
function api_urljoin(path = null, query = {}) {
let api_url = "";
if (browser) api_url = urljoin(import.meta.env.WEBSITE_API_PATH, api_version);
else api_url = urljoin(import.meta.env.WEBSITE_API_URL, api_version);
return urljoin(api_url, path, query);
}

View File

@ -1,36 +1,43 @@
<script>
import { color, date_from_ts } from "$lib/util.js";
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 show_counter = false,
data = {};
let data = {};
onMount(async () => {
show_counter = true;
data = await api_get_metrics(fetch);
});
</script>
<footer style="border-top: solid 2px var(--{color()});">
<div class="links">
<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>
<Link link={import.meta.env.WEBSITE_SOURCE_URL} bold={true}>{$_("footer.source")}</Link>
</span>
<span>/</span>
<span>
<Link link="/doc/license" bold={true}>{$_("footer.license")}</Link>
</span>
<span>/</span>
<span>
<Link link="/doc/privacy" bold={true}>{$_("footer.privacy")}</Link>
{$_("footer.powered")}
</span>
</div>
{#if show_counter}
<span class="counter">
<div class="useless">
<span>
{$_("footer.number", {
values: {
total: data.total,
@ -41,9 +48,10 @@
<span style="color: var(--{color()})">({$_("footer.wow")})</span>
{/if}
</span>
{:else}
<span class="counter">{$_("footer.js")}</span>
{/if}
<span>
{$_("footer.version", { values: { api_version: "v1", frontend_version: pkg.version } })}
</span>
</div>
</footer>
<style>
@ -53,29 +61,27 @@
justify-content: space-between;
align-items: center;
background: var(--black-1);
box-sizing: border-box;
padding: 20px 50px 20px 50px;
}
div {
display: flex;
color: var(--white-2);
font-size: var(--size-2);
flex-direction: column;
gap: 5px;
}
span {
color: var(--white-2);
font-size: 15px;
}
.counter {
.useless {
margin: 25px 50px 25px 0;
text-align: right;
}
.links {
.info {
margin: 25px 0 25px 50px;
text-align: left;
}
.info .links {
display: flex;
flex-direction: row;
gap: 5px;

View File

@ -1,24 +1,22 @@
<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 name="description" content={desc} />
<meta name="author" content="ngn" />
<meta name="keywords" content="ngn,ngn13,ngn1,ngn.tf" />
<meta name="color-scheme" content="only dark" />
<meta name="theme-color" content="#000000" />
<meta property="og:title" content="[ngn.tf] | {title}" />
<meta property="og:description" content={desc} />
<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="Service news and updates"
title="Atom Feed"
/>
</svelte:head>

View File

@ -1,46 +1,15 @@
<script>
import { browser } from "$app/environment";
import { color } from "$lib/util.js";
import { onMount } from "svelte";
import { _ } from "svelte-i18n";
export let picture = "";
export let title = "";
let title_cur = title;
let show_animation = false;
function animate(title) {
if (!browser) return;
let id = window.setTimeout(function () {}, 0);
while (id--) clearTimeout(id);
title_cur = "";
for (let i = 0; i < title.length; i++) {
setTimeout(() => {
title_cur += title[i];
}, i * 70);
}
}
onMount(() => {
show_animation = true;
});
$: animate(title);
</script>
<header>
<div>
{#if show_animation}
<h1 class="title" style="color: var(--{color()})">{title_cur}</h1>
<h1 class="cursor" style="color: var(--{color()})">_</h1>
{:else}
<h1 class="title" style="color: var(--{color()})">{title}</h1>
{/if}
<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>
@ -81,6 +50,9 @@
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 {

View File

@ -1,9 +1,7 @@
<script>
import { locale_list, locale_select, locale_index } from "$lib/locale.js";
import { onMount } from "svelte";
let len = locale_list.length;
let show = false;
function get_next(indx) {
let new_indx = 0;
@ -17,17 +15,11 @@
function next() {
locale_select(get_next($locale_index).code);
}
onMount(() => {
show = true;
});
</script>
{#if show}
<button on:click={next}>
{get_next($locale_index).icon}
</button>
{/if}
<button on:click={next}>
{get_next($locale_index).icon}
</button>
<style>
button {

View File

@ -19,18 +19,28 @@ function color() {
}
function click() {
let audio = new Audio("/assets/click.wav");
let audio = new Audio("/click.wav");
audio.play();
}
function urljoin(url, path = null) {
function urljoin(url, path = null, query = {}) {
if (undefined === url || null === url) return;
if (url[url.length - 1] != "/") url += "/";
let url_len = url.length;
if (null === path || "" === path) return url;
if (path[0] === "/") return url + path.slice(1);
return url + path;
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 = {}) {
return urljoin(import.meta.env.WEBSITE_APP_URL, path, query);
}
function time_from_ts(ts) {
@ -58,4 +68,4 @@ function date_from_ts(ts) {
}).format(new Date(ts * 1000));
}
export { color, click, urljoin, time_from_ts, date_from_ts };
export { color, click, urljoin, app_url, time_from_ts, date_from_ts };

View File

@ -5,7 +5,7 @@
"donate": "donate"
},
"home": {
"title": "hello world!",
"title": "Hello world!",
"welcome": {
"title": "about",
"desc": "Welcome to my website, I'm ngn",
@ -29,9 +29,9 @@
},
"services": {
"title": "services",
"desc": "A part from working on stupid shit, I host free (as in freedom and price) services available for all",
"speed": "All of these services are available over an 1 Gbit interface",
"security": "All use SSL encrypted connection and they respect your privacy and freedom",
"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!"
@ -42,7 +42,7 @@
}
},
"services": {
"title": "service status",
"title": "Service Status",
"none": "No services found",
"search": "Search for a service",
"feed": "News and updates",
@ -54,7 +54,7 @@
}
},
"donate": {
"title": "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.",
@ -64,16 +64,20 @@
"address": "Adress/Link"
}
},
"doc": {
"title": "Documentation"
},
"error": {
"title": "something went wrong!",
"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!!",
"js": "Enable javascript to display all the elements"
"version": "Using API version {api_version}, frontend version {frontend_version}"
}
}

View File

@ -5,7 +5,7 @@
"donate": "bağış"
},
"home": {
"title": "merhaba dünya!",
"title": "Merhaba Dünya!",
"welcome": {
"title": "hakkımda",
"desc": "Websiteme hoşgeldiniz, ben ngn",
@ -30,10 +30,11 @@
"services": {
"title": "servisler",
"desc": "Salak şeyler inşa etmenin yanı sıra, herkes için kullanıma açık özgür ve ücretsiz servisler host ediyorum",
"speed": "Tüm servisler 1 Gbit ağ arayüzü üzerinden erişilebilir",
"security": "Hepsi SSL şifreli bağlantı kullanıyor ve hepsi gizliliğinize ve özgürlüğünüze saygı gösteriyor",
"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 ve diğer saçmalıklar yok",
"bullshit": "CDN yok, cloudflare yok, CAPTCHA yok, analitikler yok, boktan saçmalıklar yok",
"link": "Tüm servisleri incele!"
},
"projects": {
@ -42,7 +43,7 @@
}
},
"services": {
"title": "servis durumu",
"title": "Servis Durumu",
"none": "Servis bulunamadı",
"search": "Bir servisi ara",
"feed": "Yenilikler ve güncellemeler",
@ -54,7 +55,7 @@
}
},
"donate": {
"title": "bağış yap!",
"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.",
@ -64,16 +65,20 @@
"address": "Adres/Bağlantı"
}
},
"doc": {
"title": "Dökümantasyon"
},
"error": {
"title": "birşeyler yanlış gitti!",
"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!!",
"js": "Tüm elementleri görüntelemek için javascript'i açın"
"version": "Kullan API versiyonu {api_version}, arayüz versiyonu {frontend_version}"
}
}

View File

@ -21,7 +21,7 @@
</main>
<style>
@import "/css/global.css";
@import "/global.css";
main {
display: flex;

View File

@ -1,22 +1,14 @@
<script>
import { api_version } from "$lib/api.js";
import Header from "$lib/header.svelte";
import Error from "$lib/error.svelte";
import Head from "$lib/head.svelte";
import Card from "$lib/card.svelte";
import Link from "$lib/link.svelte";
import { browser } from "$app/environment";
import { _, locale } from "svelte-i18n";
import { color } from "$lib/util.js";
let { data } = $props();
if (browser) {
window._version = {};
window._version.app = pkg.version;
window._version.api = api_version;
}
</script>
<Head title="home" desc="home page of my personal website" />

View File

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

View File

@ -22,7 +22,7 @@
</script>
<Head title="documentation" desc="website and API documentation" />
<Header picture="reader" title={data.doc[$locale].title} />
<Header picture="reader" title={$_("doc.title")} />
{#if data.error.length !== 0}
{#if !data.error.includes("not found")}
@ -30,21 +30,94 @@
{/if}
{:else}
<main>
<div class="markdown-body" style="--link-color: var(--{color()})">
{@html marked.parse(data.doc[$locale].content)}
</div>
{#if data.doc !== undefined}
<div class="markdown-body" style="--link-color: var(--{color()})">
{@html marked.parse(data.doc[$locale].content)}
</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>
{/if}
<style>
@import "/css/markdown.css";
@import "/markdown.css";
main {
padding: 50px;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: start;
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>

View File

@ -7,11 +7,9 @@
import { api_urljoin } from "$lib/api.js";
import { locale, _ } from "svelte-i18n";
import { onMount } from "svelte";
let { data } = $props();
let services = $state(data.services);
let show_input = $state(false);
function change(input) {
let value = input.target.value.toLowerCase();
@ -33,10 +31,6 @@
return s.desc[$locale] !== "" && s.desc[$locale] !== null && s.desc[$locale] !== undefined;
});
}
onMount(() => {
show_input = true;
});
</script>
<Head title="services" desc="my self-hosted services and projects" />
@ -47,9 +41,7 @@
{:else}
<main>
<div class="title">
{#if show_input}
<input oninput={change} type="text" placeholder={$_("services.search")} />
{/if}
<input oninput={change} type="text" placeholder={$_("services.search")} />
<div>
<Link icon="nf-fa-feed" link={api_urljoin("/news/" + $locale)}>{$_("services.feed")}</Link>
</div>

View File

@ -9,3 +9,13 @@
border-color: transparent;
}
}
@keyframes typing {
from {
width: 0%;
}
to {
width: 100%;
}
}

View File

Before

Width:  |  Height:  |  Size: 8.9 KiB

After

Width:  |  Height:  |  Size: 8.9 KiB

View File

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

10
app/static/font.css Normal file
View File

@ -0,0 +1,10 @@
/* 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");
}

View File

@ -1,12 +1,4 @@
/*
* animations.css: stuff like the cursor animation
* webfont.css: webfont dumped from https://www.nerdfonts.com/assets/css/webfont.css
* font.css: the main font (Ubuntu)
*/
@import "./animations.css";
@import "./nerdfonts.css";
@import "./font.css";
:root {
@ -39,8 +31,7 @@
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("/assets/banner.png");
--background: linear-gradient(rgba(11, 11, 11, 0.808), rgba(1, 1, 1, 0.96)), url("/banner.png");
--profile-size: 220px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -1,3 +0,0 @@
User-Agent: *
Disallow: /doc/
Disallow: /api/

View File

@ -3,38 +3,21 @@ import { defineConfig } from "vite";
import { fileURLToPath } from "url";
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 = {
source_url: "https://git.ngn.tf/ngn/website",
report_url: "https://git.ngn.tf/ngn/website/issues",
doc_url: "http://localhost:7003",
api: {
url: "http://localhost:7002",
path: "http://localhost:7002",
},
REPORT_URL: "https://github.com/ngn13/website/issues",
SOURCE_URL: "https://github.com/ngn13/website",
APP_URL: "http://localhost:7001",
API_URL: "http://localhost:7002",
DOC_URL: "http://localhost:7003",
};
const package_file = fileURLToPath(new URL("package.json", import.meta.url));
const package_json = readFileSync(package_file, "utf8");
const package_data = JSON.parse(package_json);
const file = fileURLToPath(new URL("package.json", import.meta.url));
const json = readFileSync(file, "utf8");
const pkg = JSON.parse(json);
env_from("WEBSITE", default_env);
for (let env in default_env) {
if (process.env["WEBSITE_" + env] === undefined) process.env["WEBSITE_" + env] = default_env[env];
}
export default defineConfig({
plugins: [sveltekit()],
@ -48,6 +31,6 @@ export default defineConfig({
strictPort: true,
},
define: {
pkg: package_data,
pkg: pkg,
},
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

View File

@ -7,20 +7,19 @@ services:
args:
WEBSITE_SOURCE_URL: "http://github.com/ngn13/website"
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_API_URL: "http://api:7002"
WEBSITE_API_PATH: "http://localhost:7002"
security_opt:
- "no-new-privileges:true"
cap_drop:
- ALL
ports:
- "127.0.0.1:7001:7001"
restart: unless-stopped
depends_on:
- api
- doc
read_only: true
restart: unless-stopped
api:
container_name: "website_api"
@ -35,28 +34,28 @@ services:
- "127.0.0.1:7002:7002"
volumes:
- ./data.db:/api/data.db:rw
restart: unless-stopped
environment:
WEBSITE_DEBUG: "false"
WEBSITE_APP_URL: "http://localhost:7001"
WEBSITE_APP_URL: "http://localhost:7001/"
WEBSITE_PASSWORD: "change_me"
WEBSITE_HOST: "0.0.0.0:7002"
WEBSITE_IP_HEADER: "X-Real-IP"
WEBSITE_INTERVAL: "1h"
WEBSITE_TIMEOUT: "15s"
WEBSITE_LIMIT: "5s"
restart: unless-stopped
doc:
container_name: "website_doc"
image: website_doc
read_only: true
build:
context: ./doc
security_opt:
- "no-new-privileges:true"
cap_drop:
- ALL
restart: unless-stopped
environment:
WEBSITE_HOST: "0.0.0.0:7003"
WEBSITE_DOCS_DIR: "./docs"
read_only: true
restart: unless-stopped

View File

@ -1,4 +1,4 @@
{
"title": "API",
"title": "API documentation",
"desc": "Website's API documentation"
}

View File

@ -1,9 +1,8 @@
My website's API, stores information about my self-hosted services, it also allows me
to publish news and updates about these services using an Atom feed and it keeps track
of visitor metrics.
My website's API stores information about my self-hosted services, it also allows me to
publish news and updates about these services using an Atom feed and it keeps track of
visitor metrics. The API itself is written in Go and uses SQLite for storage.
This documentation contains information about all the available API endpoints. All the
endpoints can be accessed using the `/api` route.
This documentation contains information about all the available API endpoints.
## Version 1 Endpoints
Each version 1 endpoint, can be accessed using the `/v1` route.

View File

@ -1,4 +1,4 @@
{
"title": "API",
"title": "API dökümantasyonu",
"desc": "Websitesinin API dökümantasyonu"
}

View File

@ -1,9 +1,8 @@
Websitemin API, self-host edilen servisler hakkında bilgileri tutuyor, bu servisler hakkında
Websitemin API self-host edilen servisler hakkında bilgileri tutuyor, bu servisler hakkında
haberleri ve güncellemeleri bir Atom feed'i aracılığı ile paylaşmama izin veriyor ve ziyartçi
metriklerini takip ediyor.
metriklerini takip ediyor. API'ın kendisi Go ile yazıldı ve veritabanı olarak SQLite kullanıyor.
Bu dökümentasyon tüm erişime açık API endpoint'leri hakkında bilgiler içeriyor. Tüm endpoint'lere
`/api` yolu ile erişilebilir.
Bu dökümentasyon tüm erişeme açık API endpoint'leri hakkında bilgiler içeriyor.
## Versyion 1 Endpoint'leri
Tüm versiyon 1 endpoint'leri `/v1` yolu ile erişilebilir.

View File

@ -1,4 +1,4 @@
{
"title": "source license",
"title": "License",
"desc": "Source code license"
}

View File

@ -1,4 +1,4 @@
{
"title": "kaynak lisansı",
"title": "Lisans",
"desc": "Kaynak kodu lisansı"
}

View File

@ -1,4 +1,4 @@
{
"title": "privacy",
"title": "Privacy",
"desc": "Learn how I respect your privacy"
}

View File

@ -1,4 +1,4 @@
{
"title": "gizlilik",
"title": "Gizlilik",
"desc": "Gizliliğinize nasıl önem verdiğimi öğrenin"
}

View File

@ -10,7 +10,6 @@
#include "util.h"
option_t options[] = {
// name value requied
{"host", "0.0.0.0:7003", true }, // host the server should listen on
{"docs_dir", "./docs", true }, // documentation directory
{"", NULL, false},

View File

@ -13,11 +13,7 @@ int main() {
if (!config_load(&conf))
return EXIT_FAILURE;
if (NULL == (host = config_get(&conf, "host"))) {
ctorm_fail("failed to get the host configuration");
return EXIT_FAILURE;
}
host = config_get(&conf, "host");
ctorm_config_new(&app_config);
app_config.disable_logging = true;
@ -26,6 +22,7 @@ int main() {
// middlewares
MIDDLEWARE_ALL(app, "/*", route_cors);
MIDDLEWARE_ALL(app, "/*/*", route_cors);
MIDDLEWARE_ALL(app, "/*/*/*", route_cors);
// routes
GET(app, "/list", route_list);