cleanup and docs and landing
This commit is contained in:
parent
cdb8ccbf97
commit
14cbc6a1a3
34 changed files with 2065 additions and 1297 deletions
26
README.md
26
README.md
|
@ -4,16 +4,32 @@ Identity is an open-source application that helps you save your most relevant me
|
||||||
|
|
||||||
## Rationale
|
## Rationale
|
||||||
|
|
||||||
Identity is a project that initially started as an app whose purpose was to store music you like
|
Identity is a project that initially started as an app whose purpose was to store music you like (or used to like) for future use in treatment
|
||||||
(or liked) for future use in treatment for diseases like dementia. Over time, the idea evolved
|
for conditions such as dementia. Over time, the idea evolved and is now a general-purpose memory-saving app.
|
||||||
and is now general-purpose.
|
|
||||||
|
|
||||||
## Projects
|
## Projects
|
||||||
|
|
||||||
* `identity-web`. The web app that interacts with the Identiy API.
|
* `identity-web`. The web app that interacts with the Identiy API.
|
||||||
* `identity-api`. The Identity API, also takes care of storing data.
|
* `identity-api`. The Identity API, takes care of storing user data.
|
||||||
* `identity-format`. The specification for the Identity file format.
|
* `asset-api`. The Asset API, takes care of storing user-generated assets.
|
||||||
|
|
||||||
|
## Installation and building
|
||||||
|
|
||||||
|
The Identity project is composed by a web-app and two servers. In the future, Docker containers may be built to ease the installation of this project.
|
||||||
|
|
||||||
|
### Building and running
|
||||||
|
|
||||||
|
#### Building `identity-web`
|
||||||
|
|
||||||
|
1. Copy and update the `env.example` file: `cp .env.example .env`
|
||||||
|
2. Run `yarn` to install the dependencies.
|
||||||
|
* You may need to [enable Corepack](https://nodejs.org/api/corepack.html).
|
||||||
|
3. Run `yarn preview` to check that everything works properly.
|
||||||
|
4. Modify the `svelte.config.js` file to deploy to your desired environment.
|
||||||
|
5. Run `yarn build` to generate the SPA build.
|
||||||
|
* The build will be placed at the `build/` folder.
|
||||||
|
|
||||||
## Citations
|
## Citations
|
||||||
|
|
||||||
1. Van de Winckel, A., Feys, H., De Weerdt, W., & Dom, R. (2004). Cognitive and behavioural effects of music-based exercises in patients with dementia. Clinical Rehabilitation, 18(3), 253-260. https://doi.org/10.1191/0269215504cr750oa
|
1. Van de Winckel, A., Feys, H., De Weerdt, W., & Dom, R. (2004). Cognitive and behavioural effects of music-based exercises in patients with dementia. Clinical Rehabilitation, 18(3), 253-260. https://doi.org/10.1191/0269215504cr750oa
|
||||||
|
2. The dementia guide: Living well after your diagnosis. (2021, April 16). Alzheimer’s Society. https://www.alzheimers.org.uk/get-support/publications-factsheets/the-dementia-guide
|
3
identity-web/.env.example
Normal file
3
identity-web/.env.example
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
VITE_IDENTITY_API_ENDPOINT="http://localhost:3000/"
|
||||||
|
VITE_ASSET_API_ENDPOINT="http://localhost:3001/"
|
||||||
|
VITE_SUPPORT_PAGE="mailto:support@example.com"
|
|
@ -3,6 +3,7 @@
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"trailingComma": "none",
|
"trailingComma": "none",
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"plugins": ["prettier-plugin-svelte"],
|
"htmlWhitespaceSensitivity": "ignore",
|
||||||
|
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
||||||
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@sveltejs/adapter-auto": "^3.0.0",
|
"@sveltejs/adapter-auto": "^3.0.0",
|
||||||
|
"@sveltejs/adapter-static": "^3.0.2",
|
||||||
"@sveltejs/kit": "^2.0.0",
|
"@sveltejs/kit": "^2.0.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
|
@ -26,6 +27,7 @@
|
||||||
"postcss": "^8.4.38",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^3.1.1",
|
"prettier": "^3.1.1",
|
||||||
"prettier-plugin-svelte": "^3.1.2",
|
"prettier-plugin-svelte": "^3.1.2",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||||
"svelte": "^4.2.7",
|
"svelte": "^4.2.7",
|
||||||
"svelte-check": "^3.6.0",
|
"svelte-check": "^3.6.0",
|
||||||
"tailwindcss": "^3.4.4",
|
"tailwindcss": "^3.4.4",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {}
|
||||||
},
|
}
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,3 +1,19 @@
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
sup {
|
||||||
|
top: -0.3em;
|
||||||
|
vertical-align: super;
|
||||||
|
}
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
|
|
@ -1,122 +1,140 @@
|
||||||
import type { Entry, IdlessEntry } from "./entry"
|
import type { Entry, IdlessEntry } from './entry';
|
||||||
|
import { ENV_VARIABLES } from './variables';
|
||||||
|
|
||||||
const ENDPOINT = 'http://localhost:3000/'
|
const ENDPOINT = ENV_VARIABLES.IDENTITY_API_ENDPOINT;
|
||||||
const ASSET_API_ENDPOINT = 'http://localhost:3001/'
|
const ASSET_API_ENDPOINT = ENV_VARIABLES.ASSET_API_ENDPOINT;
|
||||||
|
|
||||||
export type Credentials = {
|
export type Credentials = {
|
||||||
token: string,
|
token: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type AccountHeir = {
|
export type AccountHeir = {
|
||||||
contactMethod: "email",
|
contactMethod: 'email';
|
||||||
name: string,
|
name: string;
|
||||||
value: string,
|
value: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Account = {
|
export type Account = {
|
||||||
uid: string,
|
uid: string;
|
||||||
name: string,
|
name: string;
|
||||||
heirs: AccountHeir[],
|
heirs: AccountHeir[];
|
||||||
}
|
};
|
||||||
|
|
||||||
function sendRequest(path: string, credentials?: Credentials, request: RequestInit = {}, params: string = "") {
|
function sendRequest(
|
||||||
if (typeof request !== "string" && credentials != null) {
|
path: string,
|
||||||
request.headers = { 'Authorization': `Bearer ${credentials.token}`, ...request.headers }
|
credentials?: Credentials,
|
||||||
}
|
request: RequestInit = {},
|
||||||
|
params: string = ''
|
||||||
|
) {
|
||||||
|
if (typeof request !== 'string' && credentials != null) {
|
||||||
|
request.headers = { Authorization: `Bearer ${credentials.token}`, ...request.headers };
|
||||||
|
}
|
||||||
|
|
||||||
let url = new URL(ENDPOINT);
|
let url = new URL(ENDPOINT);
|
||||||
url.pathname = path;
|
url.pathname = path;
|
||||||
url.search = params
|
url.search = params;
|
||||||
|
|
||||||
return fetch(url, request)
|
return fetch(url, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// **Safety:** The caller must enforce that the given request in progress must return the type `R`
|
/// **Safety:** The caller must enforce that the given request in progress must return the type `R`
|
||||||
async function asJson<R>(request: Promise<Response>): Promise<R> {
|
async function asJson<R>(request: Promise<Response>): Promise<R> {
|
||||||
let req = await request;
|
let req = await request;
|
||||||
return (await req.json() as R)
|
return (await req.json()) as R;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function login(credentials: {
|
export function login(credentials: {
|
||||||
email: string,
|
email: string;
|
||||||
password: string,
|
password: string;
|
||||||
}): Promise<{ token: string, } | { error: string, }> {
|
}): Promise<{ token: string } | { error: string }> {
|
||||||
return asJson(sendRequest('/auth/login', undefined, {
|
return asJson(
|
||||||
method: 'POST',
|
sendRequest('/auth/login', undefined, {
|
||||||
headers: {
|
method: 'POST',
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
},
|
'Content-Type': 'application/json'
|
||||||
body: JSON.stringify(credentials),
|
},
|
||||||
}))
|
body: JSON.stringify(credentials)
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function register(credentials: {
|
export function register(credentials: {
|
||||||
name: string,
|
name: string;
|
||||||
email: string,
|
email: string;
|
||||||
password: string,
|
password: string;
|
||||||
}): Promise<{ token: string, } | { error: string, }> {
|
}): Promise<{ token: string } | { error: string }> {
|
||||||
return asJson(sendRequest('/auth/register', undefined, {
|
return asJson(
|
||||||
method: 'POST',
|
sendRequest('/auth/register', undefined, {
|
||||||
headers: {
|
method: 'POST',
|
||||||
'Content-Type': 'application/json',
|
headers: {
|
||||||
},
|
'Content-Type': 'application/json'
|
||||||
body: JSON.stringify(credentials),
|
},
|
||||||
}))
|
body: JSON.stringify(credentials)
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function accountData(credentials: Credentials): Promise<Account | { error: string }> {
|
export function accountData(credentials: Credentials): Promise<Account | { error: string }> {
|
||||||
return asJson(sendRequest('/auth/account', credentials))
|
return asJson(sendRequest('/auth/account', credentials));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function genSessionKey(credentials: Credentials): Promise<{ session_key: string } | { error: string }> {
|
export function genSessionKey(
|
||||||
return asJson(sendRequest('/auth/genkey', credentials))
|
credentials: Credentials
|
||||||
|
): Promise<{ session_key: string } | { error: string }> {
|
||||||
|
return asJson(sendRequest('/auth/genkey', credentials));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function assetEndpoint(): Promise<string> {
|
export async function assetEndpoint(): Promise<string> {
|
||||||
let res = await sendRequest("/asset/endpoint")
|
let res = await sendRequest('/asset/endpoint');
|
||||||
return res.text()
|
return res.text();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function entryPage(credentials: Credentials, offset: number, limit: number): Promise<Entry[]> {
|
export async function entryPage(
|
||||||
return asJson(sendRequest('/entry/list', credentials, undefined, `?offset=${offset}&limit=${limit}`))
|
credentials: Credentials,
|
||||||
|
offset: number,
|
||||||
|
limit: number
|
||||||
|
): Promise<Entry[]> {
|
||||||
|
return asJson(
|
||||||
|
sendRequest('/entry/list', credentials, undefined, `?offset=${offset}&limit=${limit}`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addEntry(credentials: Credentials, entry: IdlessEntry): Promise<void> {
|
export async function addEntry(credentials: Credentials, entry: IdlessEntry): Promise<void> {
|
||||||
await sendRequest('/entry', credentials, {
|
await sendRequest('/entry', credentials, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify({entry}),
|
body: JSON.stringify({ entry })
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function deleteEntry(credentials: Credentials, entry_id: string): Promise<void> {
|
export async function deleteEntry(credentials: Credentials, entry_id: string): Promise<void> {
|
||||||
await sendRequest('/entry', credentials, { method: 'DELETE' }, `?entry_id=${entry_id}`)
|
await sendRequest('/entry', credentials, { method: 'DELETE' }, `?entry_id=${entry_id}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function updateHeirs(credentials: Credentials, heirs: AccountHeir[]): Promise<void> {
|
export async function updateHeirs(credentials: Credentials, heirs: AccountHeir[]): Promise<void> {
|
||||||
await sendRequest('/auth/heirs', credentials, {
|
await sendRequest('/auth/heirs', credentials, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json'
|
||||||
},
|
},
|
||||||
body: JSON.stringify(heirs),
|
body: JSON.stringify(heirs)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function uploadAsset(session_key: string, file: File): Promise<string> {
|
export async function uploadAsset(session_key: string, file: File): Promise<string> {
|
||||||
let url = new URL('/asset', ASSET_API_ENDPOINT);
|
let url = new URL('/asset', ASSET_API_ENDPOINT);
|
||||||
url.search = `?session_key=${session_key}`
|
url.search = `?session_key=${session_key}`;
|
||||||
|
|
||||||
let form = new FormData()
|
let form = new FormData();
|
||||||
form.append("file", file);
|
form.append('file', file);
|
||||||
|
|
||||||
let res = await fetch(url, {
|
let res = await fetch(url, {
|
||||||
method: "PUT",
|
method: 'PUT',
|
||||||
body: form,
|
body: form
|
||||||
})
|
});
|
||||||
|
|
||||||
let { asset_id } = await res.json();
|
let { asset_id } = await res.json();
|
||||||
return asset_id;
|
return asset_id;
|
||||||
}
|
}
|
BIN
identity-web/src/lib/assets/ladies.jpg
Normal file
BIN
identity-web/src/lib/assets/ladies.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.8 MiB |
BIN
identity-web/src/lib/assets/memory-photos.jpg
Normal file
BIN
identity-web/src/lib/assets/memory-photos.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 MiB |
|
@ -1,74 +1,84 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { faChevronDown, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faChevronDown, faPlus, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome";
|
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
|
||||||
import FeelingPill from "../../routes/dashboard/utils/FeelingPill.svelte";
|
import FeelingPill from '../../routes/dashboard/utils/FeelingPill.svelte';
|
||||||
import { FEELINGS, type KnownFeeling } from "$lib/entry";
|
import { FEELINGS, type KnownFeeling } from '$lib/entry';
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
export let required = false
|
export let required = false;
|
||||||
export let displayText = true
|
export let displayText = true;
|
||||||
export let slim = false
|
export let slim = false;
|
||||||
|
|
||||||
let feelingsDropdownShown = false
|
let feelingsDropdownShown = false;
|
||||||
export let chosenFeelings: KnownFeeling[] = []
|
export let chosenFeelings: KnownFeeling[] = [];
|
||||||
|
|
||||||
$: feelingsToChoose = FEELINGS.filter(v => !chosenFeelings.includes(v))
|
$: feelingsToChoose = FEELINGS.filter((v) => !chosenFeelings.includes(v));
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let dispatch = createEventDispatcher();
|
||||||
|
|
||||||
function addFeeling(feeling: KnownFeeling) {
|
function addFeeling(feeling: KnownFeeling) {
|
||||||
chosenFeelings = [feeling, ...chosenFeelings];
|
chosenFeelings = [feeling, ...chosenFeelings];
|
||||||
dispatch('choiceUpdated', chosenFeelings);
|
dispatch('choiceUpdated', chosenFeelings);
|
||||||
}
|
}
|
||||||
|
|
||||||
function removeFeeling(feeling: KnownFeeling) {
|
function removeFeeling(feeling: KnownFeeling) {
|
||||||
chosenFeelings = chosenFeelings.filter(v => v !== feeling)
|
chosenFeelings = chosenFeelings.filter((v) => v !== feeling);
|
||||||
dispatch('choiceUpdated', chosenFeelings);
|
dispatch('choiceUpdated', chosenFeelings);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
{#if displayText}
|
{#if displayText}
|
||||||
<span class="block mb-2 text-sm font-medium text-gray-900">Feelings</span>
|
<span class="mb-2 block text-sm font-medium text-gray-900">Feelings</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<button type="button" on:click={() => feelingsDropdownShown = !feelingsDropdownShown} class={`inline-flex gap-1.5 items-center px-2.5 text-sm text-gray-900 bg-gray-200 border rounded-e-0 border-gray-300 border-e-0 ${feelingsDropdownShown ? "rounded-tl-lg" : "rounded-s-lg"} hover:cursor-pointer hover:bg-gray-300`}>
|
<button
|
||||||
Feelings
|
type="button"
|
||||||
<FontAwesomeIcon icon={faChevronDown}/>
|
on:click={() => (feelingsDropdownShown = !feelingsDropdownShown)}
|
||||||
</button>
|
class={`rounded-e-0 inline-flex items-center gap-1.5 border border-e-0 border-gray-300 bg-gray-200 px-2.5 text-sm text-gray-900 ${feelingsDropdownShown ? 'rounded-tl-lg' : 'rounded-s-lg'} hover:cursor-pointer hover:bg-gray-300`}
|
||||||
<div id="add-entry__feelings" class={`rounded-none ${feelingsDropdownShown ? "rounded-tr-lg" : "rounded-e-lg"} bg-gray-50 border text-gray-900 focus:ring-violet-500 focus:border-violet-500 block flex-1 min-w-0 w-full text-sm border-gray-300 ${slim ? "p-2" : "p-2.5"}`}>
|
>
|
||||||
{#if chosenFeelings.length > 0}
|
Feelings
|
||||||
<div>
|
<FontAwesomeIcon icon={faChevronDown} />
|
||||||
<span class="mr-1">Chosen:</span>
|
</button>
|
||||||
{#each chosenFeelings as feeling (feeling)}
|
<div
|
||||||
<div class="inline">
|
id="add-entry__feelings"
|
||||||
<button type="button" on:click={() => removeFeeling(feeling)}>
|
class={`rounded-none ${feelingsDropdownShown ? 'rounded-tr-lg' : 'rounded-e-lg'} block w-full min-w-0 flex-1 border border-gray-300 bg-gray-50 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500 ${slim ? 'p-2' : 'p-2.5'}`}
|
||||||
<FeelingPill feeling={feeling} slim={slim}>
|
>
|
||||||
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faXmark}/></span>
|
{#if chosenFeelings.length > 0}
|
||||||
</FeelingPill>
|
<div>
|
||||||
</button>
|
<span class="mr-1">Chosen:</span>
|
||||||
<input type="checkbox" class="hidden" name={`feeling__${feeling}`} checked>
|
{#each chosenFeelings as feeling (feeling)}
|
||||||
</div>
|
<div class="inline">
|
||||||
{/each}
|
<button type="button" on:click={() => removeFeeling(feeling)}>
|
||||||
</div>
|
<FeelingPill {feeling} {slim}>
|
||||||
{:else}
|
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faXmark} /></span>
|
||||||
<span>No feelings chosen.</span>
|
</FeelingPill>
|
||||||
{#if required}
|
</button>
|
||||||
<span>You need to choose at least one feeling.</span>
|
<input type="checkbox" class="hidden" name={`feeling__${feeling}`} checked />
|
||||||
{/if}
|
</div>
|
||||||
{/if}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
<div class:hidden={!feelingsDropdownShown} class="bg-gray-50 border border-t-0 border-gray-300 py-3 px-1.5 rounded-b-lg">
|
<span>No feelings chosen.</span>
|
||||||
{#each feelingsToChoose as feeling (feeling)}
|
{#if required}
|
||||||
<label class={`capitalize ${slim ? "p-0.5" : "p-1"}`}>
|
<span>You need to choose at least one feeling.</span>
|
||||||
<button type="button" on:click={() => addFeeling(feeling)}>
|
{/if}
|
||||||
<FeelingPill feeling={feeling} slim={slim}>
|
{/if}
|
||||||
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faPlus}/></span>
|
</div>
|
||||||
</FeelingPill>
|
</div>
|
||||||
</button>
|
<div
|
||||||
</label>
|
class:hidden={!feelingsDropdownShown}
|
||||||
{/each}
|
class="rounded-b-lg border border-t-0 border-gray-300 bg-gray-50 px-1.5 py-3"
|
||||||
</div>
|
>
|
||||||
|
{#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>
|
</div>
|
|
@ -1,80 +1,149 @@
|
||||||
export const TITLED_ENTRIES = ["event", "environment", "memory"];
|
export const TITLED_ENTRIES = ['event', 'environment', 'memory'];
|
||||||
export const FEELINGS = ["relaxed", "afraid", "angry", "bad", "bored", "confused", "excited", "fine", "happy", "hurt", "in love", "mad", "nervous", "okay", "sad", "scared", "shy", "sleepy", "active", "surprised", "tired", "upset", "worried"];
|
export const FEELINGS = [
|
||||||
|
'relaxed',
|
||||||
|
'afraid',
|
||||||
|
'angry',
|
||||||
|
'bad',
|
||||||
|
'bored',
|
||||||
|
'confused',
|
||||||
|
'excited',
|
||||||
|
'fine',
|
||||||
|
'happy',
|
||||||
|
'hurt',
|
||||||
|
'in love',
|
||||||
|
'mad',
|
||||||
|
'nervous',
|
||||||
|
'okay',
|
||||||
|
'sad',
|
||||||
|
'scared',
|
||||||
|
'shy',
|
||||||
|
'sleepy',
|
||||||
|
'active',
|
||||||
|
'surprised',
|
||||||
|
'tired',
|
||||||
|
'upset',
|
||||||
|
'worried'
|
||||||
|
];
|
||||||
|
|
||||||
export type KnownFeeling = "relaxed" | "afraid" | "angry" | "bad" | "bored" | "confused" | "excited" | "fine" | "happy" | "hurt" | "in love" | "mad" | "nervous" | "okay" | "sad" | "scared" | "shy" | "sleepy" | "active" | "surprised" | "tired" | "upset" | "worried";
|
export type KnownFeeling =
|
||||||
export type EntryKind = "song" | "album" | "event" | "memory" | "feeling" | "environment" | "date";
|
| 'relaxed'
|
||||||
|
| 'afraid'
|
||||||
|
| 'angry'
|
||||||
|
| 'bad'
|
||||||
|
| 'bored'
|
||||||
|
| 'confused'
|
||||||
|
| 'excited'
|
||||||
|
| 'fine'
|
||||||
|
| 'happy'
|
||||||
|
| 'hurt'
|
||||||
|
| 'in love'
|
||||||
|
| 'mad'
|
||||||
|
| 'nervous'
|
||||||
|
| 'okay'
|
||||||
|
| 'sad'
|
||||||
|
| 'scared'
|
||||||
|
| 'shy'
|
||||||
|
| 'sleepy'
|
||||||
|
| 'active'
|
||||||
|
| 'surprised'
|
||||||
|
| 'tired'
|
||||||
|
| 'upset'
|
||||||
|
| 'worried';
|
||||||
|
export type EntryKind = 'song' | 'album' | 'event' | 'memory' | 'feeling' | 'environment' | 'date';
|
||||||
|
|
||||||
export type IdlessEntry = {
|
export type IdlessEntry = {
|
||||||
base: SongEntry | AlbumEntry | EventEntry | MemoryEntry | FeelingEntry | EnvironmentEntry | DateEntry,
|
base:
|
||||||
creationDate: string,
|
| SongEntry
|
||||||
feelings: (KnownFeeling | {
|
| AlbumEntry
|
||||||
identifier: string,
|
| EventEntry
|
||||||
description: string,
|
| MemoryEntry
|
||||||
backgroundColor: string,
|
| FeelingEntry
|
||||||
textColor: string,
|
| EnvironmentEntry
|
||||||
})[],
|
| DateEntry;
|
||||||
assets: string[],
|
creationDate: string;
|
||||||
title?: string,
|
feelings: (
|
||||||
description?: string,
|
| KnownFeeling
|
||||||
|
| {
|
||||||
|
identifier: string;
|
||||||
|
description: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
textColor: string;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
assets: string[];
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Entry = {
|
export type Entry = {
|
||||||
id: string,
|
id: string;
|
||||||
base: SongEntry | AlbumEntry | EventEntry | MemoryEntry | FeelingEntry | EnvironmentEntry | DateEntry,
|
base:
|
||||||
creationDate: string,
|
| SongEntry
|
||||||
feelings: (KnownFeeling | {
|
| AlbumEntry
|
||||||
identifier: string,
|
| EventEntry
|
||||||
description: string,
|
| MemoryEntry
|
||||||
backgroundColor: string,
|
| FeelingEntry
|
||||||
textColor: string,
|
| EnvironmentEntry
|
||||||
})[],
|
| DateEntry;
|
||||||
assets: string[],
|
creationDate: string;
|
||||||
title?: string,
|
feelings: (
|
||||||
description?: string,
|
| KnownFeeling
|
||||||
|
| {
|
||||||
|
identifier: string;
|
||||||
|
description: string;
|
||||||
|
backgroundColor: string;
|
||||||
|
textColor: string;
|
||||||
|
}
|
||||||
|
)[];
|
||||||
|
assets: string[];
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UniversalID = {
|
export type UniversalID = {
|
||||||
provider: string,
|
provider: string;
|
||||||
id: string,
|
id: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type SongEntry = {
|
export type SongEntry = {
|
||||||
kind: "song",
|
kind: 'song';
|
||||||
artist: string,
|
artist: string;
|
||||||
title: string,
|
title: string;
|
||||||
link: string[],
|
link: string[];
|
||||||
id: UniversalID[],
|
id: UniversalID[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type AlbumEntry = {
|
export type AlbumEntry = {
|
||||||
kind: "album",
|
kind: 'album';
|
||||||
artist: string,
|
artist: string;
|
||||||
title: string,
|
title: string;
|
||||||
link: string[],
|
link: string[];
|
||||||
id: UniversalID[],
|
id: UniversalID[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type EventEntry = {
|
export type EventEntry = {
|
||||||
kind: "event",
|
kind: 'event';
|
||||||
}
|
};
|
||||||
|
|
||||||
export type MemoryEntry = {
|
export type MemoryEntry = {
|
||||||
kind: "memory",
|
kind: 'memory';
|
||||||
}
|
};
|
||||||
|
|
||||||
export type FeelingEntry = {
|
export type FeelingEntry = {
|
||||||
kind: "feeling",
|
kind: 'feeling';
|
||||||
}
|
};
|
||||||
|
|
||||||
export type EnvironmentEntry = {
|
export type EnvironmentEntry = {
|
||||||
kind: "environment",
|
kind: 'environment';
|
||||||
location?: string | {
|
location?:
|
||||||
latitude: number,
|
| string
|
||||||
longitude: number,
|
| {
|
||||||
},
|
latitude: number;
|
||||||
}
|
longitude: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export type DateEntry = {
|
export type DateEntry = {
|
||||||
kind: "date",
|
kind: 'date';
|
||||||
referencedDate: string,
|
referencedDate: string;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,66 +1,67 @@
|
||||||
import { writable } from "svelte/store";
|
import { writable } from 'svelte/store';
|
||||||
import { accountData, assetEndpoint, genSessionKey, type Account, type Credentials } from "./api";
|
import { accountData, assetEndpoint, genSessionKey, type Account, type Credentials } from './api';
|
||||||
|
|
||||||
const CREDENTIALS_KEY = 'v0:credentials'
|
const CREDENTIALS_KEY = 'v0:credentials';
|
||||||
|
|
||||||
let _credentials: Credentials | null = null
|
let _credentials: Credentials | null = null;
|
||||||
export const credentials = writable<Credentials | null>()
|
export const credentials = writable<Credentials | null>();
|
||||||
credentials.subscribe((value) => {
|
credentials.subscribe((value) => {
|
||||||
if (value != null) {
|
if (value != null) {
|
||||||
_credentials = value;
|
_credentials = value;
|
||||||
localStorage.setItem( CREDENTIALS_KEY, JSON.stringify(value))
|
localStorage.setItem(CREDENTIALS_KEY, JSON.stringify(value));
|
||||||
} else {
|
} else {
|
||||||
_credentials = null;
|
_credentials = null;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
export const account = writable<Account | null>()
|
export const account = writable<Account | null>();
|
||||||
export const session_key = writable<string | null>()
|
export const session_key = writable<string | null>();
|
||||||
export const asset_endpoint = writable<string | null>()
|
export const asset_endpoint = writable<string | null>();
|
||||||
|
|
||||||
export async function initializeStores() {
|
export async function initializeStores() {
|
||||||
let rawCredentials = localStorage.getItem(CREDENTIALS_KEY)
|
let rawCredentials = localStorage.getItem(CREDENTIALS_KEY);
|
||||||
let parsedCredentials
|
let parsedCredentials;
|
||||||
if (rawCredentials != null && rawCredentials.length > 0) {
|
if (rawCredentials != null && rawCredentials.length > 0) {
|
||||||
try {
|
try {
|
||||||
parsedCredentials = JSON.parse(rawCredentials)
|
parsedCredentials = JSON.parse(rawCredentials);
|
||||||
credentials.set(parsedCredentials)
|
credentials.set(parsedCredentials);
|
||||||
}
|
} catch (e) {
|
||||||
catch (e) { localStorage.removeItem(CREDENTIALS_KEY) }
|
localStorage.removeItem(CREDENTIALS_KEY);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (parsedCredentials != null) {
|
if (parsedCredentials != null) {
|
||||||
let data = await accountData(parsedCredentials)
|
let data = await accountData(parsedCredentials);
|
||||||
if ('error' in data) {
|
if ('error' in data) {
|
||||||
credentials.set(null)
|
credentials.set(null);
|
||||||
localStorage.removeItem(CREDENTIALS_KEY)
|
localStorage.removeItem(CREDENTIALS_KEY);
|
||||||
} else {
|
} else {
|
||||||
account.set(data)
|
account.set(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
let key_result = await genSessionKey(parsedCredentials)
|
let key_result = await genSessionKey(parsedCredentials);
|
||||||
if ('error' in key_result) {
|
if ('error' in key_result) {
|
||||||
console.warn('Couldn\'t generate a session key!')
|
console.warn("Couldn't generate a session key!");
|
||||||
} else {
|
} else {
|
||||||
session_key.set(key_result.session_key)
|
session_key.set(key_result.session_key);
|
||||||
}
|
}
|
||||||
|
|
||||||
let asset_result = await assetEndpoint()
|
let asset_result = await assetEndpoint();
|
||||||
asset_endpoint.set(asset_result)
|
asset_endpoint.set(asset_result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshAccount() {
|
export async function refreshAccount() {
|
||||||
if (_credentials == null) {
|
if (_credentials == null) {
|
||||||
console.warn("Requested to refresh the user account but credentials are null.")
|
console.warn('Requested to refresh the user account but credentials are null.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let refreshedAccount = await accountData(_credentials)
|
let refreshedAccount = await accountData(_credentials);
|
||||||
if ('error' in refreshedAccount) {
|
if ('error' in refreshedAccount) {
|
||||||
console.warn("Failed to refresh the user account.")
|
console.warn('Failed to refresh the user account.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
account.set(refreshedAccount)
|
account.set(refreshedAccount);
|
||||||
}
|
}
|
5
identity-web/src/lib/variables.ts
Normal file
5
identity-web/src/lib/variables.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export const ENV_VARIABLES = {
|
||||||
|
IDENTITY_API_ENDPOINT: import.meta.env.VITE_IDENTITY_API_ENDPOINT,
|
||||||
|
ASSET_API_ENDPOINT: import.meta.env.VITE_ASSET_API_ENDPOINT,
|
||||||
|
SUPPORT_PAGE: import.meta.env.VITE_SUPPORT_PAGE
|
||||||
|
};
|
|
@ -1,34 +1,39 @@
|
||||||
<script>
|
<script>
|
||||||
import { credentials, initializeStores } from "$lib/stores";
|
import { credentials, initializeStores } from '$lib/stores';
|
||||||
import "../app.css";
|
import { ENV_VARIABLES } from '$lib/variables';
|
||||||
|
import '../app.css';
|
||||||
|
|
||||||
initializeStores()
|
initializeStores();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="py-3.5 flex text-white bg-violet-800 justify-center">
|
<div class="flex justify-center bg-violet-800 py-3.5 text-white">
|
||||||
<nav class="w-[60%] flex justify-between items-center">
|
<nav class="flex w-[60%] items-center justify-between">
|
||||||
<h1 class="font-serif text-3xl">
|
<h1 class="font-serif text-3xl">
|
||||||
{#if $credentials == null}
|
{#if $credentials == null}
|
||||||
<a href="/">Identity</a>
|
<a href="/">Identity</a>
|
||||||
{:else}
|
{:else}
|
||||||
<a href="/dashboard">Identity</a>
|
<a href="/dashboard">Identity</a>
|
||||||
{/if}
|
{/if}
|
||||||
</h1>
|
</h1>
|
||||||
<div class="text-xl">
|
<div class="text-xl">
|
||||||
{#if $credentials == null}
|
{#if $credentials == null}
|
||||||
| <div class="px-3 inline-block"><a href="/">Home</a></div>
|
| <div class="inline-block px-3"><a href="/">Home</a></div>
|
||||||
| <div class="px-3 inline-block"><a href="mailto:sofi@sofiaritz.com">Support</a></div>
|
|
|
||||||
| <div class="px-3 inline-block"><a href="/auth/register">Join</a></div>
|
<div class="inline-block px-3"><a href="mailto:sofi@sofiaritz.com">Support</a></div>
|
||||||
|
|
|
|
||||||
{:else}
|
<div class="inline-block px-3"><a href="/auth/register">Join</a></div>
|
||||||
| <div class="px-3 inline-block"><a href="/dashboard">Dashboard</a></div>
|
|
|
||||||
| <div class="px-3 inline-block"><a href="/auth/account">Account</a></div>
|
{:else}
|
||||||
| <div class="px-3 inline-block"><a href="mailto:sofi@sofiaritz.com">Support</a></div>
|
| <div class="inline-block px-3"><a href="/dashboard">Dashboard</a></div>
|
||||||
|
|
|
|
||||||
{/if}
|
<div class="inline-block px-3"><a href="/auth/account">Account</a></div>
|
||||||
</div>
|
|
|
||||||
</nav>
|
<div class="inline-block px-3"><a href={ENV_VARIABLES.SUPPORT_PAGE}>Support</a></div>
|
||||||
|
|
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<main class="pt-3.5">
|
<main class="pt-3.5">
|
||||||
<slot/>
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
// FIXME: Update code to support SSR
|
// FIXME: Update code to support SSR
|
||||||
export const ssr = false;
|
export const ssr = false;
|
||||||
|
export const prerender = false;
|
||||||
|
|
|
@ -1 +1,75 @@
|
||||||
<h1>Landing</h1>
|
<script lang="ts">
|
||||||
|
import photo1 from '$lib/assets/memory-photos.jpg';
|
||||||
|
import photo2 from '$lib/assets/ladies.jpg';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex justify-center bg-violet-100 pt-3.5">
|
||||||
|
<div class="flex w-[60%] flex-col">
|
||||||
|
<div class="my-1 flex">
|
||||||
|
<div class="w-full">
|
||||||
|
<h1 class="pt-6 font-serif text-4xl text-violet-700">
|
||||||
|
Store your memories for your future <span class="font-semibold italic">you.</span>
|
||||||
|
</h1>
|
||||||
|
<p class="pt-4 text-lg">
|
||||||
|
<span class="font-serif">Identity</span>
|
||||||
|
helps you store your memories and experiences. Our memories are our most precious belonging,
|
||||||
|
we should store them in a safe place.
|
||||||
|
</p>
|
||||||
|
<p class="pt-4 text-lg">
|
||||||
|
<span class="font-serif">Identity</span>
|
||||||
|
is an open-source software you can self-host to have full control over the storage of your
|
||||||
|
memories.
|
||||||
|
</p>
|
||||||
|
<p class="pt-4 text-lg">
|
||||||
|
This instance is maintained by volunnteers and financed by our community and sponsors. You
|
||||||
|
can export your data at any time.
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href="/auth/register"
|
||||||
|
class="focust:outline-none mr-3 mt-6 block rounded-lg bg-violet-700 px-5 py-2.5 text-center font-medium text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
|
||||||
|
>
|
||||||
|
Join now
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="w-2/3">
|
||||||
|
<img
|
||||||
|
class="aspect-square w-full border-8 border-violet-400 object-cover"
|
||||||
|
alt="Collage of Polaroid-like pictures"
|
||||||
|
src={photo1}
|
||||||
|
/>
|
||||||
|
<span class="my-1.5 block text-right">Photo by Lisa Fotios</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="my-1 flex">
|
||||||
|
<div class="w-2/3">
|
||||||
|
<img
|
||||||
|
class="aspect-square w-full border-8 border-violet-400 object-cover"
|
||||||
|
alt="Collage of Polaroid-like pictures"
|
||||||
|
src={photo2}
|
||||||
|
/>
|
||||||
|
<span class="my-1.5 block">Photo by cottonbro studio</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full">
|
||||||
|
<h1 class="pt-6 text-right font-serif text-4xl text-violet-700">
|
||||||
|
Remember your <span class="font-semibold italic">younger self.</span>
|
||||||
|
</h1>
|
||||||
|
<p class="pt-4 text-right text-lg">
|
||||||
|
Reminiscence and life story work can help dementia patients ease their symptoms.
|
||||||
|
<sup>
|
||||||
|
<a
|
||||||
|
href="https://www.alzheimers.org.uk/get-support/publications-factsheets/the-dementia-guide"
|
||||||
|
class="text-blue-600 hover:cursor-pointer hover:underline"
|
||||||
|
>
|
||||||
|
[1]
|
||||||
|
</a>
|
||||||
|
</sup>
|
||||||
|
</p>
|
||||||
|
<p class="pt-4 text-right text-lg">
|
||||||
|
Both your future you and your descendants may find your legacy useful:
|
||||||
|
<br />
|
||||||
|
environments, music, memories…
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -1,119 +1,177 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createForm } from "felte";
|
import { createForm } from 'felte';
|
||||||
import { account, credentials, refreshAccount } from "$lib/stores";
|
import { account, credentials, refreshAccount } from '$lib/stores';
|
||||||
import { type AccountHeir, updateHeirs } from "$lib/api";
|
import { type AccountHeir, updateHeirs } from '$lib/api';
|
||||||
|
|
||||||
credentials.subscribe((v) => v == null && (setTimeout(() => window.location.pathname = '/auth/login', 200)))
|
credentials.subscribe(
|
||||||
|
(v) => v == null && setTimeout(() => (window.location.pathname = '/auth/login'), 200)
|
||||||
|
);
|
||||||
|
|
||||||
let heirWizard = false;
|
let heirWizard = false;
|
||||||
|
|
||||||
const { form, errors } = createForm({
|
const { form, errors } = createForm({
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
let heir: AccountHeir = {
|
let heir: AccountHeir = {
|
||||||
contactMethod: values.contactMethod,
|
contactMethod: values.contactMethod,
|
||||||
name: values.name,
|
name: values.name,
|
||||||
value: values.contactDetails,
|
value: values.contactDetails
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentHeirs = structuredClone($account!.heirs)
|
let currentHeirs = structuredClone($account!.heirs);
|
||||||
let updatedHeirs = [heir, ...currentHeirs];
|
let updatedHeirs = [heir, ...currentHeirs];
|
||||||
|
|
||||||
await updateHeirs($credentials!, updatedHeirs);
|
await updateHeirs($credentials!, updatedHeirs);
|
||||||
await refreshAccount();
|
await refreshAccount();
|
||||||
|
|
||||||
heirWizard = false;
|
heirWizard = false;
|
||||||
},
|
},
|
||||||
validate: (values) => {
|
validate: (values) => {
|
||||||
let errors = {}
|
let errors = {};
|
||||||
|
|
||||||
if (values.contactMethod == null || values.contactMethod.length === 0) {
|
if (values.contactMethod == null || values.contactMethod.length === 0) {
|
||||||
errors['contactMethod'] = 'Must choose a contact method'
|
errors['contactMethod'] = 'Must choose a contact method';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.name == null || values.name.length === 0) {
|
if (values.name == null || values.name.length === 0) {
|
||||||
errors['name'] = 'Must not be empty'
|
errors['name'] = 'Must not be empty';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.contactDetails == null || values.contactDetails.length === 0) {
|
if (values.contactDetails == null || values.contactDetails.length === 0) {
|
||||||
errors['contactDetails'] = 'Must not be empty'
|
errors['contactDetails'] = 'Must not be empty';
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors
|
return errors;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
async function removeHeir(heir: AccountHeir) {
|
async function removeHeir(heir: AccountHeir) {
|
||||||
let currentHeirs = structuredClone($account!.heirs)
|
let currentHeirs = structuredClone($account!.heirs);
|
||||||
let updatedHeirs = currentHeirs
|
let updatedHeirs = currentHeirs.filter((v) => v.value !== heir.value);
|
||||||
.filter((v) => v.value !== heir.value);
|
|
||||||
|
|
||||||
await updateHeirs($credentials!, updatedHeirs);
|
await updateHeirs($credentials!, updatedHeirs);
|
||||||
await refreshAccount();
|
await refreshAccount();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mt-3.5 justify-center flex">
|
<div class="mt-3.5 flex justify-center">
|
||||||
<div class="w-[60%] flex flex-col">
|
<div class="flex w-[60%] flex-col">
|
||||||
<h1 class="text-2xl pb-3.5">Welcome back, <span class="font-bold">{$account?.name}</span>.</h1>
|
<h1 class="pb-3.5 text-2xl">
|
||||||
<div>
|
Welcome back, <span class="font-bold">{$account?.name}</span>
|
||||||
<div class="flex justify-between mb-2">
|
.
|
||||||
<h2 class="text-xl pb-2.5">Heirs</h2>
|
</h1>
|
||||||
{#if $account?.heirs.length > 0}
|
<div>
|
||||||
<button on:click={() => heirWizard = !heirWizard} class="rounded-lg bg-violet-700 text-white px-2.5 py-1 text-center hover:bg-violet-800 focus:ring-4 focus:ring-violet-300">
|
<div class="mb-2 flex justify-between">
|
||||||
+ Add a heir
|
<h2 class="pb-2.5 text-xl">Heirs</h2>
|
||||||
</button>
|
{#if $account?.heirs.length > 0}
|
||||||
{/if}
|
<button
|
||||||
</div>
|
on:click={() => (heirWizard = !heirWizard)}
|
||||||
{#if !heirWizard && $account?.heirs.length === 0}
|
class="rounded-lg bg-violet-700 px-2.5 py-1 text-center text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
|
||||||
<div class="flex flex-col">
|
>
|
||||||
<button on:click={() => heirWizard = true} class="flex h-60 flex-col items-center justify-center gap-3 rounded border border-gray-300 p-2 text-black">
|
+ Add a heir
|
||||||
<span class="text-4xl">+</span>
|
</button>
|
||||||
<h2 class="text-xl font-semibold">Add a heir</h2>
|
{/if}
|
||||||
</button>
|
</div>
|
||||||
</div>
|
{#if !heirWizard && $account?.heirs.length === 0}
|
||||||
{/if}
|
<div class="flex flex-col">
|
||||||
{#if heirWizard}
|
<button
|
||||||
<div class="border border-gray-200 rounded-lg shadow w-full flex flex-col p-3.5 mb-4">
|
on:click={() => (heirWizard = true)}
|
||||||
<form use:form>
|
class="flex h-60 flex-col items-center justify-center gap-3 rounded border border-gray-300 p-2 text-black"
|
||||||
<div class="mb-5">
|
>
|
||||||
<label for="heir__contact-method" class="block mb-2 text-sm font-medium text-gray-900">Contact method</label>
|
<span class="text-4xl">+</span>
|
||||||
<select id="heir__contact-method" name="contactMethod" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
<h2 class="text-xl font-semibold">Add a heir</h2>
|
||||||
<option value="" selected>Choose a contact method</option>
|
</button>
|
||||||
<option value="email">Email</option>
|
</div>
|
||||||
</select>
|
{/if}
|
||||||
{#if $errors.contactMethod != null}
|
{#if heirWizard}
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.contactMethod[0]}</span></p>
|
<div class="mb-4 flex w-full flex-col rounded-lg border border-gray-200 p-3.5 shadow">
|
||||||
{/if}
|
<form use:form>
|
||||||
</div>
|
<div class="mb-5">
|
||||||
<div class="mb-5">
|
<label
|
||||||
<label for="heir__name" class="block mb-2 text-sm font-medium text-gray-900">Heir name</label>
|
for="heir__contact-method"
|
||||||
<input id="heir__name" type="text" name="name" placeholder="Jane Doe" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
class="mb-2 block text-sm font-medium text-gray-900"
|
||||||
{#if $errors.name != null}
|
>
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.name[0]}</span></p>
|
Contact method
|
||||||
{/if}
|
</label>
|
||||||
</div>
|
<select
|
||||||
<div class="mb-5">
|
id="heir__contact-method"
|
||||||
<label for="heir__contactDetails" class="block mb-2 text-sm font-medium text-gray-900">Contact details</label>
|
name="contactMethod"
|
||||||
<input id="heir__contactDetails" type="text" name="contactDetails" placeholder="jane@identity.net" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
|
||||||
{#if $errors.contactDetails != null}
|
>
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.contactDetails[0]}</span></p>
|
<option value="" selected>Choose a contact method</option>
|
||||||
{/if}
|
<option value="email">Email</option>
|
||||||
</div>
|
</select>
|
||||||
<button class="mt-2 text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focust:outline-none focus:ring-violet-300 font-medium rounded-lg px-5 py-2.5 text-center" type="submit">Add new heir</button>
|
{#if $errors.contactMethod != null}
|
||||||
</form>
|
<p class="mt-2 text-sm text-red-600">
|
||||||
</div>
|
<span class="font-medium">{$errors.contactMethod[0]}</span>
|
||||||
{/if}
|
</p>
|
||||||
{#each $account?.heirs || [] as heir (heir.value)}
|
{/if}
|
||||||
<div class="border border-gray-200 rounded-lg shadow w-full flex flex-col p-3.5 mb-2.5">
|
</div>
|
||||||
<div class="flex justify-between">
|
<div class="mb-5">
|
||||||
<span class="block text-sm font-medium text-gray-900">Contact method: <span class="capitalize">{heir.contactMethod}</span></span>
|
<label for="heir__name" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
<button on:click={() => removeHeir(heir)} class="rounded-lg bg-red-600 text-white px-2.5 py-1 text-center hover:bg-red-700 focus:ring-4 focus:ring-violet-300">Delete heir</button>
|
Heir name
|
||||||
</div>
|
</label>
|
||||||
<div>
|
<input
|
||||||
<span>{heir.name}</span> · <span>{heir.value}</span>
|
id="heir__name"
|
||||||
</div>
|
type="text"
|
||||||
</div>
|
name="name"
|
||||||
{/each}
|
placeholder="Jane Doe"
|
||||||
</div>
|
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||||
</div>
|
/>
|
||||||
|
{#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>
|
</div>
|
|
@ -1,73 +1,108 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { login, type Credentials } from "$lib/api";
|
import { login, type Credentials } from '$lib/api';
|
||||||
import { credentials } from "$lib/stores";
|
import { credentials } from '$lib/stores';
|
||||||
import { createForm } from "felte";
|
import { createForm } from 'felte';
|
||||||
|
|
||||||
let submitError: string | undefined
|
let submitError: string | undefined;
|
||||||
|
|
||||||
// FIXME: This is a badly done hack
|
// FIXME: This is a badly done hack
|
||||||
credentials.subscribe((v) => v != null && (setTimeout(() => window.location.pathname = '/dashboard', 200)))
|
credentials.subscribe(
|
||||||
|
(v) => v != null && setTimeout(() => (window.location.pathname = '/dashboard'), 200)
|
||||||
|
);
|
||||||
|
|
||||||
const { form, errors } = createForm({
|
const { form, errors } = createForm({
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
return login(values)
|
return login(values);
|
||||||
},
|
},
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
// @ts-ignore - FIXME: How to tell the checker that this is right
|
// @ts-ignore - FIXME: How to tell the checker that this is right
|
||||||
if (response == null || ('error' in response && (typeof response['error'] !== "string" || !['invalid credentials'].includes(response['error'])))) {
|
if (
|
||||||
submitError = 'Something failed. Try again later.'
|
response == null ||
|
||||||
}
|
('error' in response &&
|
||||||
// @ts-ignore - FIXME: How to tell the checker that this is right
|
(typeof response['error'] !== 'string' ||
|
||||||
else if ('error' in response) {
|
!['invalid credentials'].includes(response['error'])))
|
||||||
// @ts-ignore - response is not null and the type of its key 'error' is a string
|
) {
|
||||||
submitError = 'Check your credentials and try again.'
|
submitError = 'Something failed. Try again later.';
|
||||||
} else {
|
}
|
||||||
credentials.set(response as Credentials)
|
// @ts-ignore - FIXME: How to tell the checker that this is right
|
||||||
// FIXME: This is a badly done hack
|
else if ('error' in response) {
|
||||||
setTimeout(() => window.location.pathname = '/dashboard', 200)
|
// @ts-ignore - response is not null and the type of its key 'error' is a string
|
||||||
}
|
submitError = 'Check your credentials and try again.';
|
||||||
},
|
} else {
|
||||||
validate: (values) => {
|
credentials.set(response as Credentials);
|
||||||
const errors = {}
|
// FIXME: This is a badly done hack
|
||||||
if (values.email == null || values.email.length === 0) {
|
setTimeout(() => (window.location.pathname = '/dashboard'), 200);
|
||||||
errors.email = 'Must not be empty'
|
}
|
||||||
}
|
},
|
||||||
|
validate: (values) => {
|
||||||
|
const errors = {};
|
||||||
|
if (values.email == null || values.email.length === 0) {
|
||||||
|
errors.email = 'Must not be empty';
|
||||||
|
}
|
||||||
|
|
||||||
if (values.password == null || values.password.length === 0) {
|
if (values.password == null || values.password.length === 0) {
|
||||||
errors.password = 'Must not be empty'
|
errors.password = 'Must not be empty';
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors
|
return errors;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mt-3.5 justify-center flex">
|
<div class="mt-3.5 flex justify-center">
|
||||||
<div class="w-[25%]">
|
<div class="w-[25%]">
|
||||||
<h1 class="text-2xl pb-3.5">Log in</h1>
|
<h1 class="pb-3.5 text-2xl">Log in</h1>
|
||||||
<form use:form>
|
<form use:form>
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<label for="register__email" class="block mb-2 text-sm font-medium text-gray-900">Your e-mail</label>
|
<label for="register__email" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
<input id="register__email" type="text" name="email" placeholder="jane@identity.net" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
Your e-mail
|
||||||
{#if $errors.email != null}
|
</label>
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.email[0]}</span></p>
|
<input
|
||||||
{/if}
|
id="register__email"
|
||||||
</div>
|
type="text"
|
||||||
<div class="mb-5">
|
name="email"
|
||||||
<label for="register__password" class="block mb-2 text-sm font-medium text-gray-900">Your password</label>
|
placeholder="jane@identity.net"
|
||||||
<input id="register__password" type="password" name="password" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||||
{#if $errors.password != null}
|
/>
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.password[0]}</span></p>
|
{#if $errors.email != null}
|
||||||
{/if}
|
<p class="mt-2 text-sm text-red-600">
|
||||||
</div>
|
<span class="font-medium">{$errors.email[0]}</span>
|
||||||
<button type="submit" class="text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focust:outline-none focus:ring-violet-300 font-medium rounded-lg w-full px-5 py-2.5 text-center">Log in</button>
|
</p>
|
||||||
{#if submitError != null && submitError.length > 0}
|
{/if}
|
||||||
<p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p>
|
</div>
|
||||||
{/if}
|
<div class="mb-5">
|
||||||
<div class="flex pt-3.5 w-full justify-between">
|
<label for="register__password" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
<a href="/auth/register" class="text-center font-medium text-blue-600 hover:underline">Create an account</a>
|
Your password
|
||||||
<a href="/auth/recovery" class="text-center font-medium text-blue-600 hover:underline">Forgotten password?</a>
|
</label>
|
||||||
</div>
|
<input
|
||||||
</form>
|
id="register__password"
|
||||||
</div>
|
type="password"
|
||||||
|
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>
|
</div>
|
|
@ -1,83 +1,128 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { register, type Credentials } from "$lib/api";
|
import { register, type Credentials } from '$lib/api';
|
||||||
import { credentials } from "$lib/stores";
|
import { credentials } from '$lib/stores';
|
||||||
import { createForm } from "felte";
|
import { createForm } from 'felte';
|
||||||
|
|
||||||
let submitError: string | undefined
|
let submitError: string | undefined;
|
||||||
|
|
||||||
// FIXME: This is a badly done hack
|
// FIXME: This is a badly done hack
|
||||||
credentials.subscribe((v) => v != null && (setTimeout(() => window.location.pathname = '/dashboard', 200)))
|
credentials.subscribe(
|
||||||
|
(v) => v != null && setTimeout(() => (window.location.pathname = '/dashboard'), 200)
|
||||||
|
);
|
||||||
|
|
||||||
const { form, errors } = createForm({
|
const { form, errors } = createForm({
|
||||||
onSubmit: (values) => {
|
onSubmit: (values) => {
|
||||||
return register(values)
|
return register(values);
|
||||||
},
|
},
|
||||||
onSuccess: (response) => {
|
onSuccess: (response) => {
|
||||||
// @ts-ignore - FIXME: How to tell the checker that this is right
|
// @ts-ignore - FIXME: How to tell the checker that this is right
|
||||||
if (response == null || ('error' in response && (typeof response['error'] !== "string" || !['invalid data'].includes(response['error'])))) {
|
if (
|
||||||
submitError = 'Something failed. Try again later.'
|
response == null ||
|
||||||
}
|
('error' in response &&
|
||||||
// @ts-ignore - FIXME: How to tell the checker that this is right
|
(typeof response['error'] !== 'string' || !['invalid data'].includes(response['error'])))
|
||||||
else if ('error' in response) {
|
) {
|
||||||
// @ts-ignore - response is not null and the type of its key 'error' is a string
|
submitError = 'Something failed. Try again later.';
|
||||||
submitError = 'Check your credentials and try again, this user may already exist.'
|
}
|
||||||
} else {
|
// @ts-ignore - FIXME: How to tell the checker that this is right
|
||||||
credentials.set(response as Credentials)
|
else if ('error' in response) {
|
||||||
// FIXME: This is a badly done hack
|
// @ts-ignore - response is not null and the type of its key 'error' is a string
|
||||||
setTimeout(() => window.location.pathname = '/dashboard', 200)
|
submitError = 'Check your credentials and try again, this user may already exist.';
|
||||||
}
|
} else {
|
||||||
},
|
credentials.set(response as Credentials);
|
||||||
validate: (values) => {
|
// FIXME: This is a badly done hack
|
||||||
const errors = {}
|
setTimeout(() => (window.location.pathname = '/dashboard'), 200);
|
||||||
if (values.name == null || values.name.length === 0) {
|
}
|
||||||
errors.name = 'Must not be empty'
|
},
|
||||||
}
|
validate: (values) => {
|
||||||
|
const errors = {};
|
||||||
|
if (values.name == null || values.name.length === 0) {
|
||||||
|
errors.name = 'Must not be empty';
|
||||||
|
}
|
||||||
|
|
||||||
if (values.email == null || !/^[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+/.test(values.email)) {
|
if (values.email == null || !/^[^@ \t\r\n]+@[^@ \t\r\n]+\.[^@ \t\r\n]+/.test(values.email)) {
|
||||||
errors.email = 'Must be a valid e-mail'
|
errors.email = 'Must be a valid e-mail';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.password == null || values.password.length === 0) {
|
if (values.password == null || values.password.length === 0) {
|
||||||
errors.password = 'Must not be empty'
|
errors.password = 'Must not be empty';
|
||||||
} else if (values.password != null && values.password.length < 12) {
|
} else if (values.password != null && values.password.length < 12) {
|
||||||
errors.password = 'Must be over 12 characters'
|
errors.password = 'Must be over 12 characters';
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors
|
return errors;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mt-3.5 justify-center flex">
|
<div class="mt-3.5 flex justify-center">
|
||||||
<div class="w-[25%]">
|
<div class="w-[25%]">
|
||||||
<h1 class="text-2xl pb-3.5">Register</h1>
|
<h1 class="pb-3.5 text-2xl">Register</h1>
|
||||||
<form use:form>
|
<form use:form>
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<label for="register__name" class="block mb-2 text-sm font-medium text-gray-900">Your name</label>
|
<label for="register__name" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
<input id="register__name" type="text" name="name" placeholder="Jane Doe" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
Your name
|
||||||
{#if $errors.name != null}
|
</label>
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.name[0]}</span></p>
|
<input
|
||||||
{/if}
|
id="register__name"
|
||||||
</div>
|
type="text"
|
||||||
<div class="mb-5">
|
name="name"
|
||||||
<label for="register__email" class="block mb-2 text-sm font-medium text-gray-900">Your e-mail</label>
|
placeholder="Jane Doe"
|
||||||
<input id="register__email" type="text" name="email" placeholder="jane@identity.net" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||||
{#if $errors.email != null}
|
/>
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.email[0]}</span></p>
|
{#if $errors.name != null}
|
||||||
{/if}
|
<p class="mt-2 text-sm text-red-600">
|
||||||
</div>
|
<span class="font-medium">{$errors.name[0]}</span>
|
||||||
<div class="mb-5">
|
</p>
|
||||||
<label for="register__password" class="block mb-2 text-sm font-medium text-gray-900">Your password</label>
|
{/if}
|
||||||
<input id="register__password" type="password" name="password" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
</div>
|
||||||
{#if $errors.password != null}
|
<div class="mb-5">
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.password[0]}</span></p>
|
<label for="register__email" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
{/if}
|
Your e-mail
|
||||||
</div>
|
</label>
|
||||||
<button type="submit" class="text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focust:outline-none focus:ring-violet-300 font-medium rounded-lg w-full px-5 py-2.5 text-center">Create user</button>
|
<input
|
||||||
{#if submitError != null && submitError.length > 0}
|
id="register__email"
|
||||||
<p class="mt-3.5 text-sm text-red-600"><span class="font-medium">{submitError}</span></p>
|
type="text"
|
||||||
{/if}
|
name="email"
|
||||||
<a href="/auth/login" class="block w-full text-center pt-3.5 font-medium text-blue-600 hover:underline">Already have an account?</a>
|
placeholder="jane@identity.net"
|
||||||
</form>
|
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||||
</div>
|
/>
|
||||||
|
{#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>
|
</div>
|
|
@ -1,154 +1,213 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { entryPage } from "$lib/api";
|
import { entryPage } from '$lib/api';
|
||||||
import { account, credentials } from "$lib/stores";
|
import { account, credentials } from '$lib/stores';
|
||||||
import { onMount } from "svelte";
|
import { onMount } from 'svelte';
|
||||||
import Entries from "./Entries.svelte";
|
import Entries from './Entries.svelte';
|
||||||
import Overview from "./Overview.svelte";
|
import Overview from './Overview.svelte';
|
||||||
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome";
|
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
|
||||||
import { faFilter } from "@fortawesome/free-solid-svg-icons";
|
import { faFilter } from '@fortawesome/free-solid-svg-icons';
|
||||||
import FilterSelector from "./utils/FilterSelector.svelte";
|
import FilterSelector from './utils/FilterSelector.svelte';
|
||||||
|
|
||||||
credentials.subscribe((v) => v == null && (setTimeout(() => window.location.pathname = '/auth/login', 200)))
|
credentials.subscribe(
|
||||||
|
(v) => v == null && setTimeout(() => (window.location.pathname = '/auth/login'), 200)
|
||||||
|
);
|
||||||
|
|
||||||
function createPageHandler({ onLoadingStatusChanged, onEndReached }: { onLoadingStatusChanged: (status: boolean) => any, onEndReached: () => any }) {
|
function createPageHandler({
|
||||||
let loadingPage = false;
|
onLoadingStatusChanged,
|
||||||
let rechedEnd = false;
|
onEndReached
|
||||||
let currentOffset = 10;
|
}: {
|
||||||
let step = 5;
|
onLoadingStatusChanged: (status: boolean) => any;
|
||||||
|
onEndReached: () => any;
|
||||||
|
}) {
|
||||||
|
let loadingPage = false;
|
||||||
|
let rechedEnd = false;
|
||||||
|
let currentOffset = 10;
|
||||||
|
let step = 5;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
initialEntries: entryPage($credentials!, 0, currentOffset),
|
initialEntries: entryPage($credentials!, 0, currentOffset),
|
||||||
nextPage: async () => {
|
nextPage: async () => {
|
||||||
if (loadingPage || reachedEnd) {
|
if (loadingPage || reachedEnd) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadingPage = true;
|
loadingPage = true;
|
||||||
onLoadingStatusChanged(loadingPage);
|
onLoadingStatusChanged(loadingPage);
|
||||||
|
|
||||||
let page = await entryPage($credentials!, currentOffset, step);
|
let page = await entryPage($credentials!, currentOffset, step);
|
||||||
currentOffset += step;
|
currentOffset += step;
|
||||||
|
|
||||||
loadingPage = false;
|
loadingPage = false;
|
||||||
onLoadingStatusChanged(loadingPage);
|
onLoadingStatusChanged(loadingPage);
|
||||||
if (page.length === 0) {
|
if (page.length === 0) {
|
||||||
reachedEnd = true;
|
reachedEnd = true;
|
||||||
onEndReached();
|
onEndReached();
|
||||||
}
|
}
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let overview = Promise.allSettled([entryPage($credentials!, 0, 3), entryPage($credentials!, 20, 3)])
|
let overview = Promise.allSettled([
|
||||||
|
entryPage($credentials!, 0, 3),
|
||||||
|
entryPage($credentials!, 20, 3)
|
||||||
|
]);
|
||||||
|
|
||||||
let loadingPage = false;
|
let loadingPage = false;
|
||||||
let reachedEnd = false;
|
let reachedEnd = false;
|
||||||
let filterStatus = false;
|
let filterStatus = false;
|
||||||
let { initialEntries: entries, nextPage } = createPageHandler({
|
let { initialEntries: entries, nextPage } = createPageHandler({
|
||||||
onLoadingStatusChanged: (status) => loadingPage = status,
|
onLoadingStatusChanged: (status) => (loadingPage = status),
|
||||||
onEndReached: () => reachedEnd = true,
|
onEndReached: () => (reachedEnd = true)
|
||||||
})
|
});
|
||||||
|
|
||||||
let showFilterSelector = false
|
let showFilterSelector = false;
|
||||||
let chosenFilterFeelings = []
|
let chosenFilterFeelings = [];
|
||||||
let filters = {
|
let filters = {
|
||||||
fromDate: null,
|
fromDate: null,
|
||||||
toDate: null,
|
toDate: null,
|
||||||
kind: null,
|
kind: null,
|
||||||
feelings: null,
|
feelings: null,
|
||||||
searchQuery: null,
|
searchQuery: null
|
||||||
}
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
function handleScroll() {
|
function handleScroll() {
|
||||||
if (!filterStatus && window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
if (!filterStatus && window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
||||||
entries.then(async (page) => {
|
entries.then(async (page) => {
|
||||||
let secondPage = await nextPage()
|
let secondPage = await nextPage();
|
||||||
if (secondPage != null) {
|
if (secondPage != null) {
|
||||||
page = [...page, ...secondPage];
|
page = [...page, ...secondPage];
|
||||||
entries = new Promise((resolve) => resolve(page))
|
entries = new Promise((resolve) => resolve(page));
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('scroll', handleScroll)
|
window.addEventListener('scroll', handleScroll);
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
})
|
});
|
||||||
|
|
||||||
function refreshEntries() {
|
function refreshEntries() {
|
||||||
entries = entryPage($credentials!, 0, 20);
|
entries = entryPage($credentials!, 0, 20);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $account != null}
|
{#if $account != null}
|
||||||
{#await entries}
|
{#await entries}
|
||||||
<div class="justify-center flex mt-3.5">
|
<div class="mt-3.5 flex justify-center">
|
||||||
<div role="status" class="flex flex-col justify-center items-center gap-5">
|
<div role="status" class="flex flex-col items-center justify-center gap-5">
|
||||||
<span class="text-2xl">Loading entries...</span>
|
<span class="text-2xl">Loading entries...</span>
|
||||||
<svg aria-hidden="true" class="inline w-9 h-9 text-gray-200 animate-spin fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
|
aria-hidden="true"
|
||||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
|
class="inline h-9 w-9 animate-spin fill-blue-600 text-gray-200"
|
||||||
</svg>
|
viewBox="0 0 100 101"
|
||||||
</div>
|
fill="none"
|
||||||
</div>
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
{:then entries}
|
>
|
||||||
<div class="mt-3.5 justify-center flex">
|
<path
|
||||||
<div class="w-[60%] flex flex-col">
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
{#if entries.length === 0}
|
fill="currentColor"
|
||||||
<a href="/entry/new" class="flex h-60 flex-col items-center justify-center gap-3 rounded border border-gray-300 p-2 text-black">
|
/>
|
||||||
<span class="text-4xl">+</span>
|
<path
|
||||||
<h2 class="text-xl font-semibold">Add an entry</h2>
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
</a>
|
fill="currentFill"
|
||||||
{:else}
|
/>
|
||||||
<h1 class="text-2xl pb-3.5">Welcome back, <span class="font-bold">{$account?.name}</span>.</h1>
|
</svg>
|
||||||
<div class="flex gap-2">
|
</div>
|
||||||
{#await overview}
|
</div>
|
||||||
<span>Loading...</span>
|
{:then entries}
|
||||||
{:then overview}
|
<div class="mt-3.5 flex justify-center">
|
||||||
<Overview latest={overview[0].value.filter(v => !["feeling"].includes(v.base.kind))} past={overview[1].value}/>
|
<div class="flex w-[60%] flex-col">
|
||||||
{/await}
|
{#if entries.length === 0}
|
||||||
</div>
|
<a
|
||||||
|
href="/entry/new"
|
||||||
|
class="flex h-60 flex-col items-center justify-center gap-3 rounded border border-gray-300 p-2 text-black"
|
||||||
|
>
|
||||||
|
<span class="text-4xl">+</span>
|
||||||
|
<h2 class="text-xl font-semibold">Add an entry</h2>
|
||||||
|
</a>
|
||||||
|
{:else}
|
||||||
|
<h1 class="pb-3.5 text-2xl">
|
||||||
|
Welcome back, <span class="font-bold">{$account?.name}</span>
|
||||||
|
.
|
||||||
|
</h1>
|
||||||
|
<div class="flex gap-2">
|
||||||
|
{#await overview}
|
||||||
|
<span>Loading...</span>
|
||||||
|
{:then overview}
|
||||||
|
<Overview
|
||||||
|
latest={overview[0].value.filter((v) => !['feeling'].includes(v.base.kind))}
|
||||||
|
past={overview[1].value}
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
|
</div>
|
||||||
|
|
||||||
<h2 class="text-2xl mt-6">Entries</h2>
|
<h2 class="mt-6 text-2xl">Entries</h2>
|
||||||
<div class="w-full flex items-baseline justify-between mt-2.5">
|
<div class="mt-2.5 flex w-full items-baseline justify-between">
|
||||||
<a class="rounded-lg bg-violet-700 text-white px-3 py-1.5 text-center hover:bg-violet-800 focus:ring-4 focus:ring-violet-300" href="/entry/new">+ Add an entry</a>
|
<a
|
||||||
<button on:click={() => showFilterSelector = !showFilterSelector}>
|
class="rounded-lg bg-violet-700 px-3 py-1.5 text-center text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
|
||||||
<FontAwesomeIcon icon={faFilter}/>
|
href="/entry/new"
|
||||||
<span class="ml-1.5">Filter entries</span>
|
>
|
||||||
</button>
|
+ Add an entry
|
||||||
</div>
|
</a>
|
||||||
|
<button on:click={() => (showFilterSelector = !showFilterSelector)}>
|
||||||
|
<FontAwesomeIcon icon={faFilter} />
|
||||||
|
<span class="ml-1.5">Filter entries</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if showFilterSelector}
|
{#if showFilterSelector}
|
||||||
<FilterSelector on:updatedChosenFeelings={(e) => chosenFilterFeelings = e.detail} on:updatedFilter={(e) => filters = e.detail} chosenFeelings={chosenFilterFeelings} filters={filters}/>
|
<FilterSelector
|
||||||
{/if}
|
on:updatedChosenFeelings={(e) => (chosenFilterFeelings = e.detail)}
|
||||||
|
on:updatedFilter={(e) => (filters = e.detail)}
|
||||||
|
chosenFeelings={chosenFilterFeelings}
|
||||||
|
{filters}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<div class="mt-3.5 flex flex-col gap-1">
|
<div class="mt-3.5 flex flex-col gap-1">
|
||||||
<Entries on:updatedFilterStatus={(e) => filterStatus = e.detail} on:deleted={() => refreshEntries()} entries={entries} filters={filters}/>
|
<Entries
|
||||||
</div>
|
on:updatedFilterStatus={(e) => (filterStatus = e.detail)}
|
||||||
{#if loadingPage && !reachedEnd}
|
on:deleted={() => refreshEntries()}
|
||||||
<div class="justify-center flex py-6">
|
{entries}
|
||||||
<div role="status" class="flex justify-center items-center gap-5">
|
{filters}
|
||||||
<span class="text-xl">Loading entries...</span>
|
/>
|
||||||
<svg aria-hidden="true" class="inline w-9 h-9 text-gray-200 animate-spin fill-blue-600" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
</div>
|
||||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor"/>
|
{#if loadingPage && !reachedEnd}
|
||||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill"/>
|
<div class="flex justify-center py-6">
|
||||||
</svg>
|
<div role="status" class="flex items-center justify-center gap-5">
|
||||||
</div>
|
<span class="text-xl">Loading entries...</span>
|
||||||
</div>
|
<svg
|
||||||
{/if}
|
aria-hidden="true"
|
||||||
|
class="inline h-9 w-9 animate-spin fill-blue-600 text-gray-200"
|
||||||
|
viewBox="0 0 100 101"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if reachedEnd}
|
{#if reachedEnd}
|
||||||
<div class="justify-center flex py-6">
|
<div class="flex justify-center py-6">
|
||||||
<div role="status" class="flex justify-center items-center gap-5">
|
<div role="status" class="flex items-center justify-center gap-5">
|
||||||
<span class="text-xl">You've reached the end</span>
|
<span class="text-xl">You've reached the end</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
|
@ -1,156 +1,171 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Entry as EntryType, EntryKind, KnownFeeling } from "$lib/entry";
|
import type { Entry as EntryType, EntryKind, KnownFeeling } from '$lib/entry';
|
||||||
import ExternalLink from "./utils/ExternalLink.svelte";
|
import ExternalLink from './utils/ExternalLink.svelte';
|
||||||
import FeelingPill from "./utils/FeelingPill.svelte";
|
import FeelingPill from './utils/FeelingPill.svelte';
|
||||||
import Entry from "./utils/Entry.svelte";
|
import Entry from './utils/Entry.svelte';
|
||||||
import EntryDescription from "./utils/EntryDescription.svelte";
|
import EntryDescription from './utils/EntryDescription.svelte';
|
||||||
import AssetPreview from "./utils/AssetPreview.svelte";
|
import AssetPreview from './utils/AssetPreview.svelte';
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from 'svelte';
|
||||||
import Fuse from "fuse.js";
|
import Fuse from 'fuse.js';
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let entries: EntryType[]
|
export let entries: EntryType[];
|
||||||
let filteredEntries = entries
|
let filteredEntries = entries;
|
||||||
export let filters: {
|
export let filters: {
|
||||||
fromDate: null | Date,
|
fromDate: null | Date;
|
||||||
toDate: null | Date,
|
toDate: null | Date;
|
||||||
kind: null | EntryKind[],
|
kind: null | EntryKind[];
|
||||||
feelings: null | {
|
feelings: null | {
|
||||||
exclusive: boolean,
|
exclusive: boolean;
|
||||||
feelings: KnownFeeling[],
|
feelings: KnownFeeling[];
|
||||||
},
|
};
|
||||||
searchQuery: null | string,
|
searchQuery: null | string;
|
||||||
}
|
};
|
||||||
let extended: string[] = []
|
let extended: string[] = [];
|
||||||
|
|
||||||
function applyFilters(filters: {
|
function applyFilters(filters: {
|
||||||
fromDate: null | Date,
|
fromDate: null | Date;
|
||||||
toDate: null | Date,
|
toDate: null | Date;
|
||||||
kind: null | EntryKind[],
|
kind: null | EntryKind[];
|
||||||
feelings: null | {
|
feelings: null | {
|
||||||
exclusive: boolean,
|
exclusive: boolean;
|
||||||
feelings: KnownFeeling[],
|
feelings: KnownFeeling[];
|
||||||
},
|
};
|
||||||
searchQuery: null | string,
|
searchQuery: null | string;
|
||||||
}) {
|
}) {
|
||||||
filteredEntries = entries
|
filteredEntries = entries;
|
||||||
|
|
||||||
if (filters.fromDate != null) {
|
if (filters.fromDate != null) {
|
||||||
filteredEntries = entries.filter((v) => new Date(v.creationDate) >= filters.fromDate!);
|
filteredEntries = entries.filter((v) => new Date(v.creationDate) >= filters.fromDate!);
|
||||||
}
|
}
|
||||||
if (filters.toDate != null) {
|
if (filters.toDate != null) {
|
||||||
filteredEntries = entries.filter((v) => new Date(v.creationDate) <= filters.toDate!);
|
filteredEntries = entries.filter((v) => new Date(v.creationDate) <= filters.toDate!);
|
||||||
}
|
}
|
||||||
if (filters.kind != null) {
|
if (filters.kind != null) {
|
||||||
filteredEntries = entries.filter((v) => filters.kind!.includes(v.base.kind));
|
filteredEntries = entries.filter((v) => filters.kind!.includes(v.base.kind));
|
||||||
}
|
}
|
||||||
if (filters.feelings != null) {
|
if (filters.feelings != null) {
|
||||||
let feelings = filters.feelings!.feelings
|
let feelings = filters.feelings!.feelings;
|
||||||
if (filters.feelings.exclusive) {
|
if (filters.feelings.exclusive) {
|
||||||
filteredEntries = entries.filter((v) => {
|
filteredEntries = entries.filter((v) => {
|
||||||
let v1 = v.feelings.filter((f) => typeof f === "string" && feelings.includes(f))
|
let v1 = v.feelings.filter((f) => typeof f === 'string' && feelings.includes(f));
|
||||||
return v.feelings.length === v1.length;
|
return v.feelings.length === v1.length;
|
||||||
})
|
});
|
||||||
} else {
|
} else {
|
||||||
filteredEntries = entries.filter((v) => {
|
filteredEntries = entries.filter((v) => {
|
||||||
let includes = false
|
let includes = false;
|
||||||
feelings.forEach((f) => {
|
feelings.forEach((f) => {
|
||||||
if (v.feelings.includes(f)) {
|
if (v.feelings.includes(f)) {
|
||||||
includes = true
|
includes = true;
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
return includes
|
return includes;
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (filters.searchQuery != null) {
|
if (filters.searchQuery != null) {
|
||||||
let fuse = new Fuse(entries, {
|
let fuse = new Fuse(entries, {
|
||||||
keys: [
|
keys: [
|
||||||
{
|
{
|
||||||
name: "title",
|
name: 'title',
|
||||||
weight: 2,
|
weight: 2
|
||||||
},
|
},
|
||||||
"description",
|
'description'
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
let results = fuse.search(filters.searchQuery!);
|
let results = fuse.search(filters.searchQuery!);
|
||||||
filteredEntries = results.map((v) => v.item);
|
filteredEntries = results.map((v) => v.item);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filteredEntries.length !== entries.length) {
|
if (filteredEntries.length !== entries.length) {
|
||||||
dispatch('updatedFilterStatus', true)
|
dispatch('updatedFilterStatus', true);
|
||||||
} else {
|
} else {
|
||||||
dispatch('updatedFilterStatus', false)
|
dispatch('updatedFilterStatus', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: applyFilters(filters)
|
$: applyFilters(filters);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if entries.length != filteredEntries.length && filteredEntries.length === 0}
|
{#if entries.length != filteredEntries.length && filteredEntries.length === 0}
|
||||||
<div class="justify-center flex py-6">
|
<div class="flex justify-center py-6">
|
||||||
<div role="status" class="flex justify-center items-center gap-5">
|
<div role="status" class="flex items-center justify-center gap-5">
|
||||||
<span class="text-xl">No results found</span>
|
<span class="text-xl">No results found</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#each filteredEntries as entry (entry.id)}
|
{#each filteredEntries as entry (entry.id)}
|
||||||
<Entry
|
<Entry
|
||||||
on:extended={(e) => extended = [e.detail.id, ...extended]}
|
on:extended={(e) => (extended = [e.detail.id, ...extended])}
|
||||||
on:contracted={(e) => extended = extended.filter(v => v !== e.detail.id)}
|
on:contracted={(e) => (extended = extended.filter((v) => v !== e.detail.id))}
|
||||||
on:deleted={(e) => { dispatch('deleted', e.detail) }}
|
on:deleted={(e) => {
|
||||||
|
dispatch('deleted', e.detail);
|
||||||
|
}}
|
||||||
|
id={entry.id}
|
||||||
|
kind={entry.base.kind}
|
||||||
|
creationDate={new Date(entry.creationDate)}
|
||||||
|
title={entry.base.kind === 'date'
|
||||||
|
? new Date(entry.base.referencedDate).toLocaleDateString()
|
||||||
|
: entry.title}
|
||||||
|
isExtended={extended.includes(entry.id)}
|
||||||
|
>
|
||||||
|
<div slot="contracted">
|
||||||
|
{#if entry.base.kind === 'song' || entry.base.kind === 'album'}
|
||||||
|
<ExternalLink href={entry.base.link[0]}>
|
||||||
|
{entry.base.artist} ‐ {entry.base.title}
|
||||||
|
</ExternalLink>
|
||||||
|
{/if}
|
||||||
|
|
||||||
id={entry.id}
|
{#if entry.base.kind === 'feeling'}
|
||||||
kind={entry.base.kind}
|
<div class="flex gap-1">
|
||||||
creationDate={new Date(entry.creationDate)}
|
{#each entry.feelings as feeling}
|
||||||
title={entry.base.kind === "date" ? new Date(entry.base.referencedDate).toLocaleDateString() : entry.title}
|
{#if typeof feeling === 'string'}
|
||||||
isExtended={extended.includes(entry.id)}
|
<FeelingPill {feeling} />
|
||||||
>
|
{:else}
|
||||||
<div slot="contracted">
|
<FeelingPill
|
||||||
{#if entry.base.kind === "song" || entry.base.kind === "album"}
|
feeling={feeling.identifier}
|
||||||
<ExternalLink href={entry.base.link[0]}>{entry.base.artist} ‐ {entry.base.title}</ExternalLink>
|
bgColor={feeling.backgroundColor}
|
||||||
{/if}
|
textColor={feeling.textColor}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
{#if entry.base.kind === "feeling"}
|
<div slot="extended">
|
||||||
<div class="flex gap-1">
|
<div class="mb-2 flex gap-1">
|
||||||
{#each entry.feelings as feeling}
|
{#each entry.feelings as feeling}
|
||||||
{#if typeof feeling === "string"}
|
{#if typeof feeling === 'string'}
|
||||||
<FeelingPill feeling={feeling}/>
|
<FeelingPill {feeling} />
|
||||||
{:else}
|
{:else}
|
||||||
<FeelingPill feeling={feeling.identifier} bgColor={feeling.backgroundColor} textColor={feeling.textColor}/>
|
<FeelingPill
|
||||||
{/if}
|
feeling={feeling.identifier}
|
||||||
{/each}
|
bgColor={feeling.backgroundColor}
|
||||||
</div>
|
textColor={feeling.textColor}
|
||||||
{/if}
|
/>
|
||||||
</div>
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div slot="extended">
|
{#if entry.base.kind === 'song' || entry.base.kind === 'album'}
|
||||||
<div class="flex gap-1 mb-2">
|
<ExternalLink href={entry.base.link[0]}>
|
||||||
{#each entry.feelings as feeling}
|
{entry.base.artist} ‐ {entry.base.title}
|
||||||
{#if typeof feeling === "string"}
|
</ExternalLink>
|
||||||
<FeelingPill feeling={feeling}/>
|
{/if}
|
||||||
{:else}
|
|
||||||
<FeelingPill feeling={feeling.identifier} bgColor={feeling.backgroundColor} textColor={feeling.textColor}/>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if entry.base.kind === "song" || entry.base.kind === "album"}
|
{#if entry.description != null}
|
||||||
<ExternalLink href={entry.base.link[0]}>{entry.base.artist} ‐ {entry.base.title}</ExternalLink>
|
<EntryDescription>{entry.description}</EntryDescription>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if entry.description != null}
|
<div class="mt-2 flex gap-1">
|
||||||
<EntryDescription>{entry.description}</EntryDescription>
|
{#each entry.assets as asset}
|
||||||
{/if}
|
<AssetPreview asset_id={asset} />
|
||||||
|
{/each}
|
||||||
<div class="flex gap-1 mt-2">
|
</div>
|
||||||
{#each entry.assets as asset}
|
</div>
|
||||||
<AssetPreview asset_id={asset}/>
|
</Entry>
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Entry>
|
|
||||||
{/each}
|
{/each}
|
|
@ -1,26 +1,26 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type Entry } from "$lib/entry";
|
import { type Entry } from '$lib/entry';
|
||||||
import OverviewEntry from "./utils/OverviewEntry.svelte";
|
import OverviewEntry from './utils/OverviewEntry.svelte';
|
||||||
|
|
||||||
export let latest: Entry[];
|
export let latest: Entry[];
|
||||||
export let past: Entry[];
|
export let past: Entry[];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="p-6 border border-gray-200 rounded-lg shadow w-full">
|
<div class="w-full rounded-lg border border-gray-200 p-6 shadow">
|
||||||
<h2 class="text-xl">Latest activity</h2>
|
<h2 class="text-xl">Latest activity</h2>
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
{#each latest as entry (entry.id)}
|
{#each latest as entry (entry.id)}
|
||||||
<OverviewEntry entry={entry}/>
|
<OverviewEntry {entry} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if past.length > 0}
|
{#if past.length > 0}
|
||||||
<div class="p-6 border border-gray-200 rounded-lg shadow w-full">
|
<div class="w-full rounded-lg border border-gray-200 p-6 shadow">
|
||||||
<h2 class="text-xl">Memories from the past</h2>
|
<h2 class="text-xl">Memories from the past</h2>
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
{#each past as entry (entry.id)}
|
{#each past as entry (entry.id)}
|
||||||
<OverviewEntry entry={entry} showDate={true}/>
|
<OverviewEntry {entry} showDate={true} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
|
@ -1,32 +1,44 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { asset_endpoint, session_key } from "$lib/stores";
|
import { asset_endpoint, session_key } from '$lib/stores';
|
||||||
import { faArrowUpRightFromSquare, faFileAudio, faFileVideo, faImage } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome";
|
faArrowUpRightFromSquare,
|
||||||
import mime from "mime"
|
faFileAudio,
|
||||||
|
faFileVideo,
|
||||||
|
faImage
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
|
||||||
|
import mime from 'mime';
|
||||||
|
|
||||||
export let asset_id: string
|
export let asset_id: string;
|
||||||
|
|
||||||
// FIXME: This feels correct, but how is it guaranteed that session_key and asset_endpoint are not null?
|
// FIXME: This feels correct, but how is it guaranteed that session_key and asset_endpoint are not null?
|
||||||
$: href = new URL(`/asset?asset_id=${encodeURIComponent(asset_id)}&session_key=${encodeURIComponent($session_key!)}`, $asset_endpoint!).href
|
$: href = new URL(
|
||||||
$: kind = mime.getType(asset_id.split(".")[1])?.split("/")[0]
|
`/asset?asset_id=${encodeURIComponent(asset_id)}&session_key=${encodeURIComponent($session_key!)}`,
|
||||||
|
$asset_endpoint!
|
||||||
|
).href;
|
||||||
|
$: kind = mime.getType(asset_id.split('.')[1])?.split('/')[0];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a class="font-bold bg-violet-600 text-white px-2.5 py-1 rounded flex gap-2 items-center" target="_blank" href={href}>
|
<a
|
||||||
{#if kind == null}
|
class="flex items-center gap-2 rounded bg-violet-600 px-2.5 py-1 font-bold text-white"
|
||||||
<FontAwesomeIcon icon={faArrowUpRightFromSquare}/>
|
target="_blank"
|
||||||
{:else if kind === "image"}
|
{href}
|
||||||
<FontAwesomeIcon icon={faImage}/>
|
>
|
||||||
{:else if kind === "audio"}
|
{#if kind == null}
|
||||||
<FontAwesomeIcon icon={faFileAudio}/>
|
<FontAwesomeIcon icon={faArrowUpRightFromSquare} />
|
||||||
{:else if kind === "video"}
|
{:else if kind === 'image'}
|
||||||
<FontAwesomeIcon icon={faFileVideo}/>
|
<FontAwesomeIcon icon={faImage} />
|
||||||
{:else}
|
{:else if kind === 'audio'}
|
||||||
<FontAwesomeIcon icon={faArrowUpRightFromSquare}/>
|
<FontAwesomeIcon icon={faFileAudio} />
|
||||||
{/if}
|
{:else if kind === 'video'}
|
||||||
|
<FontAwesomeIcon icon={faFileVideo} />
|
||||||
|
{:else}
|
||||||
|
<FontAwesomeIcon icon={faArrowUpRightFromSquare} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if kind != null && kind !== "application"}
|
{#if kind != null && kind !== 'application'}
|
||||||
<span class="capitalize">{kind}</span>
|
<span class="capitalize">{kind}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span>Asset</span>
|
<span>Asset</span>
|
||||||
{/if}
|
{/if}
|
||||||
</a>
|
</a>
|
|
@ -1,78 +1,96 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { deleteEntry } from "$lib/api";
|
import { deleteEntry } from '$lib/api';
|
||||||
import { credentials } from "$lib/stores";
|
import { credentials } from '$lib/stores';
|
||||||
import { TITLED_ENTRIES } from "$lib/entry";
|
import { TITLED_ENTRIES } from '$lib/entry';
|
||||||
import EntryKind from "./EntryKind.svelte";
|
import EntryKind from './EntryKind.svelte';
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let creationDate: Date;
|
export let creationDate: Date;
|
||||||
export let kind: "song" | "album" | "event" | "feeling" | "environment" | "date" | "memory";
|
export let kind: 'song' | 'album' | 'event' | 'feeling' | 'environment' | 'date' | 'memory';
|
||||||
export let title: string | undefined;
|
export let title: string | undefined;
|
||||||
|
|
||||||
export let isExtended = false;
|
export let isExtended = false;
|
||||||
let prevExtended = isExtended
|
let prevExtended = isExtended;
|
||||||
|
|
||||||
$: if (prevExtended !== isExtended) {
|
$: if (prevExtended !== isExtended) {
|
||||||
dispatch(isExtended ? 'extended' : 'contracted', { id })
|
dispatch(isExtended ? 'extended' : 'contracted', { id });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processDeletion(id: string) {
|
async function processDeletion(id: string) {
|
||||||
await deleteEntry($credentials!, id);
|
await deleteEntry($credentials!, id);
|
||||||
dispatch('deleted', {
|
dispatch('deleted', {
|
||||||
id,
|
id
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$: cardClass = () => {
|
$: cardClass = () => {
|
||||||
let cardClass = "border border-gray-200 rounded-lg shadow w-full flex p-3.5"
|
let cardClass = 'border border-gray-200 rounded-lg shadow w-full flex p-3.5';
|
||||||
|
|
||||||
if (isExtended) {
|
if (isExtended) {
|
||||||
cardClass += " flex-col gap-1.5"
|
cardClass += ' flex-col gap-1.5';
|
||||||
} else {
|
} else {
|
||||||
if (TITLED_ENTRIES.includes(kind)) {
|
if (TITLED_ENTRIES.includes(kind)) {
|
||||||
cardClass += " flex-col"
|
cardClass += ' flex-col';
|
||||||
} else {
|
} else {
|
||||||
cardClass += " gap-4 items-center"
|
cardClass += ' gap-4 items-center';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return cardClass
|
return cardClass;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={cardClass()} id={`entry__${id}`}>
|
<div class={cardClass()} id={`entry__${id}`}>
|
||||||
<button on:click={() => { prevExtended = isExtended; isExtended = !isExtended }}>
|
<button
|
||||||
<div class="flex justify-between items-center">
|
on:click={() => {
|
||||||
<div class="flex items-center gap-2.5">
|
prevExtended = isExtended;
|
||||||
<EntryKind kind={kind}/>
|
isExtended = !isExtended;
|
||||||
{#if title != null && isExtended}
|
}}
|
||||||
<span>Created at: <time datetime={creationDate.toISOString()}>{creationDate.toLocaleDateString()}</time></span>
|
>
|
||||||
{:else if title != null}
|
<div class="flex items-center justify-between">
|
||||||
<h2 class="text-xl text-left font-semibold">{title}</h2>
|
<div class="flex items-center gap-2.5">
|
||||||
{:else if isExtended}
|
<EntryKind {kind} />
|
||||||
<span>Created at: <time datetime={creationDate.toISOString()}>{creationDate.toLocaleDateString()}</time></span>
|
{#if title != null && isExtended}
|
||||||
{/if}
|
<span>
|
||||||
</div>
|
Created at: <time datetime={creationDate.toISOString()}>
|
||||||
{#if isExtended}
|
{creationDate.toLocaleDateString()}
|
||||||
<button on:click={() => processDeletion(id)} class="rounded-lg bg-red-600 text-white px-2.5 py-1.5 text-center hover:bg-red-700 focus:ring-4 focus:ring-violet-300">Delete entry</button>
|
</time>
|
||||||
{/if}
|
</span>
|
||||||
</div>
|
{:else if title != null}
|
||||||
|
<h2 class="text-left text-xl font-semibold">{title}</h2>
|
||||||
|
{: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}
|
{#if title != null && isExtended}
|
||||||
<h2 class="text-xl text-left font-semibold mt-2">{title}</h2>
|
<h2 class="mt-2 text-left text-xl font-semibold">{title}</h2>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<slot/>
|
<slot />
|
||||||
|
|
||||||
{#if !isExtended}
|
{#if !isExtended}
|
||||||
<slot name="contracted"/>
|
<slot name="contracted" />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if isExtended}
|
{#if isExtended}
|
||||||
<slot name="extended"/>
|
<slot name="extended" />
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
|
@ -1,3 +1,3 @@
|
||||||
<p class="w-full text-left">
|
<p class="w-full text-left">
|
||||||
<slot/>
|
<slot />
|
||||||
</p>
|
</p>
|
|
@ -1,24 +1,45 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { faCalendarDays, faChampagneGlasses, faHeartPulse, faLandmarkDome, faMusic, faNewspaper, faRecordVinyl, faSeedling } from "@fortawesome/free-solid-svg-icons";
|
import {
|
||||||
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome";
|
faCalendarDays,
|
||||||
|
faChampagneGlasses,
|
||||||
|
faHeartPulse,
|
||||||
|
faLandmarkDome,
|
||||||
|
faMusic,
|
||||||
|
faNewspaper,
|
||||||
|
faRecordVinyl,
|
||||||
|
faSeedling
|
||||||
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
|
||||||
|
|
||||||
export let kind: "song" | "album" | "event" | "feeling" | "environment" | "date" | "memory"
|
export let kind: 'song' | 'album' | 'event' | 'feeling' | 'environment' | 'date' | 'memory';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if kind === "song"}
|
{#if kind === 'song'}
|
||||||
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faMusic}/> Song</span>
|
<span class="flex items-center gap-2.5 text-xl"><FontAwesomeIcon icon={faMusic} /> Song</span>
|
||||||
{:else if kind === "album"}
|
{:else if kind === 'album'}
|
||||||
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faRecordVinyl}/> Album</span>
|
<span class="flex items-center gap-2.5 text-xl">
|
||||||
{:else if kind === "event"}
|
<FontAwesomeIcon icon={faRecordVinyl} /> Album
|
||||||
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faChampagneGlasses}/> Event</span>
|
</span>
|
||||||
{:else if kind === "memory"}
|
{:else if kind === 'event'}
|
||||||
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faNewspaper}/> Memory</span>
|
<span class="flex items-center gap-2.5 text-xl">
|
||||||
{:else if kind === "feeling"}
|
<FontAwesomeIcon icon={faChampagneGlasses} /> Event
|
||||||
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faHeartPulse}/> Feeling</span>
|
</span>
|
||||||
{:else if kind === "environment"}
|
{:else if kind === 'memory'}
|
||||||
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faSeedling}/> Environment</span>
|
<span class="flex items-center gap-2.5 text-xl">
|
||||||
{:else if kind === "date"}
|
<FontAwesomeIcon icon={faNewspaper} /> Memory
|
||||||
<span class="text-xl flex gap-2.5 items-center"><FontAwesomeIcon icon={faCalendarDays}/> Date</span>
|
</span>
|
||||||
|
{:else if kind === 'feeling'}
|
||||||
|
<span class="flex items-center gap-2.5 text-xl">
|
||||||
|
<FontAwesomeIcon icon={faHeartPulse} /> Feeling
|
||||||
|
</span>
|
||||||
|
{:else if kind === 'environment'}
|
||||||
|
<span class="flex items-center gap-2.5 text-xl">
|
||||||
|
<FontAwesomeIcon icon={faSeedling} /> Environment
|
||||||
|
</span>
|
||||||
|
{:else if kind === 'date'}
|
||||||
|
<span class="flex items-center gap-2.5 text-xl">
|
||||||
|
<FontAwesomeIcon icon={faCalendarDays} /> Date
|
||||||
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span>Unknown value. Try loading the page again.</span>
|
<span>Unknown value. Try loading the page again.</span>
|
||||||
{/if}
|
{/if}
|
|
@ -1,11 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { faArrowUpRightFromSquare } from "@fortawesome/free-solid-svg-icons";
|
import { faArrowUpRightFromSquare } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome";
|
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
|
||||||
|
|
||||||
export let href: string
|
export let href: string;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<a class="font-bold text-violet-600 text-left flex gap-2 items-center hover:underline" target="_blank" href={href}>
|
<a
|
||||||
<FontAwesomeIcon icon={faArrowUpRightFromSquare}/>
|
class="flex items-center gap-2 text-left font-bold text-violet-600 hover:underline"
|
||||||
<slot/>
|
target="_blank"
|
||||||
|
{href}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faArrowUpRightFromSquare} />
|
||||||
|
<slot />
|
||||||
</a>
|
</a>
|
|
@ -1,40 +1,67 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// TODO: Design a more _formal_ color system for emotions (>strong = >weight, etc).
|
// TODO: Design a more _formal_ color system for emotions (>strong = >weight, etc).
|
||||||
const DEFAULT_COLORS: {[index: string]: string[]} = {
|
const DEFAULT_COLORS: { [index: string]: string[] } = {
|
||||||
"__DEFAULT__": ["#0a0a0a", "#fafafa"],
|
__DEFAULT__: ['#0a0a0a', '#fafafa'],
|
||||||
"afraid": ["#fda4af", "#0a0a0a"],
|
afraid: ['#fda4af', '#0a0a0a'],
|
||||||
"angry": ["#dc2626", "#fafafa"],
|
angry: ['#dc2626', '#fafafa'],
|
||||||
"bad": ["#450a0a", "#fafafa"],
|
bad: ['#450a0a', '#fafafa'],
|
||||||
"bored": ["#d4d4d8", "#0a0a0a"],
|
bored: ['#d4d4d8', '#0a0a0a'],
|
||||||
"confused": ["#fef3c7", "#0a0a0a"],
|
confused: ['#fef3c7', '#0a0a0a'],
|
||||||
"excited": ["#f97316", "#fafafa"],
|
excited: ['#f97316', '#fafafa'],
|
||||||
"fine": ["#bef264", "#0a0a0a"],
|
fine: ['#bef264', '#0a0a0a'],
|
||||||
"happy": ["#facc15", "#0a0a0a"],
|
happy: ['#facc15', '#0a0a0a'],
|
||||||
"hurt": ["#ff69b4", "#0a0a0a"],
|
hurt: ['#ff69b4', '#0a0a0a'],
|
||||||
"in love": ["#ff1493", "#fafafa"],
|
'in love': ['#ff1493', '#fafafa'],
|
||||||
"mad": ["#450a0a", "#fafafa"],
|
mad: ['#450a0a', '#fafafa'],
|
||||||
"nervous": ["#7e22ce", "#fafafa"],
|
nervous: ['#7e22ce', '#fafafa'],
|
||||||
"okay": ["#86efac", "#0a0a0a"],
|
okay: ['#86efac', '#0a0a0a'],
|
||||||
"sad": ["#0284c7", "#fafafa"],
|
sad: ['#0284c7', '#fafafa'],
|
||||||
"scared": ["#334155", "#fafafa"],
|
scared: ['#334155', '#fafafa'],
|
||||||
"shy": ["#cbd5e1", "#0a0a0a"],
|
shy: ['#cbd5e1', '#0a0a0a'],
|
||||||
"sleepy": ["#7dd3fc", "#0a0a0a"],
|
sleepy: ['#7dd3fc', '#0a0a0a'],
|
||||||
"active": ["#059669", "#fafafa"],
|
active: ['#059669', '#fafafa'],
|
||||||
"surprised": ["#fbbf24", "#0a0a0a"],
|
surprised: ['#fbbf24', '#0a0a0a'],
|
||||||
"tired": ["#92400e", "#fafafa"],
|
tired: ['#92400e', '#fafafa'],
|
||||||
"upset": ["#b91c1c", "#fafafa"],
|
upset: ['#b91c1c', '#fafafa'],
|
||||||
"worried": ["#d4d4d8", "#0a0a0a"],
|
worried: ['#d4d4d8', '#0a0a0a'],
|
||||||
"relaxed": ["#86efac", "#0a0a0a"],
|
relaxed: ['#86efac', '#0a0a0a']
|
||||||
};
|
};
|
||||||
|
|
||||||
export let feeling: "relaxed" | "afraid" | "angry" | "bad" | "bored" | "confused" | "excited" | "fine" | "happy" | "hurt" | "in love" | "mad" | "nervous" | "okay" | "sad" | "scared" | "shy" | "sleepy" | "active" | "surprised" | "tired" | "upset" | "worried" | string
|
export let feeling:
|
||||||
export let bgColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS["__DEFAULT__"])[0]
|
| 'relaxed'
|
||||||
export let textColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS["__DEFAULT__"])[1]
|
| 'afraid'
|
||||||
export let slim = false;
|
| 'angry'
|
||||||
|
| 'bad'
|
||||||
|
| 'bored'
|
||||||
|
| 'confused'
|
||||||
|
| 'excited'
|
||||||
|
| 'fine'
|
||||||
|
| 'happy'
|
||||||
|
| 'hurt'
|
||||||
|
| 'in love'
|
||||||
|
| 'mad'
|
||||||
|
| 'nervous'
|
||||||
|
| 'okay'
|
||||||
|
| 'sad'
|
||||||
|
| 'scared'
|
||||||
|
| 'shy'
|
||||||
|
| 'sleepy'
|
||||||
|
| 'active'
|
||||||
|
| 'surprised'
|
||||||
|
| 'tired'
|
||||||
|
| 'upset'
|
||||||
|
| 'worried'
|
||||||
|
| string;
|
||||||
|
export let bgColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS['__DEFAULT__'])[0];
|
||||||
|
export let textColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS['__DEFAULT__'])[1];
|
||||||
|
export let slim = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={`inline-block py-0.5 px-1.5 rounded-full ${slim ? "text-xs" : "text-sm"} font-semibold w-22 text-center`} style={`background-color: ${bgColor}; color: ${textColor}`}>
|
<div
|
||||||
<slot name="pre"/>
|
class={`inline-block rounded-full px-1.5 py-0.5 ${slim ? 'text-xs' : 'text-sm'} w-22 text-center font-semibold`}
|
||||||
|
style={`background-color: ${bgColor}; color: ${textColor}`}
|
||||||
|
>
|
||||||
|
<slot name="pre" />
|
||||||
|
|
||||||
<span>{feeling.charAt(0).toUpperCase() + feeling.slice(1)}</span>
|
<span>{feeling.charAt(0).toUpperCase() + feeling.slice(1)}</span>
|
||||||
</div>
|
</div>
|
|
@ -1,105 +1,143 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FeelingsChooser from "$lib/components/FeelingsChooser.svelte";
|
import FeelingsChooser from '$lib/components/FeelingsChooser.svelte';
|
||||||
import type { EntryKind, KnownFeeling } from "$lib/entry";
|
import type { EntryKind, KnownFeeling } from '$lib/entry';
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from 'svelte';
|
||||||
import EntryKind from "./EntryKind.svelte";
|
import EntryKind from './EntryKind.svelte';
|
||||||
|
|
||||||
export let filters: {
|
export let filters: {
|
||||||
fromDate: null | Date,
|
fromDate: null | Date;
|
||||||
toDate: null | Date,
|
toDate: null | Date;
|
||||||
kind: null | EntryKind[],
|
kind: null | EntryKind[];
|
||||||
feelings: null | {
|
feelings: null | {
|
||||||
exclusive: boolean,
|
exclusive: boolean;
|
||||||
feelings: KnownFeeling[],
|
feelings: KnownFeeling[];
|
||||||
},
|
};
|
||||||
searchQuery: null | string,
|
searchQuery: null | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
let dispatch = createEventDispatcher()
|
let dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let chosenFeelings: KnownFeeling[] = []
|
export let chosenFeelings: KnownFeeling[] = [];
|
||||||
let touched = {
|
let touched = {
|
||||||
fromDate: false,
|
fromDate: false,
|
||||||
toDate: false,
|
toDate: false
|
||||||
}
|
};
|
||||||
|
|
||||||
function upstreamChanges() {
|
function upstreamChanges() {
|
||||||
dispatch('updatedFilter', filters)
|
dispatch('updatedFilter', filters);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleKind(kind: string | EntryKind) {
|
function handleKind(kind: string | EntryKind) {
|
||||||
if (kind.length === 0) {
|
if (kind.length === 0) {
|
||||||
filters.kind = null;
|
filters.kind = null;
|
||||||
} else {
|
} else {
|
||||||
filters.kind = [kind as EntryKind];
|
filters.kind = [kind as EntryKind];
|
||||||
}
|
}
|
||||||
upstreamChanges();
|
upstreamChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDate(date: string | null, kind: "fromDate" | "toDate") {
|
function handleDate(date: string | null, kind: 'fromDate' | 'toDate') {
|
||||||
touched[kind] = true;
|
touched[kind] = true;
|
||||||
|
|
||||||
if (date == null || date.length === 0) {
|
if (date == null || date.length === 0) {
|
||||||
filters[kind] = null
|
filters[kind] = null;
|
||||||
} else {
|
} else {
|
||||||
filters[kind] = new Date(date)
|
filters[kind] = new Date(date);
|
||||||
}
|
}
|
||||||
upstreamChanges()
|
upstreamChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSearchQuery(query: string) {
|
function handleSearchQuery(query: string) {
|
||||||
if (query.length === 0) {
|
if (query.length === 0) {
|
||||||
filters.searchQuery = null;
|
filters.searchQuery = null;
|
||||||
} else {
|
} else {
|
||||||
filters.searchQuery = query.trim();
|
filters.searchQuery = query.trim();
|
||||||
}
|
}
|
||||||
upstreamChanges()
|
upstreamChanges();
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFeelings(feelings: KnownFeeling[]) {
|
function handleFeelings(feelings: KnownFeeling[]) {
|
||||||
chosenFeelings = feelings;
|
chosenFeelings = feelings;
|
||||||
dispatch('updatedChosenFeelings', chosenFeelings)
|
dispatch('updatedChosenFeelings', chosenFeelings);
|
||||||
|
|
||||||
if (feelings.length === 0) {
|
if (feelings.length === 0) {
|
||||||
filters.feelings = null;
|
filters.feelings = null;
|
||||||
} else {
|
} else {
|
||||||
filters.feelings = {
|
filters.feelings = {
|
||||||
exclusive: false,
|
exclusive: false,
|
||||||
feelings,
|
feelings
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
upstreamChanges();
|
upstreamChanges();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex flex-col justify-between p-1 pt-3 gap-3">
|
<div class="flex flex-col justify-between gap-3 p-1 pt-3">
|
||||||
<div class="flex gap-4 justify-between w-full">
|
<div class="flex w-full justify-between gap-4">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<label for="filter__from-date" class="block mb-0.5 text-sm font-medium text-gray-900">From date</label>
|
<label for="filter__from-date" class="mb-0.5 block text-sm font-medium text-gray-900">
|
||||||
<input value={touched.fromDate ? undefined : filters.fromDate?.toISOString().split('T')[0]} id="filter__from-date" max={filters.toDate?.toISOString().split('T')[0]} on:change={(e) => handleDate(e.target.valueAsDate, "fromDate")} type="date" name="date" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-1.5">
|
From date
|
||||||
</div>
|
</label>
|
||||||
<div class="w-full">
|
<input
|
||||||
<label for="filter__to-date" class="block mb-0.5 text-sm font-medium text-gray-900">To date</label>
|
value={touched.fromDate ? undefined : filters.fromDate?.toISOString().split('T')[0]}
|
||||||
<input value={touched.toDate ? undefined : filters.toDate?.toISOString().split('T')[0]} id="filter__to-date" min={filters.fromDate?.toISOString().split('T')[0]} on:change={(e) => handleDate(e.target.valueAsDate, "toDate")} type="date" name="date" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-1.5">
|
id="filter__from-date"
|
||||||
</div>
|
max={filters.toDate?.toISOString().split('T')[0]}
|
||||||
<div class="w-full">
|
on:change={(e) => handleDate(e.target.valueAsDate, 'fromDate')}
|
||||||
<label for="filter__kind" class="block mb-0.5 text-sm font-medium text-gray-900">Entry kind</label>
|
type="date"
|
||||||
<select on:change={(e) => handleKind(e.target.value)} id="filter__kind" name="date" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-1.5">
|
name="date"
|
||||||
<option value="" selected>Choose an entry kind</option>
|
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-1.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||||
<option value="song">Song</option>
|
/>
|
||||||
<option value="album">Album</option>
|
</div>
|
||||||
<option value="event">Event</option>
|
<div class="w-full">
|
||||||
<option value="memory">Memory</option>
|
<label for="filter__to-date" class="mb-0.5 block text-sm font-medium text-gray-900">
|
||||||
<option value="feeling">Feeling</option>
|
To date
|
||||||
<option value="environment">Environment</option>
|
</label>
|
||||||
<option value="date">Date</option>
|
<input
|
||||||
</select>
|
value={touched.toDate ? undefined : filters.toDate?.toISOString().split('T')[0]}
|
||||||
</div>
|
id="filter__to-date"
|
||||||
</div>
|
min={filters.fromDate?.toISOString().split('T')[0]}
|
||||||
<div>
|
on:change={(e) => handleDate(e.target.valueAsDate, 'toDate')}
|
||||||
<FeelingsChooser chosenFeelings={chosenFeelings} on:choiceUpdated={(e) => handleFeelings(e.detail)} displayText={false} slim={true} />
|
type="date"
|
||||||
</div>
|
name="date"
|
||||||
<div>
|
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-1.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||||
<input value={filters.searchQuery} on:keydown={(e) => handleSearchQuery(e.target.value)} type="text" placeholder="Search query" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div 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>
|
</div>
|
|
@ -1,22 +1,35 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { TITLED_ENTRIES, type Entry } from "$lib/entry"
|
import { TITLED_ENTRIES, type Entry } from '$lib/entry';
|
||||||
|
|
||||||
export let entry: Entry
|
export let entry: Entry;
|
||||||
export let showDate = false
|
export let showDate = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
{#if showDate}
|
{#if showDate}
|
||||||
<time class="pr-2.5 font-mono" datetime={entry.creationDate}>{new Date(entry.creationDate).toLocaleDateString()}</time>
|
<time class="pr-2.5 font-mono" datetime={entry.creationDate}>
|
||||||
{/if}
|
{new Date(entry.creationDate).toLocaleDateString()}
|
||||||
|
</time>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if TITLED_ENTRIES.includes(entry.base.kind)}
|
{#if TITLED_ENTRIES.includes(entry.base.kind)}
|
||||||
New {entry.base.kind}: <a href={`#entry__${entry.id}`} class="font-bold text-violet-600 hover:underline">§ {entry.title}</a>
|
New {entry.base.kind}:
|
||||||
{:else if ["song", "album"].includes(entry.base.kind)}
|
<a href={`#entry__${entry.id}`} class="font-bold text-violet-600 hover:underline">
|
||||||
New {entry.base.kind}: <a href={`#entry__${entry.id}`} class="font-bold">{entry.base.artist} ‐ {entry.base.title}</a>
|
§ {entry.title}
|
||||||
{:else if entry.base.kind === "date"}
|
</a>
|
||||||
New {entry.base.kind}: <a href={`#entry__${entry.id}`} class="font-bold">{new Date(entry.base.referencedDate).toLocaleDateString()}</a>
|
{:else if ['song', 'album'].includes(entry.base.kind)}
|
||||||
{:else}
|
New {entry.base.kind}:
|
||||||
<a href={`#entry__${entry.id}`} class="font-bold text-violet-600 hover:underline">New {entry.base.kind}</a>
|
<a href={`#entry__${entry.id}`} class="font-bold">
|
||||||
{/if}
|
{entry.base.artist} ‐ {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>
|
</p>
|
|
@ -1,219 +1,351 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createForm } from "felte";
|
import { createForm } from 'felte';
|
||||||
import EntryKind from "../../dashboard/utils/EntryKind.svelte";
|
import EntryKind from '../../dashboard/utils/EntryKind.svelte';
|
||||||
import { FEELINGS, TITLED_ENTRIES, type AlbumEntry, type IdlessEntry, type KnownFeeling, type SongEntry } from "$lib/entry";
|
import {
|
||||||
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome";
|
FEELINGS,
|
||||||
import { faSpotify, faYoutube } from "@fortawesome/free-brands-svg-icons";
|
TITLED_ENTRIES,
|
||||||
import { faChevronDown, faLink, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
type AlbumEntry,
|
||||||
import FeelingPill from "../../dashboard/utils/FeelingPill.svelte";
|
type IdlessEntry,
|
||||||
import { addEntry, uploadAsset } from "$lib/api";
|
type KnownFeeling,
|
||||||
import { credentials, session_key } from "$lib/stores";
|
type SongEntry
|
||||||
import FeelingsChooser from "$lib/components/FeelingsChooser.svelte";
|
} from '$lib/entry';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/svelte-fontawesome';
|
||||||
|
import { faSpotify, faYoutube } from '@fortawesome/free-brands-svg-icons';
|
||||||
|
import { faChevronDown, faLink, faPlus, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import FeelingPill from '../../dashboard/utils/FeelingPill.svelte';
|
||||||
|
import { addEntry, uploadAsset } from '$lib/api';
|
||||||
|
import { credentials, session_key } from '$lib/stores';
|
||||||
|
import FeelingsChooser from '$lib/components/FeelingsChooser.svelte';
|
||||||
|
|
||||||
credentials.subscribe((v) => v == null && (setTimeout(() => window.location.pathname = '/auth/login', 200)))
|
credentials.subscribe(
|
||||||
|
(v) => v == null && setTimeout(() => (window.location.pathname = '/auth/login'), 200)
|
||||||
|
);
|
||||||
|
|
||||||
let kind: EntryKind | null = "song"
|
let kind: EntryKind | null;
|
||||||
const { form, errors } = createForm({
|
const { form, errors } = createForm({
|
||||||
onSubmit: async (values) => {
|
onSubmit: async (values) => {
|
||||||
let feelings = Object.keys(values)
|
let feelings = Object.keys(values)
|
||||||
.filter(v => v.startsWith("feeling__"))
|
.filter((v) => v.startsWith('feeling__'))
|
||||||
.map(v => v.replaceAll("feeling__", "")) as KnownFeeling[];
|
.map((v) => v.replaceAll('feeling__', '')) as KnownFeeling[];
|
||||||
|
|
||||||
let base;
|
let base;
|
||||||
if (values.kind === "song" || values.kind === "album") {
|
if (values.kind === 'song' || values.kind === 'album') {
|
||||||
base = {
|
base = {
|
||||||
kind: values.kind,
|
kind: values.kind,
|
||||||
artist: values.artist,
|
artist: values.artist,
|
||||||
title: values.musicTitle,
|
title: values.musicTitle,
|
||||||
link: [values.spotify, values.yt, values.otherProvider].filter(v => v != null && v.length > 0),
|
link: [values.spotify, values.yt, values.otherProvider].filter(
|
||||||
// FIXME: infer univeersal ids
|
(v) => v != null && v.length > 0
|
||||||
id: [],
|
),
|
||||||
}
|
// FIXME: infer univeersal ids
|
||||||
} else if (values.kind === "environment") {
|
id: []
|
||||||
base = {
|
};
|
||||||
kind: values.kind,
|
} else if (values.kind === 'environment') {
|
||||||
location: (values.location != null && values.location.length > 0) ? values.location : undefined,
|
base = {
|
||||||
}
|
kind: values.kind,
|
||||||
} else if (values.kind === "date") {
|
location:
|
||||||
base = {
|
values.location != null && values.location.length > 0 ? values.location : undefined
|
||||||
kind: values.kind,
|
};
|
||||||
referencedDate: values.date,
|
} else if (values.kind === 'date') {
|
||||||
}
|
base = {
|
||||||
} else {
|
kind: values.kind,
|
||||||
base = {
|
referencedDate: values.date
|
||||||
kind: values.kind,
|
};
|
||||||
}
|
} else {
|
||||||
}
|
base = {
|
||||||
|
kind: values.kind
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let asset_id
|
let asset_id;
|
||||||
if (values.asset != null && typeof values.asset === "object") {
|
if (values.asset != null && typeof values.asset === 'object') {
|
||||||
asset_id = await uploadAsset($session_key!, values.asset)
|
asset_id = await uploadAsset($session_key!, values.asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
let entry: IdlessEntry = {
|
let entry: IdlessEntry = {
|
||||||
base,
|
base,
|
||||||
creationDate: new Date().toISOString(),
|
creationDate: new Date().toISOString(),
|
||||||
assets: [asset_id].filter(v => v != null) as string[],
|
assets: [asset_id].filter((v) => v != null) as string[],
|
||||||
feelings,
|
feelings,
|
||||||
title: TITLED_ENTRIES.includes(values.kind) ? values.title : undefined,
|
title: TITLED_ENTRIES.includes(values.kind) ? values.title : undefined,
|
||||||
description: values.description,
|
description: values.description
|
||||||
}
|
};
|
||||||
|
|
||||||
await addEntry($credentials!, entry)
|
await addEntry($credentials!, entry);
|
||||||
window.location.pathname = '/dashboard'
|
window.location.pathname = '/dashboard';
|
||||||
},
|
},
|
||||||
validate: (values) => {
|
validate: (values) => {
|
||||||
let errors = {}
|
let errors = {};
|
||||||
|
|
||||||
if (values.kind == null || values.kind.length === 0) {
|
if (values.kind == null || values.kind.length === 0) {
|
||||||
errors['kind'] = 'Must choose an entry kind'
|
errors['kind'] = 'Must choose an entry kind';
|
||||||
return errors
|
return errors;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.kind === "song" || values.kind === "album") {
|
if (values.kind === 'song' || values.kind === 'album') {
|
||||||
if (values.artist == null || values.artist.length === 0) {
|
if (values.artist == null || values.artist.length === 0) {
|
||||||
errors['artist'] = "Must not be empty";
|
errors['artist'] = 'Must not be empty';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.musicTitle == null || values.musicTitle.length === 0) {
|
if (values.musicTitle == null || values.musicTitle.length === 0) {
|
||||||
errors["musicTitle"] = "Must not be empty";
|
errors['musicTitle'] = 'Must not be empty';
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: When asset support is added, another precondition is that no asset is uploaded
|
// FIXME: When asset support is added, another precondition is that no asset is uploaded
|
||||||
if (values.spotify.length === 0 && values.yt.length === 0 && values.otherProvider.length === 0) {
|
if (
|
||||||
errors["links"] = "You must add at least one link or upload an audio asset";
|
values.spotify.length === 0 &&
|
||||||
}
|
values.yt.length === 0 &&
|
||||||
} else if (values.kind === "date") {
|
values.otherProvider.length === 0
|
||||||
if (values.date == null || values.date.length === 0) {
|
) {
|
||||||
errors['date'] = "Must choose a date";
|
errors['links'] = 'You must add at least one link or upload an audio asset';
|
||||||
}
|
}
|
||||||
} else if (values.kind === "feeling") {
|
} else if (values.kind === 'date') {
|
||||||
if (Object.keys(values).filter(v => v.startsWith("feeling__")).length === 0) {
|
if (values.date == null || values.date.length === 0) {
|
||||||
errors['feelings'] = "Must choose at least one feeling";
|
errors['date'] = 'Must choose a date';
|
||||||
}
|
}
|
||||||
} else {
|
} else if (values.kind === 'feeling') {
|
||||||
if (values.title == null || values.title.length === 0) {
|
if (Object.keys(values).filter((v) => v.startsWith('feeling__')).length === 0) {
|
||||||
errors["title"] = "Must not be empty";
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="mt-3.5 justify-center flex">
|
<div class="mt-3.5 flex justify-center">
|
||||||
<div class="w-[60%] flex flex-col">
|
<div class="flex w-[60%] flex-col">
|
||||||
<h1 class="text-2xl pb-3.5">Add an entry</h1>
|
<h1 class="pb-3.5 text-2xl">Add an entry</h1>
|
||||||
<form use:form>
|
<form use:form>
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<label for="add-entry__kind" class="block mb-2 text-sm font-medium text-gray-900">Entry kind</label>
|
<label for="add-entry__kind" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
<select bind:value={kind} id="add-entry__kind" name="kind" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
Entry kind
|
||||||
<option value="" selected>Choose an entry kind</option>
|
</label>
|
||||||
<option value="song">Song</option>
|
<select
|
||||||
<option value="album">Album</option>
|
bind:value={kind}
|
||||||
<option value="event">Event</option>
|
id="add-entry__kind"
|
||||||
<option value="memory">Memory</option>
|
name="kind"
|
||||||
<option value="feeling">Feeling</option>
|
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
|
||||||
<option value="environment">Environment</option>
|
>
|
||||||
<option value="date">Date</option>
|
<option value="" selected>Choose an entry kind</option>
|
||||||
</select>
|
<option value="song">Song</option>
|
||||||
{#if $errors.kind != null}
|
<option value="album">Album</option>
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.kind[0]}</span></p>
|
<option value="event">Event</option>
|
||||||
{/if}
|
<option value="memory">Memory</option>
|
||||||
</div>
|
<option value="feeling">Feeling</option>
|
||||||
{#if TITLED_ENTRIES.includes(kind)}
|
<option value="environment">Environment</option>
|
||||||
<div class="mb-5">
|
<option value="date">Date</option>
|
||||||
<label for="add-entry__title" class="block mb-2 text-sm font-medium text-gray-900">Title</label>
|
</select>
|
||||||
<input id="add-entry__title" type="text" name="title" placeholder="At the sunflower field with my friends" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
{#if $errors.kind != null}
|
||||||
{#if $errors.title != null}
|
<p class="mt-2 text-sm text-red-600">
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.title[0]}</span></p>
|
<span class="font-medium">{$errors.kind[0]}</span>
|
||||||
{/if}
|
</p>
|
||||||
</div>
|
{/if}
|
||||||
{/if}
|
</div>
|
||||||
|
{#if TITLED_ENTRIES.includes(kind)}
|
||||||
|
<div class="mb-5">
|
||||||
|
<label for="add-entry__title" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
|
Title
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="add-entry__title"
|
||||||
|
type="text"
|
||||||
|
name="title"
|
||||||
|
placeholder="At the sunflower field with my friends"
|
||||||
|
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||||
|
/>
|
||||||
|
{#if $errors.title != null}
|
||||||
|
<p class="mt-2 text-sm text-red-600">
|
||||||
|
<span class="font-medium">{$errors.title[0]}</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if ["song", "album"].includes(kind)}
|
{#if ['song', 'album'].includes(kind)}
|
||||||
<div class="flex flex-col mb-5 gap-5 md:flex-row">
|
<div class="mb-5 flex flex-col gap-5 md:flex-row">
|
||||||
<div class="w-full">
|
<div class="w-full">
|
||||||
<label for="add-entry__artist" class="block mb-2 text-sm font-medium text-gray-900">Artist name</label>
|
<label for="add-entry__artist" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
<input id="add-entry__artist" type="text" name="artist" placeholder="Claude Debussy" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
Artist name
|
||||||
{#if $errors.artist != null}
|
</label>
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.artist[0]}</span></p>
|
<input
|
||||||
{/if}
|
id="add-entry__artist"
|
||||||
</div>
|
type="text"
|
||||||
<div class="w-full">
|
name="artist"
|
||||||
<label for="add-entry__music-title" class="block mb-2 text-sm font-medium text-gray-900"><span class="capitalize">{kind}</span> title</label>
|
placeholder="Claude Debussy"
|
||||||
<input id="add-entry__music-title" type="text" name="musicTitle" placeholder="Clair de Lune" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||||
{#if $errors.musicTitle != null}
|
/>
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.musicTitle[0]}</span></p>
|
{#if $errors.artist != null}
|
||||||
{/if}
|
<p class="mt-2 text-sm text-red-600">
|
||||||
</div>
|
<span class="font-medium">{$errors.artist[0]}</span>
|
||||||
</div>
|
</p>
|
||||||
<div class="mb-5">
|
{/if}
|
||||||
<label for="add-entry__spotify" class="block mb-2 text-sm font-medium text-gray-900">Spotify link</label>
|
</div>
|
||||||
<div class="flex">
|
<div class="w-full">
|
||||||
<span class="inline-flex items-center px-2.5 text-sm text-gray-900 bg-gray-200 border rounded-e-0 border-gray-300 border-e-0 rounded-s-md">
|
<label
|
||||||
<FontAwesomeIcon size="lg" icon={faSpotify}/>
|
for="add-entry__music-title"
|
||||||
</span>
|
class="mb-2 block text-sm font-medium text-gray-900"
|
||||||
<input type="text" id="add-entry__spotify" name="spotify" class="rounded-none rounded-e-lg bg-gray-50 border text-gray-900 focus:ring-violet-500 focus:border-violet-500 block flex-1 min-w-0 w-full text-sm border-gray-300 p-2.5" placeholder={kind === "song" ? "https://open.spotify.com/track/..." : "https://open.spotify.com/album/..."}>
|
>
|
||||||
</div>
|
<span class="capitalize">{kind}</span>
|
||||||
</div>
|
title
|
||||||
<div class="mb-5">
|
</label>
|
||||||
<label for="add-entry__yt" class="block mb-2 text-sm font-medium text-gray-900">YouTube link</label>
|
<input
|
||||||
<div class="flex">
|
id="add-entry__music-title"
|
||||||
<span class="inline-flex items-center px-2.5 text-sm text-gray-900 bg-gray-200 border rounded-e-0 border-gray-300 border-e-0 rounded-s-md">
|
type="text"
|
||||||
<FontAwesomeIcon size="lg" icon={faYoutube}/>
|
name="musicTitle"
|
||||||
</span>
|
placeholder="Clair de Lune"
|
||||||
<input type="text" id="add-entry__yt" name="yt" class="rounded-none rounded-e-lg bg-gray-50 border text-gray-900 focus:ring-violet-500 focus:border-violet-500 block flex-1 min-w-0 w-full text-sm border-gray-300 p-2.5" placeholder="https://www.youtube.com/watch...">
|
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||||
</div>
|
/>
|
||||||
</div>
|
{#if $errors.musicTitle != null}
|
||||||
<div class="mb-5">
|
<p class="mt-2 text-sm text-red-600">
|
||||||
<label for="add-entry__other" class="block mb-2 text-sm font-medium text-gray-900">Link to other provider</label>
|
<span class="font-medium">{$errors.musicTitle[0]}</span>
|
||||||
<div class="flex">
|
</p>
|
||||||
<span class="inline-flex items-center px-2.5 text-sm text-gray-900 bg-gray-200 border rounded-e-0 border-gray-300 border-e-0 rounded-s-md">
|
{/if}
|
||||||
<FontAwesomeIcon size="lg" icon={faLink}/>
|
</div>
|
||||||
</span>
|
</div>
|
||||||
<input type="text" name="otherProvider" id="add-entry__other" class="rounded-none rounded-e-lg bg-gray-50 border text-gray-900 focus:ring-violet-500 focus:border-violet-500 block flex-1 min-w-0 w-full text-sm border-gray-300 p-2.5" placeholder="https://www.music.tld/play/...">
|
<div class="mb-5">
|
||||||
</div>
|
<label for="add-entry__spotify" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
</div>
|
Spotify link
|
||||||
{#if $errors.links != null}
|
</label>
|
||||||
<p class="mt-2.5 mb-3.5 text-sm text-red-600"><span class="font-medium">{$errors.links[0]}</span></p>
|
<div class="flex">
|
||||||
{/if}
|
<span
|
||||||
{:else if kind === "environment"}
|
class="rounded-e-0 inline-flex items-center rounded-s-md border border-e-0 border-gray-300 bg-gray-200 px-2.5 text-sm text-gray-900"
|
||||||
<div class="w-full mb-5">
|
>
|
||||||
<label for="add-entry__location" class="block mb-2 text-sm font-medium text-gray-900">Location</label>
|
<FontAwesomeIcon size="lg" icon={faSpotify} />
|
||||||
<input id="add-entry__location" type="text" name="location" placeholder="South of Almond Park" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
</span>
|
||||||
</div>
|
<input
|
||||||
{:else if kind === "date"}
|
type="text"
|
||||||
<div class="w-full mb-5">
|
id="add-entry__spotify"
|
||||||
<label for="add-entry__date" class="block mb-2 text-sm font-medium text-gray-900">Referenced date</label>
|
name="spotify"
|
||||||
<input id="add-entry__date" type="date" name="date" class="bg-gray-50 border border-gray-300 text-greay-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full p-2.5">
|
class="block w-full min-w-0 flex-1 rounded-none rounded-e-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
|
||||||
{#if $errors.date != null}
|
placeholder={kind === 'song'
|
||||||
<p class="mt-2 text-sm text-red-600"><span class="font-medium">{$errors.date[0]}</span></p>
|
? 'https://open.spotify.com/track/...'
|
||||||
{/if}
|
: 'https://open.spotify.com/album/...'}
|
||||||
</div>
|
/>
|
||||||
{/if}
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label for="add-entry__yt" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
|
YouTube link
|
||||||
|
</label>
|
||||||
|
<div class="flex">
|
||||||
|
<span
|
||||||
|
class="rounded-e-0 inline-flex items-center rounded-s-md border border-e-0 border-gray-300 bg-gray-200 px-2.5 text-sm text-gray-900"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon size="lg" icon={faYoutube} />
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="add-entry__yt"
|
||||||
|
name="yt"
|
||||||
|
class="block w-full min-w-0 flex-1 rounded-none rounded-e-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
|
||||||
|
placeholder="https://www.youtube.com/watch..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-5">
|
||||||
|
<label for="add-entry__other" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
|
Link to other provider
|
||||||
|
</label>
|
||||||
|
<div class="flex">
|
||||||
|
<span
|
||||||
|
class="rounded-e-0 inline-flex items-center rounded-s-md border border-e-0 border-gray-300 bg-gray-200 px-2.5 text-sm text-gray-900"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon size="lg" icon={faLink} />
|
||||||
|
</span>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="otherProvider"
|
||||||
|
id="add-entry__other"
|
||||||
|
class="block w-full min-w-0 flex-1 rounded-none rounded-e-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
|
||||||
|
placeholder="https://www.music.tld/play/..."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if $errors.links != null}
|
||||||
|
<p class="mb-3.5 mt-2.5 text-sm text-red-600">
|
||||||
|
<span class="font-medium">{$errors.links[0]}</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
{:else if kind === 'environment'}
|
||||||
|
<div class="mb-5 w-full">
|
||||||
|
<label for="add-entry__location" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
|
Location
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="add-entry__location"
|
||||||
|
type="text"
|
||||||
|
name="location"
|
||||||
|
placeholder="South of Almond Park"
|
||||||
|
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{:else if kind === 'date'}
|
||||||
|
<div class="mb-5 w-full">
|
||||||
|
<label for="add-entry__date" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
|
Referenced date
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="add-entry__date"
|
||||||
|
type="date"
|
||||||
|
name="date"
|
||||||
|
class="text-greay-900 block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm focus:border-violet-500 focus:ring-violet-500"
|
||||||
|
/>
|
||||||
|
{#if $errors.date != null}
|
||||||
|
<p class="mt-2 text-sm text-red-600">
|
||||||
|
<span class="font-medium">{$errors.date[0]}</span>
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if kind != null && kind.length > 0}
|
{#if kind != null && kind.length > 0}
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<label for="add-entry__description" class="block mb-2 text-sm font-medium text-gray-900">Description</label>
|
<label for="add-entry__description" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
<textarea name="description" id="add-entry__description" rows="7" class="block p-2.5 w-full text-sm text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-violet-500 focus:border-violet-500" placeholder="Write your thoughts here..."></textarea>
|
Description
|
||||||
</div>
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="description"
|
||||||
|
id="add-entry__description"
|
||||||
|
rows="7"
|
||||||
|
class="block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-violet-500 focus:ring-violet-500"
|
||||||
|
placeholder="Write your thoughts here..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<label for="add-entry__assets" class="block mb-2 text-sm font-medium text-gray-900">Linked assets (max 5MB)</label>
|
<label for="add-entry__assets" class="mb-2 block text-sm font-medium text-gray-900">
|
||||||
<input name="asset" id="add-entry__assets" class="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-violet-500 focus:border-violet-500 block w-full hover:cursor-pointer file:bg-gray-200 file:border-gray-300 file:border-0 file:me-4 file:py-2.5 file:px-4 hover:file:bg-gray-300" type="file">
|
Linked assets (max 5MB)
|
||||||
</div>
|
</label>
|
||||||
|
<input
|
||||||
|
name="asset"
|
||||||
|
id="add-entry__assets"
|
||||||
|
class="block w-full rounded-lg border border-gray-300 bg-gray-50 text-sm text-gray-900 file:me-4 file:border-0 file:border-gray-300 file:bg-gray-200 file:px-4 file:py-2.5 hover:cursor-pointer hover:file:bg-gray-300 focus:border-violet-500 focus:ring-violet-500"
|
||||||
|
type="file"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<FeelingsChooser required={kind === "feeling"}/>
|
<FeelingsChooser required={kind === 'feeling'} />
|
||||||
{#if $errors.feelings != null}
|
{#if $errors.feelings != null}
|
||||||
<p class="text-sm text-red-600 mt-1.5"><span class="font-medium">{$errors.feelings[0]}</span></p>
|
<p class="mt-1.5 text-sm text-red-600">
|
||||||
{/if}
|
<span class="font-medium">{$errors.feelings[0]}</span>
|
||||||
</div>
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
<button class="mt-2 text-white bg-violet-700 hover:bg-violet-800 focus:ring-4 focust:outline-none focus:ring-violet-300 font-medium rounded-lg px-5 py-2.5 text-center" type="submit">Add new entry</button>
|
<button
|
||||||
{/if}
|
class="focust:outline-none mt-2 rounded-lg bg-violet-700 px-5 py-2.5 text-center font-medium text-white hover:bg-violet-800 focus:ring-4 focus:ring-violet-300"
|
||||||
</form>
|
type="submit"
|
||||||
</div>
|
>
|
||||||
|
Add new entry
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
|
@ -1,4 +1,5 @@
|
||||||
import adapter from '@sveltejs/adapter-auto';
|
import adapterStatic from '@sveltejs/adapter-static';
|
||||||
|
import adapterAuto from '@sveltejs/adapter-auto';
|
||||||
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Config} */
|
/** @type {import('@sveltejs/kit').Config} */
|
||||||
|
@ -8,10 +9,16 @@ const config = {
|
||||||
preprocess: vitePreprocess(),
|
preprocess: vitePreprocess(),
|
||||||
|
|
||||||
kit: {
|
kit: {
|
||||||
|
adapter: adapterStatic({
|
||||||
|
fallback: 'index.html'
|
||||||
|
})
|
||||||
|
|
||||||
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
// adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
|
||||||
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
// If your environment is not supported, or you settled on a specific environment, switch out the adapter.
|
||||||
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
// See https://kit.svelte.dev/docs/adapters for more information about adapters.
|
||||||
adapter: adapter()
|
//
|
||||||
|
// To use adapter-auto, uncomment the line below:
|
||||||
|
// adapter: adapterAuto(),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,8 @@
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
content: ['./src/**/*.{html,css,js,svelte,ts}'],
|
content: ['./src/**/*.{html,css,js,svelte,ts}'],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {}
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [require('@tailwindcss/forms')]
|
||||||
require("@tailwindcss/forms")
|
};
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
|
@ -559,6 +559,15 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@sveltejs/adapter-static@npm:^3.0.2":
|
||||||
|
version: 3.0.2
|
||||||
|
resolution: "@sveltejs/adapter-static@npm:3.0.2"
|
||||||
|
peerDependencies:
|
||||||
|
"@sveltejs/kit": ^2.0.0
|
||||||
|
checksum: 10c0/db3c287f0ed52b9c3c42e27cb54c18977627dd7596ac2f778b6a70500b6e07cc48f3fa305a39171d0279311cfe11ecf8afcd367abbb13e582ad10d96223721fe
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@sveltejs/kit@npm:^2.0.0":
|
"@sveltejs/kit@npm:^2.0.0":
|
||||||
version: 2.5.10
|
version: 2.5.10
|
||||||
resolution: "@sveltejs/kit@npm:2.5.10"
|
resolution: "@sveltejs/kit@npm:2.5.10"
|
||||||
|
@ -1930,6 +1939,7 @@ __metadata:
|
||||||
"@fortawesome/free-solid-svg-icons": "npm:^6.5.2"
|
"@fortawesome/free-solid-svg-icons": "npm:^6.5.2"
|
||||||
"@fortawesome/svelte-fontawesome": "npm:^0.2.2"
|
"@fortawesome/svelte-fontawesome": "npm:^0.2.2"
|
||||||
"@sveltejs/adapter-auto": "npm:^3.0.0"
|
"@sveltejs/adapter-auto": "npm:^3.0.0"
|
||||||
|
"@sveltejs/adapter-static": "npm:^3.0.2"
|
||||||
"@sveltejs/kit": "npm:^2.0.0"
|
"@sveltejs/kit": "npm:^2.0.0"
|
||||||
"@sveltejs/vite-plugin-svelte": "npm:^3.0.0"
|
"@sveltejs/vite-plugin-svelte": "npm:^3.0.0"
|
||||||
"@tailwindcss/forms": "npm:^0.5.7"
|
"@tailwindcss/forms": "npm:^0.5.7"
|
||||||
|
@ -1945,6 +1955,7 @@ __metadata:
|
||||||
postcss: "npm:^8.4.38"
|
postcss: "npm:^8.4.38"
|
||||||
prettier: "npm:^3.1.1"
|
prettier: "npm:^3.1.1"
|
||||||
prettier-plugin-svelte: "npm:^3.1.2"
|
prettier-plugin-svelte: "npm:^3.1.2"
|
||||||
|
prettier-plugin-tailwindcss: "npm:^0.6.5"
|
||||||
svelte: "npm:^4.2.7"
|
svelte: "npm:^4.2.7"
|
||||||
svelte-check: "npm:^3.6.0"
|
svelte-check: "npm:^3.6.0"
|
||||||
tailwindcss: "npm:^3.4.4"
|
tailwindcss: "npm:^3.4.4"
|
||||||
|
@ -2855,6 +2866,61 @@ __metadata:
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"prettier-plugin-tailwindcss@npm:^0.6.5":
|
||||||
|
version: 0.6.5
|
||||||
|
resolution: "prettier-plugin-tailwindcss@npm:0.6.5"
|
||||||
|
peerDependencies:
|
||||||
|
"@ianvs/prettier-plugin-sort-imports": "*"
|
||||||
|
"@prettier/plugin-pug": "*"
|
||||||
|
"@shopify/prettier-plugin-liquid": "*"
|
||||||
|
"@trivago/prettier-plugin-sort-imports": "*"
|
||||||
|
"@zackad/prettier-plugin-twig-melody": "*"
|
||||||
|
prettier: ^3.0
|
||||||
|
prettier-plugin-astro: "*"
|
||||||
|
prettier-plugin-css-order: "*"
|
||||||
|
prettier-plugin-import-sort: "*"
|
||||||
|
prettier-plugin-jsdoc: "*"
|
||||||
|
prettier-plugin-marko: "*"
|
||||||
|
prettier-plugin-organize-attributes: "*"
|
||||||
|
prettier-plugin-organize-imports: "*"
|
||||||
|
prettier-plugin-sort-imports: "*"
|
||||||
|
prettier-plugin-style-order: "*"
|
||||||
|
prettier-plugin-svelte: "*"
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@ianvs/prettier-plugin-sort-imports":
|
||||||
|
optional: true
|
||||||
|
"@prettier/plugin-pug":
|
||||||
|
optional: true
|
||||||
|
"@shopify/prettier-plugin-liquid":
|
||||||
|
optional: true
|
||||||
|
"@trivago/prettier-plugin-sort-imports":
|
||||||
|
optional: true
|
||||||
|
"@zackad/prettier-plugin-twig-melody":
|
||||||
|
optional: true
|
||||||
|
prettier-plugin-astro:
|
||||||
|
optional: true
|
||||||
|
prettier-plugin-css-order:
|
||||||
|
optional: true
|
||||||
|
prettier-plugin-import-sort:
|
||||||
|
optional: true
|
||||||
|
prettier-plugin-jsdoc:
|
||||||
|
optional: true
|
||||||
|
prettier-plugin-marko:
|
||||||
|
optional: true
|
||||||
|
prettier-plugin-organize-attributes:
|
||||||
|
optional: true
|
||||||
|
prettier-plugin-organize-imports:
|
||||||
|
optional: true
|
||||||
|
prettier-plugin-sort-imports:
|
||||||
|
optional: true
|
||||||
|
prettier-plugin-style-order:
|
||||||
|
optional: true
|
||||||
|
prettier-plugin-svelte:
|
||||||
|
optional: true
|
||||||
|
checksum: 10c0/30d62928592b48cab03c46ff63edd35d4a33c4e7c40e583f12bff7223eba8b6f780fd394965b0250160bcf39688f6fb602420374b2055bcbb6a69560b818ca4e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"prettier@npm:^3.1.1":
|
"prettier@npm:^3.1.1":
|
||||||
version: 3.3.2
|
version: 3.3.2
|
||||||
resolution: "prettier@npm:3.3.2"
|
resolution: "prettier@npm:3.3.2"
|
||||||
|
|
Loading…
Reference in a new issue