feat(frontend): export ui

This commit is contained in:
sam 2024-12-03 20:02:09 +01:00
parent 74222ead45
commit c20831f20d
Signed by: sam
GPG key ID: B4EF20DDE721CAA1
8 changed files with 104 additions and 9 deletions

View file

@ -112,7 +112,13 @@
"create-member-name-label": "Member name", "create-member-name-label": "Member name",
"auth-remove-method": "Remove", "auth-remove-method": "Remove",
"force-log-out-warning": "Make sure you're still able to log in before using this!", "force-log-out-warning": "Make sure you're still able to log in before using this!",
"force-log-out-confirmation": "Are you sure you want to log out from all devices? If you just want to log out from this device, click the \"Log out\" button on your settings page." "force-log-out-confirmation": "Are you sure you want to log out from all devices? If you just want to log out from this device, click the \"Log out\" button on your settings page.",
"export-request-success": "Successfully requested a new export! Please note that it may take a few minutes to complete, especially if you have a lot of members.",
"export-title": "Request a copy of your data",
"export-info": "You can request a copy of your data once every 24 hours. Exports are stored for 15 days (a little over two weeks) and then deleted.",
"export-expires-at": "(expires {{expiresAt}})",
"export-download": "Download export",
"export-request-button": "Request a new export"
}, },
"yes": "Yes", "yes": "Yes",
"no": "No", "no": "No",

View file

@ -7,6 +7,7 @@
import Error from "$components/Error.svelte"; import Error from "$components/Error.svelte";
import { idTimestamp } from "$lib"; import { idTimestamp } from "$lib";
import { DateTime } from "luxon"; import { DateTime } from "luxon";
import { enhance } from "$app/forms";
type Props = { data: PageData; form: ActionData }; type Props = { data: PageData; form: ActionData };
let { data, form }: Props = $props(); let { data, form }: Props = $props();
@ -20,7 +21,7 @@
<div class="row mb-3"> <div class="row mb-3">
<div class="col-md-9"> <div class="col-md-9">
<h5>Change your username</h5> <h5>Change your username</h5>
<form method="POST" action="?/changeUsername"> <form method="POST" action="?/changeUsername" use:enhance>
<FormGroup class="mb-3"> <FormGroup class="mb-3">
<InputGroup class="m-1 mt-3 w-md-75"> <InputGroup class="m-1 mt-3 w-md-75">
<Input <Input

View file

@ -0,0 +1,35 @@
import { apiRequest, fastRequest } from "$api";
import ApiError from "$api/error.js";
import log from "$lib/log.js";
import { DateTime, Duration } from "luxon";
type Export = { url: string | null; expires_at: string | null };
export const load = async ({ fetch, cookies }) => {
const resp = await apiRequest<Export>("GET", "/data-exports", {
fetch,
cookies,
isInternal: true,
});
let canExport = true;
if (resp.expires_at) {
const created = DateTime.fromISO(resp.expires_at).minus(Duration.fromObject({ days: 15 }));
canExport = DateTime.now().diff(created, "seconds").seconds >= 86400;
}
return { url: resp.url, expiresAt: resp.expires_at, canExport };
};
export const actions = {
default: async ({ fetch, cookies }) => {
try {
fastRequest("POST", "/data-exports", { fetch, cookies, isInternal: true });
return { ok: true, error: null };
} catch (e) {
if (e instanceof ApiError) return { ok: false, error: e.obj };
log.error("Error requesting data export:", e);
throw e;
}
},
};

View file

