cleanup and docs and landing

This commit is contained in:
Sofía Aritz 2024-06-29 18:32:27 +02:00
parent cdb8ccbf97
commit 14cbc6a1a3
Signed by: sofia
GPG key ID: 90B5116E3542B28F
34 changed files with 2065 additions and 1297 deletions

View file

@ -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). Alzheimers Society. https://www.alzheimers.org.uk/get-support/publications-factsheets/the-dementia-guide

View 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"

View file

@ -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" } }]
}

View file

@ -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",

View file

@ -1,6 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};

View file

@ -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;
}

View file

@ -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;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View file

@ -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>

View file

@ -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;
};

View file

@ -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);
}

View 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
};

View file

@ -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>

View file

@ -1,2 +1,3 @@
// FIXME: Update code to support SSR
export const ssr = false;
export const ssr = false;
export const prerender = false;

View file

@ -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&mldr;
</p>
</div>
</div>
</div>
</div>

View file

@ -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>

View file

@ -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>

View file

@ -1,83 +1,128 @@