cleanup and docs and landing

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

View file

@ -4,16 +4,32 @@ Identity is an open-source application that helps you save your most relevant me
## Rationale
Identity is a project that initially started as an app whose purpose was to store music you like
(or liked) for future use in treatment for diseases like dementia. Over time, the idea evolved
and is now general-purpose.
Identity is a project that initially started as an app whose purpose was to store music you like (or used to like) for future use in treatment
for conditions such as dementia. Over time, the idea evolved and is now a general-purpose memory-saving app.
## Projects
* `identity-web`. The web app that interacts with the Identiy API.
* `identity-api`. The Identity API, also takes care of storing data.
* `identity-format`. The specification for the Identity file format.
* `identity-api`. The Identity API, takes care of storing user data.
* `asset-api`. The Asset API, takes care of storing user-generated assets.
## Installation and building
The Identity project is composed by a web-app and two servers. In the future, Docker containers may be built to ease the installation of this project.
### Building and running
#### Building `identity-web`
1. Copy and update the `env.example` file: `cp .env.example .env`
2. Run `yarn` to install the dependencies.
* You may need to [enable Corepack](https://nodejs.org/api/corepack.html).
3. Run `yarn preview` to check that everything works properly.
4. Modify the `svelte.config.js` file to deploy to your desired environment.
5. Run `yarn build` to generate the SPA build.
* The build will be placed at the `build/` folder.
## Citations
1. Van de Winckel, A., Feys, H., De Weerdt, W., & Dom, R. (2004). Cognitive and behavioural effects of music-based exercises in patients with dementia. Clinical Rehabilitation, 18(3), 253-260. https://doi.org/10.1191/0269215504cr750oa
2. The dementia guide: Living well after your diagnosis. (2021, April 16). Alzheimers Society. https://www.alzheimers.org.uk/get-support/publications-factsheets/the-dementia-guide

View file

@ -0,0 +1,3 @@
VITE_IDENTITY_API_ENDPOINT="http://localhost:3000/"
VITE_ASSET_API_ENDPOINT="http://localhost:3001/"
VITE_SUPPORT_PAGE="mailto:support@example.com"

View file

@ -3,6 +3,7 @@
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"htmlWhitespaceSensitivity": "ignore",
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
}

View file

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

View file

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

View file

@ -1,3 +1,19 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.3em;
vertical-align: super;
}
sub {
bottom: -0.25em;
vertical-align: sub;
}

View file

