Skip to content

Commit 35193a6

Browse files
committed
Add settings page
1 parent 5f73b4b commit 35193a6

File tree

10 files changed

+279
-38
lines changed

10 files changed

+279
-38
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "User" ADD COLUMN "settings" JSONB NOT NULL DEFAULT '{}';

src/lib/server/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ model User {
1717
password String
1818
name String
1919
verified Boolean @default(false)
20+
settings Json @default("{}")
2021
pastes Paste[]
2122
AuthToken AuthToken[]
2223
}

src/lib/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,11 @@ export interface PastePatchResponse {
3737
};
3838
error?: string;
3939
}
40+
41+
export interface UserSettings {
42+
defaults?: {
43+
encrypted?: boolean;
44+
burnAfterRead?: boolean;
45+
expiresAfterSeconds?: number;
46+
};
47+
}

src/lib/utils/time.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export function secondsToDHM(seconds: number) {
2+
const days = Math.floor(seconds / (3600 * 24));
3+
const hours = Math.floor((seconds % (3600 * 24)) / 3600);
4+
const minutes = Math.floor((seconds % 3600) / 60);
5+
return { days, hours, minutes };
6+
}
7+
8+
export function DHMToSeconds({
9+
days,
10+
hours,
11+
minutes
12+
}: {
13+
days?: number;
14+
hours?: number;
15+
minutes?: number;
16+
}) {
17+
return (days ?? 0) * 3600 * 24 + (hours ?? 0) * 3600 + (minutes ?? 0) * 60;
18+
}

src/routes/(auth)/logout/+page.server.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { redirect, type Actions, type RequestHandler } from '@sveltejs/kit';
1+
import { redirect, type Actions } from '@sveltejs/kit';
22
import type { PageServerLoad } from './$types';
33

