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,121 +1,139 @@
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(
sendRequest('/auth/login', undefined, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', '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(
sendRequest('/auth/register', undefined, {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', '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() let form = new FormData();
form.append("file", file); form.append('file', file);
let res = await fetch(url, { let res = await fetch(url, {
method: "PUT", method: 'PUT',
body: form, body: form
}) });
let { asset_id } = await res.json(); let { asset_id } = await res.json();
return asset_id; 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,20 +1,20 @@
<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];
@ -22,33 +22,40 @@
} }
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
type="button"
on:click={() => (feelingsDropdownShown = !feelingsDropdownShown)}
class={`rounded-e-0 inline-flex items-center gap-1.5 border border-e-0 border-gray-300 bg-gray-200 px-2.5 text-sm text-gray-900 ${feelingsDropdownShown ? 'rounded-tl-lg' : 'rounded-s-lg'} hover:cursor-pointer hover:bg-gray-300`}
>
Feelings Feelings
<FontAwesomeIcon icon={faChevronDown}/> <FontAwesomeIcon icon={faChevronDown} />
</button> </button>
<div id="add-entry__feelings" class={`rounded-none ${feelingsDropdownShown ? "rounded-tr-lg" : "rounded-e-lg"} bg-gray-50 border text-gray-900 focus:ring-violet-500 focus:border-violet-500 block flex-1 min-w-0 w-full text-sm border-gray-300 ${slim ? "p-2" : "p-2.5"}`}> <div
id="add-entry__feelings"
class={`rounded-none ${feelingsDropdownShown ? 'rounded-tr-lg' : 'rounded-e-lg'} block w-full min-w-0 flex-1 border border-gray-300 bg-gray-50 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500 ${slim ? 'p-2' : 'p-2.5'}`}
>
{#if chosenFeelings.length > 0} {#if chosenFeelings.length > 0}
<div> <div>
<span class="mr-1">Chosen:</span> <span class="mr-1">Chosen:</span>
{#each chosenFeelings as feeling (feeling)} {#each chosenFeelings as feeling (feeling)}
<div class="inline"> <div class="inline">
<button type="button" on:click={() => removeFeeling(feeling)}> <button type="button" on:click={() => removeFeeling(feeling)}>
<FeelingPill feeling={feeling} slim={slim}> <FeelingPill {feeling} {slim}>
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faXmark}/></span> <span class="pr-1" slot="pre"><FontAwesomeIcon icon={faXmark} /></span>
</FeelingPill> </FeelingPill>
</button> </button>
<input type="checkbox" class="hidden" name={`feeling__${feeling}`} checked> <input type="checkbox" class="hidden" name={`feeling__${feeling}`} checked />
</div> </div>
{/each} {/each}
</div> </div>
@ -60,12 +67,15 @@
{/if} {/if}
</div> </div>
</div> </div>
<div class:hidden={!feelingsDropdownShown} class="bg-gray-50 border border-t-0 border-gray-300 py-3 px-1.5 rounded-b-lg"> <div
class:hidden={!feelingsDropdownShown}
class="rounded-b-lg border border-t-0 border-gray-300 bg-gray-50 px-1.5 py-3"
>
{#each feelingsToChoose as feeling (feeling)} {#each feelingsToChoose as feeling (feeling)}
<label class={`capitalize ${slim ? "p-0.5" : "p-1"}`}> <label class={`capitalize ${slim ? 'p-0.5' : 'p-1'}`}>
<button type="button" on:click={() => addFeeling(feeling)}> <button type="button" on:click={() => addFeeling(feeling)}>
<FeelingPill feeling={feeling} slim={slim}> <FeelingPill {feeling} {slim}>
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faPlus}/></span> <span class="pr-1" slot="pre"><FontAwesomeIcon icon={faPlus} /></span>
</FeelingPill> </FeelingPill>
</button> </button>
</label> </label>

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) {
localStorage.removeItem(CREDENTIALS_KEY);
} }
catch (e) { 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,12 +1,13 @@
<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>
@ -16,19 +17,23 @@
</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>
|
<div class="inline-block px-3"><a href="/auth/register">Join</a></div>
| |
{:else} {:else}
| <div class="px-3 inline-block"><a href="/dashboard">Dashboard</a></div> | <div class="inline-block px-3"><a href="/dashboard">Dashboard</a></div>
| <div class="px-3 inline-block"><a href="/auth/account">Account</a></div> |
| <div class="px-3 inline-block"><a href="mailto:sofi@sofiaritz.com">Support</a></div> <div class="inline-block px-3"><a href="/auth/account">Account</a></div>
|
<div class="inline-block px-3"><a href={ENV_VARIABLES.SUPPORT_PAGE}>Support</a></div>
| |
{/if} {/if}
</div> </div>
</nav> </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,9 +1,11 @@
<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;
@ -12,10 +14,10 @@
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);
@ -24,93 +26,149 @@
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">
Welcome back, <span class="font-bold">{$account?.name}</span>
.
</h1>
<div> <div>
<div class="flex justify-between mb-2"> <div class="mb-2 flex justify-between">
<h2 class="text-xl pb-2.5">Heirs</h2> <h2 class="pb-2.5 text-xl">Heirs</h2>
{#if $account?.heirs.length > 0} {#if $account?.heirs.length > 0}
<button on:click={() => heirWizard = !heirWizard} class="rounded-lg bg-violet-700 text-white px-2.5 py-1 text-center hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"> <button
on:click={() => (heirWizard = !heirWizard)}
class="rounded-lg bg-violet-700 px-2.5 py-1 text-center text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
>
+ Add a heir + Add a heir
</button> </button>
{/if} {/if}
</div> </div>
{#if !heirWizard && $account?.heirs.length === 0} {#if !heirWizard && $account?.heirs.length === 0}
<div class="flex flex-col"> <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"> <button
on:click={() => (heirWizard = true)}
class="flex h-60 flex-col items-center justify-center gap-3 rounded border border-gray-300 p-2 text-black"
>
<span class="text-4xl">+</span> <span class="text-4xl">+</span>
<h2 class="text-xl font-semibold">Add a heir</h2> <h2 class="text-xl font-semibold">Add a heir</h2>
</button> </button>
</div> </div>
{/if} {/if}
{#if heirWizard} {#if heirWizard}
<div class="border border-gray-200 rounded-lg shadow w-full flex flex-col p-3.5 mb-4"> <div class="mb-4 flex w-full flex-col rounded-lg border border-gray-200 p-3.5 shadow">
<form use:form> <form use:form>
<div class="mb-5"> <div class="mb-5">
<label for="heir__contact-method" class="block mb-2 text-sm font-medium text-gray-900">Contact method</label> <label
<select id="heir__contact-method" name="contactMethod" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5"> for="heir__contact-method"
class="mb-2 block text-sm font-medium text-gray-900"
>
Contact method
</label>
<select
id="heir__contact-method"
name="contactMethod"
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
>
<option value="" selected>Choose a contact method</option> <option value="" selected>Choose a contact method</option>
<option value="email">Email</option> <option value="email">Email</option>
</select> </select>
{#if $errors.contactMethod != null} {#if $errors.contactMethod != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.contactMethod[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.contactMethod[0]}</span>
</p>
{/if} {/if}
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label for="heir__name" class="block mb-2 text-sm font-medium text-gray-900">Heir name</label> <label for="heir__name" class="mb-2 block text-sm font-medium text-gray-900">
<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"> Heir name
</label>
<input
id="heir__name"
type="text"
name="name"
placeholder="Jane Doe"
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
/>
{#if $errors.name != null} {#if $errors.name != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.name[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.name[0]}</span>
</p>
{/if} {/if}
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label for="heir__contactDetails" class="block mb-2 text-sm font-medium text-gray-900">Contact details</label> <label
<input id="heir__contactDetails" type="text" name="contactDetails" placeholder="jane@identity.net" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5"> 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} {#if $errors.contactDetails != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.contactDetails[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.contactDetails[0]}</span>
</p>
{/if} {/if}
</div> </div>
<button class="mt-2 text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focust:outline-none focus:ring-violet-300 font-medium rounded-lg px-5 py-2.5 text-center" type="submit">Add new heir</button> <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> </form>
</div> </div>
{/if} {/if}
{#each $account?.heirs || [] as heir (heir.value)} {#each $account?.heirs || [] as heir (heir.value)}
<div class="border border-gray-200 rounded-lg shadow w-full flex flex-col p-3.5 mb-2.5"> <div class="mb-2.5 flex w-full flex-col rounded-lg border border-gray-200 p-3.5 shadow">
<div class="flex justify-between"> <div class="flex justify-between">
<span class="block text-sm font-medium text-gray-900">Contact method: <span class="capitalize">{heir.contactMethod}</span></span> <span class="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> 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>
<div> <div>
<span>{heir.name}</span> · <span>{heir.value}</span> <span>{heir.name}</span>
·
<span>{heir.value}</span>
</div> </div>
</div> </div>
{/each} {/each}

View file

@ -1,72 +1,107 @@
<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 &&
(typeof response['error'] !== 'string' ||
!['invalid credentials'].includes(response['error'])))
) {
submitError = 'Something failed. Try again later.';
} }
// @ts-ignore - FIXME: How to tell the checker that this is right // @ts-ignore - FIXME: How to tell the checker that this is right
else if ('error' in response) { else if ('error' in response) {
// @ts-ignore - response is not null and the type of its key 'error' is a string // @ts-ignore - response is not null and the type of its key 'error' is a string
submitError = 'Check your credentials and try again.' submitError = 'Check your credentials and try again.';
} else { } else {
credentials.set(response as Credentials) credentials.set(response as Credentials);
// FIXME: This is a badly done hack // FIXME: This is a badly done hack
setTimeout(() => window.location.pathname = '/dashboard', 200) setTimeout(() => (window.location.pathname = '/dashboard'), 200);
} }
}, },
validate: (values) => { validate: (values) => {
const errors = {} const errors = {};
if (values.email == null || values.email.length === 0) { if (values.email == null || values.email.length === 0) {
errors.email = 'Must not be empty' 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
</label>
<input
id="register__email"
type="text"
name="email"
placeholder="jane@identity.net"
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
/>
{#if $errors.email != null} {#if $errors.email != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.email[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.email[0]}</span>
</p>
{/if} {/if}
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label for="register__password" class="block mb-2 text-sm font-medium text-gray-900">Your password</label> <label for="register__password" class="mb-2 block text-sm font-medium text-gray-900">
<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"> 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} {#if $errors.password != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.password[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.password[0]}</span>
</p>
{/if} {/if}
</div> </div>
<button type="submit" class="text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focust:outline-none focus:ring-violet-300 font-medium rounded-lg w-full px-5 py-2.5 text-center">Log in</button> <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} {#if submitError != null && submitError.length > 0}
<p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p> <p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p>
{/if} {/if}
<div class="flex pt-3.5 w-full justify-between"> <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/register" class="text-center font-medium text-blue-600 hover:underline">
<a href="/auth/recovery" class="text-center font-medium text-blue-600 hover:underline">Forgotten password?</a> Create an account
</a>
<a href="/auth/recovery" class="text-center font-medium text-blue-600 hover:underline">
Forgotten password?
</a>
</div> </div>
</form> </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 &&
(typeof response['error'] !== 'string' || !['invalid data'].includes(response['error'])))
) {
submitError = 'Something failed. Try again later.';
} }
// @ts-ignore - FIXME: How to tell the checker that this is right // @ts-ignore - FIXME: How to tell the checker that this is right
else if ('error' in response) { else if ('error' in response) {
// @ts-ignore - response is not null and the type of its key 'error' is a string // @ts-ignore - response is not null and the type of its key 'error' is a string
submitError = 'Check your credentials and try again, this user may already exist.' submitError = 'Check your credentials and try again, this user may already exist.';
} else { } else {
credentials.set(response as Credentials) credentials.set(response as Credentials);
// FIXME: This is a badly done hack // FIXME: This is a badly done hack
setTimeout(() => window.location.pathname = '/dashboard', 200) setTimeout(() => (window.location.pathname = '/dashboard'), 200);
} }
}, },
validate: (values) => { validate: (values) => {
const errors = {} const errors = {};
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.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
</label>
<input
id="register__name"
type="text"
name="name"
placeholder="Jane Doe"
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
/>
{#if $errors.name != null} {#if $errors.name != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.name[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.name[0]}</span>
</p>
{/if} {/if}
</div> </div>
<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
</label>
<input
id="register__email"
type="text"
name="email"
placeholder="jane@identity.net"
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
/>
{#if $errors.email != null} {#if $errors.email != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.email[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.email[0]}</span>
</p>
{/if} {/if}
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label for="register__password" class="block mb-2 text-sm font-medium text-gray-900">Your password</label> <label for="register__password" class="mb-2 block text-sm font-medium text-gray-900">
<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"> 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} {#if $errors.password != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.password[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.password[0]}</span>
</p>
{/if} {/if}
</div> </div>
<button type="submit" class="text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focust:outline-none focus:ring-violet-300 font-medium rounded-lg w-full px-5 py-2.5 text-center">Create user</button> <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} {#if submitError != null && submitError.length > 0}
<p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p> <p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p>
{/if} {/if}
<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> <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> </form>
</div> </div>
</div> </div>

View file

@ -1,16 +1,24 @@
<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({
onLoadingStatusChanged,
onEndReached
}: {
onLoadingStatusChanged: (status: boolean) => any;
onEndReached: () => any;
}) {
let loadingPage = false; let loadingPage = false;
let rechedEnd = false; let rechedEnd = false;
let currentOffset = 10; let currentOffset = 10;
@ -38,45 +46,48 @@
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);
@ -85,64 +96,112 @@
{#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"
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> </svg>
</div> </div>
</div> </div>
{:then entries} {:then entries}
<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">
{#if entries.length === 0} {#if entries.length === 0}
<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"> <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> <span class="text-4xl">+</span>
<h2 class="text-xl font-semibold">Add an entry</h2> <h2 class="text-xl font-semibold">Add an entry</h2>
</a> </a>
{:else} {:else}
<h1 class="text-2xl pb-3.5">Welcome back, <span class="font-bold">{$account?.name}</span>.</h1> <h1 class="pb-3.5 text-2xl">
Welcome back, <span class="font-bold">{$account?.name}</span>
.
</h1>
<div class="flex gap-2"> <div class="flex gap-2">
{#await overview} {#await overview}
<span>Loading...</span> <span>Loading...</span>
{:then overview} {:then overview}
<Overview latest={overview[0].value.filter(v => !["feeling"].includes(v.base.kind))} past={overview[1].value}/> <Overview
latest={overview[0].value.filter((v) => !['feeling'].includes(v.base.kind))}
past={overview[1].value}
/>
{/await} {/await}
</div> </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"
>
+ Add an entry
</a>
<button on:click={() => (showFilterSelector = !showFilterSelector)}>
<FontAwesomeIcon icon={faFilter} />
<span class="ml-1.5">Filter entries</span> <span class="ml-1.5">Filter entries</span>
</button> </button>
</div> </div>
{#if showFilterSelector} {#if showFilterSelector}
<FilterSelector on:updatedChosenFeelings={(e) => chosenFilterFeelings = e.detail} on:updatedFilter={(e) => filters = e.detail} chosenFeelings={chosenFilterFeelings} filters={filters}/> <FilterSelector
on:updatedChosenFeelings={(e) => (chosenFilterFeelings = e.detail)}
on:updatedFilter={(e) => (filters = e.detail)}
chosenFeelings={chosenFilterFeelings}
{filters}
/>
{/if} {/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
on:updatedFilterStatus={(e) => (filterStatus = e.detail)}
on:deleted={() => refreshEntries()}
{entries}
{filters}
/>
</div> </div>
{#if loadingPage && !reachedEnd} {#if loadingPage && !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">Loading entries...</span> <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"> <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"
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> </svg>
</div> </div>
</div> </div>
{/if} {/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>

View file

@ -1,40 +1,40 @@
<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!);
@ -46,34 +46,34 @@
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!);
@ -81,46 +81,55 @@
} }
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} id={entry.id}
kind={entry.base.kind} kind={entry.base.kind}
creationDate={new Date(entry.creationDate)} creationDate={new Date(entry.creationDate)}
title={entry.base.kind === "date" ? new Date(entry.base.referencedDate).toLocaleDateString() : entry.title} title={entry.base.kind === 'date'
? new Date(entry.base.referencedDate).toLocaleDateString()
: entry.title}
isExtended={extended.includes(entry.id)} isExtended={extended.includes(entry.id)}
> >
<div slot="contracted"> <div slot="contracted">
{#if entry.base.kind === "song" || entry.base.kind === "album"} {#if entry.base.kind === 'song' || entry.base.kind === 'album'}
<ExternalLink href={entry.base.link[0]}>{entry.base.artist} &dash; {entry.base.title}</ExternalLink> <ExternalLink href={entry.base.link[0]}>
{entry.base.artist} &dash; {entry.base.title}
</ExternalLink>
{/if} {/if}
{#if entry.base.kind === "feeling"} {#if entry.base.kind === 'feeling'}
<div class="flex gap-1"> <div class="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
feeling={feeling.identifier}
bgColor={feeling.backgroundColor}
textColor={feeling.textColor}
/>
{/if} {/if}
{/each} {/each}
</div> </div>
@ -128,27 +137,33 @@
</div> </div>
<div slot="extended"> <div slot="extended">
<div class="flex gap-1 mb-2"> <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
feeling={feeling.identifier}
bgColor={feeling.backgroundColor}
textColor={feeling.textColor}
/>
{/if} {/if}
{/each} {/each}
</div> </div>
{#if entry.base.kind === "song" || entry.base.kind === "album"} {#if entry.base.kind === 'song' || entry.base.kind === 'album'}
<ExternalLink href={entry.base.link[0]}>{entry.base.artist} &dash; {entry.base.title}</ExternalLink> <ExternalLink href={entry.base.link[0]}>
{entry.base.artist} &dash; {entry.base.title}
</ExternalLink>
{/if} {/if}
{#if entry.description != null} {#if entry.description != null}
<EntryDescription>{entry.description}</EntryDescription> <EntryDescription>{entry.description}</EntryDescription>
{/if} {/if}
<div class="flex gap-1 mt-2"> <div class="mt-2 flex gap-1">
{#each entry.assets as asset} {#each entry.assets as asset}
<AssetPreview asset_id={asset}/> <AssetPreview asset_id={asset} />
{/each} {/each}
</div> </div>
</div> </div>

View file

@ -1,25 +1,25 @@
<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>

View file

@ -1,30 +1,42 @@
<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
class="flex items-center gap-2 rounded bg-violet-600 px-2.5 py-1 font-bold text-white"
target="_blank"
{href}
>
{#if kind == null} {#if kind == null}
<FontAwesomeIcon icon={faArrowUpRightFromSquare}/> <FontAwesomeIcon icon={faArrowUpRightFromSquare} />
{:else if kind === "image"} {:else if kind === 'image'}
<FontAwesomeIcon icon={faImage}/> <FontAwesomeIcon icon={faImage} />
{:else if kind === "audio"} {:else if kind === 'audio'}
<FontAwesomeIcon icon={faFileAudio}/> <FontAwesomeIcon icon={faFileAudio} />
{:else if kind === "video"} {:else if kind === 'video'}
<FontAwesomeIcon icon={faFileVideo}/> <FontAwesomeIcon icon={faFileVideo} />
{:else} {:else}
<FontAwesomeIcon icon={faArrowUpRightFromSquare}/> <FontAwesomeIcon icon={faArrowUpRightFromSquare} />
{/if} {/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>

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={() => {
prevExtended = isExtended;
isExtended = !isExtended;
}}
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<EntryKind kind={kind}/> <EntryKind {kind} />
{#if title != null && isExtended} {#if title != null && isExtended}
<span>Created at: <time datetime={creationDate.toISOString()}>{creationDate.toLocaleDateString()}</time></span> <span>
Created at: <time datetime={creationDate.toISOString()}>
{creationDate.toLocaleDateString()}
</time>
</span>
{:else if title != null} {:else if title != null}
<h2 class="text-xl text-left font-semibold">{title}</h2> <h2 class="text-left text-xl font-semibold">{title}</h2>
{:else if isExtended} {:else if isExtended}
<span>Created at: <time datetime={creationDate.toISOString()}>{creationDate.toLocaleDateString()}</time></span> <span>
Created at: <time datetime={creationDate.toISOString()}>
{creationDate.toLocaleDateString()}
</time>
</span>
{/if} {/if}
</div> </div>
{#if isExtended} {#if isExtended}
<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> <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} {/if}
</div> </div>
{#if title != null && isExtended} {#if title != null && isExtended}
<h2 class="text-xl text-left font-semibold mt-2">{title}</h2> <h2 class="mt-2 text-left text-xl font-semibold">{title}</h2>
{/if} {/if}
</button> </button>
<slot/> <slot />
{#if !isExtended} {#if !isExtended}
<slot name="contracted"/> <slot name="contracted" />
{/if} {/if}
{#if isExtended} {#if isExtended}
<slot name="extended"/> <slot name="extended" />
{/if} {/if}
</div> </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"
{href}
>
<FontAwesomeIcon icon={faArrowUpRightFromSquare} />
<slot />
</a> </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'
| '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; 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,30 +1,30 @@
<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() let dispatch = createEventDispatcher();
export let chosenFeelings: KnownFeeling[] = [] export let chosenFeelings: KnownFeeling[] = [];
let touched = { let touched = {
fromDate: false, fromDate: false,
toDate: false, toDate: false
} };
function upstreamChanges() { function upstreamChanges() {
dispatch('updatedFilter', filters) dispatch('updatedFilter', filters);
} }
function handleKind(kind: string | EntryKind) { function handleKind(kind: string | EntryKind) {
@ -36,15 +36,15 @@
upstreamChanges(); upstreamChanges();
} }
function handleDate(date: string | null, kind: "fromDate" | "toDate") { function handleDate(date: string | null, kind: 'fromDate' | 'toDate') {
touched[kind] = true; touched[kind] = true;
if (date == null || date.length === 0) { if (date == null || date.length === 0) {
filters[kind] = null filters[kind] = null;
} else { } else {
filters[kind] = new Date(date) filters[kind] = new Date(date);
} }
upstreamChanges() upstreamChanges();
} }
function handleSearchQuery(query: string) { function handleSearchQuery(query: string) {
@ -53,38 +53,65 @@
} else { } else {
filters.searchQuery = query.trim(); filters.searchQuery = query.trim();
} }
upstreamChanges() upstreamChanges();
} }
function handleFeelings(feelings: KnownFeeling[]) { function handleFeelings(feelings: KnownFeeling[]) {
chosenFeelings = feelings; chosenFeelings = feelings;
dispatch('updatedChosenFeelings', chosenFeelings) dispatch('updatedChosenFeelings', chosenFeelings);
if (feelings.length === 0) { if (feelings.length === 0) {
filters.feelings = null; filters.feelings = null;
} else { } else {
filters.feelings = { filters.feelings = {
exclusive: false, exclusive: false,
feelings, feelings
}; };
} }
upstreamChanges(); 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
</label>
<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="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"
/>
</div> </div>
<div class="w-full"> <div class="w-full">
<label for="filter__to-date" class="block mb-0.5 text-sm font-medium text-gray-900">To date</label> <label for="filter__to-date" class="mb-0.5 block text-sm font-medium text-gray-900">
<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"> To date
</label>
<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="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"
/>
</div> </div>
<div class="w-full"> <div class="w-full">
<label for="filter__kind" class="block mb-0.5 text-sm font-medium text-gray-900">Entry kind</label> <label for="filter__kind" class="mb-0.5 block text-sm font-medium text-gray-900">
<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"> 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="" selected>Choose an entry kind</option>
<option value="song">Song</option> <option value="song">Song</option>
<option value="album">Album</option> <option value="album">Album</option>
@ -97,9 +124,20 @@
</div> </div>
</div> </div>
<div> <div>
<FeelingsChooser chosenFeelings={chosenFeelings} on:choiceUpdated={(e) => handleFeelings(e.detail)} displayText={false} slim={true} /> <FeelingsChooser
{chosenFeelings}
on:choiceUpdated={(e) => handleFeelings(e.detail)}
displayText={false}
slim={true}
/>
</div> </div>
<div> <div>
<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"> <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>
</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}>
{new Date(entry.creationDate).toLocaleDateString()}
</time>
{/if} {/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)}
New {entry.base.kind}:
<a href={`#entry__${entry.id}`} class="font-bold">
{entry.base.artist} &dash; {entry.base.title}
</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} {:else}
<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 text-violet-600 hover:underline">
New {entry.base.kind}
</a>
{/if} {/if}
</p> </p>

View file

@ -1,114 +1,137 @@
<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; let base;
if (values.kind === "song" || values.kind === "album") { if (values.kind === 'song' || values.kind === 'album') {
base = { base = {
kind: values.kind, kind: values.kind,
artist: values.artist, artist: values.artist,
title: values.musicTitle, title: values.musicTitle,
link: [values.spotify, values.yt, values.otherProvider].filter(v => v != null && v.length > 0), link: [values.spotify, values.yt, values.otherProvider].filter(
(v) => v != null && v.length > 0
),
// FIXME: infer univeersal ids // FIXME: infer univeersal ids
id: [], id: []
} };
} else if (values.kind === "environment") { } else if (values.kind === 'environment') {
base = { base = {
kind: values.kind, kind: values.kind,
location: (values.location != null && values.location.length > 0) ? values.location : undefined, location:
} values.location != null && values.location.length > 0 ? values.location : undefined
} else if (values.kind === "date") { };
} else if (values.kind === 'date') {
base = { base = {
kind: values.kind, kind: values.kind,
referencedDate: values.date, referencedDate: values.date
} };
} else { } else {
base = { base = {
kind: values.kind, kind: values.kind
} };
} }
let asset_id let asset_id;
if (values.asset != null && typeof values.asset === "object") { if (values.asset != null && typeof values.asset === 'object') {
asset_id = await uploadAsset($session_key!, values.asset) asset_id = await uploadAsset($session_key!, values.asset);
} }
let entry: IdlessEntry = { let entry: IdlessEntry = {
base, base,
creationDate: new Date().toISOString(), creationDate: new Date().toISOString(),
assets: [asset_id].filter(v => v != null) as string[], assets: [asset_id].filter((v) => v != null) as string[],
feelings, feelings,
title: TITLED_ENTRIES.includes(values.kind) ? values.title : undefined, title: TITLED_ENTRIES.includes(values.kind) ? values.title : undefined,
description: values.description, description: values.description
} };
await addEntry($credentials!, entry) await addEntry($credentials!, entry);
window.location.pathname = '/dashboard' window.location.pathname = '/dashboard';
}, },
validate: (values) => { validate: (values) => {
let errors = {} let errors = {};
if (values.kind == null || values.kind.length === 0) { if (values.kind == null || values.kind.length === 0) {
errors['kind'] = 'Must choose an entry kind' errors['kind'] = 'Must choose an entry kind';
return errors return errors;
} }
if (values.kind === "song" || values.kind === "album") { if (values.kind === 'song' || values.kind === 'album') {
if (values.artist == null || values.artist.length === 0) { if (values.artist == null || values.artist.length === 0) {
errors['artist'] = "Must not be empty"; errors['artist'] = 'Must not be empty';
} }
if (values.musicTitle == null || values.musicTitle.length === 0) { if (values.musicTitle == null || values.musicTitle.length === 0) {
errors["musicTitle"] = "Must not be empty"; errors['musicTitle'] = 'Must not be empty';
} }
// FIXME: When asset support is added, another precondition is that no asset is uploaded // 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) { if (
errors["links"] = "You must add at least one link or upload an audio asset"; 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") { } else if (values.kind === 'date') {
if (values.date == null || values.date.length === 0) { if (values.date == null || values.date.length === 0) {
errors['date'] = "Must choose a date"; errors['date'] = 'Must choose a date';
} }
} else if (values.kind === "feeling") { } else if (values.kind === 'feeling') {
if (Object.keys(values).filter(v => v.startsWith("feeling__")).length === 0) { if (Object.keys(values).filter((v) => v.startsWith('feeling__')).length === 0) {
errors['feelings'] = "Must choose at least one feeling"; errors['feelings'] = 'Must choose at least one feeling';
} }
} else { } else {
if (values.title == null || values.title.length === 0) { if (values.title == null || values.title.length === 0) {
errors["title"] = "Must not be empty"; errors['title'] = '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-[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
</label>
<select
bind:value={kind}
id="add-entry__kind"
name="kind"
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
>
<option value="" selected>Choose an entry kind</option> <option value="" selected>Choose an entry kind</option>
<option value="song">Song</option> <option value="song">Song</option>
<option value="album">Album</option> <option value="album">Album</option>
@ -119,100 +142,209 @@
<option value="date">Date</option> <option value="date">Date</option>
</select> </select>
{#if $errors.kind != null} {#if $errors.kind != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.kind[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.kind[0]}</span>
</p>
{/if} {/if}
</div> </div>
{#if TITLED_ENTRIES.includes(kind)} {#if TITLED_ENTRIES.includes(kind)}
<div class="mb-5"> <div class="mb-5">
<label for="add-entry__title" class="block mb-2 text-sm font-medium text-gray-900">Title</label> <label for="add-entry__title" class="mb-2 block text-sm font-medium text-gray-900">
<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"> 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} {#if $errors.title != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.title[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.title[0]}</span>
</p>
{/if} {/if}
</div> </div>
{/if} {/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
</label>
<input
id="add-entry__artist"
type="text"
name="artist"
placeholder="Claude Debussy"
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.artist != null} {#if $errors.artist != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.artist[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.artist[0]}</span>
</p>
{/if} {/if}
</div> </div>
<div class="w-full"> <div class="w-full">
<label for="add-entry__music-title" class="block mb-2 text-sm font-medium text-gray-900"><span class="capitalize">{kind}</span> title</label> <label
<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"> for="add-entry__music-title"
class="mb-2 block text-sm font-medium text-gray-900"
>
<span class="capitalize">{kind}</span>
title
</label>
<input
id="add-entry__music-title"
type="text"
name="musicTitle"
placeholder="Clair de Lune"
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} {#if $errors.musicTitle != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.musicTitle[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.musicTitle[0]}</span>
</p>
{/if} {/if}
</div> </div>
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label for="add-entry__spotify" class="block mb-2 text-sm font-medium text-gray-900">Spotify link</label> <label for="add-entry__spotify" class="mb-2 block text-sm font-medium text-gray-900">
Spotify link
</label>
<div class="flex"> <div class="flex">
<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"> <span
<FontAwesomeIcon size="lg" icon={faSpotify}/> 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={faSpotify} />
</span> </span>
<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/..."}> <input
type="text"
id="add-entry__spotify"
name="spotify"
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={kind === 'song'
? 'https://open.spotify.com/track/...'
: 'https://open.spotify.com/album/...'}
/>
</div> </div>
</div> </div>
<div class="mb-5"> <div class="mb-5">
<label for="add-entry__yt" class="block mb-2 text-sm font-medium text-gray-900">YouTube link</label> <label for="add-entry__yt" class="mb-2 block text-sm font-medium text-gray-900">
YouTube link
</label>
<div class="flex"> <div class="flex">
<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"> <span
<FontAwesomeIcon size="lg" icon={faYoutube}/> 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> </span>
<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..."> <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> </div>
<div class="mb-5"> <div class="mb-5">
<label for="add-entry__other" class="block mb-2 text-sm font-medium text-gray-900">Link to other provider</label> <label for="add-entry__other" class="mb-2 block text-sm font-medium text-gray-900">
Link to other provider
</label>
<div class="flex"> <div class="flex">
<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"> <span
<FontAwesomeIcon size="lg" icon={faLink}/> 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> </span>
<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/..."> <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>
</div> </div>
{#if $errors.links != null} {#if $errors.links != null}
<p class="mt-2.5 mb-3.5 text-sm text-red-600"><span class="font-medium">{$errors.links[0]}</span></p> <p class="mb-3.5 mt-2.5 text-sm text-red-600">
<span class="font-medium">{$errors.links[0]}</span>
</p>
{/if} {/if}
{:else if kind === "environment"} {:else if kind === 'environment'}
<div class="w-full mb-5"> <div class="mb-5 w-full">
<label for="add-entry__location" class="block mb-2 text-sm font-medium text-gray-900">Location</label> <label for="add-entry__location" class="mb-2 block text-sm font-medium text-gray-900">
<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"> 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> </div>
{:else if kind === "date"} {:else if kind === 'date'}
<div class="w-full mb-5"> <div class="mb-5 w-full">
<label for="add-entry__date" class="block mb-2 text-sm font-medium text-gray-900">Referenced date</label> <label for="add-entry__date" class="mb-2 block text-sm font-medium text-gray-900">
<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"> 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} {#if $errors.date != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.date[0]}</span></p> <p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.date[0]}</span>
</p>
{/if} {/if}
</div> </div>
{/if} {/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
</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>
<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)
</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>
<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">
<span class="font-medium">{$errors.feelings[0]}</span>
</p>
{/if} {/if}
</div> </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
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 entry
</button>
{/if} {/if}
</form> </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

@ -2,10 +2,7 @@
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"