add: frontend profile data etc
This commit is contained in:
625
frontend/src/routes/auth/about/[id]/+page.svelte
Normal file
625
frontend/src/routes/auth/about/[id]/+page.svelte
Normal file
@@ -0,0 +1,625 @@
|
||||
<script>
|
||||
import ImageInput from "$lib/components/ImageInput.svelte";
|
||||
import InputField from "$lib/components/InputField.svelte";
|
||||
import SmallLoader from "$lib/components/SmallLoader.svelte";
|
||||
import Modal from "$lib/components/Modal.svelte";
|
||||
import Avatar from "$lib/img/TeamAvatar.jpeg";
|
||||
import { onMount } from "svelte";
|
||||
import { applyAction, enhance } from "$app/forms";
|
||||
import { page } from "$app/stores";
|
||||
import { receive, send } from "$lib/utils/helpers";
|
||||
import { t } from "svelte-i18n";
|
||||
|
||||
$: ({ user } = $page.data);
|
||||
|
||||
/** @typedef {{name: string, src: string}} Avatar */
|
||||
const avatarFiles = import.meta.glob("$lib/img/Avatar-*.jpeg", {
|
||||
eager: true,
|
||||
});
|
||||
/** @type{Avatar[]} */
|
||||
let avatars = [];
|
||||
|
||||
/**
|
||||
* @typedef {Object} FormData
|
||||
* @property {string} first_name
|
||||
* @property {string} last_name
|
||||
* @property {string} email
|
||||
* @property {string} [password]
|
||||
* @property {string} [password2]
|
||||
* @property {string} [phone]
|
||||
* @property {string} address
|
||||
* @property {string} zip_code
|
||||
* @property {string} city
|
||||
*/
|
||||
/**
|
||||
* @typedef {Object.<string, string>} ValidationErrors
|
||||
*/
|
||||
/**
|
||||
* @type {ValidationErrors}
|
||||
*/
|
||||
let validationErrors = {};
|
||||
|
||||
const TABS = ["profile", "membership", "bankaccount"];
|
||||
let activeTab = TABS[0];
|
||||
|
||||
let showModal = false,
|
||||
isUploading = false,
|
||||
isUpdating = false,
|
||||
showAvatars = false;
|
||||
const open = () => (showModal = true);
|
||||
const close = () => (showModal = false);
|
||||
const toggleAvatars = () => (showAvatars = !showAvatars);
|
||||
|
||||
onMount(() => {
|
||||
avatars = Object.entries(avatarFiles).map(([path, module]) => {
|
||||
if (typeof path !== "string") {
|
||||
throw new Error("Unexpected non-string path");
|
||||
}
|
||||
if (
|
||||
typeof module !== "object" ||
|
||||
module === null ||
|
||||
!("default" in module)
|
||||
) {
|
||||
throw new Error("Unexpected module format");
|
||||
}
|
||||
const src = module.default;
|
||||
if (typeof src !== "string") {
|
||||
throw new Error("Unexpected default export type");
|
||||
}
|
||||
return {
|
||||
name: path.split("/").pop()?.split(".")[0] ?? "Unknown",
|
||||
src: src,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
/** @type {import('./$types').ActionData} */
|
||||
export let form;
|
||||
|
||||
/**
|
||||
* Sets the active tab
|
||||
* @param {string} tab - The tab to set as active
|
||||
*/
|
||||
function setActiveTab(tab) {
|
||||
activeTab = tab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates thek form data
|
||||
* @param {Object} data - The form data to validate
|
||||
* @returns {ValidationErrors} An object containing validation errors
|
||||
*/
|
||||
function validateForm(data) {
|
||||
/** @type {ValidationErrors} */
|
||||
let errors = {};
|
||||
|
||||
if ("first_name" in data && !String(data.first_name).trim()) {
|
||||
errors.first_name = $t("required");
|
||||
}
|
||||
if ("last_name" in data && !String(data.last_name).trim()) {
|
||||
errors.last_name = $t("required");
|
||||
}
|
||||
if (
|
||||
"email" in data &&
|
||||
(!data.email || !/^\S+@\S+\.\S+$/.test(String(data.email)))
|
||||
) {
|
||||
errors.email = $t("required");
|
||||
}
|
||||
if ("password" in data) {
|
||||
if (String(data.password).length < 8) {
|
||||
errors.password = $t("required_password");
|
||||
}
|
||||
if ("password2" in data && data.password !== data.password2) {
|
||||
errors.password2 = $t("required_password_match");
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
/** @type {import('./$types').SubmitFunction} */
|
||||
const handleUpdate = async ({ form, formData, action, cancel }) => {
|
||||
/** @type {Object.<string, FormDataEntryValue>} */
|
||||
const fd = Object.fromEntries(formData);
|
||||
validationErrors = validateForm(fd);
|
||||
if (Object.keys(validationErrors).length > 0) {
|
||||
cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
isUpdating = true;
|
||||
return async ({ result }) => {
|
||||
isUpdating = false;
|
||||
if (result.type === "success" || result.type === "redirect") {
|
||||
validationErrors = {};
|
||||
close();
|
||||
} else if (result.type == "failure" && result.data?.errors) {
|
||||
/** @type {ValidationErrors} */
|
||||
validationErrors = {};
|
||||
// Assuming result.data.errors is an array of {error: string, id: string}
|
||||
result.data.errors.forEach(({ error, id }) => {
|
||||
validationErrors[id] = error;
|
||||
});
|
||||
}
|
||||
await applyAction(result);
|
||||
};
|
||||
};
|
||||
|
||||
/** @type {import('./$types').SubmitFunction} */
|
||||
const handleUpload = async () => {
|
||||
isUploading = true;
|
||||
return async ({ result }) => {
|
||||
isUploading = false;
|
||||
/** @type {any} */
|
||||
const res = result;
|
||||
if (result.type === "success" || result.type === "redirect") {
|
||||
user.thumbnail = res.data.thumbnail;
|
||||
}
|
||||
await applyAction(result);
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="hero-container">
|
||||
<div class="hero-logo">
|
||||
<img
|
||||
src={user.thumbnail ? user.thumbnail : Avatar}
|
||||
alt={`${user.first_name} ${user.last_name}`}
|
||||
width="200"
|
||||
/>
|
||||
</div>
|
||||
<div class="user-info">
|
||||
{#if user.status}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Status:</span>
|
||||
<span class="value block-value">
|
||||
<span
|
||||
>{$t(`userStatus.${user.status}`, {
|
||||
default: "unknown status",
|
||||
})}</span
|
||||
>
|
||||
<span
|
||||
>{$t(`userRole.${user.role_id}`, { default: "unknown role" })}</span
|
||||
>
|
||||
</span>
|
||||
</h3>
|
||||
{/if}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Name:</span>
|
||||
<span class="value">{`${user.first_name} ${user.last_name}`}</span>
|
||||
</h3>
|
||||
{#if user.email}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Email:</span>
|
||||
<span class="value">{user.email}</span>
|
||||
</h3>
|
||||
{/if}
|
||||
{#if user.address}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Adresse:</span>
|
||||
<span class="value block-value">
|
||||
<span>{user.address}</span>
|
||||
<span>{`${user.zip_code} ${user.city}`}</span>
|
||||
</span>
|
||||
</h3>
|
||||
{/if}
|
||||
{#if user.phone}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Telefon:</span>
|
||||
<span class="value">{user.phone}</span>
|
||||
</h3>
|
||||
{/if}
|
||||
{#if user.birth_date}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">Geburtstag:</span>
|
||||
<span class="value">{user.birth_date}</span>
|
||||
</h3>
|
||||
{/if}
|
||||
{#if user.notes}
|
||||
<h3 class="hero-subtitle subtitle info-row">
|
||||
<span class="label">{$t("notes")}:</span>
|
||||
<span class="value">{user.notes}</span>
|
||||
</h3>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="hero-buttons-container">
|
||||
<button class="button-dark" on:click={open}>Ändern</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showModal}
|
||||
<Modal on:close={close}>
|
||||
<div class="avatar-container">
|
||||
<form
|
||||
class="avatar-form"
|
||||
action="?/uploadImage"
|
||||
method="post"
|
||||
enctype="multipart/form-data"
|
||||
use:enhance={handleUpload}
|
||||
>
|
||||
<div class="current-avatar">
|
||||
<ImageInput
|
||||
avatar={user.thumbnail}
|
||||
fieldName="thumbnail"
|
||||
title="Nutzerbild auswählen"
|
||||
/>
|
||||
</div>
|
||||
<div class="avatar-buttons">
|
||||
{#if !user.thumbnail}
|
||||
{#if isUploading}
|
||||
<SmallLoader width={30} message={"Uploading..."} />
|
||||
{:else}
|
||||
<button class="button-dark" type="submit">Bild hochladen</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<input
|
||||
type="hidden"
|
||||
hidden
|
||||
name="thumbnail_url"
|
||||
value={user.thumbnail}
|
||||
required
|
||||
/>
|
||||
{#if isUploading}
|
||||
<SmallLoader width={30} message={"Lösche..."} />
|
||||
{:else}
|
||||
<button
|
||||
class="button-dark"
|
||||
formaction="?/deleteImage"
|
||||
type="submit"
|
||||
>
|
||||
Bild löschen
|
||||
</button>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
<div class="avatar-buttons">
|
||||
<button class="button-dark" on:click={toggleAvatars}>
|
||||
{showAvatars ? "Abbrechen" : "Profilbild auswählen"}
|
||||
</button>
|
||||
</div>
|
||||
{#if showAvatars}
|
||||
<div class="avatar-selection">
|
||||
{#each avatars as avatar}
|
||||
<button
|
||||
class="avatar-option"
|
||||
on:click={() => {
|
||||
user.thumbnail = avatar.src;
|
||||
showAvatars = false;
|
||||
}}
|
||||
>
|
||||
<img src={avatar.src} alt={avatar.name} width="80" />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<form
|
||||
class="content"
|
||||
action="?/updateUser"
|
||||
method="POST"
|
||||
use:enhance={handleUpdate}
|
||||
>
|
||||
<h1 class="step-title" style="text-align: center;">{$t("user_edit")}</h1>
|
||||
{#if form?.success}
|
||||
<h4
|
||||
class="step-subtitle warning"
|
||||
in:receive={{ key: Math.floor(Math.random() * 100) }}
|
||||
out:send={{ key: Math.floor(Math.random() * 100) }}
|
||||
>
|
||||
Um einen fehlerhaften upload Ihres Bildes zu vermeiden, clicke bitte
|
||||
auf den "Update" Button unten.
|
||||
</h4>
|
||||
{/if}
|
||||
{#if form?.errors}
|
||||
{#each form?.errors as error (error.id)}
|
||||
<h4
|
||||
class="step-subtitle warning"
|
||||
in:receive={{ key: error.id }}
|
||||
out:send={{ key: error.id }}
|
||||
>
|
||||
{error.error}
|
||||
</h4>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
<input type="hidden" hidden name="thumbnail" value={user.thumbnail} />
|
||||
|
||||
<div class="button-container">
|
||||
{#each TABS as tab}
|
||||
<button
|
||||
type="button"
|
||||
class="button-dark"
|
||||
class:active={activeTab === tab}
|
||||
on:click={() => setActiveTab(tab)}
|
||||
>
|
||||
{$t(tab)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{#if activeTab == "profile"}
|
||||
<div class="tab-content">
|
||||
<InputField
|
||||
name="password"
|
||||
type="password"
|
||||
label={$t("password")}
|
||||
placeholder={$t("placeholder_password")}
|
||||
/>
|
||||
<InputField
|
||||
name="password2"
|
||||
type="password"
|
||||
label={$t("password_repeat")}
|
||||
placeholder={$t("placeholder_password")}
|
||||
/>
|
||||
<InputField
|
||||
name="first_name"
|
||||
label={$t("first_name")}
|
||||
value={user.first_name}
|
||||
placeholder={$t("placeholder_first_name")}
|
||||
/>
|
||||
<InputField
|
||||
name="last_name"
|
||||
label={$t("last_name")}
|
||||
value={user.last_name}
|
||||
placeholder={$t("placeholder_last_name")}
|
||||
/>
|
||||
<InputField
|
||||
name="email"
|
||||
type="email"
|
||||
label={$t("email")}
|
||||
value={user.email}
|
||||
placeholder={$t("placeholder_email")}
|
||||
/>
|
||||
<InputField
|
||||
name="phone"
|
||||
type="tel"
|
||||
label={$t("phone")}
|
||||
value={user.phone || ""}
|
||||
placeholder={$t("placeholder_phone")}
|
||||
/>
|
||||
<InputField
|
||||
name="birth_date"
|
||||
type="date"
|
||||
label={$t("birth_date")}
|
||||
value={user.birth_date || ""}
|
||||
placeholder={$t("placeholder_birth_date")}
|
||||
/>
|
||||
<InputField
|
||||
name="address"
|
||||
label={$t("address")}
|
||||
value={user.address || ""}
|
||||
placeholder={$t("placeholder_address")}
|
||||
/>
|
||||
<InputField
|
||||
name="zip_code"
|
||||
label={$t("zip_code")}
|
||||
value={user.zip_code || ""}
|
||||
placeholder={$t("placeholder_zip_code")}
|
||||
/>
|
||||
<InputField
|
||||
name="city"
|
||||
label={$t("city")}
|
||||
value={user.city || ""}
|
||||
placeholder={$t("placeholder_city")}
|
||||
/>
|
||||
</div>
|
||||
{:else if activeTab == "membership"}
|
||||
<div class="tab-content">
|
||||
<InputField
|
||||
name="membership_status"
|
||||
label={$t("status")}
|
||||
value={user.membership?.status || ""}
|
||||
/>
|
||||
<InputField
|
||||
name="membership_start_date"
|
||||
type="date"
|
||||
label={$t("start")}
|
||||
value={user.membership?.start_date || ""}
|
||||
placeholder={$t("placeholder_start_date")}
|
||||
/>
|
||||
<InputField
|
||||
name="membership_end_date"
|
||||
type="date"
|
||||
label={$t("end")}
|
||||
value={user.membership?.end_date || ""}
|
||||
placeholder={$t("placeholder_end_date")}
|
||||
/>
|
||||
<InputField
|
||||
name="parent_member_id"
|
||||
type="number"
|
||||
label={$t("parent_member_id")}
|
||||
value={user.membership?.parent_member_id || ""}
|
||||
placeholder={$t("placeholder_parent_member_id")}
|
||||
/>
|
||||
</div>
|
||||
{:else if activeTab == "bankaccount"}
|
||||
<div class="tab-content">
|
||||
<InputField
|
||||
name="account_holder_name"
|
||||
label={$t("bank_account_holder")}
|
||||
value={user.bank_account?.account_holder_name || ""}
|
||||
placeholder={$t("placeholder_bank_account_holder")}
|
||||
/>
|
||||
<InputField
|
||||
name="bank"
|
||||
label={$t("bank_name")}
|
||||
value={user.bank_account?.bank || ""}
|
||||
placeholder={$t("placeholder_bank_name")}
|
||||
/>
|
||||
<InputField
|
||||
name="iban"
|
||||
label={$t("iban")}
|
||||
value={user.bank_account?.iban || ""}
|
||||
placeholder={$t("placeholder_iban")}
|
||||
/>
|
||||
<InputField
|
||||
name="bic"
|
||||
label={$t("bic")}
|
||||
value={user.bank_account?.bic || ""}
|
||||
placeholder={$t("placeholder_bic")}
|
||||
/>
|
||||
<InputField
|
||||
name="mandate_reference"
|
||||
label={$t("mandate_reference")}
|
||||
value={user.bank_account?.mandate_reference || ""}
|
||||
placeholder={$t("placeholder_mandate_reference")}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="button-container">
|
||||
{#if isUpdating}
|
||||
<SmallLoader width={30} message={"Aktualisiere..."} />
|
||||
{:else}
|
||||
<button type="button" class="button-dark" on:click={close}
|
||||
>Abbrechen</button
|
||||
>
|
||||
<button type="submit" class="button-dark">Bestätigen</button>
|
||||
{/if}
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.tab-content {
|
||||
padding: 1rem;
|
||||
border-radius: 0 0 3px 3px;
|
||||
}
|
||||
.hero-container .hero-subtitle:not(:last-of-type) {
|
||||
margin: 0 0 0 0;
|
||||
}
|
||||
|
||||
.hero-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: 0.5rem 1rem;
|
||||
align-items: start;
|
||||
text-align: left;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.3rem;
|
||||
font-weight: bold;
|
||||
text-align: left;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.value {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.block-value {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.hero-buttons-container {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.label,
|
||||
.value {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.avatar-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.current-avatar {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.avatar-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.avatar-buttons button {
|
||||
margin-top: 1.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.avatar-selection {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.avatar-option {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.avatar-option:hover,
|
||||
.avatar-option:focus {
|
||||
transform: scale(1.8);
|
||||
}
|
||||
|
||||
.avatar-option img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.button-container button {
|
||||
flex: 1 1 0;
|
||||
min-width: 120px;
|
||||
max-width: calc(50%-5px);
|
||||
}
|
||||
@media (max-width: 480px) {
|
||||
.button-container button {
|
||||
flex-basis: 100%;
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user