44
export const load: PageServerLoad = async ({ cookies }) => {

src/routes/+page.server.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,19 @@
11
import { getUserIdFromCookie } from '$lib/server/auth';
2+
import type { UserSettings } from '$lib/types';
3+
import prisma from '@db';
24

35
export async function load({ cookies }) {
46
const userId = await getUserIdFromCookie(cookies);
5-
return { loggedIn: !!userId };
7+
8+
let settings: UserSettings | undefined;
9+
10+
if (userId) {
11+
const user = await prisma.user.findUnique({
12+
where: { id: userId },
13+
select: { settings: true }
14+
});
15+
settings = user?.settings as UserSettings;
16+
}
17+
18+
return { loggedIn: !!userId, settings };
619
}

src/routes/+page.svelte

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
<script lang="ts">
22
import { goto } from '$app/navigation';
33
import { languageKeysByName } from '$lib/data';
4-
import type { Paste, PasteConfig, PasteCreateResponse } from '$lib/types';
4+
import type { Paste, PasteConfig, PasteCreateResponse, UserSettings } from '$lib/types';
55
import { onMount } from 'svelte';
66
import Select from 'svelte-select';
77
import { encrypt, encryptWithPassword } from '$lib/crypto';
88
import Hamburger from '$lib/components/Hamburger.svelte';
99
import { PUBLIC_REGISRATION_ENABLED } from '$env/static/public';
1010
import type { PageData } from './$types';
11+
import { DHMToSeconds, secondsToDHM } from '$lib/utils/time';
1112
1213
export let data: PageData;
1314
@@ -25,42 +26,18 @@
2526
} = {};
2627
2728
$: {
28-
if (expiresAfter.days) {
29-
expiresAfter.days = Math.max(0, Math.round(expiresAfter.days));
29+
let expiresAfterSeconds = DHMToSeconds(expiresAfter);
30+
// Don't allow pastes to be saved for more than a year
31+
expiresAfterSeconds = Math.min(expiresAfterSeconds, 365 * 24 * 60 * 60);
32+
// Don't allow pastes to be saved for less than 5 minutes
33+
if (expiresAfterSeconds > 0) {
34+
expiresAfterSeconds = Math.max(expiresAfterSeconds, 5 * 60);
35+
expiresAfter = secondsToDHM(expiresAfterSeconds);
36+
} else {
37+
expiresAfter = {};
3038
}
31-
if (expiresAfter.hours) {
32-
expiresAfter.hours = Math.max(0, Math.round(expiresAfter.hours));
33-
if (expiresAfter.hours > 23) {
34-
expiresAfter.days ??= 0;
35-
expiresAfter.days += Math.floor(expiresAfter.hours / 24);
36-
expiresAfter.hours = expiresAfter.hours % 24;
37-
}
38-
}
39-
if (expiresAfter.minutes) {
40-
expiresAfter.minutes = Math.max(0, Math.round(expiresAfter.minutes));
41-
if (expiresAfter.minutes > 59) {
42-
expiresAfter.days ??= 0;
43-
expiresAfter.hours ??= 0;
44-
expiresAfter.days += Math.floor(expiresAfter.minutes / 1440);
45-
expiresAfter.hours += Math.floor((expiresAfter.minutes % 1440) / 60);
46-
expiresAfter.minutes = expiresAfter.minutes % 60;
47-
}
4839
49-
if (
50-
!expiresAfter.days &&
51-
!expiresAfter.hours &&
52-
expiresAfter.minutes > 0 &&
53-
expiresAfter.minutes < 5
54-
) {
55-
expiresAfter.minutes = 5;
56-
}
57-
}
58-
59-
config.expiresAfter =
60-
((expiresAfter.days ?? 0) * 1440 +
61-
(expiresAfter.hours ?? 0) * 60 +
62-
(expiresAfter.minutes ?? 0)) *
63-
60;
40+
config.expiresAfter = expiresAfterSeconds;
6441
}
6542
6643
let inputRef: HTMLTextAreaElement;
@@ -71,6 +48,17 @@
7148
let config: PasteConfig = { ...initialConfig };
7249
let sidebarOpen = false;
7350
51+
const updateInitialConfig = (defaults: UserSettings['defaults']) => {
52+
if (!defaults) return;
53+
if (defaults?.encrypted !== undefined) config.encrypted = defaults.encrypted;
54+
if (defaults?.burnAfterRead !== undefined) config.burnAfterRead = defaults.burnAfterRead;
55+
if (defaults?.expiresAfterSeconds) {
56+
expiresAfter = secondsToDHM(defaults.expiresAfterSeconds);
57+
config.expiresAfter = defaults.expiresAfterSeconds;
58+
}
59+
};
60+
$: updateInitialConfig(data?.settings?.defaults);
61+
7462
let _sessionStorage: Storage | undefined;
7563
7664
$: if (_sessionStorage) {
@@ -84,7 +72,7 @@
8472
if (contentBackup) {
8573
const data: { content: string; config: PasteConfig } = JSON.parse(contentBackup);
8674
content = data.content;
87-
config = data.config;
75+
config = { ...config, language: data.config.language ?? config.language };
8876
}
8977
9078
inputRef.focus();
@@ -229,6 +217,7 @@
229217
{#if PUBLIC_REGISRATION_ENABLED == 'true'}
230218
<div class="flex flex-row gap-4 mb-4 justify-center">
231219
{#if data.loggedIn}
220+
<a href="/dashboard/settings" class="underline underline-offset-4 py-1">Dashboard</a>
232221
<form action="/logout" method="post">
233222
<button class="underline underline-offset-4 py-1">Logout</button>
234223
</form>

src/routes/dashboard/+layout.svelte

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<script lang="ts">
2+
import { goto } from '$app/navigation';
3+
import { onMount } from 'svelte';
4+
5+
let cmdKey = 'Ctrl';
6+
onMount(() => {
7+
const isMac =
8+
(navigator as any).userAgentData?.platform?.toLowerCase() === 'macos' ||
9+
navigator.platform?.toLowerCase().startsWith('mac');
10+
cmdKey = isMac ? '' : 'Ctrl';
11+
12+
document.addEventListener('keydown', (e) => {
13+
if (e.key === 'n' && (e.ctrlKey || e.metaKey)) {
14+
e.preventDefault();
15+
goto('/');
16+
}
17+
18+
if (e.key === 'i' && (e.ctrlKey || e.metaKey)) {
19+
e.preventDefault();
20+
goto('/info');
21+
}
22+
});
23+
});
24+
</script>
25+
26+
<div class="p-2 min-h-screen w-screen flex flex-col text-primary">
27+
<div class="pb-4">
28+
<div class="flex flex-row items-center gap-4">
29+
<h1 class="mr-auto text-2xl"><a href="/">YABin</a></h1>
30+
31+
<!-- <a class="underline underline-offset-4 px-2 py-1" href="/dashboard/pastes">Pastes</a> -->
32+
<a class="underline underline-offset-4 px-2 py-1" href="/dashboard/settings">Settings</a>
33+
34+
<button
35+
class="underline underline-offset-4 px-2 py-1"
36+
title="{cmdKey}+I"
37+
on:click={() => goto('/info')}
38+
>
39+
Info
40+
</button>
41+
42+
<button
43+
class="bg-amber-500 text-black text-lg px-4 py-1"
44+
title="{cmdKey}+N"
45+
on:click={() => goto('/')}
46+
>
47+
New
48+
</button>
49+
</div>
50+
</div>
51+
52+
<div class="px-24 py-4">
53+
<slot />
54+
</div>
55+
</div>
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { getUserIdFromCookie } from '$lib/server/auth';
2+
import { redirect, type Actions } from '@sveltejs/kit';
3+
import type { PageServerLoad } from './$types';
4+
import prisma from '@db';
5+
import type { UserSettings } from '$lib/types';
6+
7+
export const load: PageServerLoad = async ({ cookies }) => {
8+
const userId = await getUserIdFromCookie(cookies);
9+
if (!userId) throw redirect(303, '/login');
10+
11+
const user = await prisma.user.findUnique({
12+
where: { id: userId },
13+
select: { settings: true }
14+
});
15+
16+
return { settings: user?.settings as UserSettings };
17+
};
18+
19+
export const actions: Actions = {
20+
defaults: async ({ cookies, request }) => {
21+
const userId = await getUserIdFromCookie(cookies);
22+
if (!userId) throw redirect(303, '/login');
23+
24+
const formData = await request.formData();
25+
const expiresAfterSeconds = parseInt(formData.get('expires-after-seconds')?.toString() ?? '0');
26+
const data = {
27+
encrypted: formData.get('encrypted') === 'on',
28+
burnAfterRead: formData.get('burn-after-read') === 'on',
29+
expiresAfterSeconds: Math.max(0, Math.min(365 * 24 * 3600, expiresAfterSeconds))
30+
};
31+
32+
const user = await prisma.user.update({
33+
where: { id: userId },
34+
data: { settings: { defaults: data } },
35+
select: { settings: true }
36+
});
37+
38+
return { defaultsForm: { success: true, settings: user.settings as UserSettings } };
39+
}
40+
};
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
<script lang="ts">
2+
import { applyAction, enhance } from '$app/forms';
3+
import { goto } from '$app/navigation';
4+
import type { UserSettings } from '$lib/types';
5+
import { DHMToSeconds, secondsToDHM } from '$lib/utils/time';
6+
import type { ActionData, PageData } from './$types';
7+
8+
export let data: PageData;
9+
export let form: ActionData;
10+
11+
let expiresAfter: {
12+
days?: number;
13+
hours?: number;
14+
minutes?: number;
15+
} = {};
16+
let expiresAfterSeconds: number = 0;
17+
18+
$: settings = form?.defaultsForm?.settings || data?.settings;
19+
20+
const updateInitialConfig = (defaults: UserSettings['defaults']) => {
21+
if (!defaults) return;
22+
if (defaults?.expiresAfterSeconds) {
23+
expiresAfterSeconds = defaults.expiresAfterSeconds;
24+
expiresAfter = secondsToDHM(expiresAfterSeconds);
25+
}
26+
};
27+
$: updateInitialConfig(settings?.defaults);
28+
29+
$: {
30+
expiresAfterSeconds = DHMToSeconds(expiresAfter);
31+
// Don't allow pastes to be saved for more than a year
32+
expiresAfterSeconds = Math.min(expiresAfterSeconds, 365 * 24 * 60 * 60);
33+
// Don't allow pastes to be saved for less than 5 minutes
34+
if (expiresAfterSeconds > 0) {
35+
expiresAfterSeconds = Math.max(expiresAfterSeconds, 5 * 60);
36+
expiresAfter = secondsToDHM(expiresAfterSeconds);
37+
} else {
38+
expiresAfter = {};
39+
}
40+
}
41+
</script>
42+
43+
<h1 class="text-5xl">Settings</h1>
44+
45+
<div class="px-4">
46+
<h4 class="text-2xl mt-6 mb-4">Defaults</h4>
47+
48+
<form
49+
method="post"
50+
action="?/defaults"
51+
class="mt-2 flex flex-col gap-4"
52+
use:enhance={() => {
53+
return async ({ result }) => {
54+
if (result.type === 'redirect') await goto(result.location);
55+
else await applyAction(result);
56+
};
57+
}}
58+
>
59+
<div>
60+
<label for="encrypted" class="py-2">Encrypted</label>
61+
<input
62+
id="encrypted"
63+
class="bg-dark px-2 py-1"
64+
type="checkbox"
65+
name="encrypted"
66+
checked={settings?.defaults?.encrypted}
67+
/>
68+
</div>
69+
70+
<div>
71+
<label for="burn-after-read" class="py-2">Burn after read</label>
72+
<input
73+
id="burn-after-read"
74+
class="bg-dark px-2 py-1"
75+
type="checkbox"
76+
name="burn-after-read"
77+
checked={settings?.defaults?.burnAfterRead}
78+
/>
79+
</div>
80+
81+
<div>
82+
<span>Expires in:</span>
83+
<div class="grid grid-cols-3 gap-2 justify-center items-center">
84+
<input
85+
type="number"
86+
class="bg-dark py-1 text-center"
87+
placeholder="DD"
88+
bind:value={expiresAfter.days}
89+
/>
90+
<input
91+
type="number"
92+
class="bg-dark py-1 text-center"
93+
placeholder="HH"
94+
bind:value={expiresAfter.hours}
95+
/>
96+
<input
97+
type="number"
98+
class="bg-dark py-1 text-center"
99+
placeholder="MM"
100+
bind:value={expiresAfter.minutes}
101+
/>
102+
<input type="hidden" name="expires-after-seconds" bind:value={expiresAfterSeconds} />
103+
</div>
104+
</div>
105+
106+
<div class="mt-2">
107+
<button class="bg-amber-500 text-black text-lg px-4 py-1">Save</button>
108+
{#if form?.defaultsForm.success}
109+
<span class="text-green-500">Saved</span>
110+
<!-- {:else if form?.defaultsForm.error}
111+
<span class="text-red-500">Error</span> -->
112+
{/if}
113+
</div>
114+
</form>
115+
</div>

0 commit comments

Comments
 (0)