feat: add invites page
This commit is contained in:
parent
fb10f29e2b
commit
1647ec16a4
8 changed files with 141 additions and 7 deletions
|
@ -11,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
type inviteResponse struct {
|
||||
Code string `json:"string"`
|
||||
Code string `json:"code"`
|
||||
Created time.Time `json:"created"`
|
||||
Used bool `json:"used"`
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package meta
|
|||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"codeberg.org/u1f320/pronouns.cc/backend/server"
|
||||
"emperror.dev/errors"
|
||||
|
@ -24,6 +25,7 @@ type MetaResponse struct {
|
|||
GitCommit string `json:"git_commit"`
|
||||
Users int64 `json:"users"`
|
||||
Members int64 `json:"members"`
|
||||
RequireInvite bool `json:"require_invite"`
|
||||
}
|
||||
|
||||
func (s *Server) meta(w http.ResponseWriter, r *http.Request) error {
|
||||
|
@ -45,6 +47,7 @@ func (s *Server) meta(w http.ResponseWriter, r *http.Request) error {
|
|||
GitCommit: server.Revision,
|
||||
Users: numUsers,
|
||||
Members: numMembers,
|
||||
RequireInvite: os.Getenv("REQUIRE_INVITE") == "true",
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -16,4 +16,5 @@ interface MetaResponse {
|
|||
git_commit: string;
|
||||
users: number;
|
||||
members: number;
|
||||
require_invite: boolean;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { Alert } from "sveltestrap";
|
||||
import { Alert, Icon } from "sveltestrap";
|
||||
|
||||
import { goto } from "$app/navigation";
|
||||
import type { APIError, MeUser } from "$lib/api/entities";
|
||||
|
@ -82,7 +82,8 @@
|
|||
aria-describedby="invite-help"
|
||||
/>
|
||||
<div id="invite-help" class="form-text">
|
||||
You currently need an invite code to sign up. You can get one from an existing user.
|
||||
<Icon name="info" /> You currently need an invite code to sign up. You can get one from an
|
||||
existing user.
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import type { LayoutData } from "./$types";
|
||||
|
||||
import { ListGroup, ListGroupItem } from "sveltestrap";
|
||||
|
||||
export let data: LayoutData;
|
||||
</script>
|
||||
|
||||
<div class="grid">
|
||||
|
@ -8,9 +13,32 @@
|
|||
<h1>Settings</h1>
|
||||
|
||||
<ListGroup>
|
||||
<ListGroupItem tag="a" href="/settings">Your profile</ListGroupItem>
|
||||
<ListGroupItem tag="a" href="/settings/invites">Invites</ListGroupItem>
|
||||
<ListGroupItem tag="a" href="/settings/tokens">API tokens</ListGroupItem>
|
||||
<ListGroupItem tag="a" active={$page.url.pathname === "/settings"} href="/settings">
|
||||
Your profile
|
||||
</ListGroupItem>
|
||||
{#if data.require_invite}
|
||||
<ListGroupItem
|
||||
tag="a"
|
||||
active={$page.url.pathname === "/settings/invites"}
|
||||
href="/settings/invites"
|
||||
>
|
||||
Invites
|
||||
</ListGroupItem>
|
||||
{/if}
|
||||
<ListGroupItem
|
||||
tag="a"
|
||||
active={$page.url.pathname === "/settings/tokens"}
|
||||
href="/settings/tokens"
|
||||
>
|
||||
API tokens
|
||||
</ListGroupItem>
|
||||
<ListGroupItem
|
||||
tag="a"
|
||||
active={$page.url.pathname === "/settings/export"}
|
||||
href="/settings/export"
|
||||
>
|
||||
Data export
|
||||
</ListGroupItem>
|
||||
</ListGroup>
|
||||
</div>
|
||||
<div class="col-md m-3">
|
||||
|
|
|
@ -1 +1,8 @@
|
|||
import type { LayoutLoad } from "./$types";
|
||||
|
||||
export const ssr = false;
|
||||
|
||||
export const load = (async ({ parent }) => {
|
||||
const data = await parent();
|
||||
return data;
|
||||
}) satisfies LayoutLoad;
|
||||
|
|
69
frontend/src/routes/settings/invites/+page.svelte
Normal file
69
frontend/src/routes/settings/invites/+page.svelte
Normal file
|
@ -0,0 +1,69 @@
|
|||
<script lang="ts">
|
||||
import type { APIError, Invite } from "$lib/api/entities";
|
||||
import { apiFetchClient } from "$lib/api/fetch";
|
||||
import { Alert, Button, Modal, Table } from "sveltestrap";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let error: APIError | null = null;
|
||||
let latestInvite: Invite | null = null;
|
||||
let open = false;
|
||||
|
||||
const toggle = () => (open = !open);
|
||||
const createInvite = async () => {
|
||||
try {
|
||||
const invite = await apiFetchClient<Invite>("/auth/invites", "POST");
|
||||
|
||||
error = null;
|
||||
data.invites = [...data.invites, invite];
|
||||
latestInvite = invite;
|
||||
open = true;
|
||||
} catch (e) {
|
||||
latestInvite = null;
|
||||
error = e as APIError;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<h1>Invites ({data.invites.length})</h1>
|
||||
|
||||
<div>
|
||||
{#if !data.invitesEnabled}
|
||||
<p>Invites aren't required to sign up to pronouns.cc right now!</p>
|
||||
{:else}
|
||||
<Table striped hover>
|
||||
<thead>
|
||||
<th>Code</th>
|
||||
<th>Created at</th>
|
||||
<th>Used?</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.invites as invite}
|
||||
<tr>
|
||||
<td><code>{invite.code}</code></td>
|
||||
<td>{invite.created}</td>
|
||||
<td>{invite.used ? "yes" : "no"}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</Table>
|
||||
<div class="row">
|
||||
<div class="col-md-4">
|
||||
<Button color="primary" on:click={createInvite}>Create invite</Button>
|
||||
</div>
|
||||
<div class="col-md">
|
||||
{#if error}
|
||||
<Alert color="danger" fade={false}>
|
||||
<h4 class="alert-heading">An error occurred</h4>
|
||||
<b>{error.code}</b>: {error.message}
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<Modal body header="Invite created" isOpen={open} {toggle}>
|
||||
Successfully created a new invite! Give the person you're sending it to this code:
|
||||
<code>{latestInvite?.code}</code>
|
||||
</Modal>
|
||||
{/if}
|
||||
</div>
|
25
frontend/src/routes/settings/invites/+page.ts
Normal file
25
frontend/src/routes/settings/invites/+page.ts
Normal file
|
@ -0,0 +1,25 @@
|
|||
import { ErrorCode, type APIError, type Invite } from "$lib/api/entities";
|
||||
import { apiFetchClient } from "$lib/api/fetch";
|
||||
import { error } from "@sveltejs/kit";
|
||||
import type { PageLoad } from "../$types";
|
||||
|
||||
export const load = (async () => {
|
||||
const data = {
|
||||
invitesEnabled: true,
|
||||
invites: [] as Invite[],
|
||||
};
|
||||
|
||||
try {
|
||||
const invites = await apiFetchClient<Invite[]>("/auth/invites");
|
||||
data.invites = invites;
|
||||
} catch (e) {
|
||||
if ((e as APIError).code === ErrorCode.InvitesDisabled) {
|
||||
data.invitesEnabled = false;
|
||||
data.invites = [];
|
||||
} else {
|
||||
throw error(500, (e as APIError).message);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
}) satisfies PageLoad;
|
Loading…
Reference in a new issue