@ -1,122 +1,140 @@
import type { Entry, IdlessEntry } from "./entry"
import type { Entry, IdlessEntry } from './entry';
import { ENV_VARIABLES } from './variables';
const ENDPOINT = 'http://localhost:3000/'
const ASSET_API_ENDPOINT = 'http://localhost:3001/'
const ENDPOINT = ENV_VARIABLES.IDENTITY_API_ENDPOINT;
const ASSET_API_ENDPOINT = ENV_VARIABLES.ASSET_API_ENDPOINT;
export type Credentials = {
token: string,
}
token: string;
};
export type AccountHeir = {
contactMethod: "email",
name: string,
value: string,
}
contactMethod: 'email';
name: string;
value: string;
};
export type Account = {
uid: string,
name: string,
heirs: AccountHeir[],
}
uid: string;
name: string;
heirs: AccountHeir[];
};
function sendRequest(path: string, credentials?: Credentials, request: RequestInit = {}, params: string = "") {
if (typeof request !== "string" && credentials != null) {
request.headers = { 'Authorization': `Bearer ${credentials.token}`, ...request.headers }
}
function sendRequest(
path: string,
credentials?: Credentials,
request: RequestInit = {},
params: string = ''
) {
if (typeof request !== 'string' && credentials != null) {
request.headers = { Authorization: `Bearer ${credentials.token}`, ...request.headers };
}
let url = new URL(ENDPOINT);
url.pathname = path;
url.search = params
let url = new URL(ENDPOINT);
url.pathname = path;
url.search = params;
return fetch(url, request)
return fetch(url, request);
}
/// **Safety:** The caller must enforce that the given request in progress must return the type `R`
async function asJson<R>(request: Promise<Response>): Promise<R> {
let req = await request;
return (await req.json() as R)
let req = await request;
return (await req.json()) as R;
}
export function login(credentials: {
email: string,
password: string,
}): Promise<{ token: string, } | { error: string, }> {
return asJson(sendRequest('/auth/login', undefined, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
}))
email: string;
password: string;
}): Promise<{ token: string } | { error: string }> {
return asJson(
sendRequest('/auth/login', undefined, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
})
);
}
export function register(credentials: {
name: string,
email: string,
password: string,
}): Promise<{ token: string, } | { error: string, }> {
return asJson(sendRequest('/auth/register', undefined, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
}))
name: string;
email: string;
password: string;
}): Promise<{ token: string } | { error: string }> {
return asJson(
sendRequest('/auth/register', undefined, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(credentials)
})
);
}
export function accountData(credentials: Credentials): Promise<Account | { error: string }> {
return asJson(sendRequest('/auth/account', credentials))
return asJson(sendRequest('/auth/account', credentials));
}
export function genSessionKey(credentials: Credentials): Promise<{ session_key: string } | { error: string }> {
return asJson(sendRequest('/auth/genkey', credentials))
export function genSessionKey(
credentials: Credentials
): Promise<{ session_key: string } | { error: string }> {
return asJson(sendRequest('/auth/genkey', credentials));
}
export async function assetEndpoint(): Promise<string> {
let res = await sendRequest("/asset/endpoint")
return res.text()
let res = await sendRequest('/asset/endpoint');
return res.text();
}
export async function entryPage(credentials: Credentials, offset: number, limit: number): Promise<Entry[]> {
return asJson(sendRequest('/entry/list', credentials, undefined, `?offset=${offset}&limit=${limit}`))
export async function entryPage(
credentials: Credentials,
offset: number,
limit: number
): Promise<Entry[]> {
return asJson(
sendRequest('/entry/list', credentials, undefined, `?offset=${offset}&limit=${limit}`)
);
}
export async function addEntry(credentials: Credentials, entry: IdlessEntry): Promise<void> {
await sendRequest('/entry', credentials, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({entry}),
})
await sendRequest('/entry', credentials, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ entry })
});
}
export async function deleteEntry(credentials: Credentials, entry_id: string): Promise<void> {
await sendRequest('/entry', credentials, { method: 'DELETE' }, `?entry_id=${entry_id}`)
await sendRequest('/entry', credentials, { method: 'DELETE' }, `?entry_id=${entry_id}`);
}
export async function updateHeirs(credentials: Credentials, heirs: AccountHeir[]): Promise<void> {
await sendRequest('/auth/heirs', credentials, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(heirs),
});
await sendRequest('/auth/heirs', credentials, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(heirs)
});
}
export async function uploadAsset(session_key: string, file: File): Promise<string> {
let url = new URL('/asset', ASSET_API_ENDPOINT);
url.search = `?session_key=${session_key}`
let url = new URL('/asset', ASSET_API_ENDPOINT);
url.search = `?session_key=${session_key}`;
let form = new FormData()
form.append("file", file);
let form = new FormData();
form.append('file', file);
let res = await fetch(url, {
method: "PUT",
body: form,
})
let res = await fetch(url, {
method: 'PUT',
body: form
});
let { asset_id } = await res.json();
return asset_id;
let { asset_id } = await res.json();
return asset_id;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

View file

@ -1,74 +1,84 @@
<script lang="ts">
import { faChevronDown, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome";
import FeelingPill from "../../routes/dashboard/utils/FeelingPill.svelte";
import { FEELINGS, type KnownFeeling } from "$lib/entry";
import { createEventDispatcher } from "svelte";
import { faChevronDown, faPlus, faXmark } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
import FeelingPill from '../../routes/dashboard/utils/FeelingPill.svelte';
import { FEELINGS, type KnownFeeling } from '$lib/entry';
import { createEventDispatcher } from 'svelte';
export let required = false
export let displayText = true
export let slim = false
export let required = false;
export let displayText = true;
export let slim = false;
let feelingsDropdownShown = false
export let chosenFeelings: KnownFeeling[] = []
let feelingsDropdownShown = false;
export let chosenFeelings: KnownFeeling[] = [];
$: feelingsToChoose = FEELINGS.filter(v => !chosenFeelings.includes(v))
$: feelingsToChoose = FEELINGS.filter((v) => !chosenFeelings.includes(v));
let dispatch = createEventDispatcher()
let dispatch = createEventDispatcher();
function addFeeling(feeling: KnownFeeling) {
chosenFeelings = [feeling, ...chosenFeelings];
dispatch('choiceUpdated', chosenFeelings);
}
function addFeeling(feeling: KnownFeeling) {
chosenFeelings = [feeling, ...chosenFeelings];
dispatch('choiceUpdated', chosenFeelings);
}
function removeFeeling(feeling: KnownFeeling) {
chosenFeelings = chosenFeelings.filter(v => v !== feeling)
dispatch('choiceUpdated', chosenFeelings);
}
function removeFeeling(feeling: KnownFeeling) {
chosenFeelings = chosenFeelings.filter((v) => v !== feeling);
dispatch('choiceUpdated', chosenFeelings);
}
</script>
<div class="flex flex-col">
{#if displayText}
<span class="block mb-2 text-sm font-medium text-gray-900">Feelings</span>
{/if}
{#if displayText}
<span class="mb-2 block text-sm font-medium text-gray-900">Feelings</span>
{/if}
<div class="flex">
<button type="button" on:click={() => feelingsDropdownShown = !feelingsDropdownShown} class={`inline-flex gap-1.5 items-center px-2.5 text-sm text-gray-900 bg-gray-200 border rounded-e-0 border-gray-300 border-e-0 ${feelingsDropdownShown ? "rounded-tl-lg" : "rounded-s-lg"} hover:cursor-pointer hover:bg-gray-300`}>
Feelings
<FontAwesomeIcon icon={faChevronDown}/>
</button>
<div id="add-entry__feelings" class={`rounded-none ${feelingsDropdownShown ? "rounded-tr-lg" : "rounded-e-lg"} bg-gray-50 border text-gray-900 focus:ring-violet-500 focus:border-violet-500 block flex-1 min-w-0 w-full text-sm border-gray-300 ${slim ? "p-2" : "p-2.5"}`}>
{#if chosenFeelings.length > 0}
<div>
<span class="mr-1">Chosen:</span>
{#each chosenFeelings as feeling (feeling)}
<div class="inline">
<button type="button" on:click={() => removeFeeling(feeling)}>
<FeelingPill feeling={feeling} slim={slim}>
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faXmark}/></span>
</FeelingPill>
</button>
<input type="checkbox" class="hidden" name={`feeling__${feeling}`} checked>
</div>
{/each}
</div>
{:else}
<span>No feelings chosen.</span>
{#if required}
<span>You need to choose at least one feeling.</span>
{/if}
{/if}
</div>
</div>
<div class:hidden={!feelingsDropdownShown} class="bg-gray-50 border border-t-0 border-gray-300 py-3 px-1.5 rounded-b-lg">
{#each feelingsToChoose as feeling (feeling)}
<label class={`capitalize ${slim ? "p-0.5" : "p-1"}`}>
<button type="button" on:click={() => addFeeling(feeling)}>
<FeelingPill feeling={feeling} slim={slim}>
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faPlus}/></span>
</FeelingPill>
</button>
</label>
{/each}
</div>
<div class="flex">
<button
type="button"
on:click={() => (feelingsDropdownShown = !feelingsDropdownShown)}
class={`rounded-e-0 inline-flex items-center gap-1.5 border border-e-0 border-gray-300 bg-gray-200 px-2.5 text-sm text-gray-900 ${feelingsDropdownShown ? 'rounded-tl-lg' : 'rounded-s-lg'} hover:cursor-pointer hover:bg-gray-300`}
>
Feelings
<FontAwesomeIcon icon={faChevronDown} />
</button>
<div
id="add-entry__feelings"
class={`rounded-none ${feelingsDropdownShown ? 'rounded-tr-lg' : 'rounded-e-lg'} block w-full min-w-0 flex-1 border border-gray-300 bg-gray-50 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500 ${slim ? 'p-2' : 'p-2.5'}`}
>
{#if chosenFeelings.length > 0}
<div>
<span class="mr-1">Chosen:</span>
{#each chosenFeelings as feeling (feeling)}
<div class="inline">
<button type="button" on:click={() => removeFeeling(feeling)}>
<FeelingPill {feeling} {slim}>
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faXmark} /></span>
</FeelingPill>
</button>
<input type="checkbox" class="hidden" name={`feeling__${feeling}`} checked />
</div>
{/each}
</div>
{:else}
<span>No feelings chosen.</span>
{#if required}
<span>You need to choose at least one feeling.</span>
{/if}
{/if}
</div>
</div>
<div
class:hidden={!feelingsDropdownShown}
class="rounded-b-lg border border-t-0 border-gray-300 bg-gray-50 px-1.5 py-3"
>
{#each feelingsToChoose as feeling (feeling)}
<label class={`capitalize ${slim ? 'p-0.5' : 'p-1'}`}>
<button type="button" on:click={() => addFeeling(feeling)}>
<FeelingPill {feeling} {slim}>
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faPlus} /></span>
</FeelingPill>
</button>
</label>
{/each}
</div>
</div>

View file

@ -1,80 +1,149 @@
export const TITLED_ENTRIES = ["event", "environment", "memory"];
export const FEELINGS = ["relaxed", "afraid", "angry", "bad", "bored", "confused", "excited", "fine", "happy", "hurt", "in love", "mad", "nervous", "okay", "sad", "scared", "shy", "sleepy", "active", "surprised", "tired", "upset", "worried"];
export const TITLED_ENTRIES = ['event', 'environment', 'memory'];
export const FEELINGS = [
'relaxed',
'afraid',
'angry',
'bad',
'bored',
'confused',
'excited',
'fine',
'happy',
'hurt',
'in love',
'mad',
'nervous',
'okay',
'sad',
'scared',
'shy',
'sleepy',
'active',
'surprised',
'tired',
'upset',
'worried'
];
export type KnownFeeling = "relaxed" | "afraid" | "angry" | "bad" | "bored" | "confused" | "excited" | "fine" | "happy" | "hurt" | "in love" | "mad" | "nervous" | "okay" | "sad" | "scared" | "shy" | "sleepy" | "active" | "surprised" | "tired" | "upset" | "worried";
export type EntryKind = "song" | "album" | "event" | "memory" | "feeling" | "environment" | "date";
export type KnownFeeling =
| 'relaxed'
| 'afraid'
| 'angry'
| 'bad'
| 'bored'
| 'confused'
| 'excited'
| 'fine'
| 'happy'
| 'hurt'
| 'in love'
| 'mad'
| 'nervous'
| 'okay'
| 'sad'
| 'scared'
| 'shy'
| 'sleepy'
| 'active'
| 'surprised'
| 'tired'
| 'upset'
| 'worried';
export type EntryKind = 'song' | 'album' | 'event' | 'memory' | 'feeling' | 'environment' | 'date';
export type IdlessEntry = {
base: SongEntry | AlbumEntry | EventEntry | MemoryEntry | FeelingEntry | EnvironmentEntry | DateEntry,
creationDate: string,
feelings: (KnownFeeling | {
identifier: string,
description: string,
backgroundColor: string,
textColor: string,
})[],
assets: string[],
title?: string,
description?: string,
base:
| SongEntry
| AlbumEntry
| EventEntry
| MemoryEntry
| FeelingEntry
| EnvironmentEntry
| DateEntry;
creationDate: string;
feelings: (
| KnownFeeling
| {
identifier: string;
description: string;
backgroundColor: string;
textColor: string;
}
)[];
assets: string[];
title?: string;
description?: string;
};
export type Entry = {
id: string,
base: SongEntry | AlbumEntry | EventEntry | MemoryEntry | FeelingEntry | EnvironmentEntry | DateEntry,
creationDate: string,
feelings: (KnownFeeling | {
identifier: string,
description: string,
backgroundColor: string,
textColor: string,
})[],
assets: string[],
title?: string,
description?: string,
id: string;
base:
| SongEntry
| AlbumEntry
| EventEntry
| MemoryEntry
| FeelingEntry
| EnvironmentEntry
| DateEntry;
creationDate: string;
feelings: (
| KnownFeeling
| {
identifier: string;
description: string;
backgroundColor: string;
textColor: string;
}
)[];
assets: string[];
title?: string;
description?: string;
};
export type UniversalID = {
provider: string,
id: string,
}
provider: string;
id: string;
};
export type SongEntry = {
kind: "song",
artist: string,
title: string,
link: string[],
id: UniversalID[],
}
kind: 'song';
artist: string;
title: string;
link: string[];
id: UniversalID[];
};
export type AlbumEntry = {
kind: "album",
artist: string,
title: string,
link: string[],
id: UniversalID[],
}
kind: 'album';
artist: string;
title: string;
link: string[];
id: UniversalID[];
};
export type EventEntry = {
kind: "event",
}
kind: 'event';
};
export type MemoryEntry = {
kind: "memory",
}
kind: 'memory';
};
export type FeelingEntry = {
kind: "feeling",
}
kind: 'feeling';
};
export type EnvironmentEntry = {
kind: "environment",
location?: string | {
latitude: number,
longitude: number,
},
}
kind: 'environment';
location?:
| string
| {
latitude: number;
longitude: number;
};
};
export type DateEntry = {
kind: "date",
referencedDate: string,
}
kind: 'date';
referencedDate: string;
};

