This commit is contained in:
Akis 2023-01-25 19:11:11 +02:00
parent add264a5d6
commit 07c0cc4a69
Signed by untrusted user: akis
GPG Key ID: 267BF5C6677944ED
117 changed files with 1554 additions and 3593 deletions

View File

@ -1,25 +1,36 @@
name: Docker
name: Docker dev
on:
push:
branches:
- "dev"
push:
branches:
- 'dev'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
push: true
tags: realprojectsegfault/website:dev
build:
name: 'Build'
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: "Build:checkout"
uses: actions/checkout@v3
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ProjectSegfault
password: ${{ secrets.ACCESS_TOKEN }}
- name: 'Build:dockerimage'
uses: docker/build-push-action@v3
with:
tags: ghcr.io/ProjectSegfault/website:dev
context: "."
push: true
no-cache: true

View File

@ -1,25 +1,36 @@
name: Docker
on:
push:
branches:
- "master"
push:
branches:
- 'master'
jobs:
docker:
runs-on: ubuntu-latest
steps:
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v3
with:
push: true
tags: realprojectsegfault/website:latest
build:
name: 'Build'
runs-on: ubuntu-latest
steps:
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: "Build:checkout"
uses: actions/checkout@v3
- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: akisblack
password: ${{ secrets.ACCESS_TOKEN }}
- name: 'Build:dockerimage'
uses: docker/build-push-action@v3
with:
tags: ghcr.io/akisblack/website:latest
context: "."
push: true
no-cache: true

3
.gitignore vendored
View File

@ -6,4 +6,5 @@ node_modules
.env
.env.*
!.env.example
package-lock.json
vite.config.js.timestamp-*
vite.config.ts.timestamp-*

1
.npmrc Normal file
View File

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

View File

@ -3,5 +3,8 @@
"useTabs": true,
"bracketSpacing": true,
"singleAttributePerLine": true,
"trailingComma": "none"
"trailingComma": "none",
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View File

