forked from mirrors/pronouns.cc
feat(frontend): working Discord login + signup
This commit is contained in:
parent
0e72097346
commit
c8b5b7e2c2
24 changed files with 287 additions and 119 deletions
|
@ -7,12 +7,9 @@ import (
|
|||
"net/url"
|
||||
"os"
|
||||
|
||||
"codeberg.org/u1f320/pronouns.cc/backend/log"
|
||||
"emperror.dev/errors"
|
||||
"github.com/Masterminds/squirrel"
|
||||
"github.com/jackc/pgconn"
|
||||
"github.com/jackc/pgx/v4"
|
||||
"github.com/jackc/pgx/v4/log/zapadapter"
|
||||
"github.com/jackc/pgx/v4/pgxpool"
|
||||
"github.com/mediocregopher/radix/v4"
|
||||
"github.com/minio/minio-go/v7"
|
||||
|
@ -38,15 +35,7 @@ type DB struct {
|
|||
}
|
||||
|
||||
func New() (*DB, error) {
|
||||
pgxCfg, err := pgxpool.ParseConfig(os.Getenv("DATABASE_URL"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "parsing config")
|
||||
}
|
||||
pgxCfg.ConnConfig.LogLevel = pgx.LogLevelDebug
|
||||
pgxCfg.ConnConfig.Logger = zapadapter.NewLogger(log.Logger)
|
||||
|
||||
pool, err := pgxpool.ConnectConfig(context.Background(), pgxCfg)
|
||||
// pool, err := pgxpool.Connect(context.Background(), os.Getenv("DATABASE_URL"))
|
||||
pool, err := pgxpool.Connect(context.Background(), os.Getenv("DATABASE_URL"))
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "creating postgres client")
|
||||
}
|
||||
|
|
|
@ -52,7 +52,7 @@ func (f Field) Validate() string {
|
|||
|
||||
// UserFields returns the fields associated with the given user ID.
|
||||
func (db *DB) UserFields(ctx context.Context, id xid.ID) (fs []Field, err error) {
|
||||
sql, args, err := sq.Select("*").From("user_fields").Where("user_id = ?", id).OrderBy("id").ToSql()
|
||||
sql, args, err := sq.Select("id", "name", "entries").From("user_fields").Where("user_id = ?", id).OrderBy("id").ToSql()
|
||||
if err != nil {
|
||||
return fs, errors.Wrap(err, "building sql")
|
||||
}
|
||||
|
@ -88,7 +88,7 @@ func (db *DB) SetUserFields(ctx context.Context, tx pgx.Tx, userID xid.ID, field
|
|||
|
||||
// MemberFields returns the fields associated with the given member ID.
|
||||
func (db *DB) MemberFields(ctx context.Context, id xid.ID) (fs []Field, err error) {
|
||||
sql, args, err := sq.Select("*").From("member_fields").Where("member_id = ?", id).OrderBy("id").ToSql()
|
||||
sql, args, err := sq.Select("id", "name", "entries").From("member_fields").Where("member_id = ?", id).OrderBy("id").ToSql()
|
||||
if err != nil {
|
||||
return fs, errors.Wrap(err, "building sql")
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ func (s *Server) discordCallback(w http.ResponseWriter, r *http.Request) error {
|
|||
}
|
||||
|
||||
cfg := discordOAuthConfig
|
||||
cfg.RedirectURL = decoded.CallbackDomain + "/login/discord"
|
||||
cfg.RedirectURL = decoded.CallbackDomain + "/auth/login/discord"
|
||||
token, err := cfg.Exchange(r.Context(), decoded.Code)
|
||||
if err != nil {
|
||||
log.Errorf("exchanging oauth code: %v", err)
|
||||
|
|
|
@ -105,10 +105,10 @@ func (s *Server) oauthURLs(w http.ResponseWriter, r *http.Request) error {
|
|||
|
||||
// copy Discord config and set redirect url
|
||||
discordCfg := discordOAuthConfig
|
||||
discordCfg.RedirectURL = req.CallbackDomain + "/login/discord"
|
||||
discordCfg.RedirectURL = req.CallbackDomain + "/auth/login/discord"
|
||||
|
||||
render.JSON(w, r, oauthURLsResponse{
|
||||
Discord: discordCfg.AuthCodeURL(state),
|
||||
Discord: discordCfg.AuthCodeURL(state) + "&prompt=none",
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -35,7 +35,9 @@ type PartialMember struct {
|
|||
ID xid.ID `json:"id"`
|
||||
Name string `json:"name"`
|
||||
DisplayName *string `json:"display_name"`
|
||||
Bio *string `json:"bio"`
|
||||
AvatarURLs []string `json:"avatar_urls"`
|
||||
Links []string `json:"links"`
|
||||
Names []db.FieldEntry `json:"names"`
|
||||
Pronouns []db.PronounEntry `json:"pronouns"`
|
||||
}
|
||||
|
@ -59,7 +61,9 @@ func dbUserToResponse(u db.User, fields []db.Field, members []db.Member) GetUser
|
|||
ID: members[i].ID,
|
||||
Name: members[i].Name,
|
||||
DisplayName: members[i].DisplayName,
|
||||
Bio: members[i].Bio,
|
||||
AvatarURLs: db.NotNull(members[i].AvatarURLs),
|
||||
Links: db.NotNull(members[i].Links),
|
||||
Names: db.NotNull(members[i].Names),
|
||||
Pronouns: db.NotNull(members[i].Pronouns),
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ type PatchUserRequest struct {
|
|||
}
|
||||
|
||||
// patchUser parses a PatchUserRequest and updates the user with the given ID.
|
||||
// TODO: could this be refactored to be less repetitive? names, pronouns, and fields are all validated in the same way
|
||||
func (s *Server) patchUser(w http.ResponseWriter, r *http.Request) error {
|
||||
ctx := r.Context()
|
||||
|
||||
|
|
|
@ -3,8 +3,8 @@ export interface User {
|
|||
name: string;
|
||||
display_name: string | null;
|
||||
bio: string | null;
|
||||
avatar_urls: string[] | null;
|
||||
links: string[] | null;
|
||||
avatar_urls: string[];
|
||||
links: string[];
|
||||
|
||||
names: FieldEntry[];
|
||||
pronouns: Pronoun[];
|
||||
|
@ -46,15 +46,16 @@ export interface PartialMember {
|
|||
id: string;
|
||||
name: string;
|
||||
display_name: string | null;
|
||||
avatar_urls: string[] | null;
|
||||
bio: string | null;
|
||||
avatar_urls: string[];
|
||||
links: string[];
|
||||
names: FieldEntry[];
|
||||
pronouns: Pronoun[];
|
||||
}
|
||||
|
||||
export interface Member extends PartialMember {
|
||||
bio: string | null;
|
||||
links: string | null;
|
||||
names: FieldEntry[];
|
||||
pronouns: Pronoun[];
|
||||
fields: Field[];
|
||||
|
||||
user: MemberPartialUser;
|
||||
}
|
||||
|
||||
|
@ -62,7 +63,7 @@ export interface MemberPartialUser {
|
|||
id: string;
|
||||
name: string;
|
||||
display_name: string | null;
|
||||
avatar_urls: string[] | null;
|
||||
avatar_urls: string[];
|
||||
}
|
||||
|
||||
export interface APIError {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<script lang="ts">
|
||||
export let urls: string[] | null;
|
||||
export let urls: string[];
|
||||
export let alt: string;
|
||||
export let width = 300;
|
||||
|
||||
const contentTypeFor = (url: string) => {
|
||||
if (url.endsWith(".webp")) {
|
||||
|
@ -15,14 +16,14 @@
|
|||
};
|
||||
</script>
|
||||
|
||||
{#if urls}
|
||||
{#if urls.length > 0}
|
||||
<picture class="rounded-circle img-fluid">
|
||||
{#each urls as url}
|
||||
<source width=300 srcSet={url} type={contentTypeFor(url)} />
|
||||
<source {width} srcSet={url} type={contentTypeFor(url)} />
|
||||
{/each}
|
||||
<img width=300 src={urls[0]} {alt} class="rounded-circle img-fluid" />
|
||||
<img {width} src={urls[0]} {alt} class="rounded-circle img-fluid" />
|
||||
</picture>
|
||||
{:else}
|
||||
<!-- TODO: actual placeholder that isn't a cat -->
|
||||
<img width=300 class="rounded-circle img-fluid" src="https://placekitten.com/512/512" {alt} />
|
||||
<img {width} class="rounded-circle img-fluid" src="https://placekitten.com/512/512" {alt} />
|
||||
{/if}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</script>
|
||||
|
||||
<div>
|
||||
<h5>{field.name}</h5>
|
||||
<h4>{field.name}</h4>
|
||||
<ul class="list-unstyled">
|
||||
{#each field.entries as entry}
|
||||
<li><StatusIcon status={entry.status} /> {entry.value}</li>
|
||||
|
|
|
@ -1,15 +1,44 @@
|
|||
<script lang="ts">
|
||||
import type { PartialMember, User } from "$lib/api/entities";
|
||||
import { WordStatus, type PartialMember, type User } from "$lib/api/entities";
|
||||
import FallbackImage from "./FallbackImage.svelte";
|
||||
|
||||
export let user: User;
|
||||
export let member: PartialMember;
|
||||
|
||||
let pronouns: string | undefined;
|
||||
|
||||
const getPronouns = (member: PartialMember) => {
|
||||
const filteredPronouns = member.pronouns.filter(
|
||||
(pronouns) => pronouns.status === WordStatus.Favourite,
|
||||
);
|
||||
if (filteredPronouns.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return filteredPronouns
|
||||
.map((pronouns) => {
|
||||
if (pronouns.display_text) {
|
||||
return pronouns.display_text;
|
||||
} else {
|
||||
const split = pronouns.pronouns.split("/");
|
||||
if (split.length < 2) return split.join("/");
|
||||
else return split.slice(0, 2).join("/");
|
||||
}
|
||||
})
|
||||
.join(", ");
|
||||
};
|
||||
|
||||
$: pronouns = getPronouns(member);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<FallbackImage
|
||||
urls={member.avatar_urls}
|
||||
alt="Avatar for {member.name}"
|
||||
/>
|
||||
<a class="text-reset" href="/@{user.name}/{member.name}"><h5 class="m-2">{member.display_name ?? member.name}</h5></a>
|
||||
<FallbackImage urls={member.avatar_urls} width={200} alt="Avatar for {member.name}" />
|
||||
<p class="m-2">
|
||||
<a class="text-reset fs-5" href="/@{user.name}/{member.name}">
|
||||
{member.display_name ?? member.name}
|
||||
</a>
|
||||
{#if pronouns}
|
||||
<br />
|
||||
{pronouns}
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
|
|
|
@ -3,8 +3,8 @@ import { browser } from "$app/environment";
|
|||
|
||||
import type { MeUser } from "./api/entities";
|
||||
|
||||
export const userStore = writable<MeUser | null>(null);
|
||||
export const tokenStore = writable<string | null>(null);
|
||||
const initialUserValue = null;
|
||||
export const userStore = writable<MeUser | null>(initialUserValue);
|
||||
|
||||
let defaultThemeValue = "dark";
|
||||
const initialThemeValue = browser
|
||||
|
|
19
frontend/src/routes/+layout.server.ts
Normal file
19
frontend/src/routes/+layout.server.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { error } from "@sveltejs/kit";
|
||||
import type { LayoutServerLoad } from "./$types";
|
||||
import type { APIError } from "$lib/api/entities";
|
||||
import { apiFetch } from "$lib/api/fetch";
|
||||
|
||||
export const load = (async (event) => {
|
||||
try {
|
||||
return await apiFetch<MetaResponse>("/meta", {});
|
||||
} catch (e) {
|
||||
throw error(500, (e as APIError).message);
|
||||
}
|
||||
}) satisfies LayoutServerLoad;
|
||||
|
||||
interface MetaResponse {
|
||||
git_repository: string;
|
||||
git_commit: string;
|
||||
users: number;
|
||||
members: number;
|
||||
}
|
|
@ -1,9 +1,26 @@
|
|||
<script>
|
||||
<script lang="ts">
|
||||
import "bootstrap/dist/css/bootstrap.min.css";
|
||||
import "bootstrap-icons/font/bootstrap-icons.css";
|
||||
|
||||
import Navigation from "./nav/Navigation.svelte";
|
||||
import type { LayoutData } from "./$types";
|
||||
import { version } from "$app/environment";
|
||||
|
||||
export let data: LayoutData;
|
||||
|
||||
const versionMismatch = data.git_commit !== version && data.git_commit !== "[unknown]";
|
||||
</script>
|
||||
|
||||
<Navigation />
|
||||
<slot />
|
||||
<div class="container">
|
||||
<slot />
|
||||
<footer>
|
||||
<hr />
|
||||
<p>
|
||||
pronouns.cc <a href={data.git_repository}>{version}</a>
|
||||
{#if versionMismatch}(backend: {data.git_commit}){/if} ·
|
||||
<a href="/page/terms">Terms of service</a>
|
||||
- <a href="/page/privacy">Privacy policy</a>
|
||||
</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,21 @@
|
|||
import { apiFetch } from "$lib/api/fetch";
|
||||
import type { User } from "$lib/api/entities";
|
||||
import { ErrorCode, type APIError, type User } from "$lib/api/entities";
|
||||
import { error } from "@sveltejs/kit";
|
||||
|
||||
export const load = async ({ params }) => {
|
||||
const resp = await apiFetch<User>(`/users/${params.username}`, {
|
||||
method: "GET",
|
||||
});
|
||||
try {
|
||||
const resp = await apiFetch<User>(`/users/${params.username}`, {
|
||||
method: "GET",
|
||||
});
|
||||
|
||||
return resp;
|
||||
return resp;
|
||||
} catch (e) {
|
||||
if ((e as APIError).code === ErrorCode.UserNotFound) {
|
||||
throw error(404, (e as APIError).message);
|
||||
}
|
||||
|
||||
console.log(e);
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -48,41 +48,37 @@
|
|||
</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 class="row row-cols-1 row-cols-sm-2 row-cols-md-3">
|
||||
{#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}
|
||||
{#each data.fields as field}
|
||||
<div class="col">
|
||||
<FieldCard {field} />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{#if data.members}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
|
@ -90,7 +86,7 @@
|
|||
<h2>Members</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-5 text-center">
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-4 text-center">
|
||||
{#each data.members as member}
|
||||
<PartialMemberCard user={data} {member} />
|
||||
{/each}
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="row row-cols-1 row-cols-sm-2 row-cols-md-3">
|
||||
{#if data.names}
|
||||
<div class="col-md">
|
||||
<h4>Names</h4>
|
||||
|
@ -73,15 +73,11 @@
|
|||
</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>
|
||||
</div>
|
||||
|
|
|
@ -1,16 +1,16 @@
|
|||
import type { MeUser } from "$lib/api/entities";
|
||||
import type { APIError, MeUser } from "$lib/api/entities";
|
||||
import { apiFetch } from "$lib/api/fetch";
|
||||
import type { PageServerLoad } from "./$types";
|
||||
import type { PageServerLoad, Actions } from "./$types";
|
||||
import { PUBLIC_BASE_URL } from "$env/static/public";
|
||||
|
||||
export const load = (async (event) => {
|
||||
export const load = (async ({ locals, url }) => {
|
||||
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"),
|
||||
code: url.searchParams.get("code"),
|
||||
state: url.searchParams.get("state"),
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -18,7 +18,9 @@ export const load = (async (event) => {
|
|||
...resp,
|
||||
};
|
||||
} catch (e) {
|
||||
return { error: e };
|
||||
console.log(e);
|
||||
|
||||
return { error: e as APIError };
|
||||
}
|
||||
}) satisfies PageServerLoad;
|
||||
|
95
frontend/src/routes/auth/login/discord/+page.svelte
Normal file
95
frontend/src/routes/auth/login/discord/+page.svelte
Normal file
|
@ -0,0 +1,95 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { Alert } from "sveltestrap";
|
||||
|
||||
import { goto } from "$app/navigation";
|
||||
import type { APIError, MeUser } from "$lib/api/entities";
|
||||
import { apiFetch } from "$lib/api/fetch";
|
||||
import { userStore } from "$lib/store";
|
||||
import type { PageData } from "./$types";
|
||||
|
||||
interface SignupResponse {
|
||||
user: MeUser;
|
||||
token: string;
|
||||
}
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
onMount(() => {
|
||||
if (data.token && data.user) {
|
||||
localStorage.setItem("pronouns-token", data.token);
|
||||
userStore.set(data.user);
|
||||
goto("/");
|
||||
}
|
||||
});
|
||||
|
||||
let username = "";
|
||||
let invite = "";
|
||||
|
||||
const signupForm = async () => {
|
||||
try {
|
||||
const resp = await apiFetch<SignupResponse>("/auth/discord/signup", {
|
||||
method: "POST",
|
||||
body: {
|
||||
ticket: data.ticket,
|
||||
username: username,
|
||||
invite_code: invite,
|
||||
},
|
||||
});
|
||||
|
||||
localStorage.setItem("pronouns-token", resp.token);
|
||||
userStore.set(resp.user);
|
||||
goto("/");
|
||||
} catch (e) {
|
||||
data.error = e as APIError;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Log in with Discord - pronouns.cc</title>
|
||||
</svelte:head>
|
||||
|
||||
<h1>Log in with Discord</h1>
|
||||
|
||||
{#if data.error}
|
||||
<Alert color="danger">
|
||||
<h4 class="alert-heading">An error occurred</h4>
|
||||
<b>{data.error.code}:</b>
|
||||
{data.error.message}
|
||||
</Alert>
|
||||
{/if}
|
||||
{#if data.ticket}
|
||||
<form on:submit|preventDefault={signupForm}>
|
||||
<div>
|
||||
<label for="discord">Discord username</label>
|
||||
<input id="discord" class="form-control" name="discord" disabled value={data.discord} />
|
||||
</div>
|
||||
<div>
|
||||
<label for="username">Username</label>
|
||||
<input id="username" class="form-control" name="username" bind:value={username} />
|
||||
</div>
|
||||
{#if data.require_invite}
|
||||
<div>
|
||||
<label for="invite">Invite code</label>
|
||||
<input
|
||||
id="invite"
|
||||
class="form-control"
|
||||
name="invite"
|
||||
bind:value={invite}
|
||||
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.
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="form-text">
|
||||
By signing up, you agree to the <a href="/page/tos">terms of service</a> and the
|
||||
<a href="/page/privacy">privacy policy</a>.
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">Sign up</button>
|
||||
</form>
|
||||
{:else}
|
||||
Loading...
|
||||
{/if}
|
16
frontend/src/routes/auth/logout/+page.svelte
Normal file
16
frontend/src/routes/auth/logout/+page.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { userStore } from "$lib/store";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
export const ssr = false;
|
||||
|
||||
onMount(() => {
|
||||
userStore.set(null);
|
||||
localStorage.removeItem("pronouns-token");
|
||||
localStorage.removeItem("pronouns-user");
|
||||
goto("/");
|
||||
});
|
||||
</script>
|
||||
|
||||
<h1>Log out</h1>
|
|
@ -1,17 +0,0 @@
|
|||
<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}
|
|
@ -60,7 +60,7 @@
|
|||
};
|
||||
|
||||
const toggleTheme = () => {
|
||||
themeStore.set(theme === "dark" ? "light" : "dark")
|
||||
themeStore.set(theme === "dark" ? "light" : "dark");
|
||||
};
|
||||
const toggleMenu = () => {
|
||||
showMenu = !showMenu;
|
||||
|
@ -78,15 +78,21 @@
|
|||
<NavbarToggler on:click={toggleMenu} />
|
||||
<Collapse isOpen={showMenu} navbar expand="lg">
|
||||
<Nav class="ms-auto" navbar>
|
||||
<NavItem>
|
||||
{#if currentUser}
|
||||
{#if currentUser}
|
||||
<NavItem>
|
||||
<NavLink href="/@{currentUser.name}">@{currentUser.name}</NavLink>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink href="/settings">Settings</NavLink>
|
||||
<NavLink href="/logout">Log out</NavLink>
|
||||
{:else}
|
||||
<NavLink href="/login">Log in</NavLink>
|
||||
{/if}
|
||||
</NavItem>
|
||||
</NavItem>
|
||||
<NavItem>
|
||||
<NavLink href="/auth/logout">Log out</NavLink>
|
||||
</NavItem>
|
||||
{:else}
|
||||
<NavItem>
|
||||
<NavLink href="/auth/login">Log in</NavLink>
|
||||
</NavItem>
|
||||
{/if}
|
||||
<NavItem>
|
||||
<NavLink
|
||||
on:click={() => toggleTheme()}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import adapter from "@sveltejs/adapter-node";
|
||||
import { vitePreprocess } from "@sveltejs/kit/vite";
|
||||
import * as child_process from "node:child_process";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
const config = {
|
||||
|
@ -9,6 +10,9 @@ const config = {
|
|||
|
||||
kit: {
|
||||
adapter: adapter(),
|
||||
version: {
|
||||
name: child_process.execSync("git rev-parse --short HEAD").toString().trim(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in a new issue