Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new user settings page #119

Merged
merged 2 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,11 @@
"It_seems_that_the_wallet_is_unable_to_connect_to_the_Internet_please_make_sure_your_internet_connection_is_working_and_retry": "It seems that the app is unable to connect to the Internet: please make sure your internet connection is working and retry",
"Close": "Close",
"Verifier": "Verifier",
"lorem_ipsum": "Below is a list of verification flows, each allows you to verify a credential type. To verify a credential, show the QR code that is produced in the verication flows. "
"lorem_ipsum": "Below is a list of verification flows, each allows you to verify a credential type. To verify a credential, show the QR code that is produced in the verication flows. ",
"User_Settings": "User Settings",
"John_Doe": "John_Doe",
"username": "username",
"save": "save",
"cancel": "cancel",
"upload_a_picture": "upload a picture"
}
69 changes: 69 additions & 0 deletions src/lib/components/forms/fieldError.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script context="module" lang="ts">
/**
* Superforms validation errors come in different shapes:
* • string[] (the most common)
* • {"1": string[], "2": string[], ...} (when using arrays)
* • {"key": string[], ...} (when using nested fields)
*
* Here we try to detect the shape of the error data.
*/

export function isNonNullable(value: unknown): value is NonNullable<unknown> {
return value !== undefined && value !== null;
}

export function isBaseError(errorData: unknown): errorData is string[] {
return Array.isArray(errorData) && errorData.length > 0;
}

export function isNestedError(errorData: unknown): errorData is Record<string, string[]> {
return (
isNonNullable(errorData) &&
!isBaseError(errorData) &&
typeof errorData === 'object' &&
Object.values(errorData).some((value) => isBaseError(value))
);
}

export function fieldHasErrors(errorData: unknown): boolean {
return isBaseError(errorData) || isNestedError(errorData);
}
</script>

<script lang="ts">
import { type FormPath, type ZodValidation } from 'sveltekit-superforms';
import { z } from 'zod';
import { formFieldProxy, type SuperForm } from 'sveltekit-superforms/client';

type T = $$Generic<AnyZodObject>;

export let field: FormPath<z.infer<T>>;
export let form: SuperForm<ZodValidation<T>, any>;
//@ts-ignore
const { errors } = formFieldProxy(form, field);
</script>

{#if isBaseError($errors)}
<div class="space-y-1">
{#each $errors as error}
<d-text size="xs" class="text-error">{error}</d-text><br />
{/each}
</div>
{/if}

{#if isNestedError($errors)}
<div class="space-y-2">
{#each Object.entries($errors) as [key, errors]}
{#if isBaseError(errors)}
<div class="space-y-1">
{#if key !== '_errors'}
<d-text size="xs" class="text-error"><span class="font-bold">{key}</span></d-text>
{/if}
{#each errors as error}
<d-text size="xs" class="text-error">{error}</d-text><br />
{/each}
</div>
{/if}
{/each}
</div>
{/if}
82 changes: 82 additions & 0 deletions src/lib/components/forms/fileInput.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<script lang="ts">
import FieldError, { fieldHasErrors } from './fieldError.svelte';

import { cameraOutline } from 'ionicons/icons';
import { Camera, CameraResultType } from '@capacitor/camera';
import type { z } from 'zod';
import type { FormPath, ZodValidation } from 'sveltekit-superforms';
import { formFieldProxy, type SuperForm } from 'sveltekit-superforms/client';
import { createEventDispatcher } from 'svelte';
import { m } from '$lib/i18n';

const dispatch = createEventDispatcher();

type T = $$Generic<AnyZodObject>;

export let form: SuperForm<ZodValidation<T>, any>;
export let field: FormPath<z.infer<T>>;

const { validate } = form;
const { errors } = formFieldProxy(form, field);

$: hasErrors = fieldHasErrors($errors);

//

let choosenFile: string;
let choosenFileFile: File;
let choosenFileDataURL: string;

const b64toBlob = (b64Data: string, contentType = '', sliceSize = 512) => {
const byteCharacters = atob(b64Data);
const byteArrays = [];

for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);

const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}

const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}

const blob = new Blob(byteArrays, { type: contentType });
return blob;
};

const takePicture = async () => {
const image = await Camera.getPhoto({
quality: 50,
allowEditing: false,
resultType: CameraResultType.Base64
});
if (!image) return;

const blob = b64toBlob(image.base64String!, image.format);

choosenFile = `avatar.${image.format}`;
choosenFileFile = new File([blob], choosenFile, { type: `image/${blob.type}` });
choosenFileDataURL = `data:image/${image.format};base64,${image.base64String}`;
//@ts-ignore
await validate(field as any, { value: choosenFileFile, taint: true });
dispatch('change', { image: choosenFileDataURL });
};
</script>

<div>
<d-horizontal-stack class="w-full items-center" gap={4}>
<d-button on:click={takePicture} on:keydown={takePicture} aria-hidden expand class="w-full pt-1"
><ion-icon
icon={cameraOutline}
slot="start"
class="h-6 w-6"
/>{m.upload_a_picture()}</d-button
>
</d-horizontal-stack>
{#if hasErrors}
<FieldError {form} {field} />
{/if}
</div>
3 changes: 2 additions & 1 deletion src/lib/components/forms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ import FieldController from './fieldController.svelte';
import FormError from './formError.svelte';
import Input from './input.svelte';
import Checkbox from './checkbox.svelte';
import { zodFile } from './utils';

export { Form, createForm, FieldController, FormError, Input, Checkbox };
export { Form, createForm, FieldController, FormError, Input, Checkbox, zodFile };
5 changes: 5 additions & 0 deletions src/lib/components/forms/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,8 @@ export type SuperformGeneric<T extends AnyZodObject = AnyZodObject, M = unknown>
ZodValidation<T>,
M
>;

export type ZodFileOptions = {
types?: string[];
size?: number;
};
17 changes: 17 additions & 0 deletions src/lib/components/forms/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { ZodFileOptions } from './types';
import { z } from 'zod';

export function zodFile(options: ZodFileOptions = {}) {
const { size, types } = options;

let schema = z.instanceof(File);

if (size) {
schema = schema.refine((v) => v.size < size, `File size exceeds ${size} bytes`);
}
if (types) {
schema = schema.refine((v) => types.includes(v.type), `File type not: ${types.join(', ')}`);
}

return schema;
}
30 changes: 16 additions & 14 deletions src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -76,19 +76,21 @@
<ParaglideJS {i18n}>
<HiddenLogsButton />
<ion-app>
<d-loading loading={!isConnected}>
<FingerPrint />
{#if !isConnected}
<d-vertical-stack class="ion-padding" gap={8}>
<d-text size="xl"
>{m.It_seems_that_the_wallet_is_unable_to_connect_to_the_Internet_please_make_sure_your_internet_connection_is_working_and_retry()}</d-text
>
<d-button color="accent" on:click={() => App.exitApp()} aria-hidden expand
>{m.Close()}</d-button
>
</d-vertical-stack>
{/if}
</d-loading>
<slot />
<div>
<d-loading loading={!isConnected}>
<FingerPrint />
{#if !isConnected}
<d-vertical-stack class="ion-padding" gap={8}>
<d-text size="xl"
>{m.It_seems_that_the_wallet_is_unable_to_connect_to_the_Internet_please_make_sure_your_internet_connection_is_working_and_retry()}</d-text
>
<d-button color="accent" on:click={() => App.exitApp()} aria-hidden expand
>{m.Close()}</d-button
>
</d-vertical-stack>
{/if}
</d-loading>
<slot />
</div>
</ion-app>
</ParaglideJS>
Loading
Loading