cleanup and docs and landing
This commit is contained in:
parent
cdb8ccbf97
commit
14cbc6a1a3
34 changed files with 2065 additions and 1297 deletions
28
README.md
28
README.md
|
@ -4,16 +4,32 @@ Identity is an open-source application that helps you save your most relevant me
|
|||
|
||||
## Rationale
|
||||
|
||||
Identity is a project that initially started as an app whose purpose was to store music you like
|
||||
(or liked) for future use in treatment for diseases like dementia. Over time, the idea evolved
|
||||
and is now general-purpose.
|
||||
Identity is a project that initially started as an app whose purpose was to store music you like (or used to like) for future use in treatment
|
||||
for conditions such as dementia. Over time, the idea evolved and is now a general-purpose memory-saving app.
|
||||
|
||||
## Projects
|
||||
|
||||
* `identity-web`. The web app that interacts with the Identiy API.
|
||||
* `identity-api`. The Identity API, also takes care of storing data.
|
||||
* `identity-format`. The specification for the Identity file format.
|
||||
* `identity-api`. The Identity API, takes care of storing user data.
|
||||
* `asset-api`. The Asset API, takes care of storing user-generated assets.
|
||||
|
||||
## Installation and building
|
||||
|
||||
The Identity project is composed by a web-app and two servers. In the future, Docker containers may be built to ease the installation of this project.
|
||||
|
||||
### Building and running
|
||||
|
||||
#### Building `identity-web`
|
||||
|
||||
1. Copy and update the `env.example` file: `cp .env.example .env`
|
||||
2. Run `yarn` to install the dependencies.
|
||||
* You may need to [enable Corepack](https://nodejs.org/api/corepack.html).
|
||||
3. Run `yarn preview` to check that everything works properly.
|
||||
4. Modify the `svelte.config.js` file to deploy to your desired environment.
|
||||
5. Run `yarn build` to generate the SPA build.
|
||||
* The build will be placed at the `build/` folder.
|
||||
|
||||
## Citations
|
||||
|
||||
1. Van de Winckel, A., Feys, H., De Weerdt, W., & Dom, R. (2004). Cognitive and behavioural effects of music-based exercises in patients with dementia. Clinical Rehabilitation, 18(3), 253-260. https://doi.org/10.1191/0269215504cr750oa
|
||||
1. Van de Winckel, A., Feys, H., De Weerdt, W., & Dom, R. (2004). Cognitive and behavioural effects of music-based exercises in patients with dementia. Clinical Rehabilitation, 18(3), 253-260. https://doi.org/10.1191/0269215504cr750oa
|
||||
2. The dementia guide: Living well after your diagnosis. (2021, April 16). Alzheimer’s Society. https://www.alzheimers.org.uk/get-support/publications-factsheets/the-dementia-guide
|
3
identity-web/.env.example
Normal file
3
identity-web/.env.example
Normal file
|
@ -0,0 +1,3 @@
|
|||
VITE_IDENTITY_API_ENDPOINT="http://localhost:3000/"
|
||||
VITE_ASSET_API_ENDPOINT="http://localhost:3001/"
|
||||
VITE_SUPPORT_PAGE="mailto:support@example.com"
|
|
@ -3,6 +3,7 @@
|
|||
"singleQuote": true,
|
||||
"trailingComma": "none",
|
||||
"printWidth": 100,
|
||||
"plugins": ["prettier-plugin-svelte"],
|
||||
"htmlWhitespaceSensitivity": "ignore",
|
||||
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.2",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
|
@ -26,6 +27,7 @@
|
|||
"postcss": "^8.4.38",
|
||||
"prettier": "^3.1.1",
|
||||
"prettier-plugin-svelte": "^3.1.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"svelte": "^4.2.7",
|
||||
"svelte-check": "^3.6.0",
|
||||
"tailwindcss": "^3.4.4",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {}
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,3 +1,19 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@tailwind utilities;
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
sup {
|
||||
top: -0.3em;
|
||||
vertical-align: super;
|
||||
}
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
|
|
@ -1,122 +1,140 @@
|
|||
import type { Entry, IdlessEntry } from "./entry"
|
||||
import type { Entry, IdlessEntry } from './entry';
|
||||
import { ENV_VARIABLES } from './variables';
|
||||
|
||||
const ENDPOINT = 'http://localhost:3000/'
|
||||
const ASSET_API_ENDPOINT = 'http://localhost:3001/'
|
||||
const ENDPOINT = ENV_VARIABLES.IDENTITY_API_ENDPOINT;
|
||||
const ASSET_API_ENDPOINT = ENV_VARIABLES.ASSET_API_ENDPOINT;
|
||||
|
||||
export type Credentials = {
|
||||
token: string,
|
||||
}
|
||||
token: string;
|
||||
};
|
||||
|
||||
export type AccountHeir = {
|
||||
contactMethod: "email",
|
||||
name: string,
|
||||
value: string,
|
||||
}
|
||||
contactMethod: 'email';
|
||||
name: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export type Account = {
|
||||
uid: string,
|
||||
name: string,
|
||||
heirs: AccountHeir[],
|
||||
}
|
||||
uid: string;
|
||||
name: string;
|
||||
heirs: AccountHeir[];
|
||||
};
|
||||
|
||||
function sendRequest(path: string, credentials?: Credentials, request: RequestInit = {}, params: string = "") {
|
||||
if (typeof request !== "string" && credentials != null) {
|
||||
request.headers = { 'Authorization': `Bearer ${credentials.token}`, ...request.headers }
|
||||
}
|
||||
function sendRequest(
|
||||
path: string,
|
||||
credentials?: Credentials,
|
||||
request: RequestInit = {},
|
||||
params: string = ''
|
||||
) {
|
||||
if (typeof request !== 'string' && credentials != null) {
|
||||
request.headers = { Authorization: `Bearer ${credentials.token}`, ...request.headers };
|
||||
}
|
||||
|
||||
let url = new URL(ENDPOINT);
|
||||
url.pathname = path;
|
||||
url.search = params
|
||||
let url = new URL(ENDPOINT);
|
||||
url.pathname = path;
|
||||
url.search = params;
|
||||
|
||||
return fetch(url, request)
|
||||
return fetch(url, request);
|
||||
}
|
||||
|
||||
/// **Safety:** The caller must enforce that the given request in progress must return the type `R`
|
||||
async function asJson<R>(request: Promise<Response>): Promise<R> {
|
||||
let req = await request;
|
||||
return (await req.json() as R)
|
||||
let req = await request;
|
||||
return (await req.json()) as R;
|
||||
}
|
||||
|
||||
export function login(credentials: {
|
||||
email: string,
|
||||
password: string,
|
||||
}): Promise<{ token: string, } | { error: string, }> {
|
||||
return asJson(sendRequest('/auth/login', undefined, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
}))
|
||||
email: string;
|
||||
password: string;
|
||||
}): Promise<{ token: string } | { error: string }> {
|
||||
return asJson(
|
||||
sendRequest('/auth/login', undefined, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(credentials)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function register(credentials: {
|
||||
name: string,
|
||||
email: string,
|
||||
password: string,
|
||||
}): Promise<{ token: string, } | { error: string, }> {
|
||||
return asJson(sendRequest('/auth/register', undefined, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
}))
|
||||
name: string;
|
||||
email: string;
|
||||
password: string;
|
||||
}): Promise<{ token: string } | { error: string }> {
|
||||
return asJson(
|
||||
sendRequest('/auth/register', undefined, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(credentials)
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function accountData(credentials: Credentials): Promise<Account | { error: string }> {
|
||||
return asJson(sendRequest('/auth/account', credentials))
|
||||
return asJson(sendRequest('/auth/account', credentials));
|
||||
}
|
||||
|
||||
export function genSessionKey(credentials: Credentials): Promise<{ session_key: string } | { error: string }> {
|
||||
return asJson(sendRequest('/auth/genkey', credentials))
|
||||
export function genSessionKey(
|
||||
credentials: Credentials
|
||||
): Promise<{ session_key: string } | { error: string }> {
|
||||
return asJson(sendRequest('/auth/genkey', credentials));
|
||||
}
|
||||
|
||||
export async function assetEndpoint(): Promise<string> {
|
||||
let res = await sendRequest("/asset/endpoint")
|
||||
return res.text()
|
||||
let res = await sendRequest('/asset/endpoint');
|
||||
return res.text();
|
||||
}
|
||||
|
||||
export async function entryPage(credentials: Credentials, offset: number, limit: number): Promise<Entry[]> {
|
||||
return asJson(sendRequest('/entry/list', credentials, undefined, `?offset=${offset}&limit=${limit}`))
|
||||
export async function entryPage(
|
||||
credentials: Credentials,
|
||||
offset: number,
|
||||
limit: number
|
||||
): Promise<Entry[]> {
|
||||
return asJson(
|
||||
sendRequest('/entry/list', credentials, undefined, `?offset=${offset}&limit=${limit}`)
|
||||
);
|
||||
}
|
||||
|
||||
export async function addEntry(credentials: Credentials, entry: IdlessEntry): Promise<void> {
|
||||
await sendRequest('/entry', credentials, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({entry}),
|
||||
})
|
||||
await sendRequest('/entry', credentials, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ entry })
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteEntry(credentials: Credentials, entry_id: string): Promise<void> {
|
||||
await sendRequest('/entry', credentials, { method: 'DELETE' }, `?entry_id=${entry_id}`)
|
||||
await sendRequest('/entry', credentials, { method: 'DELETE' }, `?entry_id=${entry_id}`);
|
||||
}
|
||||
|
||||
export async function updateHeirs(credentials: Credentials, heirs: AccountHeir[]): Promise<void> {
|
||||
await sendRequest('/auth/heirs', credentials, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(heirs),
|
||||
});
|
||||
await sendRequest('/auth/heirs', credentials, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(heirs)
|
||||
});
|
||||
}
|
||||
|
||||
export async function uploadAsset(session_key: string, file: File): Promise<string> {
|
||||
let url = new URL('/asset', ASSET_API_ENDPOINT);
|
||||
url.search = `?session_key=${session_key}`
|
||||
|
||||
let form = new FormData()
|
||||
form.append("file", file);
|
||||
let url = new URL('/asset', ASSET_API_ENDPOINT);
|
||||
url.search = `?session_key=${session_key}`;
|
||||
|
||||
let res = await fetch(url, {
|
||||
method: "PUT",
|
||||
body: form,
|
||||
})
|
||||
let form = new FormData();
|
||||
form.append('file', file);
|
||||
|
||||
let { asset_id } = await res.json();
|
||||
return asset_id;
|
||||
}
|
||||
let res = await fetch(url, {
|
||||
method: 'PUT',
|
||||
body: form
|
||||
});
|
||||
|
||||
let { asset_id } = await res.json();
|
||||
return asset_id;
|
||||
}
|
||||
|
|
BIN
identity-web/src/lib/assets/ladies.jpg
Normal file
BIN
identity-web/src/lib/assets/ladies.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 MiB |
BIN
identity-web/src/lib/assets/memory-photos.jpg
Normal file
BIN
identity-web/src/lib/assets/memory-photos.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 MiB |
|
@ -1,74 +1,84 @@
|
|||
<script lang="ts">
|
||||
import { faChevronDown, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome";
|
||||
import FeelingPill from "../../routes/dashboard/utils/FeelingPill.svelte";
|
||||
import { FEELINGS, type KnownFeeling } from "$lib/entry";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { faChevronDown, faPlus, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
|
||||
import FeelingPill from '../../routes/dashboard/utils/FeelingPill.svelte';
|
||||
import { FEELINGS, type KnownFeeling } from '$lib/entry';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
export let required = false
|
||||
export let displayText = true
|
||||
export let slim = false
|
||||
export let required = false;
|
||||
export let displayText = true;
|
||||
export let slim = false;
|
||||
|
||||
let feelingsDropdownShown = false
|
||||
export let chosenFeelings: KnownFeeling[] = []
|
||||
let feelingsDropdownShown = false;
|
||||
export let chosenFeelings: KnownFeeling[] = [];
|
||||
|
||||
$: feelingsToChoose = FEELINGS.filter(v => !chosenFeelings.includes(v))
|
||||
$: feelingsToChoose = FEELINGS.filter((v) => !chosenFeelings.includes(v));
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
let dispatch = createEventDispatcher();
|
||||
|
||||
function addFeeling(feeling: KnownFeeling) {
|
||||
chosenFeelings = [feeling, ...chosenFeelings];
|
||||
dispatch('choiceUpdated', chosenFeelings);
|
||||
}
|
||||
function addFeeling(feeling: KnownFeeling) {
|
||||
chosenFeelings = [feeling, ...chosenFeelings];
|
||||
dispatch('choiceUpdated', chosenFeelings);
|
||||
}
|
||||
|
||||
function removeFeeling(feeling: KnownFeeling) {
|
||||
chosenFeelings = chosenFeelings.filter(v => v !== feeling)
|
||||
dispatch('choiceUpdated', chosenFeelings);
|
||||
}
|
||||
function removeFeeling(feeling: KnownFeeling) {
|
||||
chosenFeelings = chosenFeelings.filter((v) => v !== feeling);
|
||||
dispatch('choiceUpdated', chosenFeelings);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col">
|
||||
{#if displayText}
|
||||
<span class="block mb-2 text-sm font-medium text-gray-900">Feelings</span>
|
||||
{/if}
|
||||
{#if displayText}
|
||||
<span class="mb-2 block text-sm font-medium text-gray-900">Feelings</span>
|
||||
{/if}
|
||||
|
||||
<div class="flex">
|
||||
<button type="button" on:click={() => feelingsDropdownShown = !feelingsDropdownShown} class={`inline-flex gap-1.5 items-center px-2.5 text-sm text-gray-900 bg-gray-200 border rounded-e-0 border-gray-300 border-e-0 ${feelingsDropdownShown ? "rounded-tl-lg" : "rounded-s-lg"} hover:cursor-pointer hover:bg-gray-300`}>
|
||||
Feelings
|
||||
<FontAwesomeIcon icon={faChevronDown}/>
|
||||
</button>
|
||||
<div id="add-entry__feelings" class={`rounded-none ${feelingsDropdownShown ? "rounded-tr-lg" : "rounded-e-lg"} bg-gray-50 border text-gray-900 focus:ring-violet-500 focus:border-violet-500 block flex-1 min-w-0 w-full text-sm border-gray-300 ${slim ? "p-2" : "p-2.5"}`}>
|
||||
{#if chosenFeelings.length > 0}
|
||||
<div>
|
||||
<span class="mr-1">Chosen:</span>
|
||||
{#each chosenFeelings as feeling (feeling)}
|
||||
<div class="inline">
|
||||
<button type="button" on:click={() => removeFeeling(feeling)}>
|
||||
<FeelingPill feeling={feeling} slim={slim}>
|
||||
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faXmark}/></span>
|
||||
</FeelingPill>
|
||||
</button>
|
||||
<input type="checkbox" class="hidden" name={`feeling__${feeling}`} checked>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<span>No feelings chosen.</span>
|
||||
{#if required}
|
||||
<span>You need to choose at least one feeling.</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class:hidden={!feelingsDropdownShown} class="bg-gray-50 border border-t-0 border-gray-300 py-3 px-1.5 rounded-b-lg">
|
||||
{#each feelingsToChoose as feeling (feeling)}
|
||||
<label class={`capitalize ${slim ? "p-0.5" : "p-1"}`}>
|
||||
<button type="button" on:click={() => addFeeling(feeling)}>
|
||||
<FeelingPill feeling={feeling} slim={slim}>
|
||||
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faPlus}/></span>
|
||||
</FeelingPill>
|
||||
</button>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex">
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (feelingsDropdownShown = !feelingsDropdownShown)}
|
||||
class={`rounded-e-0 inline-flex items-center gap-1.5 border border-e-0 border-gray-300 bg-gray-200 px-2.5 text-sm text-gray-900 ${feelingsDropdownShown ? 'rounded-tl-lg' : 'rounded-s-lg'} hover:cursor-pointer hover:bg-gray-300`}
|
||||
>
|
||||
Feelings
|
||||
<FontAwesomeIcon icon={faChevronDown} />
|
||||
</button>
|
||||
<div
|
||||
id="add-entry__feelings"
|
||||
class={`rounded-none ${feelingsDropdownShown ? 'rounded-tr-lg' : 'rounded-e-lg'} block w-full min-w-0 flex-1 border border-gray-300 bg-gray-50 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500 ${slim ? 'p-2' : 'p-2.5'}`}
|
||||
>
|
||||
{#if chosenFeelings.length > 0}
|
||||
<div>
|
||||
<span class="mr-1">Chosen:</span>
|
||||
{#each chosenFeelings as feeling (feeling)}
|
||||
<div class="inline">
|
||||
<button type="button" on:click={() => removeFeeling(feeling)}>
|
||||
<FeelingPill {feeling} {slim}>
|
||||
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faXmark} /></span>
|
||||
</FeelingPill>
|
||||
</button>
|
||||
<input type="checkbox" class="hidden" name={`feeling__${feeling}`} checked />
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<span>No feelings chosen.</span>
|
||||
{#if required}
|
||||
<span>You need to choose at least one feeling.</span>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class:hidden={!feelingsDropdownShown}
|
||||
class="rounded-b-lg border border-t-0 border-gray-300 bg-gray-50 px-1.5 py-3"
|
||||
>
|
||||
{#each feelingsToChoose as feeling (feeling)}
|
||||
<label class={`capitalize ${slim ? 'p-0.5' : 'p-1'}`}>
|
||||
<button type="button" on:click={() => addFeeling(feeling)}>
|
||||
<FeelingPill {feeling} {slim}>
|
||||
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faPlus} /></span>
|
||||
</FeelingPill>
|
||||
</button>
|
||||
</label>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,80 +1,149 @@
|
|||
export const TITLED_ENTRIES = ["event", "environment", "memory"];
|
||||
export const FEELINGS = ["relaxed", "afraid", "angry", "bad", "bored", "confused", "excited", "fine", "happy", "hurt", "in love", "mad", "nervous", "okay", "sad", "scared", "shy", "sleepy", "active", "surprised", "tired", "upset", "worried"];
|
||||
export const TITLED_ENTRIES = ['event', 'environment', 'memory'];
|
||||
export const FEELINGS = [
|
||||
'relaxed',
|
||||
'afraid',
|
||||
'angry',
|
||||
'bad',
|
||||
'bored',
|
||||
'confused',
|
||||
'excited',
|
||||
'fine',
|
||||
'happy',
|
||||
'hurt',
|
||||
'in love',
|
||||
'mad',
|
||||
'nervous',
|
||||
'okay',
|
||||
'sad',
|
||||
'scared',
|
||||
'shy',
|
||||
'sleepy',
|
||||
'active',
|
||||
'surprised',
|
||||
'tired',
|
||||
'upset',
|
||||
'worried'
|
||||
];
|
||||
|
||||
export type KnownFeeling = "relaxed" | "afraid" | "angry" | "bad" | "bored" | "confused" | "excited" | "fine" | "happy" | "hurt" | "in love" | "mad" | "nervous" | "okay" | "sad" | "scared" | "shy" | "sleepy" | "active" | "surprised" | "tired" | "upset" | "worried";
|
||||
export type EntryKind = "song" | "album" | "event" | "memory" | "feeling" | "environment" | "date";
|
||||
export type KnownFeeling =
|
||||
| 'relaxed'
|
||||
| 'afraid'
|
||||
| 'angry'
|
||||
| 'bad'
|
||||
| 'bored'
|
||||
| 'confused'
|
||||
| 'excited'
|
||||
| 'fine'
|
||||
| 'happy'
|
||||
| 'hurt'
|
||||
| 'in love'
|
||||
| 'mad'
|
||||
| 'nervous'
|
||||
| 'okay'
|
||||
| 'sad'
|
||||
| 'scared'
|
||||
| 'shy'
|
||||
| 'sleepy'
|
||||
| 'active'
|
||||
| 'surprised'
|
||||
| 'tired'
|
||||
| 'upset'
|
||||
| 'worried';
|
||||
export type EntryKind = 'song' | 'album' | 'event' | 'memory' | 'feeling' | 'environment' | 'date';
|
||||
|
||||
export type IdlessEntry = {
|
||||
base: SongEntry | AlbumEntry | EventEntry | MemoryEntry | FeelingEntry | EnvironmentEntry | DateEntry,
|
||||
creationDate: string,
|
||||
feelings: (KnownFeeling | {
|
||||
identifier: string,
|
||||
description: string,
|
||||
backgroundColor: string,
|
||||
textColor: string,
|
||||
})[],
|
||||
assets: string[],
|
||||
title?: string,
|
||||
description?: string,
|
||||
base:
|
||||
| SongEntry
|
||||
| AlbumEntry
|
||||
| EventEntry
|
||||
| MemoryEntry
|
||||
| FeelingEntry
|
||||
| EnvironmentEntry
|
||||
| DateEntry;
|
||||
creationDate: string;
|
||||
feelings: (
|
||||
| KnownFeeling
|
||||
| {
|
||||
identifier: string;
|
||||
description: string;
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
}
|
||||
)[];
|
||||
assets: string[];
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type Entry = {
|
||||
id: string,
|
||||
base: SongEntry | AlbumEntry | EventEntry | MemoryEntry | FeelingEntry | EnvironmentEntry | DateEntry,
|
||||
creationDate: string,
|
||||
feelings: (KnownFeeling | {
|
||||
identifier: string,
|
||||
description: string,
|
||||
backgroundColor: string,
|
||||
textColor: string,
|
||||
})[],
|
||||
assets: string[],
|
||||
title?: string,
|
||||
description?: string,
|
||||
id: string;
|
||||
base:
|
||||
| SongEntry
|
||||
| AlbumEntry
|
||||
| EventEntry
|
||||
| MemoryEntry
|
||||
| FeelingEntry
|
||||
| EnvironmentEntry
|
||||
| DateEntry;
|
||||
creationDate: string;
|
||||
feelings: (
|
||||
| KnownFeeling
|
||||
| {
|
||||
identifier: string;
|
||||
description: string;
|
||||
backgroundColor: string;
|
||||
textColor: string;
|
||||
}
|
||||
)[];
|
||||
assets: string[];
|
||||
title?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type UniversalID = {
|
||||
provider: string,
|
||||
id: string,
|
||||
}
|
||||
provider: string;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type SongEntry = {
|
||||
kind: "song",
|
||||
artist: string,
|
||||
title: string,
|
||||
link: string[],
|
||||
id: UniversalID[],
|
||||
}
|
||||
kind: 'song';
|
||||
artist: string;
|
||||
title: string;
|
||||
link: string[];
|
||||
id: UniversalID[];
|
||||
};
|
||||
|
||||
export type AlbumEntry = {
|
||||
kind: "album",
|
||||
artist: string,
|
||||
title: string,
|
||||
link: string[],
|
||||
id: UniversalID[],
|
||||
}
|
||||
kind: 'album';
|
||||
artist: string;
|
||||
title: string;
|
||||
link: string[];
|
||||
id: UniversalID[];
|
||||
};
|
||||
|
||||
export type EventEntry = {
|
||||
kind: "event",
|
||||
}
|
||||
kind: 'event';
|
||||
};
|
||||
|
||||
export type MemoryEntry = {
|
||||
kind: "memory",
|
||||
}
|
||||
kind: 'memory';
|
||||
};
|
||||
|
||||
export type FeelingEntry = {
|
||||
kind: "feeling",
|
||||
}
|
||||
kind: 'feeling';
|
||||
};
|
||||
|
||||
export type EnvironmentEntry = {
|
||||
kind: "environment",
|
||||
location?: string | {
|
||||
latitude: number,
|
||||
longitude: number,
|
||||
},
|
||||
}
|
||||
kind: 'environment';
|
||||
location?:
|
||||
| string
|
||||
| {
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
};
|
||||
};
|
||||
|
||||
export type DateEntry = {
|
||||
kind: "date",
|
||||
referencedDate: string,
|
||||
}
|
||||
kind: 'date';
|
||||
referencedDate: string;
|
||||
};
|
||||
|
|
|
@ -1,66 +1,67 @@
|
|||
import { writable } from "svelte/store";
|
||||
import { accountData, assetEndpoint, genSessionKey, type Account, type Credentials } from "./api";
|
||||
import { writable } from 'svelte/store';
|
||||
import { accountData, assetEndpoint, genSessionKey, type Account, type Credentials } from './api';
|
||||
|
||||
const CREDENTIALS_KEY = 'v0:credentials'
|
||||
const CREDENTIALS_KEY = 'v0:credentials';
|
||||
|
||||
let _credentials: Credentials | null = null
|
||||
export const credentials = writable<Credentials | null>()
|
||||
let _credentials: Credentials | null = null;
|
||||
export const credentials = writable<Credentials | null>();
|
||||
credentials.subscribe((value) => {
|
||||
if (value != null) {
|
||||
_credentials = value;
|
||||
localStorage.setItem( CREDENTIALS_KEY, JSON.stringify(value))
|
||||
} else {
|
||||
_credentials = null;
|
||||
}
|
||||
})
|
||||
if (value != null) {
|
||||
_credentials = value;
|
||||
localStorage.setItem(CREDENTIALS_KEY, JSON.stringify(value));
|
||||
} else {
|
||||
_credentials = null;
|
||||
}
|
||||
});
|
||||
|
||||
export const account = writable<Account | null>()
|
||||
export const session_key = writable<string | null>()
|
||||
export const asset_endpoint = writable<string | null>()
|
||||
export const account = writable<Account | null>();
|
||||
export const session_key = writable<string | null>();
|
||||
export const asset_endpoint = writable<string | null>();
|
||||
|
||||
export async function initializeStores() {
|
||||
let rawCredentials = localStorage.getItem(CREDENTIALS_KEY)
|
||||
let parsedCredentials
|
||||
if (rawCredentials != null && rawCredentials.length > 0) {
|
||||
try {
|
||||
parsedCredentials = JSON.parse(rawCredentials)
|
||||
credentials.set(parsedCredentials)
|
||||
}
|
||||
catch (e) { localStorage.removeItem(CREDENTIALS_KEY) }
|
||||
}
|
||||
let rawCredentials = localStorage.getItem(CREDENTIALS_KEY);
|
||||
let parsedCredentials;
|
||||
if (rawCredentials != null && rawCredentials.length > 0) {
|
||||
try {
|
||||
parsedCredentials = JSON.parse(rawCredentials);
|
||||
credentials.set(parsedCredentials);
|
||||
} catch (e) {
|
||||
localStorage.removeItem(CREDENTIALS_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
if (parsedCredentials != null) {
|
||||
let data = await accountData(parsedCredentials)
|
||||
if ('error' in data) {
|
||||
credentials.set(null)
|
||||
localStorage.removeItem(CREDENTIALS_KEY)
|
||||
} else {
|
||||
account.set(data)
|
||||
}
|
||||
if (parsedCredentials != null) {
|
||||
let data = await accountData(parsedCredentials);
|
||||
if ('error' in data) {
|
||||
credentials.set(null);
|
||||
localStorage.removeItem(CREDENTIALS_KEY);
|
||||
} else {
|
||||
account.set(data);
|
||||
}
|
||||
|
||||
let key_result = await genSessionKey(parsedCredentials)
|
||||
if ('error' in key_result) {
|
||||
console.warn('Couldn\'t generate a session key!')
|
||||
} else {
|
||||
session_key.set(key_result.session_key)
|
||||
}
|
||||
let key_result = await genSessionKey(parsedCredentials);
|
||||
if ('error' in key_result) {
|
||||
console.warn("Couldn't generate a session key!");
|
||||
} else {
|
||||
session_key.set(key_result.session_key);
|
||||
}
|
||||
|
||||
let asset_result = await assetEndpoint()
|
||||
asset_endpoint.set(asset_result)
|
||||
}
|
||||
let asset_result = await assetEndpoint();
|
||||
asset_endpoint.set(asset_result);
|
||||
}
|
||||
}
|
||||
|
||||
export async function refreshAccount() {
|
||||
if (_credentials == null) {
|
||||
console.warn("Requested to refresh the user account but credentials are null.")
|
||||
return;
|
||||
}
|
||||
if (_credentials == null) {
|
||||
console.warn('Requested to refresh the user account but credentials are null.');
|
||||
return;
|
||||
}
|
||||
|
||||
let refreshedAccount = await accountData(_credentials)
|
||||
if ('error' in refreshedAccount) {
|
||||
console.warn("Failed to refresh the user account.")
|
||||
return;
|
||||
}
|
||||
let refreshedAccount = await accountData(_credentials);
|
||||
if ('error' in refreshedAccount) {
|
||||
console.warn('Failed to refresh the user account.');
|
||||
return;
|
||||
}
|
||||
|
||||
account.set(refreshedAccount)
|
||||
}
|
||||
account.set(refreshedAccount);
|
||||
}
|
||||
|
|
5
identity-web/src/lib/variables.ts
Normal file
5
identity-web/src/lib/variables.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export const ENV_VARIABLES = {
|
||||
IDENTITY_API_ENDPOINT: import.meta.env.VITE_IDENTITY_API_ENDPOINT,
|
||||
ASSET_API_ENDPOINT: import.meta.env.VITE_ASSET_API_ENDPOINT,
|
||||
SUPPORT_PAGE: import.meta.env.VITE_SUPPORT_PAGE
|
||||
};
|
|
@ -1,34 +1,39 @@
|
|||
<script>
|
||||
import { credentials, initializeStores } from "$lib/stores";
|
||||
import "../app.css";
|
||||
import { credentials, initializeStores } from '$lib/stores';
|
||||
import { ENV_VARIABLES } from '$lib/variables';
|
||||
import '../app.css';
|
||||
|
||||
initializeStores()
|
||||
initializeStores();
|
||||
</script>
|
||||
|
||||
<div class="py-3.5 flex text-white bg-violet-800 justify-center">
|
||||
<nav class="w-[60%] flex justify-between items-center">
|
||||
<h1 class="font-serif text-3xl">
|
||||
{#if $credentials == null}
|
||||
<a href="/">Identity</a>
|
||||
{:else}
|
||||
<a href="/dashboard">Identity</a>
|
||||
{/if}
|
||||
</h1>
|
||||
<div class="text-xl">
|
||||
{#if $credentials == null}
|
||||
| <div class="px-3 inline-block"><a href="/">Home</a></div>
|
||||
| <div class="px-3 inline-block"><a href="mailto:sofi@sofiaritz.com">Support</a></div>
|
||||
| <div class="px-3 inline-block"><a href="/auth/register">Join</a></div>
|
||||
|
|
||||
{:else}
|
||||
| <div class="px-3 inline-block"><a href="/dashboard">Dashboard</a></div>
|
||||
| <div class="px-3 inline-block"><a href="/auth/account">Account</a></div>
|
||||
| <div class="px-3 inline-block"><a href="mailto:sofi@sofiaritz.com">Support</a></div>
|
||||
|
|
||||
{/if}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="flex justify-center bg-violet-800 py-3.5 text-white">
|
||||
<nav class="flex w-[60%] items-center justify-between">
|
||||
<h1 class="font-serif text-3xl">
|
||||
{#if $credentials == null}
|
||||
<a href="/">Identity</a>
|
||||
{:else}
|
||||
<a href="/dashboard">Identity</a>
|
||||
{/if}
|
||||
</h1>
|
||||
<div class="text-xl">
|
||||
{#if $credentials == null}
|
||||
| <div class="inline-block px-3"><a href="/">Home</a></div>
|
||||
|
|
||||
<div class="inline-block px-3"><a href="mailto:sofi@sofiaritz.com">Support</a></div>
|
||||
|
|
||||
<div class="inline-block px-3"><a href="/auth/register">Join</a></div>
|
||||
|
|
||||
{:else}
|
||||
| <div class="inline-block px-3"><a href="/dashboard">Dashboard</a></div>
|
||||
|
|
||||
<div class="inline-block px-3"><a href="/auth/account">Account</a></div>
|
||||
|
|
||||
<div class="inline-block px-3"><a href={ENV_VARIABLES.SUPPORT_PAGE}>Support</a></div>
|
||||
|
|
||||
{/if}
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
<main class="pt-3.5">
|
||||
<slot/>
|
||||
<slot />
|
||||
</main>
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
// FIXME: Update code to support SSR
|
||||
export const ssr = false;
|
||||
export const ssr = false;
|
||||
export const prerender = false;
|
||||
|
|
|
@ -1 +1,75 @@
|
|||
<h1>Landing</h1>
|
||||
<script lang="ts">
|
||||
import photo1 from '$lib/assets/memory-photos.jpg';
|
||||
import photo2 from '$lib/assets/ladies.jpg';
|
||||
</script>
|
||||
|
||||
<div class="flex justify-center bg-violet-100 pt-3.5">
|
||||
<div class="flex w-[60%] flex-col">
|
||||
<div class="my-1 flex">
|
||||
<div class="w-full">
|
||||
<h1 class="pt-6 font-serif text-4xl text-violet-700">
|
||||
Store your memories for your future <span class="font-semibold italic">you.</span>
|
||||
</h1>
|
||||
<p class="pt-4 text-lg">
|
||||
<span class="font-serif">Identity</span>
|
||||
helps you store your memories and experiences. Our memories are our most precious belonging,
|
||||
we should store them in a safe place.
|
||||
</p>
|
||||
<p class="pt-4 text-lg">
|
||||
<span class="font-serif">Identity</span>
|
||||
is an open-source software you can self-host to have full control over the storage of your
|
||||
memories.
|
||||
</p>
|
||||
<p class="pt-4 text-lg">
|
||||
This instance is maintained by volunnteers and financed by our community and sponsors. You
|
||||
can export your data at any time.
|
||||
</p>
|
||||
<a
|
||||
href="/auth/register"
|
||||
class="focust:outline-none mr-3 mt-6 block rounded-lg bg-violet-700 px-5 py-2.5 text-center font-medium text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
|
||||
>
|
||||
Join now
|
||||
</a>
|
||||
</div>
|
||||
<div class="w-2/3">
|
||||
<img
|
||||
class="aspect-square w-full border-8 border-violet-400 object-cover"
|
||||
alt="Collage of Polaroid-like pictures"
|
||||
src={photo1}
|
||||
/>
|
||||
<span class="my-1.5 block text-right">Photo by Lisa Fotios</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="my-1 flex">
|
||||
<div class="w-2/3">
|
||||
<img
|
||||
class="aspect-square w-full border-8 border-violet-400 object-cover"
|
||||
alt="Collage of Polaroid-like pictures"
|
||||
src={photo2}
|
||||
/>
|
||||
<span class="my-1.5 block">Photo by cottonbro studio</span>
|
||||
</div>
|
||||
<div class="w-full">
|
||||
<h1 class="pt-6 text-right font-serif text-4xl text-violet-700">
|
||||
Remember your <span class="font-semibold italic">younger self.</span>
|
||||
</h1>
|
||||
<p class="pt-4 text-right text-lg">
|
||||
Reminiscence and life story work can help dementia patients ease their symptoms.
|
||||
<sup>
|
||||
<a
|
||||
href="https://www.alzheimers.org.uk/get-support/publications-factsheets/the-dementia-guide"
|
||||
class="text-blue-600 hover:cursor-pointer hover:underline"
|
||||
>
|
||||
[1]
|
||||
</a>
|
||||
</sup>
|
||||
</p>
|
||||
<p class="pt-4 text-right text-lg">
|
||||
Both your future you and your descendants may find your legacy useful:
|
||||
<br />
|
||||
environments, music, memories…
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,119 +1,177 @@
|
|||
<script lang="ts">
|
||||
import { createForm } from "felte";
|
||||
import { account, credentials, refreshAccount } from "$lib/stores";
|
||||
import { type AccountHeir, updateHeirs } from "$lib/api";
|
||||
import { createForm } from 'felte';
|
||||
import { account, credentials, refreshAccount } from '$lib/stores';
|
||||
import { type AccountHeir, updateHeirs } from '$lib/api';
|
||||
|
||||
credentials.subscribe((v) => v == null && (setTimeout(() => window.location.pathname = '/auth/login', 200)))
|
||||
credentials.subscribe(
|
||||
(v) => v == null && setTimeout(() => (window.location.pathname = '/auth/login'), 200)
|
||||
);
|
||||
|
||||
let heirWizard = false;
|
||||
let heirWizard = false;
|
||||
|
||||
const { form, errors } = createForm({
|
||||
onSubmit: async (values) => {
|
||||
let heir: AccountHeir = {
|
||||
const { form, errors } = createForm({
|
||||
onSubmit: async (values) => {
|
||||
let heir: AccountHeir = {
|
||||
contactMethod: values.contactMethod,
|
||||
name: values.name,
|
||||
value: values.contactDetails,
|
||||
value: values.contactDetails
|
||||
};
|
||||
|
||||
let currentHeirs = structuredClone($account!.heirs)
|
||||
let updatedHeirs = [heir, ...currentHeirs];
|
||||
let currentHeirs = structuredClone($account!.heirs);
|
||||
let updatedHeirs = [heir, ...currentHeirs];
|
||||
|
||||
await updateHeirs($credentials!, updatedHeirs);
|
||||
await refreshAccount();
|
||||
await updateHeirs($credentials!, updatedHeirs);
|
||||
await refreshAccount();
|
||||
|
||||
heirWizard = false;
|
||||
},
|
||||
validate: (values) => {
|
||||
let errors = {}
|
||||
heirWizard = false;
|
||||
},
|
||||
validate: (values) => {
|
||||
let errors = {};
|
||||
|
||||
if (values.contactMethod == null || values.contactMethod.length === 0) {
|
||||
errors['contactMethod'] = 'Must choose a contact method'
|
||||
}
|
||||
if (values.contactMethod == null || values.contactMethod.length === 0) {
|
||||
errors['contactMethod'] = 'Must choose a contact method';
|
||||
}
|
||||
|
||||
if (values.name == null || values.name.length === 0) {
|
||||
errors['name'] = 'Must not be empty'
|
||||
}
|
||||
if (values.name == null || values.name.length === 0) {
|
||||
errors['name'] = 'Must not be empty';
|
||||
}
|
||||
|
||||
if (values.contactDetails == null || values.contactDetails.length === 0) {
|
||||
errors['contactDetails'] = 'Must not be empty'
|
||||
}
|
||||
if (values.contactDetails == null || values.contactDetails.length === 0) {
|
||||
errors['contactDetails'] = 'Must not be empty';
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
})
|
||||
return errors;
|
||||
}
|
||||
});
|
||||
|
||||
async function removeHeir(heir: AccountHeir) {
|
||||
let currentHeirs = structuredClone($account!.heirs)
|
||||
let updatedHeirs = currentHeirs
|
||||
.filter((v) => v.value !== heir.value);
|
||||
async function removeHeir(heir: AccountHeir) {
|
||||
let currentHeirs = structuredClone($account!.heirs);
|
||||
let updatedHeirs = currentHeirs.filter((v) => v.value !== heir.value);
|
||||
|
||||
await updateHeirs($credentials!, updatedHeirs);
|
||||
await refreshAccount();
|
||||
}
|
||||
await updateHeirs($credentials!, updatedHeirs);
|
||||
await refreshAccount();
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mt-3.5 justify-center flex">
|
||||
<div class="w-[60%] flex flex-col">
|
||||
<h1 class="text-2xl pb-3.5">Welcome back, <span class="font-bold">{$account?.name}</span>.</h1>
|
||||
<div>
|
||||
<div class="flex justify-between mb-2">
|
||||
<h2 class="text-xl pb-2.5">Heirs</h2>
|
||||
{#if $account?.heirs.length > 0}
|
||||
<button on:click={() => heirWizard = !heirWizard} class="rounded-lg bg-violet-700 text-white px-2.5 py-1 text-center hover:bg-violet-800 focus:ring-4 focus:ring-violet-300">
|
||||
+ Add a heir
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !heirWizard && $account?.heirs.length === 0}
|
||||
<div class="flex flex-col">
|
||||
<button on:click={() => heirWizard = true} class="flex h-60 flex-col items-center justify-center gap-3 rounded border border-gray-300 p-2 text-black">
|
||||
<span class="text-4xl">+</span>
|
||||
<h2 class="text-xl font-semibold">Add a heir</h2>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if heirWizard}
|
||||
<div class="border border-gray-200 rounded-lg shadow w-full flex flex-col p-3.5 mb-4">
|
||||
<form use:form>
|
||||
<div class="mb-5">
|
||||
<label for="heir__contact-method" class="block mb-2 text-sm font-medium text-gray-900">Contact method</label>
|
||||
<select id="heir__contact-method" name="contactMethod" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
||||
<option value="" selected>Choose a contact method</option>
|
||||
<option value="email">Email</option>
|
||||
</select>
|
||||
{#if $errors.contactMethod != null}
|
||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.contactMethod[0]}</span></p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label for="heir__name" class="block mb-2 text-sm font-medium text-gray-900">Heir name</label>
|
||||
<input id="heir__name" type="text" name="name" placeholder="Jane Doe" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
||||
{#if $errors.name != null}
|
||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.name[0]}</span></p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label for="heir__contactDetails" class="block mb-2 text-sm font-medium text-gray-900">Contact details</label>
|
||||
<input id="heir__contactDetails" type="text" name="contactDetails" placeholder="jane@identity.net" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
||||
{#if $errors.contactDetails != null}
|
||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.contactDetails[0]}</span></p>
|
||||
{/if}
|
||||
</div>
|
||||
<button class="mt-2 text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focust:outline-none focus:ring-violet-300 font-medium rounded-lg px-5 py-2.5 text-center" type="submit">Add new heir</button>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
{#each $account?.heirs || [] as heir (heir.value)}
|
||||
<div class="border border-gray-200 rounded-lg shadow w-full flex flex-col p-3.5 mb-2.5">
|
||||
<div class="flex justify-between">
|
||||
<span class="block text-sm font-medium text-gray-900">Contact method: <span class="capitalize">{heir.contactMethod}</span></span>
|
||||
<button on:click={() => removeHeir(heir)} class="rounded-lg bg-red-600 text-white px-2.5 py-1 text-center hover:bg-red-700 focus:ring-4 focus:ring-violet-300">Delete heir</button>
|
||||
</div>
|
||||
<div>
|
||||
<span>{heir.name}</span> · <span>{heir.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3.5 flex justify-center">
|
||||
<div class="flex w-[60%] flex-col">
|
||||
<h1 class="pb-3.5 text-2xl">
|
||||
Welcome back, <span class="font-bold">{$account?.name}</span>
|
||||
.
|
||||
</h1>
|
||||
<div>
|
||||
<div class="mb-2 flex justify-between">
|
||||
<h2 class="pb-2.5 text-xl">Heirs</h2>
|
||||
{#if $account?.heirs.length > 0}
|
||||
<button
|
||||
on:click={() => (heirWizard = !heirWizard)}
|
||||
class="rounded-lg bg-violet-700 px-2.5 py-1 text-center text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
|
||||
>
|
||||
+ Add a heir
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !heirWizard && $account?.heirs.length === 0}
|
||||
<div class="flex flex-col">
|
||||
<button
|
||||
on:click={() => (heirWizard = true)}
|
||||
class="flex h-60 flex-col items-center justify-center gap-3 rounded border border-gray-300 p-2 text-black"
|
||||
>
|
||||
<span class="text-4xl">+</span>
|
||||
<h2 class="text-xl font-semibold">Add a heir</h2>
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if heirWizard}
|
||||
<div class="mb-4 flex w-full flex-col rounded-lg border border-gray-200 p-3.5 shadow">
|
||||
<form use:form>
|
||||
<div class="mb-5">
|
||||
<label
|
||||
for="heir__contact-method"
|
||||
class="mb-2 block text-sm font-medium text-gray-900"
|
||||
>
|
||||
Contact method
|
||||
</label>
|
||||
<select
|
||||
id="heir__contact-method"
|
||||
name="contactMethod"
|
||||
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
|
||||
>
|
||||
<option value="" selected>Choose a contact method</option>
|
||||
<option value="email">Email</option>
|
||||
</select>
|
||||
{#if $errors.contactMethod != null}
|
||||
<p class="mt-2 text-sm text-red-600">
|
||||
<span class="font-medium">{$errors.contactMethod[0]}</span>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label for="heir__name" class="mb-2 block text-sm font-medium text-gray-900">
|
||||
Heir name
|
||||
</label>
|
||||
<input
|
||||
id="heir__name"
|
||||
type="text"
|
||||
name="name"
|
||||
placeholder="Jane Doe"
|
||||
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||
/>
|
||||
{#if $errors.name != null}
|
||||
<p class="mt-2 text-sm text-red-600">
|
||||
<span class="font-medium">{$errors.name[0]}</span>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label
|
||||
for="heir__contactDetails"
|
||||
class="mb-2 block text-sm font-medium text-gray-900"
|
||||
>
|
||||
Contact details
|
||||
</label>
|
||||
<input
|
||||
id="heir__contactDetails"
|
||||
type="text"
|
||||
name="contactDetails"
|
||||
placeholder="jane@identity.net"
|
||||
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||
/>
|
||||
{#if $errors.contactDetails != null}
|
||||
<p class="mt-2 text-sm text-red-600">
|
||||
<span class="font-medium">{$errors.contactDetails[0]}</span>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class="focust:outline-none mt-2 rounded-lg bg-violet-700 px-5 py-2.5 text-center font-medium text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
|
||||
type="submit"
|
||||
>
|
||||
Add new heir
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
{/if}
|
||||
{#each $account?.heirs || [] as heir (heir.value)}
|
||||
<div class="mb-2.5 flex w-full flex-col rounded-lg border border-gray-200 p-3.5 shadow">
|
||||
<div class="flex justify-between">
|
||||
<span class="block text-sm font-medium text-gray-900">
|
||||
Contact method: <span class="capitalize">{heir.contactMethod}</span>
|
||||
</span>
|
||||
<button
|
||||
on:click={() => removeHeir(heir)}
|
||||
class="rounded-lg bg-red-600 px-2.5 py-1 text-center text-white hover:bg-red-700 focus:ring-4 focus:ring-violet-300"
|
||||
>
|
||||
Delete heir
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<span>{heir.name}</span>
|
||||
·
|
||||
<span>{heir.value}</span>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,73 +1,108 @@
|
|||
<script lang="ts">
|
||||
import { login, type Credentials } from "$lib/api";
|
||||
import { credentials } from "$lib/stores";
|
||||
import { createForm } from "felte";
|
||||
import { login, type Credentials } from '$lib/api';
|
||||
import { credentials } from '$lib/stores';
|
||||
import { createForm } from 'felte';
|
||||
|
||||
let submitError: string | undefined
|
||||
let submitError: string | undefined;
|
||||
|
||||
// FIXME: This is a badly done hack
|
||||
credentials.subscribe((v) => v != null && (setTimeout(() => window.location.pathname = '/dashboard', 200)))
|
||||
// FIXME: This is a badly done hack
|
||||
credentials.subscribe(
|
||||
(v) => v != null && setTimeout(() => (window.location.pathname = '/dashboard'), 200)
|
||||
);
|
||||
|
||||
const { form, errors } = createForm({
|
||||
onSubmit: (values) => {
|
||||
return login(values)
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
// @ts-ignore - FIXME: How to tell the checker that this is right
|
||||
if (response == null || ('error' in response && (typeof response['error'] !== "string" || !['invalid credentials'].includes(response['error'])))) {
|
||||
submitError = 'Something failed. Try again later.'
|
||||
}
|
||||
// @ts-ignore - FIXME: How to tell the checker that this is right
|
||||
else if ('error' in response) {
|
||||
// @ts-ignore - response is not null and the type of its key 'error' is a string
|
||||
submitError = 'Check your credentials and try again.'
|
||||
} else {
|
||||
credentials.set(response as Credentials)
|
||||
// FIXME: This is a badly done hack
|
||||
setTimeout(() => window.location.pathname = '/dashboard', 200)
|
||||
}
|
||||
},
|
||||
validate: (values) => {
|
||||
const errors = {}
|
||||
if (values.email == null || values.email.length === 0) {
|
||||
errors.email = 'Must not be empty'
|
||||
}
|
||||
const { form, errors } = createForm({
|
||||
onSubmit: (values) => {
|
||||
return login(values);
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
// @ts-ignore - FIXME: How to tell the checker that this is right
|
||||
if (
|
||||
response == null ||
|
||||
('error' in response &&
|
||||
(typeof response['error'] !== 'string' ||
|
||||
!['invalid credentials'].includes(response['error'])))
|
||||
) {
|
||||
submitError = 'Something failed. Try again later.';
|
||||
}
|
||||
// @ts-ignore - FIXME: How to tell the checker that this is right
|
||||
else if ('error' in response) {
|
||||
// @ts-ignore - response is not null and the type of its key 'error' is a string
|
||||
submitError = 'Check your credentials and try again.';
|
||||
} else {
|
||||
credentials.set(response as Credentials);
|
||||
// FIXME: This is a badly done hack
|
||||
setTimeout(() => (window.location.pathname = '/dashboard'), 200);
|
||||
}
|
||||
},
|
||||
validate: (values) => {
|
||||
const errors = {};
|
||||
if (values.email == null || values.email.length === 0) {
|
||||
errors.email = 'Must not be empty';
|
||||
}
|
||||
|
||||
if (values.password == null || values.password.length === 0) {
|
||||
errors.password = 'Must not be empty'
|
||||
}
|
||||
if (values.password == null || values.password.length === 0) {
|
||||
errors.password = 'Must not be empty';
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
})
|
||||
return errors;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mt-3.5 justify-center flex">
|
||||
<div class="w-[25%]">
|
||||
<h1 class="text-2xl pb-3.5">Log in</h1>
|
||||
<form use:form>
|
||||
<div class="mb-5">
|
||||
<label for="register__email" class="block mb-2 text-sm font-medium text-gray-900">Your e-mail</label>
|
||||
<input id="register__email" type="text" name="email" placeholder="jane@identity.net" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
||||
{#if $errors.email != null}
|
||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.email[0]}</span></p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label for="register__password" class="block mb-2 text-sm font-medium text-gray-900">Your password</label>
|
||||
<input id="register__password" type="password" name="password" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
||||
{#if $errors.password != null}
|
||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.password[0]}</span></p>
|
||||
{/if}
|
||||
</div>
|
||||
<button type="submit" class="text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focust:outline-none focus:ring-violet-300 font-medium rounded-lg w-full px-5 py-2.5 text-center">Log in</button>
|
||||
{#if submitError != null && submitError.length > 0}
|
||||
<p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p>
|
||||
{/if}
|
||||
<div class="flex pt-3.5 w-full justify-between">
|
||||
<a href="/auth/register" class="text-center font-medium text-blue-600 hover:underline">Create an account</a>
|
||||
<a href="/auth/recovery" class="text-center font-medium text-blue-600 hover:underline">Forgotten password?</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3.5 flex justify-center">
|
||||
<div class="w-[25%]">
|
||||
<h1 class="pb-3.5 text-2xl">Log in</h1>
|
||||
<form use:form>
|
||||
<div class="mb-5">
|
||||
<label for="register__email" class="mb-2 block text-sm font-medium text-gray-900">
|
||||
Your e-mail
|
||||
</label>
|
||||
<input
|
||||
id="register__email"
|
||||
type="text"
|
||||
name="email"
|
||||
placeholder="jane@identity.net"
|
||||
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||
/>
|
||||
{#if $errors.email != null}
|
||||
<p class="mt-2 text-sm text-red-600">
|
||||
<span class="font-medium">{$errors.email[0]}</span>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mb-5">
|
||||
<label for="register__password" class="mb-2 block text-sm font-medium text-gray-900">
|
||||
Your password
|
||||
</label>
|
||||
<input
|
||||
id="register__password"
|
||||
type="password"
|
||||
name="password"
|
||||
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||
/>
|
||||
{#if $errors.password != null}
|
||||
<p class="mt-2 text-sm text-red-600">
|
||||
<span class="font-medium">{$errors.password[0]}</span>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="focust:outline-none w-full rounded-lg bg-violet-700 px-5 py-2.5 text-center font-medium text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
|
||||
>
|
||||
Log in
|
||||
</button>
|
||||
{#if submitError != null && submitError.length > 0}
|
||||
<p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p>
|
||||
{/if}
|
||||
<div class="flex w-full justify-between pt-3.5">
|
||||
<a href="/auth/register" class="text-center font-medium text-blue-600 hover:underline">
|
||||
Create an account
|
||||
</a>
|
||||
<a href="/auth/recovery" class="text-center font-medium text-blue-600 hover:underline">
|
||||
Forgotten password?
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,83 +1,128 @@
|
|||