View file

@ -1,66 +1,67 @@
import { writable } from "svelte/store";
import { accountData, assetEndpoint, genSessionKey, type Account, type Credentials } from "./api";
import { writable } from 'svelte/store';
import { accountData, assetEndpoint, genSessionKey, type Account, type Credentials } from './api';
const CREDENTIALS_KEY = 'v0:credentials'
const CREDENTIALS_KEY = 'v0:credentials';
let _credentials: Credentials | null = null
export const credentials = writable<Credentials | null>()
let _credentials: Credentials | null = null;
export const credentials = writable<Credentials | null>();
credentials.subscribe((value) => {
if (value != null) {
_credentials = value;
localStorage.setItem( CREDENTIALS_KEY, JSON.stringify(value))
} else {
_credentials = null;
}
})
if (value != null) {
_credentials = value;
localStorage.setItem(CREDENTIALS_KEY, JSON.stringify(value));
} else {
_credentials = null;
}
});
export const account = writable<Account | null>()
export const session_key = writable<string | null>()
export const asset_endpoint = writable<string | null>()
export const account = writable<Account | null>();
export const session_key = writable<string | null>();
export const asset_endpoint = writable<string | null>();
export async function initializeStores() {
let rawCredentials = localStorage.getItem(CREDENTIALS_KEY)
let parsedCredentials
if (rawCredentials != null && rawCredentials.length > 0) {
try {
parsedCredentials = JSON.parse(rawCredentials)
credentials.set(parsedCredentials)
}
catch (e) { localStorage.removeItem(CREDENTIALS_KEY) }
}
let rawCredentials = localStorage.getItem(CREDENTIALS_KEY);
let parsedCredentials;
if (rawCredentials != null && rawCredentials.length > 0) {
try {
parsedCredentials = JSON.parse(rawCredentials);
credentials.set(parsedCredentials);
} catch (e) {
localStorage.removeItem(CREDENTIALS_KEY);
}
}
if (parsedCredentials != null) {
let data = await accountData(parsedCredentials)
if ('error' in data) {
credentials.set(null)
localStorage.removeItem(CREDENTIALS_KEY)
} else {
account.set(data)
}
if (parsedCredentials != null) {
let data = await accountData(parsedCredentials);
if ('error' in data) {
credentials.set(null);
localStorage.removeItem(CREDENTIALS_KEY);
} else {
account.set(data);
}
let key_result = await genSessionKey(parsedCredentials)
if ('error' in key_result) {
console.warn('Couldn\'t generate a session key!')
} else {
session_key.set(key_result.session_key)
}
let key_result = await genSessionKey(parsedCredentials);
if ('error' in key_result) {
console.warn("Couldn't generate a session key!");
} else {
session_key.set(key_result.session_key);
}
let asset_result = await assetEndpoint()
asset_endpoint.set(asset_result)
}
let asset_result = await assetEndpoint();
asset_endpoint.set(asset_result);
}
}
export async function refreshAccount() {
if (_credentials == null) {
console.warn("Requested to refresh the user account but credentials are null.")
return;
}
if (_credentials == null) {
console.warn('Requested to refresh the user account but credentials are null.');
return;
}
let refreshedAccount = await accountData(_credentials)
if ('error' in refreshedAccount) {
console.warn("Failed to refresh the user account.")
return;
}
let refreshedAccount = await accountData(_credentials);
if ('error' in refreshedAccount) {
console.warn('Failed to refresh the user account.');
return;
}
account.set(refreshedAccount)
account.set(refreshedAccount);
}