@ -0,0 +1,49 @@
<script lang="ts">
import { DateTime } from "luxon";
import type { ActionData, PageData } from "./$types";
import ErrorAlert from "$components/ErrorAlert.svelte";
import { Icon } from "@sveltestrap/sveltestrap";
import { t } from "$lib/i18n";
import { enhance } from "$app/forms";
type Props = { data: PageData; form: ActionData };
let { data, form }: Props = $props();
let expiresAt = $derived.by(() => {
if (!data.expiresAt) return null;
return DateTime.fromISO(data.expiresAt);
});
</script>
<div class="mx-auto w-lg-75">
<h3>{$t("settings.export-title")}</h3>
{#if form?.ok}
<p class="text-success-emphasis">
<Icon name="check-circle-fill" />
{$t("settings.export-request-success")}
</p>
{:else if form?.error}
<ErrorAlert error={form.error} />
{/if}
<p>
{$t("settings.export-info")}
</p>
<form method="POST" use:enhance>
<div class="btn-group">
<button type="submit" class="btn btn-primary" disabled={!data.canExport}>
{$t("settings.export-request-button")}
</button>
{#if data.url}
<a href={data.url} target="_blank" class="btn btn-success">
{$t("settings.export-download")}
{#if expiresAt}
{$t("settings.export-expires-at", { expiresAt: expiresAt.toRelative() })}
{/if}
</a>
{/if}
</div>
</form>
</div>

View file

@ -14,6 +14,7 @@
import SidEditor from "$components/editor/SidEditor.svelte"; import SidEditor from "$components/editor/SidEditor.svelte";
import BioEditor from "$components/editor/BioEditor.svelte"; import BioEditor from "$components/editor/BioEditor.svelte";
import { PUBLIC_BASE_URL } from "$env/static/public"; import { PUBLIC_BASE_URL } from "$env/static/public";
import { enhance } from "$app/forms";
type Props = { data: PageData; form: ActionData }; type Props = { data: PageData; form: ActionData };
let { data, form }: Props = $props(); let { data, form }: Props = $props();
@ -83,7 +84,7 @@
</div> </div>
<div class="col-md"> <div class="col-md">
<h4>{$t("edit-profile.member-name")}</h4> <h4>{$t("edit-profile.member-name")}</h4>
<form method="POST" action="?/changeName" class="mb-3"> <form method="POST" action="?/changeName" class="mb-3" use:enhance>
<InputGroup> <InputGroup>
<input <input
name="name" name="name"
@ -99,7 +100,7 @@
</form> </form>
<h4>{$t("edit-profile.display-name")}</h4> <h4>{$t("edit-profile.display-name")}</h4>
<form class="mb-3" method="POST" action="?/changeDisplayName"> <form class="mb-3" method="POST" action="?/changeDisplayName" use:enhance>
<InputGroup> <InputGroup>
<input <input
class="form-control" class="form-control"
@ -117,7 +118,7 @@
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<h4>{$t("edit-profile.profile-options-header")}</h4> <h4>{$t("edit-profile.profile-options-header")}</h4>
<form method="POST" action="?/options"> <form method="POST" action="?/options" use:enhance>
<div class="form-check"> <div class="form-check">
<input <input
class="form-check-input" class="form-check-input"
@ -146,7 +147,7 @@
</div> </div>
<div class="row mb-3"> <div class="row mb-3">
<h4>{$t("edit-profile.bio-tab")}</h4> <h4>{$t("edit-profile.bio-tab")}</h4>
<form method="POST" action="?/bio"> <form method="POST" action="?/bio" use:enhance>
<BioEditor bind:value={bio} maxLength={data.meta.limits.bio_length} /> <BioEditor bind:value={bio} maxLength={data.meta.limits.bio_length} />
</form> </form>
</div> </div>

View file

@ -1,4 +1,5 @@
<script lang="ts"> <script lang="ts">
import { enhance } from "$app/forms";
import ErrorAlert from "$components/ErrorAlert.svelte"; import ErrorAlert from "$components/ErrorAlert.svelte";
import { t } from "$lib/i18n"; import { t } from "$lib/i18n";
import type { ActionData } from "./$types"; import type { ActionData } from "./$types";
@ -13,7 +14,7 @@
<ErrorAlert error={form.error} /> <ErrorAlert error={form.error} />
{/if} {/if}
<form method="POST"> <form method="POST" use:enhance>
<div class="my-3"> <div class="my-3">
<label class="form-label" for="name">{$t("settings.create-member-name-label")}</label> <label class="form-label" for="name">{$t("settings.create-member-name-label")}</label>
<input class="form-control" type="text" id="name" name="name" required autocomplete="off" /> <input class="form-control" type="text" id="name" name="name" required autocomplete="off" />

View file

@ -11,6 +11,7 @@
import { DateTime, FixedOffsetZone } from "luxon"; import { DateTime, FixedOffsetZone } from "luxon";
import FormStatusMarker from "$components/editor/FormStatusMarker.svelte"; import FormStatusMarker from "$components/editor/FormStatusMarker.svelte";
import SidEditor from "$components/editor/SidEditor.svelte"; import SidEditor from "$components/editor/SidEditor.svelte";
import { enhance } from "$app/forms";
type Props = { data: PageData; form: ActionData }; type Props = { data: PageData; form: ActionData };
let { data, form }: Props = $props(); let { data, form }: Props = $props();
@ -128,7 +129,7 @@
<div class="mt-3"> <div class="mt-3">
<h4>{$t("edit-profile.profile-options-header")}</h4> <h4>{$t("edit-profile.profile-options-header")}</h4>
<form method="POST" action="?/options"> <form method="POST" action="?/options" use:enhance>
<div class="mb-3"> <div class="mb-3">
<label class="form-label" for="member-title">{$t("edit-profile.member-header-label")}</label> <label class="form-label" for="member-title">{$t("edit-profile.member-header-label")}</label>
<input <input

View file

@ -3,6 +3,7 @@
import type { ActionData, PageData } from "./$types"; import type { ActionData, PageData } from "./$types";
import BioEditor from "$components/editor/BioEditor.svelte"; import BioEditor from "$components/editor/BioEditor.svelte";
import { t } from "$lib/i18n"; import { t } from "$lib/i18n";
import { enhance } from "$app/forms";
type Props = { data: PageData; form: ActionData }; type Props = { data: PageData; form: ActionData };
let { data, form }: Props = $props(); let { data, form }: Props = $props();
@ -12,6 +13,6 @@
<h4>{$t("edit-profile.bio-tab")}</h4> <h4>{$t("edit-profile.bio-tab")}</h4>
<FormStatusMarker {form} /> <FormStatusMarker {form} />
<form method="POST"> <form method="POST" use:enhance>
<BioEditor bind:value={bio} maxLength={data.meta.limits.bio_length} /> <BioEditor bind:value={bio} maxLength={data.meta.limits.bio_length} />
</form> </form>