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