@ -4,13 +4,12 @@ Live at [projectsegfau.lt](https://projectsegfau.lt).
## Developing
> You need a lot of infrastructure to run a complete version of the website including: Ghost CMS deployment, Authentik authentication, a Discord channel with a webhook and a hCaptcha sitekey and secret from a hCaptcha account.
> You need a lot of infrastructure to run a complete version of the website including: Ghost CMS deployment and Authentik authentication.
### Prerequisites
- Install [node.js](https://nodejs.org).
- Install [pnpm](https://pnpm.io/).
- Install [MongoDB](https://mongodb.com).
- Learn [Svelte](https://svelte.dev).
- Add the environment variables from the [environment variables section](#environment-variables).
@ -40,14 +39,11 @@ The website has the following **mandatory** environment variables
| Name | Description |
|:------------------ |:------------------------- |
| AUTH_SECRET | Random 32 char secret |
| AUTH_CLIENT_ID | Authentik client ID |
| AUTH_CLIENT_SECRET | Authentik client secret |
| AUTH_ISSUER | Authentication issuer URL |
| AUTH_TRUST_HOST | Your domain |
| HCAPTCHA_SECRET | Your hCaptcha secret |
| HCAPTCHA_SITEKEY | Your hCaptcha sitekey |
| WEBHOOK | Your Discord webhook URL |
| AUTH_SECRET | Random 32 char secret |
| GHOST_URL | Your Ghost CMS URL |
| GHOST_API_KEY | Your Ghost CMS API key |
| DB_URL | Your MongoDB url |
| ORIGIN | Your domain |

View File

@ -1,38 +1,11 @@
services:
website:
container_name: website
image: realprojectsegfault/website:latest # or :dev if you want to use the dev version
image: ghcr.io/ProjectSegfault/website:latest
restart: unless-stopped
# uncomment these lines if you want to build from source
#build:
# context: .
# dockerfile: Dockerfile
ports:
- 1339:3000 # change the first number to whatever port you want to use
environment: # these are documented in the readme
AUTH_SECRET: ${AUTH_SECRET}
AUTH_CLIENT_ID: ${AUTH_CLIENT_ID}
AUTH_CLIENT_SECRET: ${AUTH_CLIENT_SECRET}
AUTH_ISSUER: ${AUTH_ISSUER}
AUTH_TRUST_HOST: ${AUTH_TRUST_HOST}
HCAPTCHA_SECRET: ${HCAPTCHA_SECRET}
HCAPTCHA_SITEKEY: ${HCAPTCHA_SITEKEY}
WEBHOOK: ${WEBHOOK}
GHOST_API_KEY: ${GHOST_API_KEY}
DB_URL: ${DB_URL}
ORIGIN: ${ORIGIN}
website-db: # this is the mongodb database container
image: mongo:6
container_name: website-db
restart: unless-stopped
volumes:
- website-db-data:/data/db
environment: # these are documented in the readme
MONGO_INITDB_ROOT_USERNAME: ${MONGO_USER}
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
MONGO_INITDB_DATABASE: website
command: [--auth]
volumes:
website-db-data:
- "127.0.0.1:1339:3000"

View File

@ -1,47 +1,40 @@
{
"name": "project-segfault-website",
"version": "2.0.0",
"version": "3.0.0",
"private": true,
"scripts": {
"dev": "vite",
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier -w --plugin-search-dir=. ."
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check .",
"format": "prettier --plugin-search-dir . --write ."
},
"devDependencies": {
"@iconify-json/ic": "^1.1.12",
"@iconify-json/simple-icons": "^1.1.40",
"@sveltejs/adapter-node": "1.0.0",
"@sveltejs/kit": "1.0.1",
"@types/marked": "^4.0.8",
"@iconify-json/simple-icons": "^1.1.41",
"@sveltejs/adapter-node": "^1.1.4",
"@sveltejs/kit": "^1.1.4",
"@types/sanitize-html": "^2.8.0",
"axios": "^1.2.2",
"consola": "^2.15.3",
"@unocss/reset": "^0.48.4",
"axios": "^1.2.4",
"dayjs": "^1.11.7",
"discord-webhook-node": "^1.1.8",
"marked": "^4.2.5",
"mdsvex": "^0.10.6",
"prettier": "^2.8.1",
"prettier": "^2.8.3",
"prettier-plugin-svelte": "^2.9.0",
"sanitize-html": "^2.8.1",
"svelte": "^3.55.0",
"svelte-check": "^3.0.1",
"svelte": "^3.55.1",
"svelte-check": "^3.0.2",
"svelte-dark-mode": "^2.1.0",
"svelte-hcaptcha": "^0.1.1",
"svelte-seo": "^1.4.1",
"svelte-vertical-timeline": "^0.0.2",
"tslib": "^2.4.1",
"typescript": "^4.9.4",
"unocss": "^0.48.0",
"vite": "4.0.3"
"unocss": "^0.48.4",
"vite": "^4.0.4"
},
"type": "module",
"dependencies": {
"@auth/core": "^0.2.4",
"@auth/sveltekit": "^0.1.11",
"dotenv": "^16.0.3",
"joi": "^17.7.0",
"mongodb": "^4.13.0"
"@auth/core": "^0.2.5",
"@auth/sveltekit": "^0.1.12",
"joi": "^17.7.0"
}
}

File diff suppressed because it is too large Load Diff

77
src/app.css Normal file
View File

@ -0,0 +1,77 @@
@font-face {
font-family: "JetBrains Mono";
src: url("/JetBrainsMono.woff2");
font-display: swap;
}
html, html.light {
--accent: #00a584;
--accent-translucent: #00a58498;
--font-primary: "JetBrains Mono", monospace;
--primary: #ffffff;
--secondary: #e9e9e9;
--tertiary: #939393;
--text: #444444;
--grey: #cecece;
--alt: #ddd;
--alt-text: #333;
--black: #151515;
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
html {
--primary: #151515;
--secondary: #1d1d1d;
--tertiary: #353535;
--text: #ffffffde;
--grey: #5454547a;
--alt: #333;
--alt-text: #ddd;
color-scheme: dark;
}
}
body {
@apply bg-primary text-text font-primary m-0 leading-loose min-h-screen transition-colors duration-200;
}
::selection {
@apply bg-accentTranslucent;
}
a {
@apply text-accent underline underline-offset-4 transition-filter duration-200;
}
a:hover {
@apply brightness-70;
}
h1 {
@apply text-4xl font-bold my-8 border-b-2 border-accent pb-2;
}
.h1-no-lg {
@apply my-8 border-b-2 border-accent pb-2;
}
h2 {
@apply text-3xl font-bold my-8;
}
h3 {
@apply text-2xl font-bold my-8;
}
h4 {
@apply text-xl font-bold my-8;
}
details {
@apply cursor-pointer;
}
.button {
@apply px-2 py-1 bg-accent text-primary rounded no-underline flex flex-row items-center gap-2;
}

17
src/app.d.ts vendored
View File

@ -1,10 +1,13 @@
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare namespace App {
// interface Locals {}
// interface Platform {}
// interface Session {}
// interface Stuff {}
// and what to do when importing types
declare global {
namespace App {
// interface Error {}
// interface Locals {}
// interface PageData {}
// interface Platform {}
}
}
export {};

View File

@ -2,14 +2,8 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link
rel="icon"
href="%sveltekit.assets%/logo_transparent.svg"
/>
<meta
name="viewport"
content="width=device-width, initial-scale=1"
/>
<link rel="icon" href="%sveltekit.assets%/logo.svg" />
<meta name="viewport" content="width=device-width" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

View File

@ -1,27 +1,44 @@
import { SvelteKitAuth } from "@auth/sveltekit"
import Authentik from '@auth/core/providers/authentik';
import { SvelteKitAuth } from "@auth/sveltekit";
import Authentik from "@auth/core/providers/authentik";
import { env } from "$env/dynamic/private";
import statusData from "$lib/statusData";
import map from "$lib/map";
import type { Provider } from "@auth/core/providers";
import type { Profile } from "@auth/core/types";
import { redirect, type Handle } from "@sveltejs/kit";
import { sequence } from "@sveltejs/kit/hooks";
export const handle = SvelteKitAuth({
providers: [
//@ts-ignore
Authentik({
clientId: env.AUTH_CLIENT_ID,
clientSecret: env.AUTH_CLIENT_SECRET,
issuer: env.AUTH_ISSUER
})
]
})
const hasAuth = !env.AUTH_CLIENT_ID || !env.AUTH_CLIENT_SECRET || !env.AUTH_ISSUER || !env.AUTH_TRUST_HOST || !env.AUTH_SECRET ? false : true;
const updateMap = () => {
map.set("data", {
status: statusData,
updated: Math.floor(Date.now() / 1000)
});
};
updateMap();
setInterval(updateMap, 30000);
export const handle: Handle = sequence(
//@ts-ignore
SvelteKitAuth({
providers: [
Authentik({
clientId: env.AUTH_CLIENT_ID,
clientSecret: env.AUTH_CLIENT_SECRET,
issuer: env.AUTH_ISSUER
}) as Provider<Profile>
]
}),
hasAuth ? async ({ event, resolve }) => {
if (event.url.pathname.startsWith("/admin")) {
const session = await event.locals.getSession();
if (!session) {
throw redirect(303, "/login");
}
}
const result = await resolve(event, {
transformPageChunk: ({ html }) => html
});
return result;
} : async ({ event, resolve }) => {
if (event.url.pathname.startsWith("/admin")) {
throw redirect(303, "/login");
}
const result = await resolve(event, {
transformPageChunk: ({ html }) => html
});
return result;
}
);

View File

@ -10,7 +10,7 @@
<div class="flex flex-row items-center gap-2">
<div class="i-ic:outline-bookmarks text-xl -ml-1" />
{#each post.tags as tag}
<a href="/blog/tags/{tag.slug}" class="no-underline rounded-2 p-1 {isPost ? "bg-secondary" : "bg-primary"}">{tag.name}</a>
<a href="/blog/tags/{tag.slug}" class="no-underline rounded p-1 {isPost ? "bg-secondary" : "bg-primary"}">{tag.name}</a>
{/each}
</div>
{/if}

View File

@ -3,6 +3,6 @@
</script>
<div class="prose flex flex-col text-justify m-auto">
<img src={data.post.feature_image} alt="{data.post.title} image" class="rounded-2">
<img src={data.post.feature_image} alt="{data.post.title} image" class="rounded">
{@html data.post.html}
</div>

View File

@ -3,7 +3,7 @@
export let isPost: boolean = false;
</script>
<div class="flex flex-col gap-4 p-4 rounded-2 {isPost ? "" : "w-120 bg-secondary"}">
<div class="flex flex-col gap-4 p-4 rounded {isPost ? "" : "w-110 bg-secondary"}">
<slot />
{#if url}

View File

@ -1,8 +1,3 @@
<script lang="ts">
export let hasMt: boolean = false;
export let isHome: boolean = false;
</script>
<div class="flex flex-row flex-wrap gap-10 {hasMt ? "mt-16" : ""} {isHome ? "justify-center" : ""}">
<div class="flex flex-row flex-wrap gap-4">
<slot />
</div>

View File

@ -6,6 +6,6 @@
<div class="flex flex-col gap-4">
{#each items as item}
<a href="/blog/{name}/{item.slug}" class="bg-secondary sm:w-md p-2 rounded-2 no-underline">{item.name}</a>
<a href="/blog/{name}/{item.slug}" class="bg-secondary sm:w-md p-2 rounded no-underline">{item.name}</a>
{/each}
</div>

View File

@ -5,7 +5,7 @@
{#if !isPost}
{#if post.feature_image}
<img src={post.feature_image} alt="{post.title} image" class="rounded-2">
<img src={post.feature_image} alt="{post.title} image" class="rounded">
{/if}
<a href="/blog/{post.slug}" class="text-text no-underline hover:underline"><span class="text-xl font-bold">{post.title}</span></a>
{:else}

View File

@ -1,36 +0,0 @@
<script lang="ts">
export let title: string = "";
export let position: string = "";
export let description: string = "";
export let icon: string = "";
export let positionStyles: string = "";
</script>
<div class="bg-secondary rounded-2 p-4 w-[18rem] sm:w-md flex flex-col">
<div class="flex-1 flex flex-row gap-4">
{#if icon}
<div>
<img
src={icon}
class="h-20 rounded-2"
alt="{title} icon"
/>
</div>
{/if}
<div>
<span class="text-2xl font-bold">
{title}
{#if position}
<span>- </span>
<span style={positionStyles}>{position}</span>
{/if}
</span>
{#if description}
<p class="description">{description}</p>
{/if}
</div>
</div>
<slot />
</div>

View File

@ -1,3 +0,0 @@
<div class="flex gap-8 flex-row flex-wrap">
<slot />
</div>

View File

@ -1,49 +0,0 @@
<script lang="ts">
export let url: string = "";
let classes: string = "";
export { classes as class };
</script>
<a
href={url}
class="border-none rounded-2 p-2 cursor-pointer font-primary text-secondary decoration-none w-fit text-xl flex items-center {classes}"
>
<slot />
</a>
<style>
.web,
.email,
.picture,
.pgp,
.link {
@apply bg-alt text-alt-text transition-all duration-250;
}
.web:hover,
.email:hover,
.picture:hover,
.pgp:hover {
@apply bg-accent text-alt;
}
.matrixcolored {
@apply bg-alt text-alt-text;
}
.discordcolored {
@apply bg-[#5865f2] text-white;
}
.gitcolored {
@apply bg-[#f05032] text-white;
}
.githubcolored {
@apply bg-alt text-alt-text;
}
.torcolored {
@apply bg-[#7d4698] text-white;
}
</style>

View File

@ -1,3 +0,0 @@
<div class="flex flex-row flex-wrap gap-2">
<slot />
</div>

View File

@ -1,4 +0,0 @@
export { default as CardOuter } from "./CardOuter.svelte";
export { default as CardInner } from "./CardInner.svelte";
export { default as LinksOuter } from "./LinksOuter.svelte";
export { default as Link } from "./Link.svelte";

View File

@ -1,8 +1,7 @@
<footer class="flex flex-col text-xl sticky top-full">
<div
class="flex flex-col justify-center sm:flex-row gap-1 border-t border-t-solid border-t-grey p-3 text-sm"
>
<span class="flex flex-row items-center gap-1">Made with <div class="i-simple-icons:svelte text-[#FF3E00]" />SvelteKit <span class="hidden sm:block">-</span></span>
<footer class="sticky top-full">
<div class="flex flex-col justify-center sm:flex-row gap-1 border-t border-t-solid border-t-grey p-3 text-sm">
<p class="flex flex-row items-center gap-1">Made with <i class="i-simple-icons:svelte text-[#FF3E00] block" /> SvelteKit</p>
<span class="hidden sm:block">-</span>
<a href="https://github.com/ProjectSegfault/website">Source code</a>
</div>
</footer>
</footer>

View File

@ -1,33 +0,0 @@
<script lang="ts">
import HCaptcha from "svelte-hcaptcha";
import { Note } from "$lib/Form";
let submit = false;
const showSubmitButton = () => {
submit = !submit;
};
export let sitekey: string = "";
</script>
<Note
content="The submit button will be visible when you complete the Captcha."
icon="i-ic:outline-info text-xl"
/>
<HCaptcha
{sitekey}
on:success={showSubmitButton}
/>
<slot />
{#if submit}
<button
type="submit"
value="Submit"
class="form-button"
>
Submit
</button>
{/if}

View File

@ -1,28 +0,0 @@
<script lang="ts">
export let action: string = "";
export let method: string = "";
export let id: string = "";
</script>
<form
{action}
{method}
{id}
class="flex flex-col gap-4 w-fit"
>
<slot />
</form>
<style>
:global(.form-button) {
@apply bg-secondary border-none rounded-2 p-2 cursor-pointer text-text font-primary decoration-none;
}
:global(.form-button:not(select):hover) {
@apply bg-accent decoration-none transition-all duration-500 text-secondary;
}
:global(.form-textbox) {
@apply bg-secondary text-text rounded-2 border-none p-2 font-primary outline-none;
}
</style>

View File

@ -1,51 +0,0 @@
<script lang="ts">
export let inputType: string = "";
export let inputName: string = "";
export let inputPlaceholder: string = "";
export let select: boolean = true;
export let selectType: string = "";
export let input2: boolean = false;
export let input2Type: string = "";
export let input2Name: string = "";
export let input2Placeholder: string = "";
</script>
<div
class="flex items-center flex-row gap-4 children:w-[50%] lt-sm:(flex-col items-start justify-center children:w-[calc(100%-1rem)])"
>
<input
type={inputType}
name={inputName}
class="form-textbox"
placeholder={inputPlaceholder}
required
/>
{#if input2}
<input
type={input2Type}
name={input2Name}
class="form-textbox"
placeholder={input2Placeholder}
required
/>
{/if}
{#if select}
<select
name={selectType}
required
class="form-button"
>
<slot />
</select>
{/if}
</div>
{#if select}
<style>
@media screen and (max-width: 640px) {
div > :nth-child(2) {
width: 100%;
}
}
</style>
{/if}

View File

@ -1,11 +0,0 @@
<script lang="ts">
export let content: string = "";
export let icon: string = "";
</script>
<div class="flex items-center gap-2">
{#if icon}
<div class={icon} />
{/if}
<b>{content}</b>
</div>

View File

@ -1,15 +0,0 @@
<script lang="ts">
export let id: string = "";
export let name: string = "";
export let placeholder: string = "";
</script>
<textarea
{id}
{name}
rows="4"
cols="25"
required
class="form-textbox resize-y"
{placeholder}
/>

View File

@ -1,5 +0,0 @@
export { default as Note } from "./Note.svelte";
export { default as Captcha } from "./Captcha.svelte";
export { default as Form } from "./Form.svelte";
export { default as Meta } from "./Meta.svelte";
export { default as TextArea } from "./TextArea.svelte";

View File

@ -1,20 +1,8 @@
<script lang="ts">
export let title: string = "";
export let description: string = "";
export let marginTop: string = "";
let styles: string = "";
export { styles as style };
</script>
<div
class="flex flex-col items-center justify-center children:(m-4 p-0 text-center)"
style="margin-top: {marginTop}%; {styles}"
>
{#if title}
<h1 class="text-5xl font-800 text-accent">{title}</h1>
{/if}
{#if description}
<p class="text-3xl text-text">{description}</p>
{/if}
<slot />
</div>
<div class="flex flex-col gap-6 items-center text-center mt-[7%]">
<h1 class="text-5xl font-extrabold text-accent my-0 border-b-0 pb-0">Project Segfault</h1>
<p class="text-2xl">Open source development and hosted services.</p>
<div class="flex flex-row gap-4">
<a href="/instances" class="button"><div class="i-ic:outline-computer" /> Instances</a>
<a href="/donate" class="button !bg-amber !text-black"><div class="i-ic:outline-attach-money" /> Donate</a>
</div>
</div>

View File

@ -1,19 +0,0 @@
<script lang="ts">
export let url: string = "";
export let icon: string = "";
export let title: string = "";
export let bg: string = "";
export let color: string = "";
export let styles: string = "";
</script>
<a
href={url}
class="decoration-none bg-accent px-3 py-2 text-primary rounded-2 transition-filter hover:brightness-125 flex items-center w-fit gap-2"
style="background-color: {bg}; color: {color}; {styles}"
>
{#if icon}
<div class={icon} />
{/if}
{title}
</a>

View File

@ -6,61 +6,51 @@
$: currentPage = $page.url.pathname;
let showMenuButton = false;
$: innerWidth = 0;
let innerWidth: number = 0;
$: isMobile = innerWidth < 1090;
$: showMenuButton = innerWidth < 1090;
$: hasJS = typeof Window !== "undefined";
let menuOpen = false;
$: showMenuButton = hasJS && isMobile;
$: menuOpen = innerWidth > 1090;
$: menuOpen = !hasJS || hasJS && !isMobile;
$: menuOpenMobile = innerWidth < 1090 && menuOpen;
$: menuOpenMobile = isMobile && menuOpen;
let showThemeToggle: boolean = true;
$: showThemeToggle = hasJS;
const toggleMenu = () => {
menuOpen = !menuOpen;
};
const toggleMenu = () => menuOpen = !menuOpen;
const handleNavigation = () => {
if (showMenuButton) {
menuOpen = false;
} else {
menuOpen = true;
}
};
const handleNavigation = () => showMenuButton ? menuOpen = false : menuOpen = true;
const menus = [
{ name: "Instances", url: "/instances" },
{ name: "Donate", url: "/donate" },
// { name: "Pubnix", url: "/pubnix" },
{ name: "Contact us", url: "/contact" },
{ name: "Our team", url: "/team" },
{ name: "Timeline", url: "/timeline" },
{ name: "Contact", url: "/contact" },
{ name: "Team", url: "/team" },
{
name: "Wiki",
url: "https://wiki.projectsegfau.lt/",
external: true
},
{ name: "Blog", url: "/blog" },
{ name: "Legal", url: "/legal" },
{
name: "Status",
url: "https://status.projectsegfau.lt/",
external: true
}
},
{ name: "Legal", url: "/legal" }
];
$: if (typeof Window === "undefined") {
menuOpen = true;
showMenuButton = false;
showThemeToggle = false;
}
</script>
<svelte:window bind:innerWidth />
<nav
class="bg-primary {menuOpenMobile ? "border-none" : "border-b border-b-solid border-b-grey"} flex p-2 flex-col justify-between nav:(flex-row items-center)"
class:hasJSNav={typeof Window !== "undefined"}
class:noJSNav={typeof Window === "undefined"}
class="bg-primary {menuOpenMobile ? "border-none" : "border-b border-b-solid border-b-grey"} {isMobile ? "py-2" : ""} flex px-2 flex-col justify-between nav:(flex-row items-center)"
class:hasJSNav={hasJS}
class:noJSNav={!hasJS}
>
<div class="flex flex-row items-center justify-between">
<a
@ -85,9 +75,9 @@
{#if menuOpen}
<div
class="links"
class:hasJS={typeof Window !== "undefined"}
class:noJS={typeof Window === "undefined"}
class="links {isMobile ? "!children:py-2" : ""}"
class:hasJS={hasJS}
class:noJS={!hasJS}
transition:slide="{{duration: 300, easing: quintOut }}"
>
{#each menus as { url, name, external }}
@ -134,7 +124,7 @@
}
.links > * {
@apply p-2 cursor-pointer text-text decoration-none transition-color duration-250 text-sm font-500 flex items-center hover\:text-accent;
@apply py-4 px-2 cursor-pointer text-text decoration-none transition-color duration-250 text-sm font-500 flex items-center hover\:(text-accent);
}
.icon > span {

View File

@ -17,7 +17,7 @@
<button
on:click={toggle}
class="cursor-pointer flex items-center py-1 px-0 bg-transparent border-0 font-primary color-text"
class="text-text flex items-center text-sm"
>
<div class="i-ic:{theme === 'dark' ? 'outline-light-mode' : 'outline-dark-mode'} h-4 w-4" />
<span class="ml-2 nav:(hidden ml-1)">Toggle theme</span>

8
src/lib/PMargin.svelte Normal file
View File

@ -0,0 +1,8 @@
<script lang="ts">
let classes: string = "";
export { classes as class };
</script>
<p class="my-4 {classes}">
<slot />
</p>

View File

@ -0,0 +1,10 @@
<script>
import { fly } from "svelte/transition";
export let pathname = "";
</script>
{#key pathname}
<div in:fly={{ x: -10, duration: 250, delay: 250 }} out:fly={{ x: 5, duration: 250 }}>
<slot />
</div>
{/key}

View File

@ -1,52 +0,0 @@
@font-face {
font-family: "JetBrains Mono";
src: url("/JetBrainsMono.woff2");
font-display: swap;
}
html, html.light {
--accent: #00a584;
--accent-translucent: #00a58498;
--font-primary: "JetBrains Mono", monospace;
--primary: #ffffff;
--secondary: #eeeeee;
--tertiary: #939393;
--text: #444444;
--grey: #cecece;
--alt: #ddd;
--alt-text: #333;
color-scheme: light;
}
@media (prefers-color-scheme: dark) {
html {
--primary: #151515;
--secondary: #252525;
--tertiary: #353535;
--text: #ffffffde;
--grey: #5454547a;
--alt: #333;
--alt-text: #ddd;
color-scheme: dark;
}
}
body {
@apply font-primary bg-primary text-text m-0 flex flex-col relative min-h-screen leading-relaxed transition-all duration-250;
}
::selection {
@apply bg-accentTranslucent;
}
main {
@apply p-4;
}
a {
@apply underline text-accent underline-offset-5 transition-filter duration-250;
}
a:hover {
@apply brightness-125;
}

View File

@ -1,9 +0,0 @@
import { env } from "$env/dynamic/private";
const fetchApi = async (action: string, additional?: string) => {
const data = await fetch("https://blog.projectsegfau.lt/ghost/api/content/" + action + "/?key=" + env.GHOST_API_KEY + "&include=authors,tags&limit=all&formats=html,plaintext" + (additional ? additional : ""));
return data.json();
};
export default fetchApi;

View File

@ -1,3 +0,0 @@
const map = new Map();
export default map;

View File

@ -1,14 +0,0 @@
import { env } from "$env/dynamic/private";
import { building } from "$app/environment";
import { MongoClient } from "mongodb";
import type { Db } from "mongodb";
export let db: Db;
if (!building) {
const client = new MongoClient(env.DB_URL);
await client.connect();
db = client.db("website");
}

View File

@ -1,5 +1,8 @@
<script lang="ts">
<script>
import { page } from '$app/stores';
</script>
<h1>{$page.status}: {$page.error?.message}</h1>
<h1>
{$page.status}
<p class="text-base font-normal mt-4">{$page.error?.message}</p>
</h1>

View File

@ -1,43 +1,30 @@
<script>
import "$lib/app.css";
import Nav from "$lib/Nav.svelte";
import Footer from "$lib/Footer.svelte";
import SvelteSeo from "svelte-seo";
<script lang="ts">
import "uno.css";
import "@unocss/reset/tailwind.css";
import "../app.css";
import Nav from "$lib/Nav/Nav.svelte";
import Footer from "$lib/Footer.svelte";
import { page } from "$app/stores";
import PageTransition from "$lib/PageTransition.svelte";
import type { LayoutData } from "./$types";
export let data: LayoutData;
</script>
<SvelteSeo
title="Project Segfault"
description="Open source development and hosted services."
canonical="https://projectsegfau.lt/"
keywords="projectsegfault, project segfault, privacy services, privacy instances, invidious, nitter, searxng"
openGraph={{
url: "https://projectsegfau.lt/",
title: "Project Segfault",
description: "Open source development and hosted services.",
images: [
{
url: "/ProjectSegfault_Desktop_16-9.png",
width: 850,
height: 650,
alt: "Our banner"
}
]
}}
/>
<svelte:head>
<script
defer
data-domain="projectsegfau.lt"
src="https://analytics.projectsegfau.lt/js/plausible.js"
></script>
<title>{$page.data.title} | Project Segfault {$page.url.pathname.startsWith("/blog") ? "blog" : ""}</title>
{#if $page.data.description}
<meta name="description" content={$page.data.description} />
{/if}
</svelte:head>
<Nav />
<main>
<slot />
</main>
<main class="px-8 mb-8 max-w-90rem m-auto">
<PageTransition pathname={data.pathname}>
<slot />
</PageTransition>
</main>
<Footer />

7
src/routes/+layout.ts Normal file
View File

@ -0,0 +1,7 @@
import type { LayoutLoad } from "./$types";
export const load = (async ({ url: { pathname } }) => {
return {
pathname
}
}) satisfies LayoutLoad

View File

@ -1,21 +1,27 @@
import type { PageServerLoad } from "./$types";
import { marked } from "marked";
import sanitizeHtml from "sanitize-html";
import { db } from "$lib/server/db";
import axios from "axios";
import { Agent } from "https";
import { env } from "$env/dynamic/private";
export const load: PageServerLoad = async () => {
const agent = new Agent({
family: 4
});
const collection = db.collection("announcements");
const data = await collection.find({}, { projection: { _id: 0 } }).toArray();
if (data.length !== 0 || data[0] !== undefined) {
const sanitizedContent = sanitizeHtml(data[0].title)
return {
announcements: data[0],
content: marked(sanitizedContent)
}
export const load = (async () => {
const meta = {
title: "Home",
description: "Open source development and hosted services."
}
};
try {
const res = await axios(env.KUMA_URL, { httpsAgent: agent });
if (res.status === 200) {
return { announcements: res.data, ...meta };
} else {
return { error: true, message: "Error: " + res.status };
}
} catch (err) {
return { error: true, message: "Error: " + err };
}
}) satisfies PageServerLoad;

View File

@ -1,41 +1,52 @@
<script lang="ts">
import SvelteSeo from "svelte-seo";
import Hero from "$lib/Hero.svelte";
import LinkButton from "$lib/LinkButton.svelte";
import Announcements from "./Announcements.svelte";
import sanitizeHtml from "sanitize-html";
import type { PageData } from "./$types";
export let data: PageData;
let description: string = "Open source development and hosted services.";
$: backgroundColor = "#5cdd8b";
$: if (!data.error) {
if (data.announcements.incident) {
if (data.announcements.incident.style === "info") {
backgroundColor = "#0dcaf0";
} else if (data.announcements.incident.style === "warning") {
backgroundColor = "#f8a306";
} else if (data.announcements.incident.style === "danger") {
backgroundColor = "#dc3545";
} else if (data.announcements.incident.style === "primary") {
backgroundColor = "#5cdd8b";
} else if (data.announcements.incident.style === "light") {
backgroundColor = "#f8f9fa";
} else if (data.announcements.incident.style === "dark") {
backgroundColor = "#212529";
}
}
}
</script>
<SvelteSeo
title="Home | Project Segfault"
{description}
/>
<Hero />
<Hero
title="Project Segfault"
{description}
marginTop="4"
>
<div
class="flex flex-col sm:flex-row justify-center items-center gap-4 m-4"
>
<LinkButton
url="/instances"
title="Explore our instances"
icon="i-ic:outline-room-service text-xl"
/>
<LinkButton
url="/donate"
icon="i-ic:outline-attach-money text-xl"
title="Donate"
bg="#F6C915"
color="#151515"
/>
</div>
</Hero>
{#if !data.error}
{#if data.announcements.incident}
<div class="flex flex-col items-center mt-16">
<div class="flex flex-col prose break-words rounded p-4 lt-sm:max-w-74 sm:(p-8) {backgroundColor === "#212529" ? "text-text" : "text-black"}" style="background-color: {backgroundColor};">
{#if data.announcements.incident.title}
<span class="text-xl font-semibold">{data.announcements.incident.title}</span>
{/if}
<Announcements {data} />
{#if data.announcements.incident.content}
<p>{@html sanitizeHtml(data.announcements.incident.content.replace(/\n/g, "<br />"))}</p>
{/if}
<span>Created - {data.announcements.incident.createdDate}</span>
{#if data.announcements.incident.lastUpdatedDate}
<span>Updated - {data.announcements.incident.lastUpdatedDate}</span>
{/if}
</div>
</div>
{/if}
{:else}
<p>{data.message}</p>
{/if}

View File

@ -1,96 +0,0 @@
<script lang="ts">
import type { PageData } from "./$types";
export let data: PageData;
let announcements = data.announcements;
import dayjs from "dayjs";
</script>
{#if announcements}
<div class="announcements">
<div class="flex justify-center mt-16">
<div
class="announcement !text-[#252525] p-6 rounded-2 w-fit flex flex-col gap-4"
>
<div
class="flex gap-4 flex-col sm:flex-row border-b-2 border-b-solid p-2 pt-0"
>
{#if announcements.severity === "info"}
<div class="flex items-center gap-2">
<div class="i-ic:outline-info text-xl" />
<span>Info</span>
</div>
{:else if announcements.severity === "low"}
<div class="flex items-center gap-2">
<div class="i-ic:outline-check-circle text-xl" />
<span>Resolved</span>
</div>
{:else if announcements.severity === "medium"}
<div class="flex items-center gap-2">
<div class="i-ic:outline-priority-high text-xl" />
<span>Attention</span>
</div>
{:else if announcements.severity === "high"}
<div class="flex items-center gap-2">
<div class="i-ic:outline-block text-xl" />
<span>Attention</span>
</div>
{/if}
<span class="flex items-center gap-2">
<div class="i-ic:outline-person text-xl" />
{announcements.author}
</span>
<span class="flex items-center gap-2">
<div class="i-ic:outline-calendar-month text-xl" />
{dayjs
.unix(announcements.created)
.format("DD/MM/YYYY HH:mm")}
</span>
</div>
<div class="title">
<div class="prose">
{@html data.content}
</div>
</div>
{#if announcements.link}
<div class="read-more">
<a
href={announcements.link}
class="!text-[#252525]">Read more...</a
>
</div>
{/if}
</div>
</div>
</div>
{#if announcements.severity === "info"}
<style>
.announcement {
background-color: #8caaee;
}
</style>
{:else if announcements.severity === "low"}
<style>
.announcement {
background-color: #a6d189;
}
</style>
{:else if announcements.severity === "medium"}
<style>
.announcement {
background-color: #e5c890;
}
</style>
{:else if announcements.severity === "high"}
<style>
.announcement {
background-color: #e78284;
}
</style>
{/if}
{/if}

View File

@ -1,8 +0,0 @@
import type { LayoutServerLoad } from "./$types"
import { redirect } from "@sveltejs/kit";
export const load: LayoutServerLoad = async ({ locals }) => {
if (!await locals.getSession()) {
throw redirect(302, "/login");
}
}

View File

@ -1,10 +0,0 @@
<slot />
<style>
:global(.col) {
display: flex;
flex-direction: column;
width: fit-content;
gap: 10px;
}
</style>

View File

@ -1,5 +1,8 @@
<h1>Admin dashboard</h1>
<script lang="ts">
import type { PageData } from "./$types";
<div class="col">
<a href="/admin/announcements">Announcements</a>
</div>
export let data: PageData;
</script>
<h1>{data.title}</h1>
<p>Nothing here yet.</p>

View File

@ -0,0 +1,7 @@
import type { PageLoad } from "./$types";
export const load = (() => {
return {
title: "Admin dashboard"
};
}) satisfies PageLoad;

View File

@ -1,51 +0,0 @@
import type { Actions } from "./$types";
import Joi from "joi";
import { fail } from "@sveltejs/kit";
import { db } from "$lib/server/db";
export const actions: Actions = {
add: async ({ request, locals }) => {
if (!await locals.getSession()) {
return fail(401, { addError: true, addMessage: "You must be logged in to post an announcement." });
} else {
const formData = await request.formData();
const BodyTypeSchema = Joi.object({
title: Joi.string().required(),
severity: Joi.string().required(),
author: Joi.string().required(),
link: Joi.string().optional().allow("")
});
if (BodyTypeSchema.validate(Object.fromEntries(formData.entries())).error) {
return fail(400, { addError: true, addMessage: String(BodyTypeSchema.validate(Object.fromEntries(formData.entries())).error) });
} else {
const now = Math.floor(Date.now() / 1000);
const data = {
...Object.fromEntries(formData.entries()),
created: now
};
const collection = db.collection("announcements");
await collection.deleteMany({});
await collection.insertOne(data);
return { addSuccess: true, addMessage: "Your announcement has been posted." };
}
}
},
delete: async ({ locals }) => {
if (!await locals.getSession()) {
return fail(401, { deleteError: true, deleteMessage: "You must be logged in to delete an announcement." });
} else {
const collection = db.collection("announcements");
await collection.deleteMany({});
return { deleteSuccess: true, deleteMessage: "Your announcement has been deleted." };
}
}
}

View File

@ -1,55 +0,0 @@
<script lang="ts">
import type { ActionData } from '.$/types';
export let form: ActionData;
</script>
<div class="col">
<h1>Post Announcement</h1>
<form
action="?/add"
method="POST"
class="col"
>
<select id="severity" name="severity" required>
<option value="" selected disabled>
Select severity of announcement
</option>
<option value="info">Information announcement</option>
<option value="low">Low severity</option>
<option value="medium">Medium severity</option>
<option value="high">High severity</option>
</select>
<textarea
name="title"
rows="4"
cols="25"
placeholder="The announcement text"
></textarea>
<input
type="text"
name="link"
placeholder="Your link for more details"
/>
<input type="text" name="author" placeholder="Your name" />
{#if form?.addSuccess}
{form.addMessage}
{/if}
{#if form?.addError}
{form.addMessage}
{/if}
<button type="submit">Submit</button>
</form>
<h1 style="margin-top: 20px">Delete Announcement</h1>
<form
action="?/delete"
method="POST"
class="col"
>
{#if form?.deleteSuccess}
{form.deleteMessage}
{/if}
<button type="submit">Delete</button>
</form>
</div>

View File

@ -1,12 +0,0 @@
import type { RequestHandler } from './$types';
import map from "$lib/map";
export const GET = (() => {
const data = map.get("data");
return new Response(JSON.stringify(data), {
headers: {
"content-type": "application/json; charset=utf-8"
}
});
}) satisfies RequestHandler;

View File

@ -1,10 +1,13 @@
import type { PageServerLoad } from "./$types";
import fetchApi from "$lib/ghost";
import fetchGhost from "./fetchGhost";
export const load = (async () => {
const data = await fetchApi("posts");
export const load = (async ({ fetch }) => {
const meta = {
title: "Blog"
}
return {
posts: data.posts
posts: fetchGhost("posts"),
...meta
};
}) satisfies PageServerLoad;
}) satisfies PageServerLoad;

View File

@ -1,50 +1,27 @@
<script lang="ts">
import Hero from "$lib/Hero.svelte";
import LinkButton from "$lib/LinkButton.svelte";
import type { PageData } from "./$types";
export let data: PageData;
import { PostsContainer, PostOuter, Title, Meta, ReadMore } from "$lib/BlogCard";
</script>
<svelte:head>
<title>Blog | Project Segfault</title>
<meta
name="description"
content="Project Segfault's blog"
/>
</svelte:head>
<div class="h1-no-lg flex flex-col sm:(flex-row items-center) gap-4">
<span class="text-4xl font-bold">{data.title}</span>
<a href="/blog/tags" class="button sm:w-fit"><div class="i-ic:outline-bookmarks" /> Tags</a>
<a href="/blog/authors" class="button sm:w-fit"><div class="i-ic:outline-people text-xl" /> Authors</a>
<a href="https://blog.projectsegfau.lt/rss" class="button sm:w-fit !bg-[#ee802f]"><div class="i-simple-icons:rss" /> RSS</a>
</div>
<Hero marginTop="4">
<h1 class="text-5xl font-800">
<span class="text-accent">Project Segfault</span> blog
</h1>
<div
class="flex flex-col sm:flex-row justify-center items-center gap-4 m-4"
>
<LinkButton
url="/blog/tags"
title="Tags"
icon="i-ic:outline-bookmarks"
/>
<LinkButton
url="/blog/authors"
title="Authors"
icon="i-ic:outline-people text-xl"
/>
<LinkButton
url="https://blog.projectsegfau.lt/rss/"
title="RSS"
icon="i-simple-icons:rss"
bg="#ee802f"
/>
</div>
</Hero>
<PostsContainer hasMt isHome>
{#each data.posts as post}
<PostOuter>
<Title {post} />
<Meta {post} />
<ReadMore {post} />
</PostOuter>
{/each}
</PostsContainer>
{#if !data.posts.error}
<PostsContainer>
{#each data.posts.posts as post}
<PostOuter>
<Title {post} />
<Meta {post} />
<ReadMore {post} />
</PostOuter>
{/each}
</PostsContainer>
{:else}
<p>{data.posts.message}</p>
{/if}

View File

@ -1,13 +1,18 @@
import type { PageServerLoad } from "./$types";
import fetchApi from "$lib/ghost";
import fetchGhost from "../fetchGhost";
export const load = (async ({ params }) => {
const data = await fetchApi("posts/slug/" + params.title);
export const load = (async ({ params, fetch }) => {
const data = await fetchGhost("posts/slug/" + params.title);
const allPosts = await fetchApi("posts");
const allPosts = await fetchGhost("posts");
const meta = {
title: !allPosts.error ? data.posts[0].title : ""
}
return {
post: data.posts[0],
allPosts: allPosts
post: !allPosts.error ? data.posts[0] : {},
allPosts: allPosts,
...meta
};
}) satisfies PageServerLoad;
}) satisfies PageServerLoad;

View File

@ -4,46 +4,46 @@
import type { PageData } from "./$types";
export let data: PageData;
$: index = data.allPosts.posts.findIndex((post: { slug: string; }) => post.slug === data.post.slug);
$: next = data.allPosts.posts[index - 1];
$: previous = data.allPosts.posts[index + 1];
$: index = !data.allPosts.error ? data.allPosts.posts.findIndex((post: { slug: string; }) => post.slug === data.post.slug) : null;
$: next = !data.allPosts.error ? data.allPosts.posts[index - 1] : null;
$: previous = !data.allPosts.error ? data.allPosts.posts[index + 1] : null;
</script>
<svelte:head>
<title>{data.post.title} | Project Segfault Blog</title>
</svelte:head>
<PostOuter url={data.post.url} isPost>
<div class="text-center mt-4 flex flex-col items-center gap-4">
<Title post={data.post} isPost />
<Meta post={data.post} isPost />
{#if !data.allPosts.error}
<PostOuter url={data.post.url} isPost>
<div class="text-center mt-4 flex flex-col items-center gap-4">
<Title post={data.post} isPost />
<Meta post={data.post} isPost />
</div>
<PostContent {data} />
</PostOuter>
<div class="flex flex-row flex-wrap justify-center my-4">
<PostsContainer>
{#if previous}
<PostOuter>
<h1 class="more-posts">Previous post</h1>
<Title post={previous} />
<Meta post={previous} />
<ReadMore post={previous} />
</PostOuter>
{/if}
{#if next}
<PostOuter>
<h1 class="more-posts">Next post</h1>
<Title post={next} />
<Meta post={next} />
<ReadMore post={next} />
</PostOuter>
{/if}
</PostsContainer>
</div>
<PostContent {data} />
</PostOuter>
<div class="flex flex-row flex-wrap justify-center my-4">
<PostsContainer>
{#if previous}
<PostOuter>
<h1 class="more-posts">Previous post</h1>
<Title post={previous} />
<Meta post={previous} />
<ReadMore post={previous} />
</PostOuter>
{/if}
{#if next}
<PostOuter>
<h1 class="more-posts">Next post</h1>
<Title post={next} />
<Meta post={next} />
<ReadMore post={next} />
</PostOuter>
{/if}
</PostsContainer>
</div>
<style>
.more-posts {
@apply border-b-solid border-b-grey border-b-1 m-0;
}
</style>
<style>
.more-posts {
@apply border-b-solid border-b-grey border-b-1 m-0;
}
</style>
{:else}
<p>{data.allPosts.message}</p>
{/if}

View File

@ -1,11 +1,15 @@
import type { PageServerLoad } from "./$types";
import fetchApi from "$lib/ghost";
import fetchGhost from "../fetchGhost";
export const load: PageServerLoad = async () => {
const data = await fetchApi("authors");
export const load = (async ({ fetch }) => {
const data = await fetchGhost("authors");
const meta = {
title: "Blog authors"
}
return {
authors: data.authors
authors: data,
...meta
};
};
}) satisfies PageServerLoad;

View File

@ -5,11 +5,10 @@
import { SingleWordLists } from "$lib/BlogCard";
</script>
<svelte:head>
<title>Blog authors | Project Segfault Blog</title>
</svelte:head>
<h1>Blog authors</h1>
<SingleWordLists items={data.authors} name="authors" />
<h1>{data.title}</h1>
{#if !data.authors.error}
<SingleWordLists items={data.authors.authors} name="authors" />
{:else}
<p>{data.authors.message}</p>
{/if}

View File

@ -1,18 +1,26 @@
import type { PageServerLoad } from "./$types";
import fetchApi from "$lib/ghost";
import fetchGhost from "../../fetchGhost";
export const load: PageServerLoad = async ({ params }) => {
const data = await fetchApi("posts", "&filter=author:" + params.author);
export const load = (async ({ params, fetch }) => {
const data = await fetchGhost("posts", "&filter=author:" + params.author);
const authorsLoop = data.posts[0].authors.map((author: { slug: string; name: any; }) => {
const authorsLoop = !data.error ? data.posts[0].authors.map((author: { slug: string; name: any; }) => {
if (author.slug === params.author) {
return author.name;
}
});
}) : [];
const authorName = authorsLoop.filter((tag: any) => tag !== undefined)[0];
const meta = {
title: "Blog author " + authorName,
description: "Blog posts by " + authorName,
};
return {
posts: data.posts,
authorName: authorsLoop.filter((tag: any) => tag !== undefined)[0]
posts: data,
authorName,
...meta,
};
};
}) satisfies PageServerLoad;

View File

@ -5,18 +5,18 @@
import { PostsContainer, PostOuter, Title, Meta, ReadMore } from "$lib/BlogCard";
</script>
<svelte:head>
<title>Blog author {data.authorName} | Project Segfault Blog</title>
</svelte:head>
<h1>Blog author <span class="text-accent">{data.authorName}</span></h1>
<PostsContainer>
{#each data.posts as post}
<PostOuter>
<Title {post} />
<Meta {post} />
<ReadMore {post} />
</PostOuter>
{/each}
</PostsContainer>
{#if !data.posts.error}
<PostsContainer>
{#each data.posts.posts as post}
<PostOuter>
<Title {post} />
<Meta {post} />
<ReadMore {post} />
</PostOuter>
{/each}
</PostsContainer>
{:else}
<p>{data.posts.message}</p>
{/if}

View File

@ -0,0 +1,23 @@
import { env } from "$env/dynamic/private";
import axios from "axios";
import { Agent } from "https";
const agent = new Agent({
family: 4
});
const fetchGhost = async (action: string, additional?: string ) => {
try {
const request = await axios(env.GHOST_URL + "/ghost/api/content/" + action + "/?key=" + env.GHOST_API_KEY + "&include=authors,tags&limit=all&formats=html,plaintext" + (additional ? additional : ""), { httpsAgent: agent });
if (request.status === 200) {
return request.data;
} else {
return { error: true, message: "Error: " + request.status };
}
} catch (err) {
return { error: true, message: "Error: " + err };
}
}
export default fetchGhost;

View File

@ -1,10 +1,15 @@
import type { PageServerLoad } from "./$types";
import fetchApi from "$lib/ghost";
import fetchGhost from "../fetchGhost";
export const load: PageServerLoad = async () => {
const data = await fetchApi("tags");
export const load = (async () => {
const data = await fetchGhost("tags");
const meta = {
title: "Blog tags"
}
return {
tags: data.tags
tags: data,
...meta
}
};
}) satisfies PageServerLoad;

View File

@ -5,10 +5,10 @@
import { SingleWordLists } from "$lib/BlogCard";
</script>
<svelte:head>
<title>Blog tags | Project Segfault Blog</title>
</svelte:head>
<h1>{data.title}</h1>
<h1>Blog tags</h1>
<SingleWordLists items={data.tags} name="tags" />
{#if !data.tags.error}
<SingleWordLists items={data.tags.tags} name="tags" />
{:else}
<p>{data.tags.message}</p>
{/if}

View File

@ -1,17 +1,24 @@
import type { PageServerLoad } from "./$types";
import fetchApi from "$lib/ghost";
import fetchGhost from "../../fetchGhost";
export const load: PageServerLoad = async ({ params }) => {
const data = await fetchApi("posts", "&filter=tags:" + params.tag);
export const load = (async ({ params, fetch }) => {
const data = await fetchGhost("posts", "&filter=tags:" + params.tag);
const tagsLoop = data.posts[0].tags.map((tag: { slug: string; name: any; }) => {
const tagsLoop = !data.error ? data.posts[0].tags.map((tag: { slug: string; name: any; }) => {
if (tag.slug === params.tag) {
return tag.name;
}
});
}) : [];
const tagName = tagsLoop.filter((tag: any) => tag !== undefined)[0];
const meta = {
title: "Blog tag " + tagName
}
return {
posts: data.posts,
tagName: tagsLoop.filter((tag: any) => tag !== undefined)[0]
posts: data,
tagName: tagName,
...meta
}
};
}) satisfies PageServerLoad;

View File

@ -5,18 +5,18 @@
import { PostsContainer, PostOuter, Title, Meta, ReadMore } from "$lib/BlogCard";
</script>
<svelte:head>
<title>Blog tag {data.tagName} | Project Segfault Blog</title>
</svelte:head>
<h1>Blog tag <span class="text-accent">{data.tagName}</span></h1>
<PostsContainer>
{#each data.posts as post}
<PostOuter>
<Title {post} />
<Meta {post} />
<ReadMore {post} />
</PostOuter>
{/each}
</PostsContainer>
{#if !data.posts.error}
<PostsContainer>
{#each data.posts.posts as post}
<PostOuter>
<Title {post} />
<Meta {post} />
<ReadMore {post} />
</PostOuter>
{/each}
</PostsContainer>
{:else}
<p>{data.posts.message}</p>
{/if}

View File

@ -1,70 +0,0 @@
import type { Actions, PageServerLoad } from "./$types";
import { Webhook, MessageBuilder } from "discord-webhook-node";
import Joi from "joi";
import { fail } from "@sveltejs/kit";
import { env } from "$env/dynamic/private";
export const load = (() => {
return {
hcaptchaSitekey: env.HCAPTCHA_SITEKEY
}
}) satisfies PageServerLoad
export const actions: Actions = {
form: async ({ request, getClientAddress, fetch }) => {
const formData = await request.formData();
const BodyTypeSchema = Joi.object({
email: Joi.string().email().required(),
commentType: Joi.string().required(),
message: Joi.string().required(),
"h-captcha-response": Joi.string().required(),
"g-recaptcha-response": Joi.string().optional().allow("")
});
if (BodyTypeSchema.validate(Object.fromEntries(formData.entries())).error) {
return fail(400, { error: true, message: String(BodyTypeSchema.validate(Object.fromEntries(formData.entries())).error) });
} else {
const ip = getClientAddress();
const verify = await fetch("https://hcaptcha.com/siteverify", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams({
secret: env.HCAPTCHA_SECRET,
response: String(formData.get("h-captcha-response")),
remoteip: ip
})
}).then((res) => res.json())
const hook = new Webhook(env.WEBHOOK);
const data = await verify;
if (data.success) {
const embed = new MessageBuilder()
.setAuthor(
`${ip}, ${formData.get("email")}, https://abuseipdb.com/check/${ip}`
)
// @ts-ignore
.addField("Comment type", formData.get("commentType"), true)
// @ts-ignore
.addField("Message", formData.get("message"))
.setTimestamp();
hook.send(embed);
return { success: true, message: "Thanks for your message, we will get back to you as soon as possible." };
} else {
hook.send(
`IP: ${ip}, https://abuseipdb.com/check/${ip}\nfailed to complete the captcha with error: ${data["error-codes"]}.`
);
return fail(400, { error: true, message: "Captcha failed or expired, please try again. If this keeps happening, assume the captcha is broken and contact us on Matrix." + " Error: " + data["error-codes"] });
}
}
}
}

View File

@ -1,99 +1,20 @@
<script lang="ts">
import { Note, Captcha, Meta, TextArea } from "$lib/Form";
import type { ActionData, PageServerData } from "./$types";
import type { PageData } from "./$types";
export let form: ActionData;
export let data: PageServerData;
export let data: PageData;
</script>
<svelte:head>
<title>Contact us | Project Segfault</title>
</svelte:head>
<h1>Contact us</h1>
<div class="contact-form">
<h2>Contact form</h2>
<form
method="POST"
action="?/form"
id="contact-form"
class="flex flex-col gap-4 w-fit"
>
<Note
content="Your IP will be logged for anti-abuse measures."
icon="i-ic:outline-lock text-xl"
/>
<Meta
inputType="email"
inputName="email"
inputPlaceholder="Your email"
selectType="commentType"
>
<option
value=""
selected
disabled>Select a type of comment</option
>
<option value="Feedback">Feedback</option>
<option value="Suggestion">Suggestion</option>
<option value="Question">Question</option>
<option value="Bug">Bug</option>
</Meta>
<TextArea
id="comment"
name="message"
placeholder="Your message"
/>
<Captcha sitekey={data.hcaptchaSitekey}>
{#if form?.success}
{form.message}
{/if}
{#if form?.error}
{form.message}
{/if}
</Captcha>
</form>
</div>
<noscript>
<Note
content="The contact form does not work without JavaScript enabled."
icon="i-ic:outline-info text-xl"
/>
<style>
.contact-form {
display: none;
}
</style>
</noscript>
<h1>{data.title}</h1>
<h2>Matrix</h2>
<span
>We have a Matrix space for general discussion, support and announcements
about Project Segfault over at <a
href="https://matrix.to/#/#project-segfault:projectsegfau.lt/"
>this link</a
>.</span
>
<h2>Our Email</h2>
<p>We have a Matrix space for general discussion, support and announcements about Project Segfault over at <a href="https://matrix.to/#/#project-segfault:projectsegfau.lt/">this link</a>.</p>
<div class="flex flex-col gap-4">
<a href="mailto:contact@projectsegfau.lt">contact@projectsegfau.lt</a>
<h2>Email</h2>
<span class="italic">
Please be aware that Microsoft and other email providers often block non-popular emails, if you do
contact us through there, make sure to check your spam and mark it as
not-spam!
</span>
</div>
<p>Our primary email address is <a href="mailto:contact@projectsegfau.lt">contact@projectsegfau.lt</a>. You can use this as a way to send feedback or other stuff to us if you don't have or don't want to make a Matrix account. When waiting for us to answer make sure to check your spam, since some email providers block non-popular domains.</p>
<h2>People</h2>
<h2>Members</h2>
<span>
You can find ways to contact individual team members <a href="/team"
>on our team page</a
>.
</span>
<p>You can contact individual members by using the links provided in <a href="/team">the team page</a>.</p>

View File

@ -0,0 +1,7 @@
import type { PageLoad } from "./$types";
export const load = (() => {
return {
title: "Contact"
};
}) satisfies PageLoad;

View File

@ -1,50 +0,0 @@
---
title: Donate
---
<script>
import LinkButton from "$lib/LinkButton.svelte";
</script>
<svelte:head>
<title>{title} | Project Segfault</title>
</svelte:head>
# { title }
## What we do with donations
These donations primarily help us pay for our VPSes, domain names and other expenses related to crucial infrastructure we have to maintain. We also sometimes donate to developers who maintain software we rely heavily on such as our authentication provider.
## Donation methods
You can currently donate by credit card through [Liberapay](https://liberapay.com) and cryptocurrencies.
### Credit card
<LinkButton url="https://liberapay.com/ProjectSegfault/donate" icon="i-simple-icons:liberapay" title="Donate" bg="#F6C915" color="#151515" />
### Cryptocurrency
You can use [projectsegfau.lt](https://projectsegfau.lt) as a crypto wallet address in supported OpenAlias clients such as [MyMonero](https://mymonero.com/), [Electrum](https://electrum.org/) and [Electrum-LTC](https://electrum-ltc.org/).
#### Monero
Address: `47L7Qsto7XcifY3CdG18ySe5Tt83kpFLDLve9jQwbc9taPBLNGv6ZrJNUKpMG9Nj9zHgCZ4FQMSyt75e8Jvx12JFLtJyFdA`
![Monero QR code](/Monero.png)
#### Bitcoin
Address: `bc1qrc8ywgp95a6p3zausp4nff70qzstp6h8z86sxd`
![Bitcoin QR code](/Bitcoin.png)
#### Litecoin
Address: `ltc1qn3ald586h2ntt0n3zkvwsmju2e5vndgtvvgatj`
![Litecoin QR code](/Litecoin.png)
_You can find all of our financial reports on [our transparency repository on Gitea](https://git.projectsegfau.lt/ProjectSegfault/transparency/)._
<style>
code {
word-wrap: break-word;
}
</style>

View File

@ -0,0 +1,35 @@
<script lang="ts">
import CryptoInfo from "./CryptoInfo.svelte";
import type { PageData } from "./$types";
export let data: PageData;
</script>
<h1>{data.title}</h1>
<h2>What we do with donations</h2>
<p>These donations primarily help us pay for our VPSes, domain names and other expenses related to crucial infrastructure we have to maintain. We also sometimes donate to developers who maintain software we rely heavily on such as our authentication provider.</p>
<h2>Donation methods</h2>
<p>You can currently donate by credit card through <a href="https://liberapay.com">Liberapay</a> and cryptocurrencies.</p>
<h3>Credit card</h3>
<a href="https://liberapay.com/ProjectSegfault/donate" class="button !bg-amber !text-black w-fit"><div class="i-simple-icons:liberapay" /> Liberapay</a>
<h3>Cryptocurrencies</h3>
<p>You can use <a href="https://projectsegfau.lt">projectsegfau.lt</a> as a crypto wallet address in supported OpenAlias clients such as <a href="https://mymonero.com">MyMonero</a>, <a href="https://electrum.org">Electrum</a> and <a href="https://electrum-ltc.org">Electrum-LTC</a>.</p>
<h4>Monero</h4>
<CryptoInfo address="47L7Qsto7XcifY3CdG18ySe5Tt83kpFLDLve9jQwbc9taPBLNGv6ZrJNUKpMG9Nj9zHgCZ4FQMSyt75e8Jvx12JFLtJyFdA" qr="Monero.png" />
<h4>Bitcoin</h4>
<CryptoInfo address="bc1qrc8ywgp95a6p3zausp4nff70qzstp6h8z86sxd" qr="Bitcoin.png" />
<h4>Litecoin</h4>
<CryptoInfo address="ltc1qn3ald586h2ntt0n3zkvwsmju2e5vndgtvvgatj" qr="Litecoin.png" />

View File

@ -0,0 +1,7 @@
import type { PageLoad } from "./$types";
export const load = (() => {
return {
title: "Donate"
};
}) satisfies PageLoad;

View File

@ -0,0 +1,18 @@
<script lang="ts">
export let address: string = "";
export let qr: string = "";
</script>
{#if address}
<details class="p-0">
<summary>Address</summary>
<code class="break-words whitespace-normal">49burTxWHyqa9NkkC9PV33D79PrwARMq8aic4XezTx36i66qyLA3afYXicycTTA5st93CV5Rr9AGkKpeE5GPueRN2PkfFQN</code>
</details>
{/if}
{#if qr}
<details class="p-0">
<summary>QR code</summary>
<img src="/qr/{qr}" alt="QR code" class="mt-2" />
</details>
{/if}

View File

@ -1,9 +1,10 @@
import instances from "./instances";
import type { PageServerLoad } from "./$types";
export const load: PageServerLoad = async ({ fetch }) => {
return {
instances: await fetch("/api/status").then(
(res) => res.json()
)
};
};
export const load = (() => {
const meta = {
title: "Instances"
}
return { instances, ...meta };
}) satisfies PageServerLoad;

View File

@ -1,91 +1,31 @@
<script lang="ts">
import { CardInner, CardOuter, LinksOuter, Link } from "$lib/Card";
import InstanceLink from "./InstanceLink.svelte";
import dayjs from "dayjs";
import type { PageData } from "./$types";
export let data: PageData;
</script>
<svelte:head>
<title>Our instances | Project Segfault</title>
<meta
name="description"
content="Our collection of instances."
/>
</svelte:head>
<div class="h1-no-lg flex flex-col sm:(flex-row items-center) gap-4 !mb-0">
<span class="text-4xl font-bold">{data.title}</span>
<a href="/instances/advanced" class="button sm:w-fit"><div class="i-ic:outline-computer" /> Advanced</a>
</div>
<h1>Our instances</h1>
<div class="flex flex-col gap-4">
<CardOuter>
<div class="flex flex-col">
{#each data.instances as category}
<div class="flex flex-col">
{#each data.instances.status as group}
<h2>{group.name}</h2>
<div class="flex flex-row flex-wrap gap-8">
{#each group.data as item}
<CardInner
title={item.name}
description={item.description}
icon={item.icon}
>
<LinksOuter>
{#if item.geo}
<InstanceLink
url={item.geo}
item={item.statusGeo}
type="geo"
/>
{/if}
{#if item.eu}
<InstanceLink
url={item.eu}
item={item.statusEu}
type="eu"
/>
{/if}
{#if item.us}
<InstanceLink
url={item.us}
item={item.statusUs}
type="us"
/>
{/if}
{#if item.bp}
<InstanceLink
url={item.bp}
item={item.statusBp}
type="backup"
/>
{/if}
{#if item.tor}
<InstanceLink
url={item.tor}
type="tor"
/>
{/if}
{#if item.torBp}
<InstanceLink
url={item.torBp}
type="torBp"
/>
{/if}
</LinksOuter>
</CardInner>
{/each}
</div>
{/each}
<h2>{category.name}</h2>
<div class="flex flex-row flex-wrap gap-4">
{#each category.data as instance}
<a href={instance.geo || instance.eu} class="flex flex-row items-center gap-4 rounded bg-secondary p-4 w-110 no-underline text-text">
{#if instance.icon}
<img src={instance.icon} alt="{instance.name} logo" class="h-20 rounded">
{/if}
<div>
<span class="text-2xl">{instance.name}</span>
<p>{instance.description}</p>
</div>
</a>
{/each}
</div>
</div>
</CardOuter>
<span class="bg-secondary w-fit p-2 rounded-2"
>Instances status last updated: {dayjs
.unix(data.instances.updated)
.format("DD/MM/YYYY HH:mm:ss")}
</span>
</div>
{/each}
</div>

View File

@ -1,50 +0,0 @@
<script lang="ts">
import { Link } from "$lib/Card";
export let url: string;
export let item: any = 200;
export let type: "geo" | "eu" | "us" | "backup" | "tor" | "torBp" = "geo";
</script>
{#if type === "tor" || type === "torBp"}
<Link
{url}
class="torcolored"
>
<div class="flex flex-row items-center gap-1">
<div class="i-simple-icons:tor {type === "torBp" ? "" : "h-6 w-6"}" />
{#if type === "torBp"}
<span class="text-base">Backup</span>
{/if}
</div>
</Link>
{:else}
<Link
{url}
class="web {item === 200
? ''
: 'pointer-events-none cursor-default opacity-50'}"
>
<div class="flex items-center gap-2 text-base">
<div
class={item === 200
? "i-ic:outline-open-in-new text-xl"
: "i-ic:outline-close"}
/>
<span>
{#if item !== 200}
({item})
{/if}
{#if type === "geo"}
GeoDNS
{:else if type === "eu"}
EU
{:else if type === "us"}
US
{:else if type === "backup"}
Backup
{/if}
</span>
</div>
</Link>
{/if}

View File

@ -0,0 +1,10 @@
import instances from "../instances";
import type { PageServerLoad } from "./$types";
export const load = (() => {
const meta = {
title: "Instances"
}
return { instances, ...meta };
}) satisfies PageServerLoad;

View File

@ -0,0 +1,44 @@
<script lang="ts">
import type { PageData } from "./$types";
export let data: PageData;
</script>
<div class="h1-no-lg flex flex-col sm:(flex-row items-center) gap-4 !mb-0">
<span class="text-4xl font-bold">{data.title}</span>
<a href="/instances" class="button sm:w-fit"><div class="i-ic:outline-computer" /> Simple</a>
</div>
<div class="flex flex-col">
{#each data.instances as category}
{#each category.data as instance}
<h2>{instance.name}</h2>
<div class="flex flex-row gap-2">
{#if instance.geo}
<a href={instance.geo}>GeoDNS</a>
{/if}
{#if instance.eu}
<a href={instance.eu}>EU</a>
{/if}
{#if instance.us}
<a href={instance.us}>US</a>
{/if}
{#if instance.bp}
<a href={instance.bp}>Backup</a>
{/if}
{#if instance.tor}
<a href={instance.tor}>Tor</a>
{/if}
{#if instance.torBp}
<a href={instance.torBp}>Tor backup</a>
{/if}
{#if instance.i2p}
<a href={instance.i2p}>I2P</a>
{/if}
{#if instance.i2pBp}
<a href={instance.i2pBp}>I2P backup</a>
{/if}
</div>
{/each}
{/each}
</div>

View File

@ -1,14 +1,23 @@
import axios from "axios";
interface Instances {
name: string;
data: Instance[];
}
const fetchStatus = (domain: string) => {
const req = axios("https://" + domain, { timeout: 5000 })
.then((res) => res.status)
.catch((error) => (error.response ? error.response.status : 500));
interface Instance {
name: string;
description: string;
geo?: string;
eu?: string;
us?: string;
bp?: string;
tor?: string;
torBp?: string;
i2p?: string;
i2pBp?: string;
icon?: string;
}
return req;
};
const statusData = [
const instances: Instances[] = [
{
name: "Privacy front-ends",
data: [
@ -20,88 +29,74 @@ const statusData = [
bp: "https://inv.bp.projectsegfau.lt",
tor: "http://inv.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
torBp: "http://invbp.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/invidious.svg",
statusEu: await fetchStatus("invidious.projectsegfau.lt"),
statusUs: await fetchStatus("inv.us.projectsegfau.lt"),
statusBp: await fetchStatus("inv.bp.projectsegfau.lt")
i2p: "http:////pjsfhqamc7k6htnumrvn4cwqqdoggeepj7u5viyimgnxg3gar72q.b32.i2p",
i2pBp: "http://pjsfi2szfkb4guqzmfmlyq4no46fayertjrwt4h2uughccrh2lvq.b32.i2p",
icon: "/icons/invidious.svg"
},
{
name: "Librarian",
description: "A frontend for LBRY/Odysee.",
eu: "https://lbry.projectsegfau.lt/",
geo: "https://lbry.g.projectsegfau.lt/",
geo: "https://lbry.projectsegfau.lt/",
eu: "https://lbry.eu.projectsegfau.lt/",
tor: "http://lbry.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/librarian.png",
statusEu: await fetchStatus("lbry.projectsegfau.lt"),
statusGeo: await fetchStatus("lbry.g.projectsegfau.lt")
i2p: "http://pjsf7uucpqf2crcmfo3nvwdmjhirxxjfyuvibdfp5x3af2ghqnaa.b32.i2p",
icon: "/icons/librarian.png"
},
{
name: "Libreddit",
description: "A frontend for Reddit.",
eu: "https://libreddit.projectsegfau.lt/",
geo: "https://libreddit.projectsegfau.lt/",
eu: "https://libreddit.eu.projectsegfau.lt/",
us: "https://libreddit.us.projectsegfau.lt",
geo: "https://libreddit.g.projectsegfau.lt/",
tor: "http://libreddit.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/libreddit.png",
statusEu: await fetchStatus("libreddit.projectsegfau.lt"),
statusUs: await fetchStatus("libreddit.us.projectsegfau.lt"),
statusGeo: await fetchStatus("libreddit.g.projectsegfau.lt")
i2p: "http://pjsfkref7g66mji45kyccqnn5hmjtjp3cfodozabpyplj2rmv5sa.b32.i2p",
icon: "/icons/libreddit.png"
},
{
name: "Nitter",
description: "A frontend for Twitter.",
eu: "https://nitter.projectsegfau.lt/",
geo: "https://nitter.projectsegfau.lt/",
eu: "https://nitter.eu.projectsegfau.lt/",
us: "https://nitter.us.projectsegfau.lt",
geo: "https://nitter.g.projectsegfau.lt/",
tor: "http://nitter.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/nitter.png",
statusEu: await fetchStatus("nitter.projectsegfau.lt"),
statusUs: await fetchStatus("nitter.us.projectsegfau.lt"),
statusGeo: await fetchStatus("nitter.g.projectsegfau.lt")
i2p: "http://pjsfs4ukb6prmfx3qx3a5ef2cpcupkvcrxdh72kqn2rxc2cw4nka.b32.i2p",
icon: "/icons/nitter.png"
},
{
name: "Piped",
description: "Another frontend for YouTube.",
eu: "https://piped.projectsegfau.lt/",
us: "https://piped.us.projectsegfau.lt",
icon: "/icons/piped.svg",
statusEu: await fetchStatus("piped.projectsegfau.lt"),
statusUs: await fetchStatus("piped.us.projectsegfau.lt")
icon: "/icons/piped.svg"
},
{
name: "Beatbump",
description: "A frontend for YouTube Music.",
geo: "https://bb.projectsegfau.lt/",
eu: "https://bb.eu.projectsegfau.lt/",
us: "https://bb.us.projectsegfau.lt/",
geo: "https://bb.g.projectsegfau.lt/",
tor: "http://beatbump.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/beatbump.svg",
statusEu: await fetchStatus("bb.eu.projectsegfau.lt"),
statusUs: await fetchStatus("bb.us.projectsegfau.lt"),
statusGeo: await fetchStatus("bb.g.projectsegfau.lt")
i2p: "http://pjsflmvtqax7ii44qy4ladap65c3kqspbs7h7krqy7x43uovklla.b32.i2p",
icon: "/icons/beatbump.svg"
},
{
name: "BreezeWiki",
description: "A frontend for Fandom.",
geo: "https://bw.projectsegfau.lt/",
eu: "https://bw.eu.projectsegfau.lt/",
us: "https://bw.us.projectsegfau.lt/",
geo: "https://bw.g.projectsegfau.lt/",
tor: "http://breezewiki.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/breezewiki.svg",
statusEu: await fetchStatus("bw.eu.projectsegfau.lt"),
statusUs: await fetchStatus("bw.us.projectsegfau.lt"),
statusGeo: await fetchStatus("bw.g.projectsegfau.lt")
i2p: "http://pjsfk4xvekoc7wx4pteevp3q2wy7jmzlem7rvl74nx33zkdr4vyq.b32.i2p",
icon: "/icons/breezewiki.svg"
},
{
name: "Scribe",
description: "A frontend for Medium.",
geo: "https://scribe.projectsegfau.lt/",
eu: "https://scribe.eu.projectsegfau.lt/",
us: "https://scribe.us.projectsegfau.lt/",
geo: "https://scribe.g.projectsegfau.lt/",
tor: "http://scribe.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
statusEu: await fetchStatus("scribe.eu.projectsegfau.lt"),
statusUs: await fetchStatus("scribe.us.projectsegfau.lt"),
statusGeo: await fetchStatus("scribe.g.projectsegfau.lt")
i2p: "http://pjsflkkkcn33ahmzmpyq6idy2knkzh4atp7zaetqfsnenpyori6a.b32.i2p"
}
]
},
@ -113,24 +108,24 @@ const statusData = [
description: "A feature-rich Matrix client.",
eu: "https://chat.projectsegfau.lt/",
tor: "http://element.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/element.svg",
statusEu: await fetchStatus("chat.projectsegfau.lt")
i2p: "http://pjsfiixntiokc4ioroivnw7dd5hhqmvn2pt2jwfgrqdex5qjnroq.b32.i2p",
icon: "/icons/element.svg"
},
{
name: "Hydrogen",
description: "A lightweight Matrix client.",
eu: "https://hydrogen.projectsegfau.lt/",
tor: "http://hydrogen.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/hydrogen.svg",
statusEu: await fetchStatus("hydrogen.projectsegfau.lt")
i2p: "http://pjsfoanqklg3eb3ktvqwmpyiy54b77orr7bmymkwmu56c3zjfltq.b32.i2p",
icon: "/icons/hydrogen.svg"
},
{
name: "Cinny",
description: "An elegant Matrix client.",
eu: "https://cinny.projectsegfau.lt/",
tor: "http://cinny.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/cinny.svg",
statusEu: await fetchStatus("cinny.projectsegfau.lt")
i2p: "http://pjsfgcl4gfbfrk5qte7qb7hqnfwyhflrjcqdldlzmy6yyr2l65oa.b32.i2p",
icon: "/icons/cinny.svg"
},
{
name: "SearXNG",
@ -138,33 +133,38 @@ const statusData = [
eu: "https://search.projectsegfau.lt/",
us: "https://search.us.projectsegfau.lt",
tor: "http://search.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/searxng.svg",
statusEu: await fetchStatus("search.projectsegfau.lt"),
statusUs: await fetchStatus("search.us.projectsegfau.lt")
i2p: "http://pjsfwklrellqoj275kzeu2tz4c3j5zktnqod56s7l5dc25ro3wgq.b32.i2p",
icon: "/icons/searxng.svg"
},
{
name: "Gitea",
description: "A web interface for Git, alternative to GitHub.",
eu: "https://git.projectsegfau.lt/",
tor: "http://git.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/gitea.png",
statusEu: await fetchStatus("git.projectsegfau.lt")
i2p: "http://pjsfdrtv2465bisenvzhfvdleznx4arlih2hlnrhpzugailnm7iq.b32.i2p",
icon: "/icons/gitea.png"
},
{
name: "Akkoma",
description: "Federated microblogging platform.",
eu: "https://social.projectsegfau.lt/",
tor: "http://social.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/akkoma.png",
statusEu: await fetchStatus("social.projectsegfau.lt")
i2p: "http://pjsflarasgpaod2wqccnl7dbolexm7awjz7atqe73nfkhckr66ca.b32.i2p",
icon: "/icons/akkoma.png"
},
{
name: "Vikunja",
description: "A task management platform.",
eu: "https://todo.projectsegfau.lt/",
tor: "http://todo.pjsfkvpxlinjamtawaksbnnaqs2fc2mtvmozrzckxh7f3kis6yea25ad.onion",
icon: "/icons/vikunja.png",
statusEu: await fetchStatus("todo.projectsegfau.lt")
i2p: "http://pjsfivs2sxudfy65kojxqophc6vqjqdr6woczy6hzaxvxvbj3bkq.b32.i2p",
icon: "/icons/vikunja.png"
},
{
name: "Jitsi",
description: "An open source video conferencing platform.",
eu: "https://jitsi.projectsegfau.lt",
icon: "/icons/jitsi.svg"
}
]
},
@ -175,32 +175,34 @@ const statusData = [
name: "Portainer",
description: "Portainer instance for our servers.",
eu: "https://portainer.projectsegfau.lt/",
icon: "/icons/portainer.png",
statusEu: await fetchStatus("portainer.projectsegfau.lt")
icon: "/icons/portainer.png"
},
{
name: "Authentik",
description: "Our OAuth provider.",
eu: "https://sekuritee.projectsegfau.lt/",
icon: "/icons/authentik.png",
statusEu: await fetchStatus("sekuritee.projectsegfau.lt")
icon: "/icons/authentik.png"
},
{
name: "mailcow",
description: "Our mail server and webmail.",
eu: "https://mail.projectsegfau.lt/",
icon: "/icons/mailcow.svg",
statusEu: await fetchStatus("mail.projectsegfau.lt")
icon: "/icons/mailcow.svg"
},
{
name: "Plausible analytics",
description: "Analytics for our website.",
eu: "https://analytics.projectsegfau.lt/projectsegfau.lt",
icon: "/icons/plausible.png",
statusEu: await fetchStatus("analytics.projectsegfau.lt")
icon: "/icons/plausible.png"
},
{
name: "MediaWiki",
description: "Our wiki.",
eu: "https://wiki.projectsegfau.lt/",
icon: "/icons/mediawiki.svg"
}
]
}
];
export default statusData;
export default instances;

View File

@ -1,21 +0,0 @@
---
title: Legal stuff
---
# { title }
Since we care about transparency, privacy and safety we have created some documents regarding these topics.
- [Privacy Policy](/legal/privacy-policy)
- [Terms of Service](/legal/tos)
- [Transparency reports](https://git.projectsegfau.lt/ProjectSegfault/transparency/)
## Legal FAQ
### One of your services contains toxic people!
You can contact us by mail or Matrix and we can figure this out with you. But we recommend that you put most of these requests in our support channel at [#support:projectsegfau.lt](https://matrix.to/#/#support:projectsegfau.lt) on Matrix. If it's something personal, just say that you have a report against someone on one of our services and you'd like to be contacted by an admin and we'll contact you as soon as possible. We generally tend to be active throughout the day.
### How can I trust your services?
Well, you really can't. We don't make our logs or anything else public, however, if you would like access to the data we have on you, please contact us. If you're extremely privacy/security conscious, we have an onion link available on the [Instances](https://projectsegfau.lt/instances) page, and a .onion redirect if you're using the [Tor Browser](https://www.torproject.org/).

View File

@ -0,0 +1,32 @@
<script lang="ts">
import type { PageData } from "./$types";
export let data: PageData;
</script>
<h1>
{data.title}
<p class="text-base font-normal mt-4">{data.description}</p>
</h1>
<ul>
<li><a href="/legal/privacy-policy">Privacy policy</a></li>
<li><a href="/legal/tos">Terms of service</a></li>
<li><a href="https://git.projectsegfau.lt/ProjectSegfault/transparency/">Transparency reports</a></li>
</ul>
<h2>Legal FAQ</h2>
<h3>What do I do if a user is disturbing me?</h3>
<p>If some user of our services is disturbing you, you should immediately contact us. You can do that either by <a href="mailto:contact@projectsegfau.lt">emailing us</a> or sending us a message on <a href="https://matrix.to/#/#support:projectsegfau.lt">the Matrix support channel</a>. If you prefer to keep your report private on Matrix, contact one of our team members. You can find their info on <a href="/team">our team page</a>.</p>
<h3>How can I trust you?</h3>
<p>We have written a privacy policy, which is accessible through this website! You can find it <a href="/legal/privacy-policy">here</a>.</p>
<style>
li::before {
content: "- ";
}
</style>

View File

@ -0,0 +1,8 @@
import type { PageLoad } from "./$types";
export const load = (() => {
return {
title: "Boring legal stuff",
description: "These are some documents concerning transparency, privacy and safety."
};
}) satisfies PageLoad;

View File

@ -1,44 +0,0 @@
---
title: Privacy Policy
description: Our privacy policy
---
# { title }
We take your privacy more seriously than FAANG.
## We don't collect more information than we need to.
We have disabled request logging. This is because it is extremely identifiable. This means that, for example, what website you visited and what path you visited (like https://libreddit.projectsegfau.lt/r/cats), your IP address, your User-Agent will not be logged by us!
Some of our services have things like databases and things, for example [Matrix](https://chat.projectsegfau.lt), but that should be pretty obvious the moment you make an account on there. They may collect some things, however we barely even access those databases so you should be fine.
[Matrix](https://chat.projectsegfau.lt) logs IPs, there is nothing we can do to prevent that. We will only check IP addresses whenever there is serious abuse coming from someone's Matrix account, so we can ban their IP from using Project Segfault services. This includes, but is not limited to ban evading and harassment of communities and people.
If you want the data we've collected on you to be sent, please contact us on Matrix in [this room](https://matrix.to/#/#gdpr:projectsegfau.lt), or e-mail us at [contact@projectsegfau.lt](mailto:contact@projectsegfau.lt). You don't need an account on our Matrix instance in order to chat there, you can choose from a bunch of public instances or make your own.
### For our website and blog
We have Plausible analytics installed on our website (the one you are on right now!), and our [Blog](https://blog.projectsegfau.lt), which means we can just easily see what part of our site you're on, what country you are from, what platform you are from, potentially a referrer and all those get clumped up together.
**Plausible Analytics is completely anonymous. We can't exactly figure out who you are through Plausible. It is also GDPR compliant.**
We use analytics in order to see how many visitors we get every day, what pages get visited the most, what countries visit our website the most etc. uBlock and other privacy extensions block Plausible by default.
If you would like to see our website's statistics, [look no further](https://analytics.projectsegfau.lt/projectsegfau.lt).
## We don't give any of the data we collect to anyone outside of Project Segfault.
What we just described above won't be sold or given to anyone outside of Project Segfault, except for anonymous analytics like the website. Some data could be given to law enforcement if they have a warrant.
There is no reason for anyone outside of Project Segfault to see your data. In fact, Project Segfault really has no reason to see your data, unless there's something to suspect.
## Data may be cleared at any point upon request, but...
There are times when we are unable to delete "your" data because we sometimes we do not have a concept of "you". If you don't create an account on the services you use, we have no way to know what data is tied to "you" as most logs are anonymized.
Often times you can clear your own data simply by deleting your account.
As we said, you can request any GDPR/Privacy things in [this Matrix room](https://matrix.to/#/#gdpr:projectsegfau.lt). But, if there's any ongoing investigations and a law enforcement agency contacts us, we will comply. But, we will only do it if we can verify it's official and they have a warrant.
_Last updated 09.08.2022 16:57 UTC+1_

View File

@ -0,0 +1,44 @@
<script lang="ts">
import type { PageData } from "./$types";
import PMargin from "$lib/PMargin.svelte";
export let data: PageData;
</script>
<h1>{data.title}</h1>
<h2>We don't collect more information than we need to.</h2>
<PMargin>We have disabled request logging. This is because it is extremely identifiable. This means that, for example, what website you visited and what path you visited (like https://libreddit.projectsegfau.lt/r/cats), your IP address, your User-Agent will not be logged by us!</PMargin>
<PMargin>Some of our services have things like databases and things, for example <a href="https://chat.projectsegfau.lt">Matrix</a>, but that should be pretty obvious the moment you make an account on there. They may collect some things, however we barely even access those databases so you should be fine.</PMargin>
<PMargin><a href="https://chat.projectsegfau.lt">Matrix</a> logs IPs, there is nothing we can do to prevent that. We will only check IP addresses whenever there is serious abuse coming from someone's Matrix account, so we can ban their IP from using Project Segfault services. This includes, but is not limited to ban evading and harassment of communities and people.</PMargin>
<PMargin>If you want the data we've collected on you to be sent, please contact us on Matrix in <a href="https://matrix.to/#/#gdpr:projectsegfau.lt">this room</a>, or e-mail us at <a href="mailto:contact@projectsegfau.lt">contact@projectsegfau.lt</a>. You don't need an account on our Matrix instance in order to chat there, you can choose from a bunch of public instances or make your own.</PMargin>
<h2>For our website and blog</h2>
<PMargin>We have Plausible analytics installed on our website (the one you are on right now!), and our <a href="https://blog.projectsegfau.lt">blog</a>, which means we can just easily see what part of our site you're on, what country you are from, what platform you are from, potentially a referrer and all those get clumped up together.</PMargin>
<PMargin class="italic">Plausible Analytics is completely anonymous. We can't exactly figure out who you are through Plausible. It is also GDPR compliant.</PMargin>
<PMargin>We use analytics in order to see how many visitors we get every day, what pages get visited the most, what countries visit our website the most etc. uBlock and other privacy extensions block Plausible by default.</PMargin>
<PMargin>If you would like to see our website's statistics, <a href="https://analytics.projectsegfau.lt/projectsegfau.lt">look no further</a>.</PMargin>
<h2>We don't give any of the data we collect to anyone outside of Project Segfault.</h2>
<PMargin>What we just described above won't be sold or given to anyone outside of Project Segfault, except for anonymous analytics like the website. Some data could be given to law enforcement if they have a warrant.</PMargin>
<PMargin>There is no reason for anyone outside of Project Segfault to see your data. In fact, Project Segfault really has no reason to see your data, unless there's something to suspect.</PMargin>
<h2>Data may be cleared at any point upon request, but...</h2>
<PMargin>There are times when we are unable to delete "your" data because we sometimes we do not have a concept of "you". If you don't create an account on the services you use, we have no way to know what data is tied to "you" as most logs are anonymized.</PMargin>
<PMargin>Often times you can clear your own data simply by deleting your account.</PMargin>
<PMargin>As we said, you can request any GDPR/Privacy things in <a href="https://matrix.to/#/#gdpr:projectsegfau.lt">this Matrix room</a>. But, if there's any ongoing investigations and a law enforcement agency contacts us, we will comply. But, we will only do it if we can verify it's official and they have a warrant.</PMargin>
<span class="italic">Last updated 09.08.2022 16:57 UTC+1</span>

View File

@ -0,0 +1,7 @@
import type { PageLoad } from "./$types";
export const load = (() => {
return {
title: "Privacy policy"
};
}) satisfies PageLoad;

View File

@ -1,20 +0,0 @@
---
title: Terms of Service
description: We're not anarchists, so we have some rules.
---
# { title }
## { description }
1. **Do not use** our Services to **(D)DOS** or **attempt to disrupt** someone else's online stability.
2. **Do not use** our Services to **Dox** someone.
3. **Do not do** anything on our Services that would be **illegal** in France.
4. **Refrain** from using our services to **harass** people.
5. While we do try to keep your data safe, you have to acknowledge that **we are not responsible** if anything **unintentional** happens (such as **data loss**, **inability to extract your data** due to the server being down.). It is also **your responsibility** to **keep a backup of your data** if it matters to you.
6. The services provided by ProjectSegfault are **provided as is**. We do **not warrant the reliability, accessibility or quality of our services** and we are **not responsible for ANY DAMAGES WHATSOEVER by using our services.**
### Failure to comply will either end up **disabling** your account, and/or if it is illegal, **reporting** to the police.
- Be aware that "**reporting** to the police" only happens if the individual is using our services to **post, promote, or encourage** actions that are **morally unnacceptable**. (Such as: pedophilia, murder, or CSAM.)
- **No warnings will be issued if we find this out.**

View File

@ -0,0 +1,20 @@
<script lang="ts">
import type { PageData } from "./$types";
import PMargin from "$lib/PMargin.svelte";
export let data: PageData;
</script>
<h1>{data.title}</h1>
<ol class="list-decimal ml-7">
<li>Do not use our services to (D)DOS or attempt to disrupt someone elses online stability.</li>
<li>Do not use our services to dox someone.</li>
<li>Do not do anything on our services that would be illegal in France, the USA, Luxembourg, and India.</li>
<li>Do not harass people using our services.</li>
<li>While we do try to keep your data safe, you have to acknowledge that we are not responsible if anything unintentional happens (such as data loss, inability to extract your data due to the server being down or something else.). It is also your responsibility to keep a backup of your data if it matters to you.</li>
<li>The services provided by Project Segfault are provided as is. We do not warrant the reliability, accessibility or quality of our services and we are not responsible for ANY DAMAGES WHATSOEVER by using our services.</li>
</ol>
<PMargin>If you fail to comply with these terms we will terminate your access to our services and if you've done something illegal, report you to the police.</PMargin>
<PMargin class="italic">We will only report people to the police if they are using our services to do something that is morally unacceptable. We will do so without issuing any warnings.</PMargin>

View File

@ -0,0 +1,7 @@
import type { PageLoad } from "./$types";
export const load = (() => {
return {
title: "Terms of service"
};
}) satisfies PageLoad;

View File

@ -1,7 +0,0 @@
import type { LayoutServerLoad } from "./$types"
export const load: LayoutServerLoad = async (event) => {
return {
session: await event.locals.getSession(),
}
}

View File

@ -0,0 +1,16 @@
import { env } from "$env/dynamic/private";
import type { PageServerLoad } from "./$types"
export const load = (async ({ locals }) => {
const meta = {
title: "Login"
}
const hasAuth = !env.AUTH_CLIENT_ID || !env.AUTH_CLIENT_SECRET || !env.AUTH_ISSUER || !env.AUTH_TRUST_HOST || !env.AUTH_SECRET ? false : true;
return {
session: hasAuth ? await locals.getSession() : undefined,
hasAuth,
...meta
}
}) satisfies PageServerLoad;

View File

@ -1,23 +1,33 @@
<script>
<script lang="ts">
import { signIn, signOut } from '@auth/sveltekit/client';
import { page } from '$app/stores';
import type { PageData } from './$types';
const buttonStyles = "button w-fit";
const buttonStyles = "cursor-pointer border-none rounded-2 py-6 px-18 text-lg text-text bg-secondary hover:brightness-125 transition duration-250 font-primary"
export let data: PageData;
</script>
<h1>{data.title}</h1>
{#if Object.keys($page.data.session || {}).length}
<div class="flex flex-col items-center justify-center gap-4 mt-28">
<div class="flex flex-row items-center gap-1">
<span>Signed in as</span><br />
<span class="font-extrabold">{$page?.data?.session?.user?.email}</span>
{#if data.hasAuth}
{#if Object.keys($page.data.session || {}).length}
<div class="flex flex-col gap-4">
<div class="flex flex-row items-center gap-1">
<span>Signed in as</span><br />
<span class="font-extrabold">{$page?.data?.session?.user?.email}</span>
</div>
<a href="/admin">Go to admin dashboard</a>
<button on:click={() => signOut()} class={buttonStyles}><div class="i-ic:outline-logout" /> Sign out</button>
</div>
<a href="/admin">Go to admin dashboard</a>
<button on:click={() => signOut()} class={buttonStyles}>Sign out</button>
</div>
{:else}
<div class="flex flex-col gap-4">
<span>You are not signed in</span>
<button on:click={() => signIn("authentik")} class={buttonStyles}><div class="i-ic:outline-login" /> Sign in using Authentik</button>
</div>
{/if}
{:else}
<div class="flex flex-col items-center justify-center gap-4 mt-28">
<span>You are not signed in</span>
<button on:click={() => signIn("authentik")} class={buttonStyles}>Sign in using Authentik</button>
<div class="flex flex-col gap-4">
<span>Authentik is not configured</span>
<a href="https://goauthentik.io/docs/installation">Configure Authentik</a>
</div>
{/if}
{/if}

View File

@ -1,15 +1,22 @@
import type { PageServerLoad } from "./$types";
export const load = (async ({ fetch }) => {
const meta = {
title: "Pubnix"
}
try {
const request = await fetch("https://publapi.projectsegfau.lt/online");
const request = await fetch("https://publapi.p.projectsegfau.lt/users");
if (request.ok) {
return request.json();
return {
users: await request.json(),
...meta
};
} else {
return { error: true, message: "Error: " + request.status };
return { error: true, message: "Error: " + request.status, ...meta };
}
} catch (err) {
return { error: true, message: "Error: " + err };
return { error: true, message: "Error: " + err, ...meta };
}
}) satisfies PageServerLoad;
}) satisfies PageServerLoad;

View File

@ -1,63 +1,33 @@
<script lang="ts">
import Hero from "$lib/Hero.svelte";
import LinkButton from "$lib/LinkButton.svelte";
import type { PageData } from "./$types";
export let data: PageData;
import User, { type UserType } from "./User.svelte";
const isOnline = (user: UserType) => user.online
</script>
<svelte:head>
<title>Pubnix | Project Segfault</title>
</svelte:head>
<div class="h1-no-lg flex flex-col sm:(flex-row items-center) gap-4">
<span class="text-4xl font-bold">Pubnix</span>
<a href="/pubnix/register" class="button sm:w-fit"><div class="i-ic:outline-plus" /> Register</a>
<a href="/pubnix/users" class="button sm:w-fit"><div class="i-ic:outline-people" /> Users</a>
<a href="/pubnix/faq" class="button sm:w-fit"><div class="i-ic:outline-question-mark" /> FAQ</a>
</div>
<Hero marginTop="4">
<h1 class="text-5xl font-800">
<span class="text-accent">Project Segfault</span> pubnix
</h1>
<div
class="flex flex-col sm:flex-row justify-center items-center gap-4 m-4"
>
<LinkButton
url="/pubnix/register"
title="Register"
icon="i-ic:outline-plus"
/>
<LinkButton
url="/pubnix/users"
title="Users"
icon="i-ic:outline-people text-xl"
/>
<LinkButton
url="/pubnix/faq"
title="FAQ"
icon="i-ic:outline-question-mark"
/>
</div>
</Hero>
<h2>Online users</h2>
<div class="flex flex-col items-center text-center mt-16">
<h1>Online users</h1>
{#if !data.error}
{#if data.users.length > 0}
<div class="flex flex-col gap-4">
{#each data.users as user}
<div class="flex flex-row gap-4">
<div class="flex flex-col">
<span class="text-2xl font-800">{user.username}</span>
<span class="text-sm">{user.email}</span>
</div>
<div class="flex flex-col">
<span class="text-sm">Joined</span>
<span class="text-sm">{user.joined}</span>
</div>
</div>
{/each}
</div>
{:else}
<p>No users online</p>
{/if}
{#if !data.error}
{#if data.users.users.some(isOnline)}
<div class="flex flex-row flex-wrap gap-4">
{#each data.users.users as user}
{#if user.online}
<User {user} />
{/if}
{/each}
</div>
{:else}
<p>{data.message}</p>
<p>No users online</p>
{/if}
</div>
{:else}
<p>{data.message}</p>
{/if}

View File

@ -0,0 +1,64 @@
<script lang="ts">
export let user: UserType;
import dayjs from "dayjs";
</script>
<script context="module" lang="ts">
export type UserType = {
name: string;
fullName?: string;
desc?: string;
loc?: string;
email?: string;
matrix?: string;
fediverse?: string;
website?: string;
capsule?: string;
online: boolean;
created: number;
op: boolean;
};
</script>
<div class="flex flex-col gap-4 rounded bg-secondary p-4 w-110 no-underline text-text">
<div class="flex flex-col gap-2 flex-1">
<div>
{#if user.fullName}
<span class="text-2xl">{user.fullName} ({user.name})</span>
{:else}
<span class="text-2xl">{user.name}</span>
{/if}
{#if user.op}
<span class="text-2xl"> - Admin</span>
{/if}
</div>
{#if user.desc}
<p>{user.desc}</p>
{/if}
{#if user.loc}
<span class="button w-fit !bg-alt !text-text">{user.loc}</span>
{/if}
<span class="button w-fit !bg-alt !text-text">Joined: {dayjs.unix(user.created).format("DD/MM/YYYY")}</span>
</div>
<div class="children:text-text flex flex-row items-center gap-4 text-lg">
{#if user.email}
<a href="mailto:{user.email}"><div class="i-ic:outline-email" /></a>
{/if}
{#if user.matrix}
<a href={user.matrix}><div class="i-simple-icons:matrix" /></a>
{/if}
{#if user.fediverse}
<a href={user.fediverse}><div class="i-simple-icons:mastodon" /></a>
{/if}
{#if user.website}
<a href={user.website}><div class="i-ic:outline-language" /></a>
{/if}
{#if user.capsule}
<a href={user.capsule} class="no-underline text-base">Gemini</a>
{/if}
</div>
</div>

View File

@ -1,12 +1,10 @@
<svelte:head>
<title>Pubnix FAQ | Project Segfault</title>
</svelte:head>
<script lang="ts">
import type { PageData } from "./$types";
<div class="flex flex-col items-center m-auto text-center prose justify-center">
<h1>Pubnix FAQ</h1>
export let data: PageData;
</script>
<p>Here are some frequently asked questions about the pubnix.</p>
<h1>{data.title}</h1>
<h2>What is a pubnix?</h2>
<span>A pubnix is a [[Unix?]] server provided by a person or a group to a group for non-commercial recreational goals.</span>
</div>
<h2>What is a pubnix?</h2>
<span>A pubnix is a [[Unix?]] server provided by a person or a group to a group for non-commercial recreational goals.</span>

View File

@ -0,0 +1,7 @@
import type { PageLoad } from "./$types";
export const load = (() => {
return {
title: "Pubnix FAQ"
};
}) satisfies PageLoad;

View File

@ -1,39 +1,47 @@
import type { Actions } from "./$types";
import type { Actions, PageServerLoad } from "./$types";
import Joi from "joi";
import { fail } from "@sveltejs/kit";
import { env } from "$env/dynamic/private";
export const load = (() => {
return {
title: "Pubnix registration"
};
}) satisfies PageServerLoad;
export const actions: Actions = {
default: async ({ request, fetch, getClientAddress }) => {
const formData = await request.formData();
const BodyTypeSchema = Joi.object({
username: Joi.string().required(),
email: Joi.string().email().required()
username: Joi.string().required().alphanum().message("Username must be alphanumeric"),
email: Joi.string().email().required(),
ssh: Joi.string().required().pattern(/^(ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp256|ecdsa-sha2-nistp384|ecdsa-sha2-nistp521) [A-Za-z0-9+/]+[=]{0,3}( [^@]+@[^@]+)?$/).message("Invalid SSH key"),
ip: Joi.string().required().ip()
});
formData.append("ip", getClientAddress());
if (BodyTypeSchema.validate(Object.fromEntries(formData.entries())).error) {
return fail(400, { error: true, message: String(BodyTypeSchema.validate(Object.fromEntries(formData.entries())).error) });
} else {
const request = await fetch("https://publapi.projectsegfau.lt/signup", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
username: formData.get("username"),
email: formData.get("email"),
ip: getClientAddress()
try {
const request = await fetch("https://publapi.p.projectsegfau.lt/signup", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: new URLSearchParams(formData as any).toString()
})
}).then((res) => res.json())
.catch((err) => {
return fail(400, { error: true, message: "Error: " + err });
});
if (request.ok) {
return { success: true, message: request.message, username: request.username, email: request.email };
} else {
return fail(400, { error: true, message: "Error: " + request.status });
const json = await request.json();
if (request.ok) {
return { success: true, message: json.message, username: json.username, email: json.email };
} else {
return fail(400, { error: true, message: "Error: " + request.status });
}
} catch (err) {
return { error: true, message: "Error: " + err };
}
}
}

View File

@ -1,41 +1,25 @@
<script lang="ts">
import type { ActionData } from "./$types";
import { Note, Meta } from "$lib/Form";
import type { ActionData, PageData } from "./$types";
export let form: ActionData;
export let data: PageData;
</script>
<svelte:head>
<title>Pubnix registration | Project Segfault</title>
</svelte:head>
<h1>{data.title}</h1>
<form
method="POST"
class="flex flex-col gap-4 w-fit children:(!bg-secondary text-start text-text rounded p-2)"
>
<div class="pubnix-form flex flex-col items-center text-center">
<h2>Pubnix registration</h2>
<form
method="POST"
class="flex flex-col gap-4 w-fit"
>
<Note
content="Your IP will be logged for anti-abuse measures."
icon="i-ic:outline-lock text-xl"
/>
<Meta
inputType="email"
inputName="email"
inputPlaceholder="Your email"
input2
input2Type="text"
input2Placeholder="Your username"
input2Name="username"
select={false}
/>
{#if form?.success}
{form.message}
{/if}
<input type="text" name="username" placeholder="Username" />
<input type="email" name="email" placeholder="Email" />
<textarea name="ssh" placeholder="SSH public key" class="resize w-60 h-30 sm:(w-100 h-50)" />
{#if form?.error}
{form.message}
{/if}
<button type="submit" class="form-button">Submit</button>
</form>
</div>
{#if form?.success}
{form.message}
{/if}
{#if form?.error}
{form.message}
{/if}
<button type="submit" class="transition-all duration-200 text-text hover:brightness-70">Submit</button>
</form>

View File

@ -1,15 +1,22 @@
import type { PageServerLoad } from "./$types";
export const load = (async ({ fetch }) => {
const meta = {
title: "Pubnix users"
}
try {
const request = await fetch("https://publapi.projectsegfau.lt/users");
const request = await fetch("https://publapi.p.projectsegfau.lt/users");
if (request.ok) {
return request.json();
return {
users: await request.json(),
...meta
};
} else {
return { error: true, message: "Error: " + request.status };
return { error: true, message: "Error: " + request.status, ...meta };
}
} catch (err) {
return { error: true, message: "Error: " + err };
return { error: true, message: "Error: " + err, ...meta };
}
}) satisfies PageServerLoad;
}) satisfies PageServerLoad;

Some files were not shown because too many files have changed in this diff Show More