add new sveltekit frontend

This commit is contained in:
Sam 2023-03-11 16:54:58 +01:00
commit fc4334932a
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
40 changed files with 3802 additions and 0 deletions

View file

@ -0,0 +1,9 @@
<script>
import "bootstrap/dist/css/bootstrap.min.css";
import "bootstrap-icons/font/bootstrap-icons.css";
import Navigation from "./nav/Navigation.svelte";
</script>
<Navigation />
<slot />

View file

@ -0,0 +1,6 @@
<svelte:head>
<title>pronouns.cc</title>
</svelte:head>
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>

View file

@ -0,0 +1,10 @@
import { apiFetch } from "$lib/api/fetch";
import type { User } from "$lib/api/entities";
export const load = async ({ params }) => {
const resp = await apiFetch<User>(`/users/${params.username}`, {
method: "GET",
});
return resp;
};

View file

@ -0,0 +1,99 @@
<script lang="ts">
import { marked } from "marked";
import sanitizeHtml from "sanitize-html";
import FieldCard from "$lib/components/FieldCard.svelte";
import type { PageData } from "./$types";
import StatusIcon from "$lib/components/StatusIcon.svelte";
import PronounLink from "$lib/components/PronounLink.svelte";
import PartialMemberCard from "$lib/components/PartialMemberCard.svelte";
import FallbackImage from "$lib/components/FallbackImage.svelte";
export let data: PageData;
let bio: string | null;
$: bio = data.bio ? sanitizeHtml(marked.parse(data.bio)) : null;
</script>
<svelte:head>
<title>@{data.name} - pronouns.cc</title>
</svelte:head>
<div class="container">
<div class="grid">
<div class="row">
<div class="col-md text-center">
<FallbackImage urls={data.avatar_urls} alt="Avatar for @{data.name}" />
</div>
<div class="col-md">
{#if data.display_name}
<h2>{data.display_name}</h2>
<h5 class="text-body-secondary">@{data.name}</h5>
{:else}
<h2>@{data.name}</h2>
{/if}
<hr />
{#if bio}
<p>{@html bio}</p>
{/if}
</div>
{#if data.links}
<div class="col-md">
<ul>
{#each data.links as link}
<li><a href={link}>{link}</a></li>
{/each}
</ul>
</div>
{/if}
</div>
<div class="row">
{#if data.names}
<div class="col-md">
<h4>Names</h4>
<ul class="list-unstyled">
{#each data.names as name}
<li><StatusIcon status={name.status} /> {name.value}</li>
{/each}
</ul>
</div>
{/if}
{#if data.pronouns}
<div class="col-md">
<h4>Pronouns</h4>
<ul class="list-unstyled">
{#each data.pronouns as pronouns}
<li>
<StatusIcon status={pronouns.status} />
<PronounLink {pronouns} />
</li>
{/each}
</ul>
</div>
{/if}
</div>
</div>
{#if data.fields}
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3">
{#each data.fields as field}
<div class="col">
<FieldCard {field} />
</div>
{/each}
</div>
{/if}
{#if data.members}
<div class="row">
<div class="col">
<hr />
<h2>Members</h2>
</div>
</div>
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-5 text-center">
{#each data.members as member}
<PartialMemberCard user={data} {member} />
{/each}
</div>
{/if}
</div>

View file

@ -0,0 +1,10 @@
import { apiFetch } from "$lib/api/fetch";
import type { Member } from "$lib/api/entities";
export const load = async ({ params }) => {
const resp = await apiFetch<Member>(`/users/${params.username}/members/${params.memberName}`, {
method: "GET",
});
return resp;
};

View file

@ -0,0 +1,87 @@
<script lang="ts">
import { marked } from "marked";
import sanitizeHtml from "sanitize-html";
import FieldCard from "$lib/components/FieldCard.svelte";
import type { PageData } from "./$types";
import StatusIcon from "$lib/components/StatusIcon.svelte";
import PronounLink from "$lib/components/PronounLink.svelte";
import FallbackImage from "$lib/components/FallbackImage.svelte";
import { Button, Icon } from "sveltestrap";
export let data: PageData;
let bio: string | null;
$: bio = data.bio ? sanitizeHtml(marked.parse(data.bio)) : null;
</script>
<svelte:head>
<title>{data.display_name ?? data.name} - @{data.user.name} - pronouns.cc</title>
</svelte:head>
<div class="container">
<div>
<Button color="secondary" href="/@{data.user.name}">
<Icon name="arrow-left" /> Back to {data.user.display_name ?? data.user.name}
</Button>
</div>
<div class="grid">
<div class="row">
<div class="col-md text-center">
<FallbackImage urls={data.avatar_urls} alt="Avatar for @{data.name}" />
</div>
<div class="col-md">
<h2>{data.display_name ?? data.name}</h2>
<h5 class="text-body-secondary">{data.name} (@{data.user.name})</h5>
<hr />
{#if bio}
<p>{@html bio}</p>
{/if}
</div>
{#if data.links}
<div class="col-md">
<ul>
{#each data.links as link}
<li><a href={link}>{link}</a></li>
{/each}
</ul>
</div>
{/if}
</div>
<div class="row">
{#if data.names}
<div class="col-md">
<h4>Names</h4>
<ul class="list-unstyled">
{#each data.names as name}
<li><StatusIcon status={name.status} /> {name.value}</li>
{/each}
</ul>
</div>
{/if}
{#if data.pronouns}
<div class="col-md">
<h4>Pronouns</h4>
<ul class="list-unstyled">
{#each data.pronouns as pronouns}
<li>
<StatusIcon status={pronouns.status} />
<PronounLink {pronouns} />
</li>
{/each}
</ul>
</div>
{/if}
</div>
</div>
{#if data.fields}
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3">
{#each data.fields as field}
<div class="col">
<FieldCard {field} />
</div>
{/each}
</div>
{/if}
</div>

View file

@ -0,0 +1,17 @@
import { apiFetch } from "$lib/api/fetch";
import { PUBLIC_BASE_URL } from "$env/static/public";
export const load = async () => {
const resp = await apiFetch<UrlsResponse>("/auth/urls", {
method: "POST",
body: {
callback_domain: PUBLIC_BASE_URL,
},
});
return resp;
};
interface UrlsResponse {
discord: string;
}

View file

@ -0,0 +1,23 @@
<script lang="ts">
import type { PageData } from "./$types";
export let data: PageData;
</script>
<svelte:head>
<title>Login - pronouns.cc</title>
</svelte:head>
<div class="container">
<h1>Log in or sign up</h1>
<div class="row">
<div class="col">
<p>
<a class="btn btn-primary" href={data.discord} role="button">Log in with Discord</a>
</p>
</div>
<div class="col">
</div>
</div>
</div>

View file

@ -0,0 +1,33 @@
import type { MeUser } from "$lib/api/entities";
import { apiFetch } from "$lib/api/fetch";
import type { PageServerLoad } from "./$types";
import { PUBLIC_BASE_URL } from "$env/static/public";
export const load = (async (event) => {
try {
const resp = await apiFetch<CallbackResponse>("/auth/discord/callback", {
method: "POST",
body: {
callback_domain: PUBLIC_BASE_URL,
code: event.url.searchParams.get("code"),
state: event.url.searchParams.get("state"),
},
});
return {
...resp,
};
} catch (e) {
return { error: e };
}
}) satisfies PageServerLoad;
interface CallbackResponse {
has_account: boolean;
token?: string;
user?: MeUser;
discord?: string;
ticket?: string;
require_invite: boolean;
}

View file

@ -0,0 +1,17 @@
<script lang="ts">
import type { PageData } from "./$types";
export let data: PageData;
</script>
<svelte:head>
<title>Log in with Discord - pronouns.cc</title>
</svelte:head>
<h1>Log in with Discord</h1>
{#if data.error}
{:else}
{/if}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 11 KiB

View file

@ -0,0 +1,12 @@
<script lang="ts">
export let href: string;
export let plain: boolean = false;
</script>
{#if plain}
<a {href} class="hover:text-sky-500 dark:hover:text-sky-400"><slot /></a>
{:else}
<li>
<a {href} class="hover:text-sky-500 dark:hover:text-sky-400"><slot /></a>
</li>
{/if}

View file

@ -0,0 +1,101 @@
<script lang="ts">
import { onMount } from "svelte";
import { browser } from "$app/environment";
import {
Collapse,
Icon,
Nav,
Navbar,
NavbarBrand,
NavbarToggler,
NavItem,
NavLink,
} from "sveltestrap";
import Logo from "./Logo.svelte";
import { userStore, themeStore } from "$lib/store";
import { ErrorCode, type APIError, type MeUser } from "$lib/api/entities";
import { apiFetch } from "$lib/api/fetch";
let theme: string;
let currentUser: MeUser | null;
let showMenu: boolean = false;
$: currentUser = $userStore;
$: theme = $themeStore;
onMount(() => {
const localUser = localStorage.getItem("pronouns-user");
userStore.set(localUser ? JSON.parse(localUser) : null);
const token = localStorage.getItem("pronouns-token");
if (token) {
apiFetch<MeUser>("/users/@me", { token })
.then((user) => {
userStore.set(user);
localStorage.setItem("pronouns-user", JSON.stringify(user));
})
.catch((e) => {
console.log("getting /users/@me:", e);
if (
(e as APIError).code == ErrorCode.InvalidToken ||
(e as APIError).code == ErrorCode.Forbidden
) {
localStorage.removeItem("pronouns-token");
localStorage.removeItem("pronouns-user");
}
});
}
});
$: updateTheme(theme);
const updateTheme = (newTheme: string) => {
if (!browser) return;
document.documentElement.setAttribute("data-bs-theme", newTheme);
localStorage.setItem("pronouns-theme", newTheme);
};
const toggleTheme = () => {
themeStore.set(theme === "dark" ? "light" : "dark")
};
const toggleMenu = () => {
showMenu = !showMenu;
};
</script>
<Navbar
color={theme === "dark" ? "dark" : "light"}
light={theme !== "dark"}
dark={theme === "dark"}
expand="lg"
class="mb-4"
>
<NavbarBrand href="/"><Logo /></NavbarBrand>
<NavbarToggler on:click={toggleMenu} />
<Collapse isOpen={showMenu} navbar expand="lg">
<Nav class="ms-auto" navbar>
<NavItem>
{#if currentUser}
<NavLink href="/@{currentUser.name}">@{currentUser.name}</NavLink>
<NavLink href="/settings">Settings</NavLink>
<NavLink href="/logout">Log out</NavLink>
{:else}
<NavLink href="/login">Log in</NavLink>
{/if}
</NavItem>
<NavItem>
<NavLink
on:click={() => toggleTheme()}
title={theme === "dark" ? "Switch to light mode" : "Switch to dark mode"}
>
<Icon name={theme === "dark" ? "sun" : "moon-stars"} height="24" />
{theme === "dark" ? "Light mode" : "Dark mode"}
</NavLink>
</NavItem>
</Nav>
</Collapse>
</Navbar>