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 ## Rationale
Identity is a project that initially started as an app whose purpose was to store music you like 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
(or liked) for future use in treatment for diseases like dementia. Over time, the idea evolved for conditions such as dementia. Over time, the idea evolved and is now a general-purpose memory-saving app.
and is now general-purpose.
## Projects ## Projects
* `identity-web`. The web app that interacts with the Identiy API. * `identity-web`. The web app that interacts with the Identiy API.
* `identity-api`. The Identity API, also takes care of storing data. * `identity-api`. The Identity API, takes care of storing user data.
* `identity-format`. The specification for the Identity file format. * `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 ## 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, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100, "printWidth": 100,
"plugins": ["prettier-plugin-svelte"], "htmlWhitespaceSensitivity": "ignore",
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
} }

View file

@ -14,6 +14,7 @@
}, },
"devDependencies": { "devDependencies": {
"@sveltejs/adapter-auto": "^3.0.0", "@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-static": "^3.0.2",
"@sveltejs/kit": "^2.0.0", "@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0", "@sveltejs/vite-plugin-svelte": "^3.0.0",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
@ -26,6 +27,7 @@
"postcss": "^8.4.38", "postcss": "^8.4.38",
"prettier": "^3.1.1", "prettier": "^3.1.1",
"prettier-plugin-svelte": "^3.1.2", "prettier-plugin-svelte": "^3.1.2",
"prettier-plugin-tailwindcss": "^0.6.5",
"svelte": "^4.2.7", "svelte": "^4.2.7",
"svelte-check": "^3.6.0", "svelte-check": "^3.6.0",
"tailwindcss": "^3.4.4", "tailwindcss": "^3.4.4",

View file

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

View file

@ -1,3 +1,19 @@
@tailwind base; @tailwind base;
@tailwind components; @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 ENDPOINT = ENV_VARIABLES.IDENTITY_API_ENDPOINT;
const ASSET_API_ENDPOINT = 'http://localhost:3001/' const ASSET_API_ENDPOINT = ENV_VARIABLES.ASSET_API_ENDPOINT;
export type Credentials = { export type Credentials = {
token: string, token: string;
} };
export type AccountHeir = { export type AccountHeir = {
contactMethod: "email", contactMethod: 'email';
name: string, name: string;
value: string, value: string;
} };
export type Account = { export type Account = {
uid: string, uid: string;
name: string, name: string;
heirs: AccountHeir[], heirs: AccountHeir[];
} };
function sendRequest(path: string, credentials?: Credentials, request: RequestInit = {}, params: string = "") { function sendRequest(
if (typeof request !== "string" && credentials != null) { path: string,
request.headers = { 'Authorization': `Bearer ${credentials.token}`, ...request.headers } 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); let url = new URL(ENDPOINT);
url.pathname = path; url.pathname = path;
url.search = params 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` /// **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> { async function asJson<R>(request: Promise<Response>): Promise<R> {
let req = await request; let req = await request;
return (await req.json() as R) return (await req.json()) as R;
} }
export function login(credentials: { export function login(credentials: {
email: string, email: string;
password: string, password: string;
}): Promise<{ token: string, } | { error: string, }> { }): Promise<{ token: string } | { error: string }> {
return asJson(sendRequest('/auth/login', undefined, { return asJson(
method: 'POST', sendRequest('/auth/login', undefined, {
headers: { method: 'POST',
'Content-Type': 'application/json', headers: {
}, 'Content-Type': 'application/json'
body: JSON.stringify(credentials), },
})) body: JSON.stringify(credentials)
})
);
} }
export function register(credentials: { export function register(credentials: {
name: string, name: string;
email: string, email: string;
password: string, password: string;
}): Promise<{ token: string, } | { error: string, }> { }): Promise<{ token: string } | { error: string }> {
return asJson(sendRequest('/auth/register', undefined, { return asJson(
method: 'POST', sendRequest('/auth/register', undefined, {
headers: { method: 'POST',
'Content-Type': 'application/json', headers: {
}, 'Content-Type': 'application/json'
body: JSON.stringify(credentials), },
})) body: JSON.stringify(credentials)
})
);
} }
export function accountData(credentials: Credentials): Promise<Account | { error: string }> { 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 }> { export function genSessionKey(
return asJson(sendRequest('/auth/genkey', credentials)) credentials: Credentials
): Promise<{ session_key: string } | { error: string }> {
return asJson(sendRequest('/auth/genkey', credentials));
} }
export async function assetEndpoint(): Promise<string> { export async function assetEndpoint(): Promise<string> {
let res = await sendRequest("/asset/endpoint") let res = await sendRequest('/asset/endpoint');
return res.text() return res.text();
} }
export async function entryPage(credentials: Credentials, offset: number, limit: number): Promise<Entry[]> { export async function entryPage(
return asJson(sendRequest('/entry/list', credentials, undefined, `?offset=${offset}&limit=${limit}`)) 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> { export async function addEntry(credentials: Credentials, entry: IdlessEntry): Promise<void> {
await sendRequest('/entry', credentials, { await sendRequest('/entry', credentials, {
method: 'PUT', method: 'PUT',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify({entry}), body: JSON.stringify({ entry })
}) });
} }
export async function deleteEntry(credentials: Credentials, entry_id: string): Promise<void> { 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> { export async function updateHeirs(credentials: Credentials, heirs: AccountHeir[]): Promise<void> {
await sendRequest('/auth/heirs', credentials, { await sendRequest('/auth/heirs', credentials, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json'
}, },
body: JSON.stringify(heirs), body: JSON.stringify(heirs)
}); });
} }
export async function uploadAsset(session_key: string, file: File): Promise<string> { export async function uploadAsset(session_key: string, file: File): Promise<string> {
let url = new URL('/asset', ASSET_API_ENDPOINT); let url = new URL('/asset', ASSET_API_ENDPOINT);
url.search = `?session_key=${session_key}` url.search = `?session_key=${session_key}`;
let form = new FormData()
form.append("file", file);
let res = await fetch(url, { let form = new FormData();
method: "PUT", form.append('file', file);
body: form,
})
let { asset_id } = await res.json(); let res = await fetch(url, {
return asset_id; 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"> <script lang="ts">
import { faChevronDown, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; import { faChevronDown, faPlus, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome"; import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
import FeelingPill from "../../routes/dashboard/utils/FeelingPill.svelte"; import FeelingPill from '../../routes/dashboard/utils/FeelingPill.svelte';
import { FEELINGS, type KnownFeeling } from "$lib/entry"; import { FEELINGS, type KnownFeeling } from '$lib/entry';
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from 'svelte';
export let required = false export let required = false;
export let displayText = true export let displayText = true;
export let slim = false export let slim = false;
let feelingsDropdownShown = false let feelingsDropdownShown = false;
export let chosenFeelings: KnownFeeling[] = [] 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) { function addFeeling(feeling: KnownFeeling) {
chosenFeelings = [feeling, ...chosenFeelings]; chosenFeelings = [feeling, ...chosenFeelings];
dispatch('choiceUpdated', chosenFeelings); dispatch('choiceUpdated', chosenFeelings);
} }
function removeFeeling(feeling: KnownFeeling) { function removeFeeling(feeling: KnownFeeling) {
chosenFeelings = chosenFeelings.filter(v => v !== feeling) chosenFeelings = chosenFeelings.filter((v) => v !== feeling);
dispatch('choiceUpdated', chosenFeelings); dispatch('choiceUpdated', chosenFeelings);
} }
</script> </script>
<div class="flex flex-col"> <div class="flex flex-col">
{#if displayText} {#if displayText}
<span class="block mb-2 text-sm font-medium text-gray-900">Feelings</span> <span class="mb-2 block text-sm font-medium text-gray-900">Feelings</span>
{/if} {/if}
<div class="flex"> <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`}> <button
Feelings type="button"
<FontAwesomeIcon icon={faChevronDown}/> on:click={() => (feelingsDropdownShown = !feelingsDropdownShown)}
</button> 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`}
<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} Feelings
<div> <FontAwesomeIcon icon={faChevronDown} />
<span class="mr-1">Chosen:</span> </button>
{#each chosenFeelings as feeling (feeling)} <div
<div class="inline"> id="add-entry__feelings"
<button type="button" on:click={() => removeFeeling(feeling)}> 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'}`}
<FeelingPill feeling={feeling} slim={slim}> >
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faXmark}/></span> {#if chosenFeelings.length > 0}
</FeelingPill> <div>
</button> <span class="mr-1">Chosen:</span>
<input type="checkbox" class="hidden" name={`feeling__${feeling}`} checked> {#each chosenFeelings as feeling (feeling)}
</div> <div class="inline">
{/each} <button type="button" on:click={() => removeFeeling(feeling)}>
</div> <FeelingPill {feeling} {slim}>
{:else} <span class="pr-1" slot="pre"><FontAwesomeIcon icon={faXmark} /></span>
<span>No feelings chosen.</span> </FeelingPill>
{#if required} </button>
<span>You need to choose at least one feeling.</span> <input type="checkbox" class="hidden" name={`feeling__${feeling}`} checked />
{/if} </div>
{/if} {/each}
</div> </div>
</div> {:else}
<div class:hidden={!feelingsDropdownShown} class="bg-gray-50 border border-t-0 border-gray-300 py-3 px-1.5 rounded-b-lg"> <span>No feelings chosen.</span>
{#each feelingsToChoose as feeling (feeling)} {#if required}
<label class={`capitalize ${slim ? "p-0.5" : "p-1"}`}> <span>You need to choose at least one feeling.</span>
<button type="button" on:click={() => addFeeling(feeling)}> {/if}
<FeelingPill feeling={feeling} slim={slim}> {/if}
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faPlus}/></span> </div>
</FeelingPill> </div>
</button> <div
</label> class:hidden={!feelingsDropdownShown}
{/each} class="rounded-b-lg border border-t-0 border-gray-300 bg-gray-50 px-1.5 py-3"
</div> >
</div> {#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 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 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 KnownFeeling =
export type EntryKind = "song" | "album" | "event" | "memory" | "feeling" | "environment" | "date"; | '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 = { export type IdlessEntry = {
base: SongEntry | AlbumEntry | EventEntry | MemoryEntry | FeelingEntry | EnvironmentEntry | DateEntry, base:
creationDate: string, | SongEntry
feelings: (KnownFeeling | { | AlbumEntry
identifier: string, | EventEntry
description: string, | MemoryEntry
backgroundColor: string, | FeelingEntry
textColor: string, | EnvironmentEntry
})[], | DateEntry;
assets: string[], creationDate: string;
title?: string, feelings: (
description?: string, | KnownFeeling
| {
identifier: string;
description: string;
backgroundColor: string;
textColor: string;
}
)[];
assets: string[];
title?: string;
description?: string;
}; };
export type Entry = { export type Entry = {
id: string, id: string;
base: SongEntry | AlbumEntry | EventEntry | MemoryEntry | FeelingEntry | EnvironmentEntry | DateEntry, base:
creationDate: string, | SongEntry
feelings: (KnownFeeling | { | AlbumEntry
identifier: string, | EventEntry
description: string, | MemoryEntry
backgroundColor: string, | FeelingEntry
textColor: string, | EnvironmentEntry
})[], | DateEntry;
assets: string[], creationDate: string;
title?: string, feelings: (
description?: string, | KnownFeeling
| {
identifier: string;
description: string;
backgroundColor: string;
textColor: string;
}
)[];
assets: string[];
title?: string;
description?: string;
}; };
export type UniversalID = { export type UniversalID = {
provider: string, provider: string;
id: string, id: string;
} };
export type SongEntry = { export type SongEntry = {
kind: "song", kind: 'song';
artist: string, artist: string;
title: string, title: string;
link: string[], link: string[];
id: UniversalID[], id: UniversalID[];
} };
export type AlbumEntry = { export type AlbumEntry = {
kind: "album", kind: 'album';
artist: string, artist: string;
title: string, title: string;
link: string[], link: string[];
id: UniversalID[], id: UniversalID[];
} };
export type EventEntry = { export type EventEntry = {
kind: "event", kind: 'event';
} };
export type MemoryEntry = { export type MemoryEntry = {
kind: "memory", kind: 'memory';
} };
export type FeelingEntry = { export type FeelingEntry = {
kind: "feeling", kind: 'feeling';
} };
export type EnvironmentEntry = { export type EnvironmentEntry = {
kind: "environment", kind: 'environment';
location?: string | { location?:
latitude: number, | string
longitude: number, | {
}, latitude: number;
} longitude: number;
};
};
export type DateEntry = { export type DateEntry = {
kind: "date", kind: 'date';
referencedDate: string, referencedDate: string;
} };

View file

@ -1,66 +1,67 @@
import { writable } from "svelte/store"; import { writable } from 'svelte/store';
import { accountData, assetEndpoint, genSessionKey, type Account, type Credentials } from "./api"; 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 let _credentials: Credentials | null = null;
export const credentials = writable<Credentials | null>() export const credentials = writable<Credentials | null>();
credentials.subscribe((value) => { credentials.subscribe((value) => {
if (value != null) { if (value != null) {
_credentials = value; _credentials = value;
localStorage.setItem( CREDENTIALS_KEY, JSON.stringify(value)) localStorage.setItem(CREDENTIALS_KEY, JSON.stringify(value));
} else { } else {
_credentials = null; _credentials = null;
} }
}) });
export const account = writable<Account | null>() export const account = writable<Account | null>();
export const session_key = writable<string | null>() export const session_key = writable<string | null>();
export const asset_endpoint = writable<string | null>() export const asset_endpoint = writable<string | null>();
export async function initializeStores() { export async function initializeStores() {
let rawCredentials = localStorage.getItem(CREDENTIALS_KEY) let rawCredentials = localStorage.getItem(CREDENTIALS_KEY);
let parsedCredentials let parsedCredentials;
if (rawCredentials != null && rawCredentials.length > 0) { if (rawCredentials != null && rawCredentials.length > 0) {
try { try {
parsedCredentials = JSON.parse(rawCredentials) parsedCredentials = JSON.parse(rawCredentials);
credentials.set(parsedCredentials) credentials.set(parsedCredentials);
} } catch (e) {
catch (e) { localStorage.removeItem(CREDENTIALS_KEY) } localStorage.removeItem(CREDENTIALS_KEY);
} }
}
if (parsedCredentials != null) { if (parsedCredentials != null) {
let data = await accountData(parsedCredentials) let data = await accountData(parsedCredentials);
if ('error' in data) { if ('error' in data) {
credentials.set(null) credentials.set(null);
localStorage.removeItem(CREDENTIALS_KEY) localStorage.removeItem(CREDENTIALS_KEY);
} else { } else {
account.set(data) account.set(data);
} }
let key_result = await genSessionKey(parsedCredentials) let key_result = await genSessionKey(parsedCredentials);
if ('error' in key_result) { if ('error' in key_result) {
console.warn('Couldn\'t generate a session key!') console.warn("Couldn't generate a session key!");
} else { } else {
session_key.set(key_result.session_key) session_key.set(key_result.session_key);
} }
let asset_result = await assetEndpoint() let asset_result = await assetEndpoint();
asset_endpoint.set(asset_result) asset_endpoint.set(asset_result);
} }
} }
export async function refreshAccount() { export async function refreshAccount() {
if (_credentials == null) { if (_credentials == null) {
console.warn("Requested to refresh the user account but credentials are null.") console.warn('Requested to refresh the user account but credentials are null.');
return; return;
} }
let refreshedAccount = await accountData(_credentials) let refreshedAccount = await accountData(_credentials);
if ('error' in refreshedAccount) { if ('error' in refreshedAccount) {
console.warn("Failed to refresh the user account.") console.warn('Failed to refresh the user account.');
return; 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> <script>
import { credentials, initializeStores } from "$lib/stores"; import { credentials, initializeStores } from '$lib/stores';
import "../app.css"; import { ENV_VARIABLES } from '$lib/variables';
import '../app.css';
initializeStores() initializeStores();
</script> </script>
<div class="py-3.5 flex text-white bg-violet-800 justify-center"> <div class="flex justify-center bg-violet-800 py-3.5 text-white">
<nav class="w-[60%] flex justify-between items-center"> <nav class="flex w-[60%] items-center justify-between">
<h1 class="font-serif text-3xl"> <h1 class="font-serif text-3xl">
{#if $credentials == null} {#if $credentials == null}
<a href="/">Identity</a> <a href="/">Identity</a>
{:else} {:else}
<a href="/dashboard">Identity</a> <a href="/dashboard">Identity</a>
{/if} {/if}
</h1> </h1>
<div class="text-xl"> <div class="text-xl">
{#if $credentials == null} {#if $credentials == null}
| <div class="px-3 inline-block"><a href="/">Home</a></div> | <div class="inline-block px-3"><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> <div class="inline-block px-3"><a href="mailto:sofi@sofiaritz.com">Support</a></div>
| |
{:else} <div class="inline-block px-3"><a href="/auth/register">Join</a></div>
| <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> {:else}
| <div class="px-3 inline-block"><a href="mailto:sofi@sofiaritz.com">Support</a></div> | <div class="inline-block px-3"><a href="/dashboard">Dashboard</a></div>
| |
{/if} <div class="inline-block px-3"><a href="/auth/account">Account</a></div>
</div> |
</nav> <div class="inline-block px-3"><a href={ENV_VARIABLES.SUPPORT_PAGE}>Support</a></div>
|
{/if}
</div>
</nav>
</div> </div>
<main class="pt-3.5"> <main class="pt-3.5">
<slot/> <slot />
</main> </main>

View file

@ -1,2 +1,3 @@
// FIXME: Update code to support SSR // 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"> <script lang="ts">
import { createForm } from "felte"; import { createForm } from 'felte';
import { account, credentials, refreshAccount } from "$lib/stores"; import { account, credentials, refreshAccount } from '$lib/stores';
import { type AccountHeir, updateHeirs } from "$lib/api"; 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({ const { form, errors } = createForm({
onSubmit: async (values) => { onSubmit: async (values) => {
let heir: AccountHeir = { let heir: AccountHeir = {
contactMethod: values.contactMethod, contactMethod: values.contactMethod,
name: values.name, name: values.name,
value: values.contactDetails, value: values.contactDetails
}; };
let currentHeirs = structuredClone($account!.heirs) let currentHeirs = structuredClone($account!.heirs);
let updatedHeirs = [heir, ...currentHeirs]; let updatedHeirs = [heir, ...currentHeirs];
await updateHeirs($credentials!, updatedHeirs); await updateHeirs($credentials!, updatedHeirs);
await refreshAccount(); await refreshAccount();
heirWizard = false; heirWizard = false;
}, },
validate: (values) => { validate: (values) => {
let errors = {} let errors = {};
if (values.contactMethod == null || values.contactMethod.length === 0) { if (values.contactMethod == null || values.contactMethod.length === 0) {
errors['contactMethod'] = 'Must choose a contact method' errors['contactMethod'] = 'Must choose a contact method';
} }
if (values.name == null || values.name.length === 0) { if (values.name == null || values.name.length === 0) {
errors['name'] = 'Must not be empty' errors['name'] = 'Must not be empty';
} }
if (values.contactDetails == null || values.contactDetails.length === 0) { if (values.contactDetails == null || values.contactDetails.length === 0) {
errors['contactDetails'] = 'Must not be empty' errors['contactDetails'] = 'Must not be empty';
} }
return errors return errors;
} }
}) });
async function removeHeir(heir: AccountHeir) { async function removeHeir(heir: AccountHeir) {
let currentHeirs = structuredClone($account!.heirs) let currentHeirs = structuredClone($account!.heirs);
let updatedHeirs = currentHeirs let updatedHeirs = currentHeirs.filter((v) => v.value !== heir.value);
.filter((v) => v.value !== heir.value);
await updateHeirs($credentials!, updatedHeirs); await updateHeirs($credentials!, updatedHeirs);
await refreshAccount(); await refreshAccount();
} }
</script> </script>
<div class="mt-3.5 justify-center flex"> <div class="mt-3.5 flex justify-center">
<div class="w-[60%] flex flex-col"> <div class="flex w-[60%] flex-col">
<h1 class="text-2xl pb-3.5">Welcome back, <span class="font-bold">{$account?.name}</span>.</h1> <h1 class="pb-3.5 text-2xl">
<div> Welcome back, <span class="font-bold">{$account?.name}</span>
<div class="flex justify-between mb-2"> .
<h2 class="text-xl pb-2.5">Heirs</h2> </h1>
{#if $account?.heirs.length > 0} <div>
<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"> <div class="mb-2 flex justify-between">
+ Add a heir <h2 class="pb-2.5 text-xl">Heirs</h2>
</button> {#if $account?.heirs.length > 0}
{/if} <button
</div> on:click={() => (heirWizard = !heirWizard)}
{#if !heirWizard && $account?.heirs.length === 0} 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"
<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"> + Add a heir
<span class="text-4xl">+</span> </button>
<h2 class="text-xl font-semibold">Add a heir</h2> {/if}
</button> </div>
</div> {#if !heirWizard && $account?.heirs.length === 0}
{/if} <div class="flex flex-col">
{#if heirWizard} <button
<div class="border border-gray-200 rounded-lg shadow w-full flex flex-col p-3.5 mb-4"> on:click={() => (heirWizard = true)}
<form use:form> class="flex h-60 flex-col items-center justify-center gap-3 rounded border border-gray-300 p-2 text-black"
<div class="mb-5"> >
<label for="heir__contact-method" class="block mb-2 text-sm font-medium text-gray-900">Contact method</label> <span class="text-4xl">+</span>
<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"> <h2 class="text-xl font-semibold">Add a heir</h2>
<option value="" selected>Choose a contact method</option> </button>
<option value="email">Email</option> </div>
</select> {/if}
{#if $errors.contactMethod != null} {#if heirWizard}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.contactMethod[0]}</span></p> <div class="mb-4 flex w-full flex-col rounded-lg border border-gray-200 p-3.5 shadow">
{/if} <form use:form>
</div> <div class="mb-5">
<div class="mb-5"> <label
<label for="heir__name" class="block mb-2 text-sm font-medium text-gray-900">Heir name</label> for="heir__contact-method"
<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"> class="mb-2 block text-sm font-medium text-gray-900"
{#if $errors.name != null} >
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.name[0]}</span></p> Contact method
{/if} </label>
</div> <select
<div class="mb-5"> id="heir__contact-method"
<label for="heir__contactDetails" class="block mb-2 text-sm font-medium text-gray-900">Contact details</label> name="contactMethod"
<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"> 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"
{#if $errors.contactDetails != null} >
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.contactDetails[0]}</span></p> <option value="" selected>Choose a contact method</option>
{/if} <option value="email">Email</option>
</div> </select>
<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> {#if $errors.contactMethod != null}
</form> <p class="mt-2 text-sm text-red-600">
</div> <span class="font-medium">{$errors.contactMethod[0]}</span>
{/if} </p>
{#each $account?.heirs || [] as heir (heir.value)} {/if}
<div class="border border-gray-200 rounded-lg shadow w-full flex flex-col p-3.5 mb-2.5"> </div>
<div class="flex justify-between"> <div class="mb-5">
<span class="block text-sm font-medium text-gray-900">Contact method: <span class="capitalize">{heir.contactMethod}</span></span> <label for="heir__name" class="mb-2 block text-sm font-medium text-gray-900">
<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> Heir name
</div> </label>
<div> <input
<span>{heir.name}</span> · <span>{heir.value}</span> id="heir__name"
</div> type="text"
</div> name="name"
{/each} placeholder="Jane Doe"
</div> 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"
</div> />
</div> {#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"> <script lang="ts">
import { login, type Credentials } from "$lib/api"; import { login, type Credentials } from '$lib/api';
import { credentials } from "$lib/stores"; import { credentials } from '$lib/stores';
import { createForm } from "felte"; import { createForm } from 'felte';
let submitError: string | undefined let submitError: string | undefined;
// FIXME: This is a badly done hack // FIXME: This is a badly done hack
credentials.subscribe((v) => v != null && (setTimeout(() => window.location.pathname = '/dashboard', 200))) credentials.subscribe(
(v) => v != null && setTimeout(() => (window.location.pathname = '/dashboard'), 200)
);
const { form, errors } = createForm({ const { form, errors } = createForm({
onSubmit: (values) => { onSubmit: (values) => {
return login(values) return login(values);
}, },
onSuccess: (response) => { onSuccess: (response) => {
// @ts-ignore - FIXME: How to tell the checker that this is right // @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'])))) { if (
submitError = 'Something failed. Try again later.' response == null ||
} ('error' in response &&
// @ts-ignore - FIXME: How to tell the checker that this is right (typeof response['error'] !== 'string' ||
else if ('error' in response) { !['invalid credentials'].includes(response['error'])))
// @ts-ignore - response is not null and the type of its key 'error' is a string ) {
submitError = 'Check your credentials and try again.' submitError = 'Something failed. Try again later.';
} else { }
credentials.set(response as Credentials) // @ts-ignore - FIXME: How to tell the checker that this is right
// FIXME: This is a badly done hack else if ('error' in response) {
setTimeout(() => window.location.pathname = '/dashboard', 200) // @ts-ignore - response is not null and the type of its key 'error' is a string
} submitError = 'Check your credentials and try again.';
}, } else {
validate: (values) => { credentials.set(response as Credentials);
const errors = {} // FIXME: This is a badly done hack
if (values.email == null || values.email.length === 0) { setTimeout(() => (window.location.pathname = '/dashboard'), 200);
errors.email = 'Must not be empty' }
} },
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) { if (values.password == null || values.password.length === 0) {
errors.password = 'Must not be empty' errors.password = 'Must not be empty';
} }
return errors return errors;
} }
}) });
</script> </script>
<div class="mt-3.5 justify-center flex"> <div class="mt-3.5 flex justify-center">
<div class="w-[25%]"> <div class="w-[25%]">
<h1 class="text-2xl pb-3.5">Log in</h1> <h1 class="pb-3.5 text-2xl">Log in</h1>
<form use:form> <form use:form>
<div class="mb-5"> <div class="mb-5">
<label for="register__email" class="block mb-2 text-sm font-medium text-gray-900">Your e-mail</label> <label for="register__email" class="mb-2 block text-sm font-medium text-gray-900">
<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"> Your e-mail
{#if $errors.email != null} </label>
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.email[0]}</span></p> <input
{/if} id="register__email"
</div> type="text"
<div class="mb-5"> name="email"
<label for="register__password" class="block mb-2 text-sm font-medium text-gray-900">Your password</label> placeholder="jane@identity.net"
<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"> 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 $errors.email != null}
{/if} <p class="mt-2 text-sm text-red-600">
</div> <span class="font-medium">{$errors.email[0]}</span>
<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> </p>
{#if submitError != null && submitError.length > 0} {/if}
<p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p> </div>
{/if} <div class="mb-5">
<div class="flex pt-3.5 w-full justify-between"> <label for="register__password" class="mb-2 block text-sm font-medium text-gray-900">
<a href="/auth/register" class="text-center font-medium text-blue-600 hover:underline">Create an account</a> Your password
<a href="/auth/recovery" class="text-center font-medium text-blue-600 hover:underline">Forgotten password?</a> </label>
</div> <input
</form> id="register__password"
</div> type="password"
</div> 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 @@
<script lang="ts"> <script lang="ts">
import { register, type Credentials } from "$lib/api"; import { register, type Credentials } from '$lib/api';
import { credentials } from "$lib/stores"; import { credentials } from '$lib/stores';
import { createForm } from "felte"; import { createForm } from 'felte';
let submitError: string | undefined let submitError: string | undefined;
// FIXME: This is a badly done hack // FIXME: This is a badly done hack
credentials.subscribe((v) => v != null && (setTimeout(() => window.location.pathname = '/dashboard', 200))) credentials.subscribe(
(v) => v != null && setTimeout(() => (window.location.pathname = '/dashboard'), 200)
);
const { form, errors } = createForm({ const { form, errors } = createForm({
onSubmit: (values) => { onSubmit: (values) => {
return register(values) return register(values);
}, },
onSuccess: (response) => { onSuccess: (response) => {
// @ts-ignore - FIXME: How to tell the checker that this is right // @ts-ignore - FIXME: How to tell the checker that this is right
if (response == null || ('error' in response && (typeof response['error'] !== "string" || !['invalid data'].includes(response['error'])))) { if (
submitError = 'Something failed. Try again later.' response == null ||
} ('error' in response &&
// @ts-ignore - FIXME: How to tell the checker that this is right (typeof response['error'] !== 'string' || !['invalid data'].includes(response['error'])))
else if ('error' in response) { ) {
// @ts-ignore - response is not null and the type of its key 'error' is a string submitError = 'Something failed. Try again later.';
submitError = 'Check your credentials and try again, this user may already exist.' }
} else { // @ts-ignore - FIXME: How to tell the checker that this is right
credentials.set(response as Credentials) else if ('error' in response) {
// FIXME: This is a badly done hack // @ts-ignore - response is not null and the type of its key 'error' is a string
setTimeout(() => window.location.pathname = '/dashboard', 200) submitError = 'Check your credentials and try again, this user may already exist.';
} } else {
}, credentials.set(response as Credentials);
validate: (values) => { // FIXME: This is a badly done hack
const errors = {} setTimeout(() => (window.location.pathname = '/dashboard'), 200);
if (values.name == null || values.name.length === 0) { }
errors.name = 'Must not be empty' },
} validate: (values) => {
const errors = {};
if (values.name == null || values.name.length === 0) {
errors.name = 'Must not be empty';
}
if (values.email == null || !/^[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+/.test(values.email)) { if (values.email == null || !/^[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+/.test(values.email)) {
errors.email = 'Must be a valid e-mail' errors.email = 'Must be a valid e-mail';
} }
if (values.password == null || values.password.length === 0) { if (values.password == null || values.password.length === 0) {
errors.password = 'Must not be empty' errors.password = 'Must not be empty';
} else if (values.password != null && values.password.length < 12) { } else if (values.password != null && values.password.length < 12) {
errors.password = 'Must be over 12 characters' errors.password = 'Must be over 12 characters';
} }
return errors return errors;
} }
}) });
</script> </script>
<div class="mt-3.5 justify-center flex"> <div class="mt-3.5 flex justify-center">
<div class="w-[25%]"> <div class="w-[25%]">
<h1 class="text-2xl pb-3.5">Register</h1> <h1 class="pb-3.5 text-2xl">Register</h1>
<form use:form> <form use:form>
<div class="mb-5"> <div class="mb-5">
<label for="register__name" class="block mb-2 text-sm font-medium text-gray-900">Your name</label> <label for="register__name" class="mb-2 block text-sm font-medium text-gray-900">
<input id="register__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"> Your name
{#if $errors.name != null} </label>
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.name[0]}</span></p> <input
{/if} id="register__name"
</div> type="text"
<div class="mb-5"> name="name"
<label for="register__email" class="block mb-2 text-sm font-medium text-gray-900">Your e-mail</label> placeholder="Jane Doe"
<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"> 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 $errors.name != null}
{/if} <p class="mt-2 text-sm text-red-600">
</div> <span class="font-medium">{$errors.name[0]}</span>
<div class="mb-5"> </p>
<label for="register__password" class="block mb-2 text-sm font-medium text-gray-900">Your password</label> {/if}
<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"> </div>
{#if $errors.password != null} <div class="mb-5">
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.password[0]}</span></p> <label for="register__email" class="mb-2 block text-sm font-medium text-gray-900">
{/if} Your e-mail
</div> </label>
<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">Create user</button> <input
{#if submitError != null && submitError.length > 0} id="register__email"
<p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p> type="text"
{/if} name="email"
<a href="/auth/login" class="block w-full text-center pt-3.5 font-medium text-blue-600 hover:underline">Already have an account?</a> placeholder="jane@identity.net"
</form> 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"
</div> />
</div> {#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"
>
Create user
</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}
<a
href="/auth/login"
class="block w-full pt-3.5 text-center font-medium text-blue-600 hover:underline"
>
Already have an account?
</a>
</form>
</div>
</div>

View file

@ -1,154 +1,213 @@
<script lang="ts"> <script lang="ts">
import { entryPage } from "$lib/api"; import { entryPage } from '$lib/api';
import { account, credentials } from "$lib/stores"; import { account, credentials } from '$lib/stores';
import { onMount } from "svelte"; import { onMount } from 'svelte';
import Entries from "./Entries.svelte"; import Entries from './Entries.svelte';
import Overview from "./Overview.svelte"; import Overview from './Overview.svelte';
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome"; import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
import { faFilter } from "@fortawesome/free-solid-svg-icons"; import { faFilter } from '@fortawesome/free-solid-svg-icons';
import FilterSelector from "./utils/FilterSelector.svelte"; import FilterSelector from './utils/FilterSelector.svelte';
credentials.subscribe((v) => v == null && (setTimeout(() => window.location.pathname = '/auth/login', 200))) credentials.subscribe(
(v) => v == null && setTimeout(() => (window.location.pathname = '/auth/login'), 200)
);
function createPageHandler({ onLoadingStatusChanged, onEndReached }: { onLoadingStatusChanged: (status: boolean) => any, onEndReached: () => any }) { function createPageHandler({
let loadingPage = false; onLoadingStatusChanged,
let rechedEnd = false; onEndReached
let currentOffset = 10; }: {
let step = 5; onLoadingStatusChanged: (status: boolean) => any;
onEndReached: () => any;
}) {
let loadingPage = false;
let rechedEnd = false;
let currentOffset = 10;
let step = 5;
return { return {
initialEntries: entryPage($credentials!, 0, currentOffset), initialEntries: entryPage($credentials!, 0, currentOffset),
nextPage: async () => { nextPage: async () => {
if (loadingPage || reachedEnd) { if (loadingPage || reachedEnd) {
return undefined; return undefined;
} }
loadingPage = true; loadingPage = true;
onLoadingStatusChanged(loadingPage); onLoadingStatusChanged(loadingPage);
let page = await entryPage($credentials!, currentOffset, step); let page = await entryPage($credentials!, currentOffset, step);
currentOffset += step; currentOffset += step;
loadingPage = false; loadingPage = false;
onLoadingStatusChanged(loadingPage); onLoadingStatusChanged(loadingPage);
if (page.length === 0) { if (page.length === 0) {
reachedEnd = true; reachedEnd = true;
onEndReached(); onEndReached();
} }
return page; return page;
} }
} };
} }
let overview = Promise.allSettled([entryPage($credentials!, 0, 3), entryPage($credentials!, 20, 3)]) let overview = Promise.allSettled([
entryPage($credentials!, 0, 3),
entryPage($credentials!, 20, 3)
]);
let loadingPage = false; let loadingPage = false;
let reachedEnd = false; let reachedEnd = false;
let filterStatus = false; let filterStatus = false;
let { initialEntries: entries, nextPage } = createPageHandler({ let { initialEntries: entries, nextPage } = createPageHandler({
onLoadingStatusChanged: (status) => loadingPage = status, onLoadingStatusChanged: (status) => (loadingPage = status),
onEndReached: () => reachedEnd = true, onEndReached: () => (reachedEnd = true)
}) });
let showFilterSelector = false let showFilterSelector = false;
let chosenFilterFeelings = [] let chosenFilterFeelings = [];
let filters = { let filters = {
fromDate: null, fromDate: null,
toDate: null, toDate: null,
kind: null, kind: null,
feelings: null, feelings: null,
searchQuery: null, searchQuery: null
} };
onMount(() => { onMount(() => {
function handleScroll() { function handleScroll() {
if (!filterStatus && window.innerHeight + window.scrollY >= document.body.offsetHeight) { if (!filterStatus && window.innerHeight + window.scrollY >= document.body.offsetHeight) {
entries.then(async (page) => { entries.then(async (page) => {
let secondPage = await nextPage() let secondPage = await nextPage();
if (secondPage != null) { if (secondPage != null) {
page = [...page, ...secondPage]; page = [...page, ...secondPage];
entries = new Promise((resolve) => resolve(page)) entries = new Promise((resolve) => resolve(page));
} }
}) });
} }
} }
window.addEventListener('scroll', handleScroll) window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll);
}) });
function refreshEntries() { function refreshEntries() {
entries = entryPage($credentials!, 0, 20); entries = entryPage($credentials!, 0, 20);
} }
</script> </script>
{#if $account != null} {#if $account != null}
{#await entries} {#await entries}
<div class="justify-center flex mt-3.5"> <div class="mt-3.5 flex justify-center">
<div role="status" class="flex flex-col justify-center items-center gap-5"> <div role="status" class="flex flex-col items-center justify-center gap-5">
<span class="text-2xl">Loading entries...</span> <span class="text-2xl">Loading entries...</span>
<svg aria-hidden="true" class="inline w-9 h-9 text-gray-200 animate-spin fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/> aria-hidden="true"
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/> class="inline h-9 w-9 animate-spin fill-blue-600 text-gray-200"
</svg> viewBox="0 0 100 101"
</div> fill="none"
</div> xmlns="http://www.w3.org/2000/svg"
{:then entries} >
<div class="mt-3.5 justify-center flex"> <path
<div class="w-[60%] flex flex-col"> d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
{#if entries.length === 0} fill="currentColor"
<a href="/entry/new" 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> <path
<h2 class="text-xl font-semibold">Add an entry</h2> d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
</a> fill="currentFill"
{:else} />
<h1 class="text-2xl pb-3.5">Welcome back, <span class="font-bold">{$account?.name}</span>.</h1> </svg>
<div class="flex gap-2"> </div>
{#await overview} </div>
<span>Loading...</span> {:then entries}
{:then overview} <div class="mt-3.5 flex justify-center">
<Overview latest={overview[0].value.filter(v => !["feeling"].includes(v.base.kind))} past={overview[1].value}/> <div class="flex w-[60%] flex-col">
{/await} {#if entries.length === 0}
</div> <a
href="/entry/new"
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 an entry</h2>
</a>
{:else}
<h1 class="pb-3.5 text-2xl">
Welcome back, <span class="font-bold">{$account?.name}</span>
.
</h1>
<div class="flex gap-2">
{#await overview}
<span>Loading...</span>
{:then overview}
<Overview
latest={overview[0].value.filter((v) => !['feeling'].includes(v.base.kind))}
past={overview[1].value}
/>
{/await}
</div>
<h2 class="text-2xl mt-6">Entries</h2> <h2 class="mt-6 text-2xl">Entries</h2>
<div class="w-full flex items-baseline justify-between mt-2.5"> <div class="mt-2.5 flex w-full items-baseline justify-between">
<a class="rounded-lg bg-violet-700 text-white px-3 py-1.5 text-center hover:bg-violet-800 focus:ring-4 focus:ring-violet-300" href="/entry/new">+ Add an entry</a> <a
<button on:click={() => showFilterSelector = !showFilterSelector}> class="rounded-lg bg-violet-700 px-3 py-1.5 text-center text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
<FontAwesomeIcon icon={faFilter}/> href="/entry/new"
<span class="ml-1.5">Filter entries</span> >
</button> + Add an entry
</div> </a>
<button on:click={() => (showFilterSelector = !showFilterSelector)}>
<FontAwesomeIcon icon={faFilter} />
<span class="ml-1.5">Filter entries</span>
</button>
</div>
{#if showFilterSelector} {#if showFilterSelector}
<FilterSelector on:updatedChosenFeelings={(e) => chosenFilterFeelings = e.detail} on:updatedFilter={(e) => filters = e.detail} chosenFeelings={chosenFilterFeelings} filters={filters}/> <FilterSelector
{/if} on:updatedChosenFeelings={(e) => (chosenFilterFeelings = e.detail)}
on:updatedFilter={(e) => (filters = e.detail)}
chosenFeelings={chosenFilterFeelings}
{filters}
/>
{/if}
<div class="mt-3.5 flex flex-col gap-1"> <div class="mt-3.5 flex flex-col gap-1">
<Entries on:updatedFilterStatus={(e) => filterStatus = e.detail} on:deleted={() => refreshEntries()} entries={entries} filters={filters}/> <Entries
</div> on:updatedFilterStatus={(e) => (filterStatus = e.detail)}
{#if loadingPage && !reachedEnd} on:deleted={() => refreshEntries()}
<div class="justify-center flex py-6"> {entries}
<div role="status" class="flex justify-center items-center gap-5"> {filters}
<span class="text-xl">Loading entries...</span> />
<svg aria-hidden="true" class="inline w-9 h-9 text-gray-200 animate-spin fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg"> </div>
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/> {#if loadingPage && !reachedEnd}
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/> <div class="flex justify-center py-6">
</svg> <div role="status" class="flex items-center justify-center gap-5">
</div> <span class="text-xl">Loading entries...</span>
</div> <svg
{/if} aria-hidden="true"
class="inline h-9 w-9 animate-spin fill-blue-600 text-gray-200"
viewBox="0 0 100 101"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
fill="currentColor"
/>
<path
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
fill="currentFill"
/>
</svg>
</div>
</div>
{/if}
{#if reachedEnd} {#if reachedEnd}
<div class="justify-center flex py-6"> <div class="flex justify-center py-6">
<div role="status" class="flex justify-center items-center gap-5"> <div role="status" class="flex items-center justify-center gap-5">
<span class="text-xl">You've reached the end</span> <span class="text-xl">You've reached the end</span>
</div> </div>
</div> </div>
{/if} {/if}
{/if} {/if}
</div> </div>
</div> </div>
{/await} {/await}
{/if} {/if}

View file

@ -1,156 +1,171 @@
<script lang="ts"> <script lang="ts">
import type { Entry as EntryType, EntryKind, KnownFeeling } from "$lib/entry"; import type { Entry as EntryType, EntryKind, KnownFeeling } from '$lib/entry';
import ExternalLink from "./utils/ExternalLink.svelte"; import ExternalLink from './utils/ExternalLink.svelte';
import FeelingPill from "./utils/FeelingPill.svelte"; import FeelingPill from './utils/FeelingPill.svelte';
import Entry from "./utils/Entry.svelte"; import Entry from './utils/Entry.svelte';
import EntryDescription from "./utils/EntryDescription.svelte"; import EntryDescription from './utils/EntryDescription.svelte';
import AssetPreview from "./utils/AssetPreview.svelte"; import AssetPreview from './utils/AssetPreview.svelte';
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from 'svelte';
import Fuse from "fuse.js"; import Fuse from 'fuse.js';
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher();
export let entries: EntryType[] export let entries: EntryType[];
let filteredEntries = entries let filteredEntries = entries;
export let filters: { export let filters: {
fromDate: null | Date, fromDate: null | Date;
toDate: null | Date, toDate: null | Date;
kind: null | EntryKind[], kind: null | EntryKind[];
feelings: null | { feelings: null | {
exclusive: boolean, exclusive: boolean;
feelings: KnownFeeling[], feelings: KnownFeeling[];
}, };
searchQuery: null | string, searchQuery: null | string;
} };
let extended: string[] = [] let extended: string[] = [];
function applyFilters(filters: { function applyFilters(filters: {
fromDate: null | Date, fromDate: null | Date;
toDate: null | Date, toDate: null | Date;
kind: null | EntryKind[], kind: null | EntryKind[];
feelings: null | { feelings: null | {
exclusive: boolean, exclusive: boolean;
feelings: KnownFeeling[], feelings: KnownFeeling[];
}, };
searchQuery: null | string, searchQuery: null | string;
}) { }) {
filteredEntries = entries filteredEntries = entries;
if (filters.fromDate != null) { if (filters.fromDate != null) {
filteredEntries = entries.filter((v) => new Date(v.creationDate) >= filters.fromDate!); filteredEntries = entries.filter((v) => new Date(v.creationDate) >= filters.fromDate!);
} }
if (filters.toDate != null) { if (filters.toDate != null) {
filteredEntries = entries.filter((v) => new Date(v.creationDate) <= filters.toDate!); filteredEntries = entries.filter((v) => new Date(v.creationDate) <= filters.toDate!);
} }
if (filters.kind != null) { if (filters.kind != null) {
filteredEntries = entries.filter((v) => filters.kind!.includes(v.base.kind)); filteredEntries = entries.filter((v) => filters.kind!.includes(v.base.kind));
} }
if (filters.feelings != null) { if (filters.feelings != null) {
let feelings = filters.feelings!.feelings let feelings = filters.feelings!.feelings;
if (filters.feelings.exclusive) { if (filters.feelings.exclusive) {
filteredEntries = entries.filter((v) => { filteredEntries = entries.filter((v) => {
let v1 = v.feelings.filter((f) => typeof f === "string" && feelings.includes(f)) let v1 = v.feelings.filter((f) => typeof f === 'string' && feelings.includes(f));
return v.feelings.length === v1.length; return v.feelings.length === v1.length;
}) });
} else { } else {
filteredEntries = entries.filter((v) => { filteredEntries = entries.filter((v) => {
let includes = false let includes = false;
feelings.forEach((f) => { feelings.forEach((f) => {
if (v.feelings.includes(f)) { if (v.feelings.includes(f)) {
includes = true includes = true;
} }
}) });
return includes return includes;
}) });
} }
} }
if (filters.searchQuery != null) { if (filters.searchQuery != null) {
let fuse = new Fuse(entries, { let fuse = new Fuse(entries, {
keys: [ keys: [
{ {
name: "title", name: 'title',
weight: 2, weight: 2
}, },
"description", 'description'
], ]
}); });
let results = fuse.search(filters.searchQuery!); let results = fuse.search(filters.searchQuery!);
filteredEntries = results.map((v) => v.item); filteredEntries = results.map((v) => v.item);
} }
if (filteredEntries.length !== entries.length) { if (filteredEntries.length !== entries.length) {
dispatch('updatedFilterStatus', true) dispatch('updatedFilterStatus', true);
} else { } else {
dispatch('updatedFilterStatus', false) dispatch('updatedFilterStatus', false);
} }
} }
$: applyFilters(filters) $: applyFilters(filters);
</script> </script>
{#if entries.length != filteredEntries.length && filteredEntries.length === 0} {#if entries.length != filteredEntries.length && filteredEntries.length === 0}
<div class="justify-center flex py-6"> <div class="flex justify-center py-6">
<div role="status" class="flex justify-center items-center gap-5"> <div role="status" class="flex items-center justify-center gap-5">
<span class="text-xl">No results found</span> <span class="text-xl">No results found</span>
</div> </div>
</div> </div>
{/if} {/if}
{#each filteredEntries as entry (entry.id)} {#each filteredEntries as entry (entry.id)}
<Entry <Entry
on:extended={(e) => extended = [e.detail.id, ...extended]} on:extended={(e) => (extended = [e.detail.id, ...extended])}
on:contracted={(e) => extended = extended.filter(v => v !== e.detail.id)} on:contracted={(e) => (extended = extended.filter((v) => v !== e.detail.id))}
on:deleted={(e) => { dispatch('deleted', e.detail) }} on:deleted={(e) => {
dispatch('deleted', e.detail);
}}
id={entry.id}
kind={entry.base.kind}
creationDate={new Date(entry.creationDate)}
title={entry.base.kind === 'date'
? new Date(entry.base.referencedDate).toLocaleDateString()
: entry.title}
isExtended={extended.includes(entry.id)}
>
<div slot="contracted">
{#if entry.base.kind === 'song' || entry.base.kind === 'album'}
<ExternalLink href={entry.base.link[0]}>
{entry.base.artist} &dash; {entry.base.title}
</ExternalLink>
{/if}
id={entry.id} {#if entry.base.kind === 'feeling'}
kind={entry.base.kind} <div class="flex gap-1">
creationDate={new Date(entry.creationDate)} {#each entry.feelings as feeling}
title={entry.base.kind === "date" ? new Date(entry.base.referencedDate).toLocaleDateString() : entry.title} {#if typeof feeling === 'string'}
isExtended={extended.includes(entry.id)} <FeelingPill {feeling} />
> {:else}
<div slot="contracted"> <FeelingPill
{#if entry.base.kind === "song" || entry.base.kind === "album"} feeling={feeling.identifier}
<ExternalLink href={entry.base.link[0]}>{entry.base.artist} &dash; {entry.base.title}</ExternalLink> bgColor={feeling.backgroundColor}
{/if} textColor={feeling.textColor}
/>
{/if}
{/each}
</div>
{/if}
</div>
{#if entry.base.kind === "feeling"} <div slot="extended">
<div class="flex gap-1"> <div class="mb-2 flex gap-1">
{#each entry.feelings as feeling} {#each entry.feelings as feeling}
{#if typeof feeling === "string"} {#if typeof feeling === 'string'}
<FeelingPill feeling={feeling}/> <FeelingPill {feeling} />
{:else} {:else}
<FeelingPill feeling={feeling.identifier} bgColor={feeling.backgroundColor} textColor={feeling.textColor}/> <FeelingPill
{/if} feeling={feeling.identifier}
{/each} bgColor={feeling.backgroundColor}
</div> textColor={feeling.textColor}
{/if} />
</div> {/if}
{/each}
</div>
<div slot="extended"> {#if entry.base.kind === 'song' || entry.base.kind === 'album'}
<div class="flex gap-1 mb-2"> <ExternalLink href={entry.base.link[0]}>
{#each entry.feelings as feeling} {entry.base.artist} &dash; {entry.base.title}
{#if typeof feeling === "string"} </ExternalLink>
<FeelingPill feeling={feeling}/> {/if}
{:else}
<FeelingPill feeling={feeling.identifier} bgColor={feeling.backgroundColor} textColor={feeling.textColor}/>
{/if}
{/each}
</div>
{#if entry.base.kind === "song" || entry.base.kind === "album"} {#if entry.description != null}
<ExternalLink href={entry.base.link[0]}>{entry.base.artist} &dash; {entry.base.title}</ExternalLink> <EntryDescription>{entry.description}</EntryDescription>
{/if} {/if}
{#if entry.description != null} <div class="mt-2 flex gap-1">
<EntryDescription>{entry.description}</EntryDescription> {#each entry.assets as asset}
{/if} <AssetPreview asset_id={asset} />
{/each}
<div class="flex gap-1 mt-2"> </div>
{#each entry.assets as asset} </div>
<AssetPreview asset_id={asset}/> </Entry>
{/each} {/each}
</div>
</div>
</Entry>
{/each}

View file

@ -1,26 +1,26 @@
<script lang="ts"> <script lang="ts">
import { type Entry } from "$lib/entry"; import { type Entry } from '$lib/entry';
import OverviewEntry from "./utils/OverviewEntry.svelte"; import OverviewEntry from './utils/OverviewEntry.svelte';
export let latest: Entry[]; export let latest: Entry[];
export let past: Entry[]; export let past: Entry[];
</script> </script>
<div class="p-6 border border-gray-200 rounded-lg shadow w-full"> <div class="w-full rounded-lg border border-gray-200 p-6 shadow">
<h2 class="text-xl">Latest activity</h2> <h2 class="text-xl">Latest activity</h2>
<div class="pt-2"> <div class="pt-2">
{#each latest as entry (entry.id)} {#each latest as entry (entry.id)}
<OverviewEntry entry={entry}/> <OverviewEntry {entry} />
{/each} {/each}
</div> </div>
</div> </div>
{#if past.length > 0} {#if past.length > 0}
<div class="p-6 border border-gray-200 rounded-lg shadow w-full"> <div class="w-full rounded-lg border border-gray-200 p-6 shadow">
<h2 class="text-xl">Memories from the past</h2> <h2 class="text-xl">Memories from the past</h2>
<div class="pt-2"> <div class="pt-2">
{#each past as entry (entry.id)} {#each past as entry (entry.id)}
<OverviewEntry entry={entry} showDate={true}/> <OverviewEntry {entry} showDate={true} />
{/each} {/each}
</div> </div>
</div> </div>
{/if} {/if}

View file

@ -1,32 +1,44 @@
<script lang="ts"> <script lang="ts">
import { asset_endpoint, session_key } from "$lib/stores"; import { asset_endpoint, session_key } from '$lib/stores';
import { faArrowUpRightFromSquare, faFileAudio, faFileVideo, faImage } from "@fortawesome/free-solid-svg-icons"; import {
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome"; faArrowUpRightFromSquare,
import mime from "mime" faFileAudio,
faFileVideo,
faImage
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
import mime from 'mime';
export let asset_id: string export let asset_id: string;
// FIXME: This feels correct, but how is it guaranteed that session_key and asset_endpoint are not null? // FIXME: This feels correct, but how is it guaranteed that session_key and asset_endpoint are not null?
$: href = new URL(`/asset?asset_id=${encodeURIComponent(asset_id)}&session_key=${encodeURIComponent($session_key!)}`, $asset_endpoint!).href $: href = new URL(
$: kind = mime.getType(asset_id.split(".")[1])?.split("/")[0] `/asset?asset_id=${encodeURIComponent(asset_id)}&session_key=${encodeURIComponent($session_key!)}`,
$asset_endpoint!
).href;
$: kind = mime.getType(asset_id.split('.')[1])?.split('/')[0];
</script> </script>
<a class="font-bold bg-violet-600 text-white px-2.5 py-1 rounded flex gap-2 items-center" target="_blank" href={href}> <a
{#if kind == null} class="flex items-center gap-2 rounded bg-violet-600 px-2.5 py-1 font-bold text-white"
<FontAwesomeIcon icon={faArrowUpRightFromSquare}/> target="_blank"
{:else if kind === "image"} {href}
<FontAwesomeIcon icon={faImage}/> >
{:else if kind === "audio"} {#if kind == null}
<FontAwesomeIcon icon={faFileAudio}/> <FontAwesomeIcon icon={faArrowUpRightFromSquare} />
{:else if kind === "video"} {:else if kind === 'image'}
<FontAwesomeIcon icon={faFileVideo}/> <FontAwesomeIcon icon={faImage} />
{:else} {:else if kind === 'audio'}
<FontAwesomeIcon icon={faArrowUpRightFromSquare}/> <FontAwesomeIcon icon={faFileAudio} />
{/if} {:else if kind === 'video'}
<FontAwesomeIcon icon={faFileVideo} />
{:else}
<FontAwesomeIcon icon={faArrowUpRightFromSquare} />
{/if}
{#if kind != null && kind !== "application"} {#if kind != null && kind !== 'application'}
<span class="capitalize">{kind}</span> <span class="capitalize">{kind}</span>
{:else} {:else}
<span>Asset</span> <span>Asset</span>
{/if} {/if}
</a> </a>

View file

@ -1,78 +1,96 @@
<script lang="ts"> <script lang="ts">
import { deleteEntry } from "$lib/api"; import { deleteEntry } from '$lib/api';
import { credentials } from "$lib/stores"; import { credentials } from '$lib/stores';
import { TITLED_ENTRIES } from "$lib/entry"; import { TITLED_ENTRIES } from '$lib/entry';
import EntryKind from "./EntryKind.svelte"; import EntryKind from './EntryKind.svelte';
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from 'svelte';
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher();
export let id: string; export let id: string;
export let creationDate: Date; export let creationDate: Date;
export let kind: "song" | "album" | "event" | "feeling" | "environment" | "date" | "memory"; export let kind: 'song' | 'album' | 'event' | 'feeling' | 'environment' | 'date' | 'memory';
export let title: string | undefined; export let title: string | undefined;
export let isExtended = false; export let isExtended = false;
let prevExtended = isExtended let prevExtended = isExtended;
$: if (prevExtended !== isExtended) { $: if (prevExtended !== isExtended) {
dispatch(isExtended ? 'extended' : 'contracted', { id }) dispatch(isExtended ? 'extended' : 'contracted', { id });
} }
async function processDeletion(id: string) { async function processDeletion(id: string) {
await deleteEntry($credentials!, id); await deleteEntry($credentials!, id);
dispatch('deleted', { dispatch('deleted', {
id, id
}) });
} }
$: cardClass = () => { $: cardClass = () => {
let cardClass = "border border-gray-200 rounded-lg shadow w-full flex p-3.5" let cardClass = 'border border-gray-200 rounded-lg shadow w-full flex p-3.5';
if (isExtended) { if (isExtended) {
cardClass += " flex-col gap-1.5" cardClass += ' flex-col gap-1.5';
} else { } else {
if (TITLED_ENTRIES.includes(kind)) { if (TITLED_ENTRIES.includes(kind)) {
cardClass += " flex-col" cardClass += ' flex-col';
} else { } else {
cardClass += " gap-4 items-center" cardClass += ' gap-4 items-center';
} }
} }
return cardClass return cardClass;
}; };
</script> </script>
<div class={cardClass()} id={`entry__${id}`}> <div class={cardClass()} id={`entry__${id}`}>
<button on:click={() => { prevExtended = isExtended; isExtended = !isExtended }}> <button
<div class="flex justify-between items-center"> on:click={() => {
<div class="flex items-center gap-2.5"> prevExtended = isExtended;
<EntryKind kind={kind}/> isExtended = !isExtended;
{#if title != null && isExtended} }}
<span>Created at: <time datetime={creationDate.toISOString()}>{creationDate.toLocaleDateString()}</time></span> >
{:else if title != null} <div class="flex items-center justify-between">
<h2 class="text-xl text-left font-semibold">{title}</h2> <div class="flex items-center gap-2.5">
{:else if isExtended} <EntryKind {kind} />
<span>Created at: <time datetime={creationDate.toISOString()}>{creationDate.toLocaleDateString()}</time></span> {#if title != null && isExtended}
{/if} <span>
</div> Created at: <time datetime={creationDate.toISOString()}>
{#if isExtended} {creationDate.toLocaleDateString()}
<button on:click={() => processDeletion(id)} class="rounded-lg bg-red-600 text-white px-2.5 py-1.5 text-center hover:bg-red-700 focus:ring-4 focus:ring-violet-300">Delete entry</button> </time>
{/if} </span>
</div> {:else if title != null}
<h2 class="text-left text-xl font-semibold">{title}</h2>
{#if title != null && isExtended} {:else if isExtended}
<h2 class="text-xl text-left font-semibold mt-2">{title}</h2> <span>
{/if} Created at: <time datetime={creationDate.toISOString()}>
</button> {creationDate.toLocaleDateString()}
</time>
</span>
{/if}
</div>
{#if isExtended}
<button
on:click={() => processDeletion(id)}
class="rounded-lg bg-red-600 px-2.5 py-1.5 text-center text-white hover:bg-red-700 focus:ring-4 focus:ring-violet-300"
>
Delete entry
</button>
{/if}
</div>
<slot/> {#if title != null && isExtended}
<h2 class="mt-2 text-left text-xl font-semibold">{title}</h2>
{/if}
</button>
{#if !isExtended} <slot />
<slot name="contracted"/>
{/if}
{#if isExtended} {#if !isExtended}
<slot name="extended"/> <slot name="contracted" />
{/if} {/if}
</div>
{#if isExtended}
<slot name="extended" />
{/if}
</div>

View file

@ -1,3 +1,3 @@
<p class="w-full text-left"> <p class="w-full text-left">
<slot/> <slot />
</p> </p>

View file

@ -1,24 +1,45 @@
<script lang="ts"> <script lang="ts">
import { faCalendarDays, faChampagneGlasses, faHeartPulse, faLandmarkDome, faMusic, faNewspaper, faRecordVinyl, faSeedling } from "@fortawesome/free-solid-svg-icons"; import {
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome"; faCalendarDays,
faChampagneGlasses,
faHeartPulse,
faLandmarkDome,
faMusic,
faNewspaper,
faRecordVinyl,
faSeedling
} from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
export let kind: "song" | "album" | "event" | "feeling" | "environment" | "date" | "memory" export let kind: 'song' | 'album' | 'event' | 'feeling' | 'environment' | 'date' | 'memory';
</script> </script>
{#if kind === "song"} {#if kind === 'song'}
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faMusic}/> Song</span> <span class="flex items-center gap-2.5 text-xl"><FontAwesomeIcon icon={faMusic} /> Song</span>
{:else if kind === "album"} {:else if kind === 'album'}
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faRecordVinyl}/> Album</span> <span class="flex items-center gap-2.5 text-xl">
{:else if kind === "event"} <FontAwesomeIcon icon={faRecordVinyl} /> Album
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faChampagneGlasses}/> Event</span> </span>
{:else if kind === "memory"} {:else if kind === 'event'}
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faNewspaper}/> Memory</span> <span class="flex items-center gap-2.5 text-xl">
{:else if kind === "feeling"} <FontAwesomeIcon icon={faChampagneGlasses} /> Event
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faHeartPulse}/> Feeling</span> </span>
{:else if kind === "environment"} {:else if kind === 'memory'}
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faSeedling}/> Environment</span> <span class="flex items-center gap-2.5 text-xl">
{:else if kind === "date"} <FontAwesomeIcon icon={faNewspaper} /> Memory
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faCalendarDays}/> Date</span> </span>
{:else if kind === 'feeling'}
<span class="flex items-center gap-2.5 text-xl">
<FontAwesomeIcon icon={faHeartPulse} /> Feeling
</span>
{:else if kind === 'environment'}
<span class="flex items-center gap-2.5 text-xl">
<FontAwesomeIcon icon={faSeedling} /> Environment
</span>
{:else if kind === 'date'}
<span class="flex items-center gap-2.5 text-xl">
<FontAwesomeIcon icon={faCalendarDays} /> Date
</span>
{:else} {:else}
<span>Unknown value. Try loading the page again.</span> <span>Unknown value. Try loading the page again.</span>
{/if} {/if}

View file

@ -1,11 +1,15 @@
<script lang="ts"> <script lang="ts">
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons"; import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome"; import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
export let href: string export let href: string;
</script> </script>
<a class="font-bold text-violet-600 text-left flex gap-2 items-center hover:underline" target="_blank" href={href}> <a
<FontAwesomeIcon icon={faArrowUpRightFromSquare}/> class="flex items-center gap-2 text-left font-bold text-violet-600 hover:underline"
<slot/> target="_blank"
</a> {href}
>
<FontAwesomeIcon icon={faArrowUpRightFromSquare} />
<slot />
</a>

View file

@ -1,40 +1,67 @@
<script lang="ts"> <script lang="ts">
// TODO: Design a more _formal_ color system for emotions (>strong = >weight, etc). // TODO: Design a more _formal_ color system for emotions (>strong = >weight, etc).
const DEFAULT_COLORS: {[index: string]: string[]} = { const DEFAULT_COLORS: { [index: string]: string[] } = {
"__DEFAULT__": ["#0a0a0a", "#fafafa"], __DEFAULT__: ['#0a0a0a', '#fafafa'],
"afraid": ["#fda4af", "#0a0a0a"], afraid: ['#fda4af', '#0a0a0a'],
"angry": ["#dc2626", "#fafafa"], angry: ['#dc2626', '#fafafa'],
"bad": ["#450a0a", "#fafafa"], bad: ['#450a0a', '#fafafa'],
"bored": ["#d4d4d8", "#0a0a0a"], bored: ['#d4d4d8', '#0a0a0a'],
"confused": ["#fef3c7", "#0a0a0a"], confused: ['#fef3c7', '#0a0a0a'],
"excited": ["#f97316", "#fafafa"], excited: ['#f97316', '#fafafa'],
"fine": ["#bef264", "#0a0a0a"], fine: ['#bef264', '#0a0a0a'],
"happy": ["#facc15", "#0a0a0a"], happy: ['#facc15', '#0a0a0a'],
"hurt": ["#ff69b4", "#0a0a0a"], hurt: ['#ff69b4', '#0a0a0a'],
"in love": ["#ff1493", "#fafafa"], 'in love': ['#ff1493', '#fafafa'],
"mad": ["#450a0a", "#fafafa"], mad: ['#450a0a', '#fafafa'],
"nervous": ["#7e22ce", "#fafafa"], nervous: ['#7e22ce', '#fafafa'],
"okay": ["#86efac", "#0a0a0a"], okay: ['#86efac', '#0a0a0a'],
"sad": ["#0284c7", "#fafafa"], sad: ['#0284c7', '#fafafa'],
"scared": ["#334155", "#fafafa"], scared: ['#334155', '#fafafa'],
"shy": ["#cbd5e1", "#0a0a0a"], shy: ['#cbd5e1', '#0a0a0a'],
"sleepy": ["#7dd3fc", "#0a0a0a"], sleepy: ['#7dd3fc', '#0a0a0a'],
"active": ["#059669", "#fafafa"], active: ['#059669', '#fafafa'],
"surprised": ["#fbbf24", "#0a0a0a"], surprised: ['#fbbf24', '#0a0a0a'],
"tired": ["#92400e", "#fafafa"], tired: ['#92400e', '#fafafa'],
"upset": ["#b91c1c", "#fafafa"], upset: ['#b91c1c', '#fafafa'],
"worried": ["#d4d4d8", "#0a0a0a"], worried: ['#d4d4d8', '#0a0a0a'],
"relaxed": ["#86efac", "#0a0a0a"], relaxed: ['#86efac', '#0a0a0a']
}; };
export let feeling: "relaxed" | "afraid" | "angry" | "bad" | "bored" | "confused" | "excited" | "fine" | "happy" | "hurt" | "in love" | "mad" | "nervous" | "okay" | "sad" | "scared" | "shy" | "sleepy" | "active" | "surprised" | "tired" | "upset" | "worried" | string export let feeling:
export let bgColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS["__DEFAULT__"])[0] | 'relaxed'
export let textColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS["__DEFAULT__"])[1] | 'afraid'
export let slim = false; | 'angry'
| 'bad'
| 'bored'
| 'confused'
| 'excited'
| 'fine'
| 'happy'
| 'hurt'
| 'in love'
| 'mad'
| 'nervous'
| 'okay'
| 'sad'
| 'scared'
| 'shy'
| 'sleepy'
| 'active'
| 'surprised'
| 'tired'
| 'upset'
| 'worried'
| string;
export let bgColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS['__DEFAULT__'])[0];
export let textColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS['__DEFAULT__'])[1];
export let slim = false;
</script> </script>
<div class={`inline-block py-0.5 px-1.5 rounded-full ${slim ? "text-xs" : "text-sm"} font-semibold w-22 text-center`} style={`background-color: ${bgColor}; color: ${textColor}`}> <div
<slot name="pre"/> class={`inline-block rounded-full px-1.5 py-0.5 ${slim ? 'text-xs' : 'text-sm'} w-22 text-center font-semibold`}
style={`background-color: ${bgColor}; color: ${textColor}`}
>
<slot name="pre" />
<span>{feeling.charAt(0).toUpperCase() + feeling.slice(1)}</span> <span>{feeling.charAt(0).toUpperCase() + feeling.slice(1)}</span>
</div> </div>

View file

@ -1,105 +1,143 @@
<script lang="ts"> <script lang="ts">
import FeelingsChooser from "$lib/components/FeelingsChooser.svelte"; import FeelingsChooser from '$lib/components/FeelingsChooser.svelte';
import type { EntryKind, KnownFeeling } from "$lib/entry"; import type { EntryKind, KnownFeeling } from '$lib/entry';
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from 'svelte';
import EntryKind from "./EntryKind.svelte"; import EntryKind from './EntryKind.svelte';
export let filters: { export let filters: {
fromDate: null | Date, fromDate: null | Date;
toDate: null | Date, toDate: null | Date;
kind: null | EntryKind[], kind: null | EntryKind[];
feelings: null | { feelings: null | {
exclusive: boolean, exclusive: boolean;
feelings: KnownFeeling[], feelings: KnownFeeling[];
}, };
searchQuery: null | string, searchQuery: null | string;
}; };
let dispatch = createEventDispatcher()
export let chosenFeelings: KnownFeeling[] = [] let dispatch = createEventDispatcher();
let touched = {
fromDate: false,
toDate: false,
}
function upstreamChanges() { export let chosenFeelings: KnownFeeling[] = [];
dispatch('updatedFilter', filters) let touched = {
} fromDate: false,
toDate: false
};
function handleKind(kind: string | EntryKind) { function upstreamChanges() {
if (kind.length === 0) { dispatch('updatedFilter', filters);
filters.kind = null; }
} else {
filters.kind = [kind as EntryKind];
}
upstreamChanges();
}
function handleDate(date: string | null, kind: "fromDate" | "toDate") { function handleKind(kind: string | EntryKind) {
touched[kind] = true; if (kind.length === 0) {
filters.kind = null;
} else {
filters.kind = [kind as EntryKind];
}
upstreamChanges();
}
if (date == null || date.length === 0) { function handleDate(date: string | null, kind: 'fromDate' | 'toDate') {
filters[kind] = null touched[kind] = true;
} else {
filters[kind] = new Date(date)
}
upstreamChanges()
}
function handleSearchQuery(query: string) { if (date == null || date.length === 0) {
if (query.length === 0) { filters[kind] = null;
filters.searchQuery = null; } else {
} else { filters[kind] = new Date(date);
filters.searchQuery = query.trim(); }
} upstreamChanges();
upstreamChanges() }
}
function handleFeelings(feelings: KnownFeeling[]) { function handleSearchQuery(query: string) {
chosenFeelings = feelings; if (query.length === 0) {
dispatch('updatedChosenFeelings', chosenFeelings) filters.searchQuery = null;
} else {
filters.searchQuery = query.trim();
}
upstreamChanges();
}
if (feelings.length === 0) { function handleFeelings(feelings: KnownFeeling[]) {
filters.feelings = null; chosenFeelings = feelings;
} else { dispatch('updatedChosenFeelings', chosenFeelings);
filters.feelings = {
exclusive: false, if (feelings.length === 0) {
feelings, filters.feelings = null;
}; } else {
} filters.feelings = {
upstreamChanges(); exclusive: false,
} feelings
};
}
upstreamChanges();
}
</script> </script>
<div class="flex flex-col justify-between p-1 pt-3 gap-3"> <div class="flex flex-col justify-between gap-3 p-1 pt-3">
<div class="flex gap-4 justify-between w-full"> <div class="flex w-full justify-between gap-4">
<div class="w-full"> <div class="w-full">
<label for="filter__from-date" class="block mb-0.5 text-sm font-medium text-gray-900">From date</label> <label for="filter__from-date" class="mb-0.5 block text-sm font-medium text-gray-900">
<input value={touched.fromDate ? undefined : filters.fromDate?.toISOString().split('T')[0]} id="filter__from-date" max={filters.toDate?.toISOString().split('T')[0]} on:change={(e) => handleDate(e.target.valueAsDate, "fromDate")} type="date" name="date" 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-1.5"> From date
</div> </label>
<div class="w-full"> <input
<label for="filter__to-date" class="block mb-0.5 text-sm font-medium text-gray-900">To date</label> value={touched.fromDate ? undefined : filters.fromDate?.toISOString().split('T')[0]}
<input value={touched.toDate ? undefined : filters.toDate?.toISOString().split('T')[0]} id="filter__to-date" min={filters.fromDate?.toISOString().split('T')[0]} on:change={(e) => handleDate(e.target.valueAsDate, "toDate")} type="date" name="date" 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-1.5"> id="filter__from-date"
</div> max={filters.toDate?.toISOString().split('T')[0]}
<div class="w-full"> on:change={(e) => handleDate(e.target.valueAsDate, 'fromDate')}
<label for="filter__kind" class="block mb-0.5 text-sm font-medium text-gray-900">Entry kind</label> type="date"
<select on:change={(e) => handleKind(e.target.value)} id="filter__kind" name="date" 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-1.5"> name="date"
<option value="" selected>Choose an entry kind</option> class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-1.5 text-sm focus:border-violet-500 focus:ring-violet-500"
<option value="song">Song</option> />
<option value="album">Album</option> </div>
<option value="event">Event</option> <div class="w-full">
<option value="memory">Memory</option> <label for="filter__to-date" class="mb-0.5 block text-sm font-medium text-gray-900">
<option value="feeling">Feeling</option> To date
<option value="environment">Environment</option> </label>
<option value="date">Date</option> <input
</select> value={touched.toDate ? undefined : filters.toDate?.toISOString().split('T')[0]}
</div> id="filter__to-date"
</div> min={filters.fromDate?.toISOString().split('T')[0]}
<div> on:change={(e) => handleDate(e.target.valueAsDate, 'toDate')}
<FeelingsChooser chosenFeelings={chosenFeelings} on:choiceUpdated={(e) => handleFeelings(e.detail)} displayText={false} slim={true} /> type="date"
</div> name="date"
<div> class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-1.5 text-sm focus:border-violet-500 focus:ring-violet-500"
<input value={filters.searchQuery} on:keydown={(e) => handleSearchQuery(e.target.value)} type="text" placeholder="Search query" 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"> />
</div> </div>
</div> <div class="w-full">
<label for="filter__kind" class="mb-0.5 block text-sm font-medium text-gray-900">
Entry kind
</label>
<select
on:change={(e) => handleKind(e.target.value)}
id="filter__kind"
name="date"
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-1.5 text-sm focus:border-violet-500 focus:ring-violet-500"
>
<option value="" selected>Choose an entry kind</option>
<option value="song">Song</option>
<option value="album">Album</option>
<option value="event">Event</option>
<option value="memory">Memory</option>
<option value="feeling">Feeling</option>
<option value="environment">Environment</option>
<option value="date">Date</option>
</select>
</div>
</div>
<div>
<FeelingsChooser
{chosenFeelings}
on:choiceUpdated={(e) => handleFeelings(e.detail)}
displayText={false}
slim={true}
/>
</div>
<div>
<input
value={filters.searchQuery}
on:keydown={(e) => handleSearchQuery(e.target.value)}
type="text"
placeholder="Search query"
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"
/>
</div>
</div>

View file

@ -1,22 +1,35 @@
<script lang="ts"> <script lang="ts">
import { TITLED_ENTRIES, type Entry } from "$lib/entry" import { TITLED_ENTRIES, type Entry } from '$lib/entry';
export let entry: Entry export let entry: Entry;
export let showDate = false export let showDate = false;
</script> </script>
<p> <p>
{#if showDate} {#if showDate}
<time class="pr-2.5 font-mono" datetime={entry.creationDate}>{new Date(entry.creationDate).toLocaleDateString()}</time> <time class="pr-2.5 font-mono" datetime={entry.creationDate}>
{/if} {new Date(entry.creationDate).toLocaleDateString()}
</time>
{/if}
{#if TITLED_ENTRIES.includes(entry.base.kind)} {#if TITLED_ENTRIES.includes(entry.base.kind)}
New {entry.base.kind}: <a href={`#entry__${entry.id}`} class="font-bold text-violet-600 hover:underline">§ {entry.title}</a> New {entry.base.kind}:
{:else if ["song", "album"].includes(entry.base.kind)} <a href={`#entry__${entry.id}`} class="font-bold text-violet-600 hover:underline">
New {entry.base.kind}: <a href={`#entry__${entry.id}`} class="font-bold">{entry.base.artist} &dash; {entry.base.title}</a> § {entry.title}
{:else if entry.base.kind === "date"} </a>
New {entry.base.kind}: <a href={`#entry__${entry.id}`} class="font-bold">{new Date(entry.base.referencedDate).toLocaleDateString()}</a> {:else if ['song', 'album'].includes(entry.base.kind)}
{:else} New {entry.base.kind}:
<a href={`#entry__${entry.id}`} class="font-bold text-violet-600 hover:underline">New {entry.base.kind}</a> <a href={`#entry__${entry.id}`} class="font-bold">
{/if} {entry.base.artist} &dash; {entry.base.title}
</p> </a>
{:else if entry.base.kind === 'date'}
New {entry.base.kind}:
<a href={`#entry__${entry.id}`} class="font-bold">
{new Date(entry.base.referencedDate).toLocaleDateString()}
</a>
{:else}
<a href={`#entry__${entry.id}`} class="font-bold text-violet-600 hover:underline">
New {entry.base.kind}
</a>
{/if}
</p>

View file

@ -1,219 +1,351 @@
<script lang="ts"> <script lang="ts">
import { createForm } from "felte"; import { createForm } from 'felte';
import EntryKind from "../../dashboard/utils/EntryKind.svelte"; import EntryKind from '../../dashboard/utils/EntryKind.svelte';
import { FEELINGS, TITLED_ENTRIES, type AlbumEntry, type IdlessEntry, type KnownFeeling, type SongEntry } from "$lib/entry"; import {
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome"; FEELINGS,
import { faSpotify, faYoutube } from "@fortawesome/free-brands-svg-icons"; TITLED_ENTRIES,
import { faChevronDown, faLink, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons"; type AlbumEntry,
import FeelingPill from "../../dashboard/utils/FeelingPill.svelte"; type IdlessEntry,
import { addEntry, uploadAsset } from "$lib/api"; type KnownFeeling,
import { credentials, session_key } from "$lib/stores"; type SongEntry
import FeelingsChooser from "$lib/components/FeelingsChooser.svelte"; } from '$lib/entry';
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
import { faSpotify, faYoutube } from '@fortawesome/free-brands-svg-icons';
import { faChevronDown, faLink, faPlus, faXmark } from '@fortawesome/free-solid-svg-icons';
import FeelingPill from '../../dashboard/utils/FeelingPill.svelte';
import { addEntry, uploadAsset } from '$lib/api';
import { credentials, session_key } from '$lib/stores';
import FeelingsChooser from '$lib/components/FeelingsChooser.svelte';
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 kind: EntryKind | null = "song" let kind: EntryKind | null;
const { form, errors } = createForm({ const { form, errors } = createForm({
onSubmit: async (values) => { onSubmit: async (values) => {
let feelings = Object.keys(values) let feelings = Object.keys(values)
.filter(v => v.startsWith("feeling__")) .filter((v) => v.startsWith('feeling__'))
.map(v => v.replaceAll("feeling__", "")) as KnownFeeling[]; .map((v) => v.replaceAll('feeling__', '')) as KnownFeeling[];
let base;
if (values.kind === "song" || values.kind === "album") {
base = {
kind: values.kind,
artist: values.artist,
title: values.musicTitle,
link: [values.spotify, values.yt, values.otherProvider].filter(v => v != null && v.length > 0),
// FIXME: infer univeersal ids
id: [],
}
} else if (values.kind === "environment") {
base = {
kind: values.kind,
location: (values.location != null && values.location.length > 0) ? values.location : undefined,
}
} else if (values.kind === "date") {
base = {
kind: values.kind,
referencedDate: values.date,
}
} else {
base = {
kind: values.kind,
}
}
let asset_id let base;
if (values.asset != null && typeof values.asset === "object") { if (values.kind === 'song' || values.kind === 'album') {
asset_id = await uploadAsset($session_key!, values.asset) base = {
} kind: values.kind,
artist: values.artist,
let entry: IdlessEntry = { title: values.musicTitle,
base, link: [values.spotify, values.yt, values.otherProvider].filter(
creationDate: new Date().toISOString(), (v) => v != null && v.length > 0
assets: [asset_id].filter(v => v != null) as string[], ),
feelings, // FIXME: infer univeersal ids
title: TITLED_ENTRIES.includes(values.kind) ? values.title : undefined, id: []
description: values.description, };
} } else if (values.kind === 'environment') {
base = {
kind: values.kind,
location:
values.location != null && values.location.length > 0 ? values.location : undefined
};
} else if (values.kind === 'date') {
base = {
kind: values.kind,
referencedDate: values.date
};
} else {
base = {
kind: values.kind
};
}
await addEntry($credentials!, entry) let asset_id;
window.location.pathname = '/dashboard' if (values.asset != null && typeof values.asset === 'object') {
}, asset_id = await uploadAsset($session_key!, values.asset);
validate: (values) => { }
let errors = {}
if (values.kind == null || values.kind.length === 0) { let entry: IdlessEntry = {
errors['kind'] = 'Must choose an entry kind' base,
return errors creationDate: new Date().toISOString(),
} assets: [asset_id].filter((v) => v != null) as string[],
feelings,
title: TITLED_ENTRIES.includes(values.kind) ? values.title : undefined,
description: values.description
};
if (values.kind === "song" || values.kind === "album") { await addEntry($credentials!, entry);
if (values.artist == null || values.artist.length === 0) { window.location.pathname = '/dashboard';
errors['artist'] = "Must not be empty"; },
} validate: (values) => {
let errors = {};
if (values.musicTitle == null || values.musicTitle.length === 0) { if (values.kind == null || values.kind.length === 0) {
errors["musicTitle"] = "Must not be empty"; errors['kind'] = 'Must choose an entry kind';
} return errors;
}
// FIXME: When asset support is added, another precondition is that no asset is uploaded if (values.kind === 'song' || values.kind === 'album') {
if (values.spotify.length === 0 && values.yt.length === 0 && values.otherProvider.length === 0) { if (values.artist == null || values.artist.length === 0) {
errors["links"] = "You must add at least one link or upload an audio asset"; errors['artist'] = 'Must not be empty';
} }
} else if (values.kind === "date") {
if (values.date == null || values.date.length === 0) {
errors['date'] = "Must choose a date";
}
} else if (values.kind === "feeling") {
if (Object.keys(values).filter(v => v.startsWith("feeling__")).length === 0) {
errors['feelings'] = "Must choose at least one feeling";
}
} else {
if (values.title == null || values.title.length === 0) {
errors["title"] = "Must not be empty";
}
}
return errors; if (values.musicTitle == null || values.musicTitle.length === 0) {
} errors['musicTitle'] = 'Must not be empty';
}) }
// FIXME: When asset support is added, another precondition is that no asset is uploaded
if (
values.spotify.length === 0 &&
values.yt.length === 0 &&
values.otherProvider.length === 0
) {
errors['links'] = 'You must add at least one link or upload an audio asset';
}
} else if (values.kind === 'date') {
if (values.date == null || values.date.length === 0) {
errors['date'] = 'Must choose a date';
}
} else if (values.kind === 'feeling') {
if (Object.keys(values).filter((v) => v.startsWith('feeling__')).length === 0) {
errors['feelings'] = 'Must choose at least one feeling';
}
} else {
if (values.title == null || values.title.length === 0) {
errors['title'] = 'Must not be empty';
}
}
return errors;
}
});
</script> </script>
<div class="mt-3.5 justify-center flex"> <div class="mt-3.5 flex justify-center">
<div class="w-[60%] flex flex-col"> <div class="flex w-[60%] flex-col">
<h1 class="text-2xl pb-3.5">Add an entry</h1> <h1 class="pb-3.5 text-2xl">Add an entry</h1>
<form use:form> <form use:form>
<div class="mb-5"> <div class="mb-5">
<label for="add-entry__kind" class="block mb-2 text-sm font-medium text-gray-900">Entry kind</label> <label for="add-entry__kind" class="mb-2 block text-sm font-medium text-gray-900">
<select bind:value={kind} id="add-entry__kind" name="kind" 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"> Entry kind
<option value="" selected>Choose an entry kind</option> </label>
<option value="song">Song</option> <select
<option value="album">Album</option> bind:value={kind}
<option value="event">Event</option> id="add-entry__kind"
<option value="memory">Memory</option> name="kind"
<option value="feeling">Feeling</option> 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="environment">Environment</option> >
<option value="date">Date</option> <option value="" selected>Choose an entry kind</option>
</select> <option value="song">Song</option>
{#if $errors.kind != null} <option value="album">Album</option>
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.kind[0]}</span></p> <option value="event">Event</option>
{/if} <option value="memory">Memory</option>
</div> <option value="feeling">Feeling</option>
{#if TITLED_ENTRIES.includes(kind)} <option value="environment">Environment</option>
<div class="mb-5"> <option value="date">Date</option>
<label for="add-entry__title" class="block mb-2 text-sm font-medium text-gray-900">Title</label> </select>
<input id="add-entry__title" type="text" name="title" placeholder="At the sunflower field with my friends" 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.kind != null}
{#if $errors.title != null} <p class="mt-2 text-sm text-red-600">
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.title[0]}</span></p> <span class="font-medium">{$errors.kind[0]}</span>
{/if} </p>
</div> {/if}
{/if} </div>
{#if TITLED_ENTRIES.includes(kind)}
<div class="mb-5">
<label for="add-entry__title" class="mb-2 block text-sm font-medium text-gray-900">
Title
</label>
<input
id="add-entry__title"
type="text"
name="title"
placeholder="At the sunflower field with my friends"
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.title != null}
<p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.title[0]}</span>
</p>
{/if}
</div>
{/if}
{#if ["song", "album"].includes(kind)} {#if ['song', 'album'].includes(kind)}
<div class="flex flex-col mb-5 gap-5 md:flex-row"> <div class="mb-5 flex flex-col gap-5 md:flex-row">
<div class="w-full"> <div class="w-full">
<label for="add-entry__artist" class="block mb-2 text-sm font-medium text-gray-900">Artist name</label> <label for="add-entry__artist" class="mb-2 block text-sm font-medium text-gray-900">
<input id="add-entry__artist" type="text" name="artist" placeholder="Claude Debussy" 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"> Artist name
{#if $errors.artist != null} </label>
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.artist[0]}</span></p> <input
{/if} id="add-entry__artist"
</div> type="text"
<div class="w-full"> name="artist"
<label for="add-entry__music-title" class="block mb-2 text-sm font-medium text-gray-900"><span class="capitalize">{kind}</span> title</label> placeholder="Claude Debussy"
<input id="add-entry__music-title" type="text" name="musicTitle" placeholder="Clair de Lune" 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"> 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.musicTitle != null} />
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.musicTitle[0]}</span></p> {#if $errors.artist != null}
{/if} <p class="mt-2 text-sm text-red-600">
</div> <span class="font-medium">{$errors.artist[0]}</span>
</div> </p>
<div class="mb-5"> {/if}
<label for="add-entry__spotify" class="block mb-2 text-sm font-medium text-gray-900">Spotify link</label> </div>
<div class="flex"> <div class="w-full">
<span class="inline-flex items-center px-2.5 text-sm text-gray-900 bg-gray-200 border rounded-e-0 border-gray-300 border-e-0 rounded-s-md"> <label
<FontAwesomeIcon size="lg" icon={faSpotify}/> for="add-entry__music-title"
</span> class="mb-2 block text-sm font-medium text-gray-900"
<input type="text" id="add-entry__spotify" name="spotify" class="rounded-none 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 p-2.5" placeholder={kind === "song" ? "https://open.spotify.com/track/..." : "https://open.spotify.com/album/..."}> >
</div> <span class="capitalize">{kind}</span>
</div> title
<div class="mb-5"> </label>
<label for="add-entry__yt" class="block mb-2 text-sm font-medium text-gray-900">YouTube link</label> <input
<div class="flex"> id="add-entry__music-title"
<span class="inline-flex items-center px-2.5 text-sm text-gray-900 bg-gray-200 border rounded-e-0 border-gray-300 border-e-0 rounded-s-md"> type="text"
<FontAwesomeIcon size="lg" icon={faYoutube}/> name="musicTitle"
</span> placeholder="Clair de Lune"
<input type="text" id="add-entry__yt" name="yt" class="rounded-none 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 p-2.5" placeholder="https://www.youtube.com/watch..."> 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"
</div> />
</div> {#if $errors.musicTitle != null}
<div class="mb-5"> <p class="mt-2 text-sm text-red-600">
<label for="add-entry__other" class="block mb-2 text-sm font-medium text-gray-900">Link to other provider</label> <span class="font-medium">{$errors.musicTitle[0]}</span>
<div class="flex"> </p>
<span class="inline-flex items-center px-2.5 text-sm text-gray-900 bg-gray-200 border rounded-e-0 border-gray-300 border-e-0 rounded-s-md"> {/if}
<FontAwesomeIcon size="lg" icon={faLink}/> </div>
</span> </div>
<input type="text" name="otherProvider" id="add-entry__other" class="rounded-none 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 p-2.5" placeholder="https://www.music.tld/play/..."> <div class="mb-5">
</div> <label for="add-entry__spotify" class="mb-2 block text-sm font-medium text-gray-900">
</div> Spotify link
{#if $errors.links != null} </label>
<p class="mt-2.5 mb-3.5 text-sm text-red-600"><span class="font-medium">{$errors.links[0]}</span></p> <div class="flex">
{/if} <span
{:else if kind === "environment"} class="rounded-e-0 inline-flex items-center rounded-s-md border border-e-0 border-gray-300 bg-gray-200 px-2.5 text-sm text-gray-900"
<div class="w-full mb-5"> >
<label for="add-entry__location" class="block mb-2 text-sm font-medium text-gray-900">Location</label> <FontAwesomeIcon size="lg" icon={faSpotify} />
<input id="add-entry__location" type="text" name="location" placeholder="South of Almond Park" 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"> </span>
</div> <input
{:else if kind === "date"} type="text"
<div class="w-full mb-5"> id="add-entry__spotify"
<label for="add-entry__date" class="block mb-2 text-sm font-medium text-gray-900">Referenced date</label> name="spotify"
<input id="add-entry__date" type="date" name="date" 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"> class="block w-full min-w-0 flex-1 rounded-none rounded-e-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
{#if $errors.date != null} placeholder={kind === 'song'
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.date[0]}</span></p> ? 'https://open.spotify.com/track/...'
{/if} : 'https://open.spotify.com/album/...'}
</div> />
{/if} </div>
</div>
<div class="mb-5">
<label for="add-entry__yt" class="mb-2 block text-sm font-medium text-gray-900">
YouTube link
</label>
<div class="flex">
<span
class="rounded-e-0 inline-flex items-center rounded-s-md border border-e-0 border-gray-300 bg-gray-200 px-2.5 text-sm text-gray-900"
>
<FontAwesomeIcon size="lg" icon={faYoutube} />
</span>
<input
type="text"
id="add-entry__yt"
name="yt"
class="block w-full min-w-0 flex-1 rounded-none rounded-e-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
placeholder="https://www.youtube.com/watch..."
/>
</div>
</div>
<div class="mb-5">
<label for="add-entry__other" class="mb-2 block text-sm font-medium text-gray-900">
Link to other provider
</label>
<div class="flex">
<span
class="rounded-e-0 inline-flex items-center rounded-s-md border border-e-0 border-gray-300 bg-gray-200 px-2.5 text-sm text-gray-900"
>
<FontAwesomeIcon size="lg" icon={faLink} />
</span>
<input
type="text"
name="otherProvider"
id="add-entry__other"
class="block w-full min-w-0 flex-1 rounded-none rounded-e-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
placeholder="https://www.music.tld/play/..."
/>
</div>
</div>
{#if $errors.links != null}
<p class="mb-3.5 mt-2.5 text-sm text-red-600">
<span class="font-medium">{$errors.links[0]}</span>
</p>
{/if}
{:else if kind === 'environment'}
<div class="mb-5 w-full">
<label for="add-entry__location" class="mb-2 block text-sm font-medium text-gray-900">
Location
</label>
<input
id="add-entry__location"
type="text"
name="location"
placeholder="South of Almond Park"
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"
/>
</div>
{:else if kind === 'date'}
<div class="mb-5 w-full">
<label for="add-entry__date" class="mb-2 block text-sm font-medium text-gray-900">
Referenced date
</label>
<input
id="add-entry__date"
type="date"
name="date"
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.date != null}
<p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.date[0]}</span>
</p>
{/if}
</div>
{/if}
{#if kind != null && kind.length > 0} {#if kind != null && kind.length > 0}
<div class="mb-5"> <div class="mb-5">
<label for="add-entry__description" class="block mb-2 text-sm font-medium text-gray-900">Description</label> <label for="add-entry__description" class="mb-2 block text-sm font-medium text-gray-900">
<textarea name="description" id="add-entry__description" rows="7" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-violet-500 focus:border-violet-500" placeholder="Write your thoughts here..."></textarea> Description
</div> </label>
<textarea
name="description"
id="add-entry__description"
rows="7"
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"
placeholder="Write your thoughts here..."
></textarea>
</div>
<div class="mb-5"> <div class="mb-5">
<label for="add-entry__assets" class="block mb-2 text-sm font-medium text-gray-900">Linked assets (max 5MB)</label> <label for="add-entry__assets" class="mb-2 block text-sm font-medium text-gray-900">
<input name="asset" id="add-entry__assets" 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 hover:cursor-pointer file:bg-gray-200 file:border-gray-300 file:border-0 file:me-4 file:py-2.5 file:px-4 hover:file:bg-gray-300" type="file"> Linked assets (max 5MB)
</div> </label>
<input
name="asset"
id="add-entry__assets"
class="block w-full rounded-lg border border-gray-300 bg-gray-50 text-sm text-gray-900 file:me-4 file:border-0 file:border-gray-300 file:bg-gray-200 file:px-4 file:py-2.5 hover:cursor-pointer hover:file:bg-gray-300 focus:border-violet-500 focus:ring-violet-500"
type="file"
/>
</div>
<div class="mb-5"> <div class="mb-5">
<FeelingsChooser required={kind === "feeling"}/> <FeelingsChooser required={kind === 'feeling'} />
{#if $errors.feelings != null} {#if $errors.feelings != null}
<p class="text-sm text-red-600 mt-1.5"><span class="font-medium">{$errors.feelings[0]}</span></p> <p class="mt-1.5 text-sm text-red-600">
{/if} <span class="font-medium">{$errors.feelings[0]}</span>
</div> </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 entry</button> <button
{/if} 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"
</form> type="submit"
</div> >
</div> Add new entry
</button>
{/if}
</form>
</div>
</div>

View file

@ -1,4 +1,5 @@
import adapter from '@sveltejs/adapter-auto'; import adapterStatic from '@sveltejs/adapter-static';
import adapterAuto from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */ /** @type {import('@sveltejs/kit').Config} */
@ -8,10 +9,16 @@ const config = {
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
adapter: adapterStatic({
fallback: 'index.html'
})
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list. // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
// If your environment is not supported, or you settled on a specific environment, switch out the adapter. // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://kit.svelte.dev/docs/adapters for more information about adapters. // See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter() //
// To use adapter-auto, uncomment the line below:
// adapter: adapterAuto(),
} }
}; };

View file

@ -1,11 +1,8 @@
/** @type {import('tailwindcss').Config} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ['./src/**/*.{html,css,js,svelte,ts}'], content: ['./src/**/*.{html,css,js,svelte,ts}'],
theme: { theme: {
extend: {}, extend: {}
}, },
plugins: [ plugins: [require('@tailwindcss/forms')]
require("@tailwindcss/forms") };
],
}

View file

@ -559,6 +559,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@sveltejs/adapter-static@npm:^3.0.2":
version: 3.0.2
resolution: "@sveltejs/adapter-static@npm:3.0.2"
peerDependencies:
"@sveltejs/kit": ^2.0.0
checksum: 10c0/db3c287f0ed52b9c3c42e27cb54c18977627dd7596ac2f778b6a70500b6e07cc48f3fa305a39171d0279311cfe11ecf8afcd367abbb13e582ad10d96223721fe
languageName: node
linkType: hard
"@sveltejs/kit@npm:^2.0.0": "@sveltejs/kit@npm:^2.0.0":
version: 2.5.10 version: 2.5.10
resolution: "@sveltejs/kit@npm:2.5.10" resolution: "@sveltejs/kit@npm:2.5.10"
@ -1930,6 +1939,7 @@ __metadata:
"@fortawesome/free-solid-svg-icons": "npm:^6.5.2" "@fortawesome/free-solid-svg-icons": "npm:^6.5.2"
"@fortawesome/svelte-fontawesome": "npm:^0.2.2" "@fortawesome/svelte-fontawesome": "npm:^0.2.2"
"@sveltejs/adapter-auto": "npm:^3.0.0" "@sveltejs/adapter-auto": "npm:^3.0.0"
"@sveltejs/adapter-static": "npm:^3.0.2"
"@sveltejs/kit": "npm:^2.0.0" "@sveltejs/kit": "npm:^2.0.0"
"@sveltejs/vite-plugin-svelte": "npm:^3.0.0" "@sveltejs/vite-plugin-svelte": "npm:^3.0.0"
"@tailwindcss/forms": "npm:^0.5.7" "@tailwindcss/forms": "npm:^0.5.7"
@ -1945,6 +1955,7 @@ __metadata:
postcss: "npm:^8.4.38" postcss: "npm:^8.4.38"
prettier: "npm:^3.1.1" prettier: "npm:^3.1.1"
prettier-plugin-svelte: "npm:^3.1.2" prettier-plugin-svelte: "npm:^3.1.2"
prettier-plugin-tailwindcss: "npm:^0.6.5"
svelte: "npm:^4.2.7" svelte: "npm:^4.2.7"
svelte-check: "npm:^3.6.0" svelte-check: "npm:^3.6.0"
tailwindcss: "npm:^3.4.4" tailwindcss: "npm:^3.4.4"
@ -2855,6 +2866,61 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"prettier-plugin-tailwindcss@npm:^0.6.5":
version: 0.6.5
resolution: "prettier-plugin-tailwindcss@npm:0.6.5"
peerDependencies:
"@ianvs/prettier-plugin-sort-imports": "*"
"@prettier/plugin-pug": "*"
"@shopify/prettier-plugin-liquid": "*"
"@trivago/prettier-plugin-sort-imports": "*"
"@zackad/prettier-plugin-twig-melody": "*"
prettier: ^3.0
prettier-plugin-astro: "*"
prettier-plugin-css-order: "*"
prettier-plugin-import-sort: "*"
prettier-plugin-jsdoc: "*"
prettier-plugin-marko: "*"
prettier-plugin-organize-attributes: "*"
prettier-plugin-organize-imports: "*"
prettier-plugin-sort-imports: "*"
prettier-plugin-style-order: "*"
prettier-plugin-svelte: "*"
peerDependenciesMeta:
"@ianvs/prettier-plugin-sort-imports":
optional: true
"@prettier/plugin-pug":
optional: true
"@shopify/prettier-plugin-liquid":
optional: true
"@trivago/prettier-plugin-sort-imports":
optional: true
"@zackad/prettier-plugin-twig-melody":
optional: true
prettier-plugin-astro:
optional: true
prettier-plugin-css-order:
optional: true
prettier-plugin-import-sort:
optional: true
prettier-plugin-jsdoc:
optional: true
prettier-plugin-marko:
optional: true
prettier-plugin-organize-attributes:
optional: true
prettier-plugin-organize-imports:
optional: true
prettier-plugin-sort-imports:
optional: true
prettier-plugin-style-order:
optional: true
prettier-plugin-svelte:
optional: true
checksum: 10c0/30d62928592b48cab03c46ff63edd35d4a33c4e7c40e583f12bff7223eba8b6f780fd394965b0250160bcf39688f6fb602420374b2055bcbb6a69560b818ca4e
languageName: node
linkType: hard
"prettier@npm:^3.1.1": "prettier@npm:^3.1.1":
version: 3.3.2 version: 3.3.2
resolution: "prettier@npm:3.3.2" resolution: "prettier@npm:3.3.2"