forked from ProjectSegfault/website
v3
This commit is contained in:
parent
add264a5d6
commit
07c0cc4a69
31
.github/workflows/docker-dev.yml
vendored
31
.github/workflows/docker-dev.yml
vendored
@ -1,25 +1,36 @@
|
||||
name: Docker
|
||||
name: Docker dev
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "dev"
|
||||
- 'dev'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
build:
|
||||
name: 'Build'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to DockerHub
|
||||
|
||||
- name: "Build:checkout"
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
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
|
||||
tags: realprojectsegfault/website:dev
|
||||
no-cache: true
|
29
.github/workflows/docker.yml
vendored
29
.github/workflows/docker.yml
vendored
@ -3,23 +3,34 @@ name: Docker
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- 'master'
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
build:
|
||||
name: 'Build'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up QEMU
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
- name: Login to DockerHub
|
||||
|
||||
- name: "Build:checkout"
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build and push
|
||||
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
|
||||
tags: realprojectsegfault/website:latest
|
||||
no-cache: true
|
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,4 +6,5 @@ node_modules
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
package-lock.json
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
@ -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" } }]
|
||||
}
|
||||
|
10
README.md
10
README.md
@ -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 |
|
31
compose.yml
31
compose.yml
@ -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"
|
47
package.json
47
package.json
@ -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"
|
||||
}
|
||||
}
|
||||
|
1673
pnpm-lock.yaml
1673
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
77
src/app.css
Normal file
77
src/app.css
Normal 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;
|
||||
}
|
13
src/app.d.ts
vendored
13
src/app.d.ts
vendored
@ -1,10 +1,13 @@
|
||||
/// <reference types="@sveltejs/kit" />
|
||||
|
||||
// See https://kit.svelte.dev/docs/types#app
|
||||
// for information about these interfaces
|
||||
declare namespace App {
|
||||
// and what to do when importing types
|
||||
declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
// interface Locals {}
|
||||
// interface PageData {}
|
||||
// interface Platform {}
|
||||
// interface Session {}
|
||||
// interface Stuff {}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
|
10
src/app.html
10
src/app.html
@ -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">
|
||||
|
@ -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: [
|
||||
const hasAuth = !env.AUTH_CLIENT_ID || !env.AUTH_CLIENT_SECRET || !env.AUTH_ISSUER || !env.AUTH_TRUST_HOST || !env.AUTH_SECRET ? false : true;
|
||||
|
||||
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 updateMap = () => {
|
||||
map.set("data", {
|
||||
status: statusData,
|
||||
updated: Math.floor(Date.now() / 1000)
|
||||
const result = await resolve(event, {
|
||||
transformPageChunk: ({ html }) => html
|
||||
});
|
||||
};
|
||||
return result;
|
||||
} : async ({ event, resolve }) => {
|
||||
if (event.url.pathname.startsWith("/admin")) {
|
||||
throw redirect(303, "/login");
|
||||
}
|
||||
|
||||
updateMap();
|
||||
|
||||
setInterval(updateMap, 30000);
|
||||
const result = await resolve(event, {
|
||||
transformPageChunk: ({ html }) => html
|
||||
});
|
||||
return result;
|
||||
}
|
||||
);
|
@ -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}
|
||||
|
@ -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>
|
@ -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}
|
||||
|
@ -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>
|
@ -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>
|
@ -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}
|
||||
|
@ -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>
|
@ -1,3 +0,0 @@
|
||||
<div class="flex gap-8 flex-row flex-wrap">
|
||||
<slot />
|
||||
</div>
|
@ -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>
|
@ -1,3 +0,0 @@
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
<slot />
|
||||
</div>
|
@ -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";
|
@ -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>
|
@ -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}
|
@ -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>
|
@ -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}
|
@ -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>
|
@ -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}
|
||||
/>
|
@ -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";
|
@ -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 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>
|
@ -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>
|
@ -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 {
|
@ -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
8
src/lib/PMargin.svelte
Normal file
@ -0,0 +1,8 @@
|
||||
<script lang="ts">
|
||||
let classes: string = "";
|
||||
export { classes as class };
|
||||
</script>
|
||||
|
||||
<p class="my-4 {classes}">
|
||||
<slot />
|
||||
</p>
|
10
src/lib/PageTransition.svelte
Normal file
10
src/lib/PageTransition.svelte
Normal 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}
|
@ -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;
|
||||
}
|
@ -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;
|
@ -1,3 +0,0 @@
|
||||
const map = new Map();
|
||||
|
||||
export default map;
|
@ -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");
|
||||
}
|
@ -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>
|
@ -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>
|
||||
<main class="px-8 mb-8 max-w-90rem m-auto">
|
||||
<PageTransition pathname={data.pathname}>
|
||||
<slot />
|
||||
</main>
|
||||
</PageTransition>
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
|
7
src/routes/+layout.ts
Normal file
7
src/routes/+layout.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { LayoutLoad } from "./$types";
|
||||
|
||||
export const load = (async ({ url: { pathname } }) => {
|
||||
return {
|
||||
pathname
|
||||
}
|
||||
}) satisfies LayoutLoad
|
@ -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;
|
@ -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"
|
||||
/>
|
||||
{#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}
|
||||
|
||||
{#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>
|
||||
</Hero>
|
||||
|
||||
<Announcements {data} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<p>{data.message}</p>
|
||||
{/if}
|
@ -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}
|
@ -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");
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
<slot />
|
||||
|
||||
<style>
|
||||
:global(.col) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: fit-content;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
@ -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>
|
7
src/routes/admin/+page.ts
Normal file
7
src/routes/admin/+page.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (() => {
|
||||
return {
|
||||
title: "Admin dashboard"
|
||||
};
|
||||
}) satisfies PageLoad;
|
@ -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." };
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
@ -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;
|
@ -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;
|
@ -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}
|
||||
{#if !data.posts.error}
|
||||
<PostsContainer>
|
||||
{#each data.posts.posts as post}
|
||||
<PostOuter>
|
||||
<Title {post} />
|
||||
<Meta {post} />
|
||||
<ReadMore {post} />
|
||||
</PostOuter>
|
||||
{/each}
|
||||
</PostsContainer>
|
||||
</PostsContainer>
|
||||
{:else}
|
||||
<p>{data.posts.message}</p>
|
||||
{/if}
|
@ -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;
|
@ -4,23 +4,20 @@
|
||||
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>
|
||||
{#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">
|
||||
</PostOuter>
|
||||
<div class="flex flex-row flex-wrap justify-center my-4">
|
||||
<PostsContainer>
|
||||
{#if previous}
|
||||
<PostOuter>
|
||||
@ -40,10 +37,13 @@
|
||||
</PostOuter>
|
||||
{/if}
|
||||
</PostsContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
<style>
|
||||
.more-posts {
|
||||
@apply border-b-solid border-b-grey border-b-1 m-0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
{:else}
|
||||
<p>{data.allPosts.message}</p>
|
||||
{/if}
|
@ -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;
|
||||
|
@ -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}
|
@ -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;
|
||||
|
||||
|
@ -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}
|
||||
{#if !data.posts.error}
|
||||
<PostsContainer>
|
||||
{#each data.posts.posts as post}
|
||||
<PostOuter>
|
||||
<Title {post} />
|
||||
<Meta {post} />
|
||||
<ReadMore {post} />
|
||||
</PostOuter>
|
||||
{/each}
|
||||
</PostsContainer>
|
||||
</PostsContainer>
|
||||
{:else}
|
||||
<p>{data.posts.message}</p>
|
||||
{/if}
|
23
src/routes/blog/fetchGhost.ts
Normal file
23
src/routes/blog/fetchGhost.ts
Normal 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;
|
@ -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;
|
@ -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}
|
@ -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;
|
@ -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}
|
||||
{#if !data.posts.error}
|
||||
<PostsContainer>
|
||||
{#each data.posts.posts as post}
|
||||
<PostOuter>
|
||||
<Title {post} />
|
||||
<Meta {post} />
|
||||
<ReadMore {post} />
|
||||
</PostOuter>
|
||||
{/each}
|
||||
</PostsContainer>
|
||||
</PostsContainer>
|
||||
{:else}
|
||||
<p>{data.posts.message}</p>
|
||||
{/if}
|
@ -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"] });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
7
src/routes/contact/+page.ts
Normal file
7
src/routes/contact/+page.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (() => {
|
||||
return {
|
||||
title: "Contact"
|
||||
};
|
||||
}) satisfies PageLoad;
|
@ -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>
|
35
src/routes/donate/+page.svelte
Normal file
35
src/routes/donate/+page.svelte
Normal 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" />
|
7
src/routes/donate/+page.ts
Normal file
7
src/routes/donate/+page.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (() => {
|
||||
return {
|
||||
title: "Donate"
|
||||
};
|
||||
}) satisfies PageLoad;
|
18
src/routes/donate/CryptoInfo.svelte
Normal file
18
src/routes/donate/CryptoInfo.svelte
Normal 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}
|
@ -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;
|
@ -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>
|
||||
|
||||
<h1>Our instances</h1>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<CardOuter>
|
||||
<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}
|
||||
</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 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>
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#each data.instances as category}
|
||||
<div class="flex flex-col">
|
||||
<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>
|
||||
{/each}
|
||||
</div>
|
@ -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}
|
10
src/routes/instances/advanced/+page.server.ts
Normal file
10
src/routes/instances/advanced/+page.server.ts
Normal 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;
|
44
src/routes/instances/advanced/+page.svelte
Normal file
44
src/routes/instances/advanced/+page.svelte
Normal 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>
|
@ -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;
|
@ -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/).
|
32
src/routes/legal/+page.svelte
Normal file
32
src/routes/legal/+page.svelte
Normal 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>
|
8
src/routes/legal/+page.ts
Normal file
8
src/routes/legal/+page.ts
Normal 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;
|
@ -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_
|
44
src/routes/legal/privacy-policy/+page.svelte
Normal file
44
src/routes/legal/privacy-policy/+page.svelte
Normal 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>
|
7
src/routes/legal/privacy-policy/+page.ts
Normal file
7
src/routes/legal/privacy-policy/+page.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (() => {
|
||||
return {
|
||||
title: "Privacy policy"
|
||||
};
|
||||
}) satisfies PageLoad;
|
@ -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.**
|
20
src/routes/legal/tos/+page.svelte
Normal file
20
src/routes/legal/tos/+page.svelte
Normal 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 else’s 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>
|
7
src/routes/legal/tos/+page.ts
Normal file
7
src/routes/legal/tos/+page.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (() => {
|
||||
return {
|
||||
title: "Terms of service"
|
||||
};
|
||||
}) satisfies PageLoad;
|
@ -1,7 +0,0 @@
|
||||
import type { LayoutServerLoad } from "./$types"
|
||||
|
||||
export const load: LayoutServerLoad = async (event) => {
|
||||
return {
|
||||
session: await event.locals.getSession(),
|
||||
}
|
||||
}
|
16
src/routes/login/+page.server.ts
Normal file
16
src/routes/login/+page.server.ts
Normal 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;
|
@ -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">
|
||||
{#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}>Sign out</button>
|
||||
<button on:click={() => signOut()} class={buttonStyles}><div class="i-ic:outline-logout" /> Sign out</button>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-col items-center justify-center gap-4 mt-28">
|
||||
{:else}
|
||||
<div class="flex flex-col gap-4">
|
||||
<span>You are not signed in</span>
|
||||
<button on:click={() => signIn("authentik")} class={buttonStyles}>Sign in using Authentik</button>
|
||||
<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 gap-4">
|
||||
<span>Authentik is not configured</span>
|
||||
<a href="https://goauthentik.io/docs/installation">Configure Authentik</a>
|
||||
</div>
|
||||
{/if}
|
@ -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;
|
@ -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>
|
||||
{#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>No users online</p>
|
||||
{/if}
|
||||
{:else}
|
||||
{:else}
|
||||
<p>{data.message}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
64
src/routes/pubnix/User.svelte
Normal file
64
src/routes/pubnix/User.svelte
Normal 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>
|
@ -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>
|
7
src/routes/pubnix/faq/+page.ts
Normal file
7
src/routes/pubnix/faq/+page.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import type { PageLoad } from "./$types";
|
||||
|
||||
export const load = (() => {
|
||||
return {
|
||||
title: "Pubnix FAQ"
|
||||
};
|
||||
}) satisfies PageLoad;
|
@ -1,40 +1,48 @@
|
||||
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", {
|
||||
try {
|
||||
const request = await fetch("https://publapi.p.projectsegfau.lt/signup", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/x-www-form-urlencoded"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: formData.get("username"),
|
||||
email: formData.get("email"),
|
||||
ip: getClientAddress()
|
||||
body: new URLSearchParams(formData as any).toString()
|
||||
})
|
||||
}).then((res) => res.json())
|
||||
.catch((err) => {
|
||||
return fail(400, { error: true, message: "Error: " + err });
|
||||
});
|
||||
|
||||
const json = await request.json();
|
||||
|
||||
if (request.ok) {
|
||||
return { success: true, message: request.message, username: request.username, email: request.email };
|
||||
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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,34 +1,19 @@
|
||||
<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>
|
||||
|
||||
<div class="pubnix-form flex flex-col items-center text-center">
|
||||
<h2>Pubnix registration</h2>
|
||||
<form
|
||||
<h1>{data.title}</h1>
|
||||
<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}
|
||||
/>
|
||||
class="flex flex-col gap-4 w-fit children:(!bg-secondary text-start text-text rounded p-2)"
|
||||
>
|
||||
|
||||
<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?.success}
|
||||
{form.message}
|
||||
{/if}
|
||||
@ -36,6 +21,5 @@
|
||||
{#if form?.error}
|
||||
{form.message}
|
||||
{/if}
|
||||
<button type="submit" class="form-button">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
<button type="submit" class="transition-all duration-200 text-text hover:brightness-70">Submit</button>
|
||||
</form>
|
@ -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;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user