View file

@ -0,0 +1,5 @@
export const ENV_VARIABLES = {
IDENTITY_API_ENDPOINT: import.meta.env.VITE_IDENTITY_API_ENDPOINT,
ASSET_API_ENDPOINT: import.meta.env.VITE_ASSET_API_ENDPOINT,
SUPPORT_PAGE: import.meta.env.VITE_SUPPORT_PAGE
};

View file

@ -1,34 +1,39 @@
<script>
import { credentials, initializeStores } from "$lib/stores";
import "../app.css";
import { credentials, initializeStores } from '$lib/stores';
import { ENV_VARIABLES } from '$lib/variables';
import '../app.css';
initializeStores()
initializeStores();
</script>
<div class="py-3.5 flex text-white bg-violet-800 justify-center">
<nav class="w-[60%] flex justify-between items-center">
<h1 class="font-serif text-3xl">
{#if $credentials == null}
<a href="/">Identity</a>
{:else}
<a href="/dashboard">Identity</a>
{/if}
</h1>
<div class="text-xl">
{#if $credentials == null}
| <div class="px-3 inline-block"><a href="/">Home</a></div>
| <div class="px-3 inline-block"><a href="mailto:sofi@sofiaritz.com">Support</a></div>
| <div class="px-3 inline-block"><a href="/auth/register">Join</a></div>
|
{:else}
| <div class="px-3 inline-block"><a href="/dashboard">Dashboard</a></div>
| <div class="px-3 inline-block"><a href="/auth/account">Account</a></div>
| <div class="px-3 inline-block"><a href="mailto:sofi@sofiaritz.com">Support</a></div>
|
{/if}
</div>
</nav>
<div class="flex justify-center bg-violet-800 py-3.5 text-white">
<nav class="flex w-[60%] items-center justify-between">
<h1 class="font-serif text-3xl">
{#if $credentials == null}
<a href="/">Identity</a>
{:else}
<a href="/dashboard">Identity</a>
{/if}
</h1>
<div class="text-xl">
{#if $credentials == null}
| <div class="inline-block px-3"><a href="/">Home</a></div>
|
<div class="inline-block px-3"><a href="mailto:sofi@sofiaritz.com">Support</a></div>
|
<div class="inline-block px-3"><a href="/auth/register">Join</a></div>
|
{:else}
| <div class="inline-block px-3"><a href="/dashboard">Dashboard</a></div>
|
<div class="inline-block px-3"><a href="/auth/account">Account</a></div>
|
<div class="inline-block px-3"><a href={ENV_VARIABLES.SUPPORT_PAGE}>Support</a></div>
|
{/if}
</div>
</nav>
</div>
<main class="pt-3.5">
<slot/>
<slot />
</main>

View file

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

View file

@ -1 +1,75 @@
<h1>Landing</h1>
<script lang="ts">
import photo1 from '$lib/assets/memory-photos.jpg';
import photo2 from '$lib/assets/ladies.jpg';
</script>
<div class="flex justify-center bg-violet-100 pt-3.5">
<div class="flex w-[60%] flex-col">
<div class="my-1 flex">
<div class="w-full">
<h1 class="pt-6 font-serif text-4xl text-violet-700">
Store your memories for your future <span class="font-semibold italic">you.</span>
</h1>
<p class="pt-4 text-lg">
<span class="font-serif">Identity</span>
helps you store your memories and experiences. Our memories are our most precious belonging,
we should store them in a safe place.
</p>
<p class="pt-4 text-lg">
<span class="font-serif">Identity</span>
is an open-source software you can self-host to have full control over the storage of your
memories.
</p>
<p class="pt-4 text-lg">
This instance is maintained by volunnteers and financed by our community and sponsors. You
can export your data at any time.
</p>
<a
href="/auth/register"
class="focust:outline-none mr-3 mt-6 block rounded-lg bg-violet-700 px-5 py-2.5 text-center font-medium text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
>
Join now
</a>
</div>
<div class="w-2/3">
<img
class="aspect-square w-full border-8 border-violet-400 object-cover"
alt="Collage of Polaroid-like pictures"
src={photo1}
/>
<span class="my-1.5 block text-right">Photo by Lisa Fotios</span>
</div>
</div>
<div class="my-1 flex">
<div class="w-2/3">
<img
class="aspect-square w-full border-8 border-violet-400 object-cover"
alt="Collage of Polaroid-like pictures"
src={photo2}
/>
<span class="my-1.5 block">Photo by cottonbro studio</span>
</div>
<div class="w-full">
<h1 class="pt-6 text-right font-serif text-4xl text-violet-700">
Remember your <span class="font-semibold italic">younger self.</span>
</h1>
<p class="pt-4 text-right text-lg">
Reminiscence and life story work can help dementia patients ease their symptoms.
<sup>
<a
href="https://www.alzheimers.org.uk/get-support/publications-factsheets/the-dementia-guide"
class="text-blue-600 hover:cursor-pointer hover:underline"
>
[1]
</a>
</sup>
</p>
<p class="pt-4 text-right text-lg">
Both your future you and your descendants may find your legacy useful:
<br />
environments, music, memories&mldr;
</p>
</div>
</div>
</div>
</div>

View file

@ -1,119 +1,177 @@
<script lang="ts">
import { createForm } from "felte";
import { account, credentials, refreshAccount } from "$lib/stores";
import { type AccountHeir, updateHeirs } from "$lib/api";
import { createForm } from 'felte';
import { account, credentials, refreshAccount } from '$lib/stores';
import { type AccountHeir, updateHeirs } from '$lib/api';
credentials.subscribe((v) => v == null && (setTimeout(() => window.location.pathname = '/auth/login', 200)))
credentials.subscribe(
(v) => v == null && setTimeout(() => (window.location.pathname = '/auth/login'), 200)
);
let heirWizard = false;
let heirWizard = false;
const { form, errors } = createForm({
onSubmit: async (values) => {
let heir: AccountHeir = {
const { form, errors } = createForm({
onSubmit: async (values) => {
let heir: AccountHeir = {
contactMethod: values.contactMethod,
name: values.name,
value: values.contactDetails,
value: values.contactDetails
};
let currentHeirs = structuredClone($account!.heirs)
let updatedHeirs = [heir, ...currentHeirs];
let currentHeirs = structuredClone($account!.heirs);
let updatedHeirs = [heir, ...currentHeirs];
await updateHeirs($credentials!, updatedHeirs);
await refreshAccount();
await updateHeirs($credentials!, updatedHeirs);
await refreshAccount();
heirWizard = false;
},
validate: (values) => {
let errors = {}
heirWizard = false;
},
validate: (values) => {
let errors = {};
if (values.contactMethod == null || values.contactMethod.length === 0) {
errors['contactMethod'] = 'Must choose a contact method'
}
if (values.contactMethod == null || values.contactMethod.length === 0) {
errors['contactMethod'] = 'Must choose a contact method';
}
if (values.name == null || values.name.length === 0) {
errors['name'] = 'Must not be empty'
}
if (values.name == null || values.name.length === 0) {
errors['name'] = 'Must not be empty';
}
if (values.contactDetails == null || values.contactDetails.length === 0) {
errors['contactDetails'] = 'Must not be empty'
}
if (values.contactDetails == null || values.contactDetails.length === 0) {
errors['contactDetails'] = 'Must not be empty';
}
return errors
}
})
return errors;
}
});
async function removeHeir(heir: AccountHeir) {
let currentHeirs = structuredClone($account!.heirs)
let updatedHeirs = currentHeirs
.filter((v) => v.value !== heir.value);
async function removeHeir(heir: AccountHeir) {
let currentHeirs = structuredClone($account!.heirs);
let updatedHeirs = currentHeirs.filter((v) => v.value !== heir.value);
await updateHeirs($credentials!, updatedHeirs);
await refreshAccount();
}
await updateHeirs($credentials!, updatedHeirs);
await refreshAccount();
}
</script>
<div class="mt-3.5 justify-center flex">
<div class="w-[60%] flex flex-col">
<h1 class="text-2xl pb-3.5">Welcome back, <span class="font-bold">{$account?.name}</span>.</h1>
<div>
<div class="flex justify-between mb-2">
<h2 class="text-xl pb-2.5">Heirs</h2>
{#if $account?.heirs.length > 0}
<button on:click={() => heirWizard = !heirWizard} class="rounded-lg bg-violet-700 text-white px-2.5 py-1 text-center hover:bg-violet-800 focus:ring-4 focus:ring-violet-300">
+ Add a heir
</button>
{/if}
</div>
{#if !heirWizard && $account?.heirs.length === 0}
<div class="flex flex-col">
<button on:click={() => heirWizard = true} class="flex h-60 flex-col items-center justify-center gap-3 rounded border border-gray-300 p-2 text-black">
<span class="text-4xl">+</span>
<h2 class="text-xl font-semibold">Add a heir</h2>
</button>
</div>
{/if}
{#if heirWizard}
<div class="border border-gray-200 rounded-lg shadow w-full flex flex-col p-3.5 mb-4">
<form use:form>
<div class="mb-5">
<label for="heir__contact-method" class="block mb-2 text-sm font-medium text-gray-900">Contact method</label>
<select id="heir__contact-method" name="contactMethod" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
<option value="" selected>Choose a contact method</option>
<option value="email">Email</option>
</select>
{#if $errors.contactMethod != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.contactMethod[0]}</span></p>
{/if}
</div>
<div class="mb-5">
<label for="heir__name" class="block mb-2 text-sm font-medium text-gray-900">Heir name</label>
<input id="heir__name" type="text" name="name" placeholder="Jane Doe" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
{#if $errors.name != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.name[0]}</span></p>
{/if}
</div>
<div class="mb-5">
<label for="heir__contactDetails" class="block mb-2 text-sm font-medium text-gray-900">Contact details</label>
<input id="heir__contactDetails" type="text" name="contactDetails" placeholder="jane@identity.net" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
{#if $errors.contactDetails != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.contactDetails[0]}</span></p>
{/if}
</div>
<button class="mt-2 text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focust:outline-none focus:ring-violet-300 font-medium rounded-lg px-5 py-2.5 text-center" type="submit">Add new heir</button>
</form>
</div>
{/if}
{#each $account?.heirs || [] as heir (heir.value)}
<div class="border border-gray-200 rounded-lg shadow w-full flex flex-col p-3.5 mb-2.5">
<div class="flex justify-between">
<span class="block text-sm font-medium text-gray-900">Contact method: <span class="capitalize">{heir.contactMethod}</span></span>
<button on:click={() => removeHeir(heir)} class="rounded-lg bg-red-600 text-white px-2.5 py-1 text-center hover:bg-red-700 focus:ring-4 focus:ring-violet-300">Delete heir</button>
</div>
<div>
<span>{heir.name}</span> · <span>{heir.value}</span>
</div>
</div>
{/each}
</div>
</div>
<div class="mt-3.5 flex justify-center">
<div class="flex w-[60%] flex-col">
<h1 class="pb-3.5 text-2xl">
Welcome back, <span class="font-bold">{$account?.name}</span>
.
</h1>
<div>
<div class="mb-2 flex justify-between">
<h2 class="pb-2.5 text-xl">Heirs</h2>
{#if $account?.heirs.length > 0}
<button
on:click={() => (heirWizard = !heirWizard)}
class="rounded-lg bg-violet-700 px-2.5 py-1 text-center text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
>
+ Add a heir
</button>
{/if}
</div>
{#if !heirWizard && $account?.heirs.length === 0}
<div class="flex flex-col">
<button
on:click={() => (heirWizard = true)}
class="flex h-60 flex-col items-center justify-center gap-3 rounded border border-gray-300 p-2 text-black"
>
<span class="text-4xl">+</span>
<h2 class="text-xl font-semibold">Add a heir</h2>
</button>
</div>
{/if}
{#if heirWizard}
<div class="mb-4 flex w-full flex-col rounded-lg border border-gray-200 p-3.5 shadow">
<form use:form>
<div class="mb-5">
<label
for="heir__contact-method"
class="mb-2 block text-sm font-medium text-gray-900"
>
Contact method
</label>
<select
id="heir__contact-method"
name="contactMethod"
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
>
<option value="" selected>Choose a contact method</option>
<option value="email">Email</option>
</select>
{#if $errors.contactMethod != null}
<p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.contactMethod[0]}</span>
</p>
{/if}
</div>
<div class="mb-5">
<label for="heir__name" class="mb-2 block text-sm font-medium text-gray-900">
Heir name
</label>
<input
id="heir__name"
type="text"
name="name"
placeholder="Jane Doe"
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
/>
{#if $errors.name != null}
<p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.name[0]}</span>
</p>
{/if}
</div>
<div class="mb-5">
<label
for="heir__contactDetails"
class="mb-2 block text-sm font-medium text-gray-900"
>
Contact details
</label>
<input
id="heir__contactDetails"
type="text"
name="contactDetails"
placeholder="jane@identity.net"
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
/>
{#if $errors.contactDetails != null}
<p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.contactDetails[0]}</span>
</p>
{/if}
</div>
<button
class="focust:outline-none mt-2 rounded-lg bg-violet-700 px-5 py-2.5 text-center font-medium text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
type="submit"
>
Add new heir
</button>
</form>
</div>
{/if}
{#each $account?.heirs || [] as heir (heir.value)}
<div class="mb-2.5 flex w-full flex-col rounded-lg border border-gray-200 p-3.5 shadow">
<div class="flex justify-between">
<span class="block text-sm font-medium text-gray-900">
Contact method: <span class="capitalize">{heir.contactMethod}</span>
</span>
<button
on:click={() => removeHeir(heir)}
class="rounded-lg bg-red-600 px-2.5 py-1 text-center text-white hover:bg-red-700 focus:ring-4 focus:ring-violet-300"
>
Delete heir
</button>
</div>
<div>
<span>{heir.name}</span>
·
<span>{heir.value}</span>
</div>
</div>
{/each}
</div>
</div>
</div>

View file

@ -1,73 +1,108 @@
<script lang="ts">
import { login, type Credentials } from "$lib/api";
import { credentials } from "$lib/stores";
import { createForm } from "felte";
import { login, type Credentials } from '$lib/api';
import { credentials } from '$lib/stores';
import { createForm } from 'felte';
let submitError: string | undefined
let submitError: string | undefined;
// FIXME: This is a badly done hack
credentials.subscribe((v) => v != null && (setTimeout(() => window.location.pathname = '/dashboard', 200)))
// FIXME: This is a badly done hack
credentials.subscribe(
(v) => v != null && setTimeout(() => (window.location.pathname = '/dashboard'), 200)
);
const { form, errors } = createForm({
onSubmit: (values) => {
return login(values)
},
onSuccess: (response) => {
// @ts-ignore - FIXME: How to tell the checker that this is right
if (response == null || ('error' in response && (typeof response['error'] !== "string" || !['invalid credentials'].includes(response['error'])))) {
submitError = 'Something failed. Try again later.'
}
// @ts-ignore - FIXME: How to tell the checker that this is right
else if ('error' in response) {
// @ts-ignore - response is not null and the type of its key 'error' is a string
submitError = 'Check your credentials and try again.'
} else {
credentials.set(response as Credentials)
// FIXME: This is a badly done hack
setTimeout(() => window.location.pathname = '/dashboard', 200)
}
},
validate: (values) => {
const errors = {}
if (values.email == null || values.email.length === 0) {
errors.email = 'Must not be empty'
}
const { form, errors } = createForm({
onSubmit: (values) => {
return login(values);
},
onSuccess: (response) => {
// @ts-ignore - FIXME: How to tell the checker that this is right
if (
response == null ||
('error' in response &&
(typeof response['error'] !== 'string' ||
!['invalid credentials'].includes(response['error'])))
) {
submitError = 'Something failed. Try again later.';
}
// @ts-ignore - FIXME: How to tell the checker that this is right
else if ('error' in response) {
// @ts-ignore - response is not null and the type of its key 'error' is a string
submitError = 'Check your credentials and try again.';
} else {
credentials.set(response as Credentials);
// FIXME: This is a badly done hack
setTimeout(() => (window.location.pathname = '/dashboard'), 200);
}
},
validate: (values) => {
const errors = {};
if (values.email == null || values.email.length === 0) {
errors.email = 'Must not be empty';
}
if (values.password == null || values.password.length === 0) {
errors.password = 'Must not be empty'
}
if (values.password == null || values.password.length === 0) {
errors.password = 'Must not be empty';
}
return errors
}
})
return errors;
}
});
</script>
<div class="mt-3.5 justify-center flex">
<div class="w-[25%]">
<h1 class="text-2xl pb-3.5">Log in</h1>
<form use:form>
<div class="mb-5">
<label for="register__email" class="block mb-2 text-sm font-medium text-gray-900">Your e-mail</label>
<input id="register__email" type="text" name="email" placeholder="jane@identity.net" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
{#if $errors.email != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.email[0]}</span></p>
{/if}
</div>
<div class="mb-5">
<label for="register__password" class="block mb-2 text-sm font-medium text-gray-900">Your password</label>
<input id="register__password" type="password" name="password" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
{#if $errors.password != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.password[0]}</span></p>
{/if}
</div>
<button type="submit" class="text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focust:outline-none focus:ring-violet-300 font-medium rounded-lg w-full px-5 py-2.5 text-center">Log in</button>
{#if submitError != null && submitError.length > 0}
<p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p>
{/if}
<div class="flex pt-3.5 w-full justify-between">
<a href="/auth/register" class="text-center font-medium text-blue-600 hover:underline">Create an account</a>
<a href="/auth/recovery" class="text-center font-medium text-blue-600 hover:underline">Forgotten password?</a>
</div>
</form>
</div>
<div class="mt-3.5 flex justify-center">
<div class="w-[25%]">
<h1 class="pb-3.5 text-2xl">Log in</h1>
<form use:form>
<div class="mb-5">
<label for="register__email" class="mb-2 block text-sm font-medium text-gray-900">
Your e-mail
</label>
<input
id="register__email"
type="text"
name="email"
placeholder="jane@identity.net"
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
/>
{#if $errors.email != null}
<p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.email[0]}</span>
</p>
{/if}
</div>
<div class="mb-5">
<label for="register__password" class="mb-2 block text-sm font-medium text-gray-900">
Your password
</label>
<input
id="register__password"
type="password"
name="password"
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
/>
{#if $errors.password != null}
<p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.password[0]}</span>
</p>
{/if}
</div>
<button
type="submit"
class="focust:outline-none w-full rounded-lg bg-violet-700 px-5 py-2.5 text-center font-medium text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
>
Log in
</button>
{#if submitError != null && submitError.length > 0}
<p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p>
{/if}
<div class="flex w-full justify-between pt-3.5">
<a href="/auth/register" class="text-center font-medium text-blue-600 hover:underline">
Create an account
</a>
<a href="/auth/recovery" class="text-center font-medium text-blue-600 hover:underline">
Forgotten password?
</a>
</div>
</form>
</div>
</div>

View file

@ -1,83 +1,128 @@
<script lang="ts">
import { register, type Credentials } from "$lib/api";
import { credentials } from "$lib/stores";
import { createForm } from "felte";
import { register, type Credentials } from '$lib/api';
import { credentials } from '$lib/stores';
import { createForm } from 'felte';
let submitError: string | undefined
let submitError: string | undefined;
// FIXME: This is a badly done hack
credentials.subscribe((v) => v != null && (setTimeout(() => window.location.pathname = '/dashboard', 200)))
// FIXME: This is a badly done hack
credentials.subscribe(
(v) => v != null && setTimeout(() => (window.location.pathname = '/dashboard'), 200)
);
const { form, errors } = createForm({
onSubmit: (values) => {
return register(values)
},
onSuccess: (response) => {
// @ts-ignore - FIXME: How to tell the checker that this is right
if (response == null || ('error' in response && (typeof response['error'] !== "string" || !['invalid data'].includes(response['error'])))) {
submitError = 'Something failed. Try again later.'
}
// @ts-ignore - FIXME: How to tell the checker that this is right
else if ('error' in response) {
// @ts-ignore - response is not null and the type of its key 'error' is a string
submitError = 'Check your credentials and try again, this user may already exist.'
} else {
credentials.set(response as Credentials)
// FIXME: This is a badly done hack
setTimeout(() => window.location.pathname = '/dashboard', 200)
}
},
validate: (values) => {
const errors = {}
if (values.name == null || values.name.length === 0) {
errors.name = 'Must not be empty'
}
const { form, errors } = createForm({
onSubmit: (values) => {
return register(values);
},
onSuccess: (response) => {
// @ts-ignore - FIXME: How to tell the checker that this is right
if (
response == null ||
('error' in response &&
(typeof response['error'] !== 'string' || !['invalid data'].includes(response['error'])))
) {
submitError = 'Something failed. Try again later.';
}
// @ts-ignore - FIXME: How to tell the checker that this is right
else if ('error' in response) {
// @ts-ignore - response is not null and the type of its key 'error' is a string
submitError = 'Check your credentials and try again, this user may already exist.';
} else {
credentials.set(response as Credentials);
// FIXME: This is a badly done hack
setTimeout(() => (window.location.pathname = '/dashboard'), 200);
}
},
validate: (values) => {
const errors = {};
if (values.name == null || values.name.length === 0) {
errors.name = 'Must not be empty';
}
if (values.email == null || !/^[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+/.test(values.email)) {
errors.email = 'Must be a valid e-mail'
}
if (values.email == null || !/^[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+/.test(values.email)) {
errors.email = 'Must be a valid e-mail';
}
if (values.password == null || values.password.length === 0) {
errors.password = 'Must not be empty'
} else if (values.password != null && values.password.length < 12) {
errors.password = 'Must be over 12 characters'
}
if (values.password == null || values.password.length === 0) {
errors.password = 'Must not be empty';
} else if (values.password != null && values.password.length < 12) {
errors.password = 'Must be over 12 characters';
}
return errors
}
})
return errors;
}
});
</script>
<div class="mt-3.5 justify-center flex">
<div class="w-[25%]">
<h1 class="text-2xl pb-3.5">Register</h1>
<form use:form>
<div class="mb-5">
<label for="register__name" class="block mb-2 text-sm font-medium text-gray-900">Your name</label>
<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">
{#if $errors.name != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.name[0]}</span></p>
{/if}
</div>
<div class="mb-5">
<label for="register__email" class="block mb-2 text-sm font-medium text-gray-900">Your e-mail</label>
<input id="register__email" type="text" name="email" placeholder="jane@identity.net" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
{#if $errors.email != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.email[0]}</span></p>
{/if}
</div>
<div class="mb-5">
<label for="register__password" class="block mb-2 text-sm font-medium text-gray-900">Your password</label>
<input id="register__password" type="password" name="password" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
{#if $errors.password != null}
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.password[0]}</span></p>
{/if}
</div>
<button type="submit" class="text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focust:outline-none focus:ring-violet-300 font-medium rounded-lg w-full px-5 py-2.5 text-center">Create user</button>
{#if submitError != null && submitError.length > 0}
<p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p>
{/if}
<a href="/auth/login" class="block w-full text-center pt-3.5 font-medium text-blue-600 hover:underline">Already have an account?</a>
</form>
</div>
<div class="mt-3.5 flex justify-center">
<div class="w-[25%]">
<h1 class="pb-3.5 text-2xl">Register</h1>
<form use:form>
<div class="mb-5">
<label for="register__name" class="mb-2 block text-sm font-medium text-gray-900">
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}
<p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.name[0]}</span>
</p>
{/if}
</div>
<div class="mb-5">
<label for="register__email" class="mb-2 block text-sm font-medium text-gray-900">
Your e-mail
</label>
<input
id="register__email"
type="text"
name="email"
placeholder="jane@identity.net"
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
/>
{#if $errors.email != null}
<p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.email[0]}</span>
</p>
{/if}
</div>
<div class="mb-5">
<label for="register__password" class="mb-2 block text-sm font-medium text-gray-900">
Your password
</label>
<input
id="register__password"
type="password"
name="password"
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
/>
{#if $errors.password != null}
<p class="mt-2 text-sm text-red-600">
<span class="font-medium">{$errors.password[0]}</span>
</p>
{/if}
</div>
<button
type="submit"
class="focust:outline-none w-full rounded-lg bg-violet-700 px-5 py-2.5 text-center font-medium text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
>
Create user
</button>
{#if submitError != null && submitError.length > 0}
<p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p>
{/if}
<a
href="/auth/login"
class="block w-full pt-3.5 text-center font-medium text-blue-600 hover:underline"
>
Already have an account?
</a>
</form>
</div>
</div>

View file

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

View file

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

View file

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

View file

@ -1,32 +1,44 @@
<script lang="ts">
import { asset_endpoint, session_key } from "$lib/stores";
import { faArrowUpRightFromSquare, faFileAudio, faFileVideo, faImage } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome";
import mime from "mime"
import { asset_endpoint, session_key } from '$lib/stores';
import {
faArrowUpRightFromSquare,
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?
$: href = new URL(`/asset?asset_id=${encodeURIComponent(asset_id)}&session_key=${encodeURIComponent($session_key!)}`, $asset_endpoint!).href
$: kind = mime.getType(asset_id.split(".")[1])?.split("/")[0]
// 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;
$: kind = mime.getType(asset_id.split('.')[1])?.split('/')[0];
</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}>
{#if kind == null}
<FontAwesomeIcon icon={faArrowUpRightFromSquare}/>
{:else if kind === "image"}
<FontAwesomeIcon icon={faImage}/>
{:else if kind === "audio"}
<FontAwesomeIcon icon={faFileAudio}/>
{:else if kind === "video"}
<FontAwesomeIcon icon={faFileVideo}/>
{:else}
<FontAwesomeIcon icon={faArrowUpRightFromSquare}/>
{/if}
<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}
<FontAwesomeIcon icon={faArrowUpRightFromSquare} />
{:else if kind === 'image'}
<FontAwesomeIcon icon={faImage} />
{:else if kind === 'audio'}
<FontAwesomeIcon icon={faFileAudio} />
{:else if kind === 'video'}
<FontAwesomeIcon icon={faFileVideo} />
{:else}
<FontAwesomeIcon icon={faArrowUpRightFromSquare} />
{/if}
{#if kind != null && kind !== "application"}
<span class="capitalize">{kind}</span>
{:else}
<span>Asset</span>
{/if}
{#if kind != null && kind !== 'application'}
<span class="capitalize">{kind}</span>
{:else}
<span>Asset</span>
{/if}
</a>

View file

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

View file

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

View file

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

View file

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

View file

@ -1,40 +1,67 @@
<script lang="ts">
// TODO: Design a more _formal_ color system for emotions (>strong = >weight, etc).
const DEFAULT_COLORS: {[index: string]: string[]} = {
"__DEFAULT__": ["#0a0a0a", "#fafafa"],
"afraid": ["#fda4af", "#0a0a0a"],
"angry": ["#dc2626", "#fafafa"],
"bad": ["#450a0a", "#fafafa"],
"bored": ["#d4d4d8", "#0a0a0a"],
"confused": ["#fef3c7", "#0a0a0a"],
"excited": ["#f97316", "#fafafa"],
"fine": ["#bef264", "#0a0a0a"],
"happy": ["#facc15", "#0a0a0a"],
"hurt": ["#ff69b4", "#0a0a0a"],
"in love": ["#ff1493", "#fafafa"],
"mad": ["#450a0a", "#fafafa"],
"nervous": ["#7e22ce", "#fafafa"],
"okay": ["#86efac", "#0a0a0a"],
"sad": ["#0284c7", "#fafafa"],
"scared": ["#334155", "#fafafa"],
"shy": ["#cbd5e1", "#0a0a0a"],
"sleepy": ["#7dd3fc", "#0a0a0a"],
"active": ["#059669", "#fafafa"],
"surprised": ["#fbbf24", "#0a0a0a"],
"tired": ["#92400e", "#fafafa"],
"upset": ["#b91c1c", "#fafafa"],
"worried": ["#d4d4d8", "#0a0a0a"],
"relaxed": ["#86efac", "#0a0a0a"],
};
// TODO: Design a more _formal_ color system for emotions (>strong = >weight, etc).
const DEFAULT_COLORS: { [index: string]: string[] } = {
__DEFAULT__: ['#0a0a0a', '#fafafa'],
afraid: ['#fda4af', '#0a0a0a'],
angry: ['#dc2626', '#fafafa'],
bad: ['#450a0a', '#fafafa'],
bored: ['#d4d4d8', '#0a0a0a'],
confused: ['#fef3c7', '#0a0a0a'],
excited: ['#f97316', '#fafafa'],
fine: ['#bef264', '#0a0a0a'],
happy: ['#facc15', '#0a0a0a'],
hurt: ['#ff69b4', '#0a0a0a'],
'in love': ['#ff1493', '#fafafa'],
mad: ['#450a0a', '#fafafa'],
nervous: ['#7e22ce', '#fafafa'],
okay: ['#86efac', '#0a0a0a'],
sad: ['#0284c7', '#fafafa'],
scared: ['#334155', '#fafafa'],
shy: ['#cbd5e1', '#0a0a0a'],
sleepy: ['#7dd3fc', '#0a0a0a'],
active: ['#059669', '#fafafa'],
surprised: ['#fbbf24', '#0a0a0a'],
tired: ['#92400e', '#fafafa'],
upset: ['#b91c1c', '#fafafa'],
worried: ['#d4d4d8', '#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 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 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 bgColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS['__DEFAULT__'])[0];
export let textColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS['__DEFAULT__'])[1];
export let slim = false;
</script>
<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}`}>
<slot name="pre"/>
<div
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>

View file

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

View file

@ -1,22 +1,35 @@
<script lang="ts">
import { TITLED_ENTRIES, type Entry } from "$lib/entry"
import { TITLED_ENTRIES, type Entry } from '$lib/entry';
export let entry: Entry
export let showDate = false
export let entry: Entry;
export let showDate = false;
</script>
<p>
{#if showDate}
<time class="pr-2.5 font-mono" datetime={entry.creationDate}>{new Date(entry.creationDate).toLocaleDateString()}</time>
{/if}
{#if showDate}
<time class="pr-2.5 font-mono" datetime={entry.creationDate}>
{new Date(entry.creationDate).toLocaleDateString()}
</time>
{/if}
{#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>
{: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}
<a href={`#entry__${entry.id}`} class="font-bold text-violet-600 hover:underline">New {entry.base.kind}</a>
{/if}
{#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>
{: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}
<a href={`#entry__${entry.id}`} class="font-bold text-violet-600 hover:underline">
New {entry.base.kind}
</a>
{/if}
</p>

View file

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

View file

@ -1,4 +1,5 @@
import adapter from '@sveltejs/adapter-auto';
import adapterStatic from '@sveltejs/adapter-static';
import adapterAuto from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
@ -8,10 +9,16 @@ const config = {
preprocess: vitePreprocess(),
kit: {
adapter: adapterStatic({
fallback: 'index.html'
})
// 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.
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
adapter: adapter()
//
// To use adapter-auto, uncomment the line below:
// adapter: adapterAuto(),
}
};

View file

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

View file

@ -559,6 +559,15 @@ __metadata:
languageName: node
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":
version: 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/svelte-fontawesome": "npm:^0.2.2"
"@sveltejs/adapter-auto": "npm:^3.0.0"
"@sveltejs/adapter-static": "npm:^3.0.2"
"@sveltejs/kit": "npm:^2.0.0"
"@sveltejs/vite-plugin-svelte": "npm:^3.0.0"
"@tailwindcss/forms": "npm:^0.5.7"
@ -1945,6 +1955,7 @@ __metadata:
postcss: "npm:^8.4.38"
prettier: "npm:^3.1.1"
prettier-plugin-svelte: "npm:^3.1.2"
prettier-plugin-tailwindcss: "npm:^0.6.5"
svelte: "npm:^4.2.7"
svelte-check: "npm:^3.6.0"
tailwindcss: "npm:^3.4.4"
@ -2855,6 +2866,61 @@ __metadata:
languageName: node
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":
version: 3.3.2
resolution: "prettier@npm:3.3.2"