implement filtering!

This commit is contained in:
Sofía Aritz 2024-06-27 23:50:14 +02:00
parent e4d65e303f
commit 3e6bed20a6
Signed by: sofia
GPG key ID: 90B5116E3542B28F
16 changed files with 439 additions and 72 deletions

View file

@ -7,6 +7,7 @@ import { join } from "node:path";
import mime from "mime"; import mime from "mime";
import { promisify } from "node:util"; import { promisify } from "node:util";
import { pipeline } from "node:stream"; import { pipeline } from "node:stream";
import cors from "@fastify/cors";
const M2M_ALGORITHM = "RSA-SHA512"; const M2M_ALGORITHM = "RSA-SHA512";
const { private: M2M_PRIVATE_KEY, public: M2M_PUBLIC_KEY } = loadM2MKeys(); const { private: M2M_PRIVATE_KEY, public: M2M_PUBLIC_KEY } = loadM2MKeys();
@ -25,6 +26,9 @@ const fastify = new Fastify({
}); });
fastify.register(multipart); fastify.register(multipart);
fastify.register(cors, {
origin: true,
})
fastify.get("/", async () => { fastify.get("/", async () => {
return signString(ASSET_API_LANDING_MESSAGE); return signString(ASSET_API_LANDING_MESSAGE);

View file

@ -4,6 +4,7 @@
"type": "module", "type": "module",
"packageManager": "yarn@4.3.0", "packageManager": "yarn@4.3.0",
"dependencies": { "dependencies": {
"@fastify/cors": "^9.0.1",
"@fastify/multipart": "^8.3.0", "@fastify/multipart": "^8.3.0",
"fastify": "^4.28.0", "fastify": "^4.28.0",
"mime": "^4.0.3", "mime": "^4.0.3",

View file

@ -83,6 +83,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@fastify/cors@npm:^9.0.1":
version: 9.0.1
resolution: "@fastify/cors@npm:9.0.1"
dependencies:
fastify-plugin: "npm:^4.0.0"
mnemonist: "npm:0.39.6"
checksum: 10c0/4db9d3d02edbca741c8ed053819bf3b235ecd70e07c640ed91ba0fc1ee2dc8abedbbffeb79ae1a38ccbf59832e414cad90a554ee44227d0811d5a2d062940611
languageName: node
linkType: hard
"@fastify/deepmerge@npm:^1.0.0": "@fastify/deepmerge@npm:^1.0.0":
version: 1.3.0 version: 1.3.0
resolution: "@fastify/deepmerge@npm:1.3.0" resolution: "@fastify/deepmerge@npm:1.3.0"
@ -284,6 +294,7 @@ __metadata:
resolution: "asset-api@workspace:." resolution: "asset-api@workspace:."
dependencies: dependencies:
"@eslint/js": "npm:^9.5.0" "@eslint/js": "npm:^9.5.0"
"@fastify/cors": "npm:^9.0.1"
"@fastify/multipart": "npm:^8.3.0" "@fastify/multipart": "npm:^8.3.0"
eslint: "npm:9.x" eslint: "npm:9.x"
fastify: "npm:^4.28.0" fastify: "npm:^4.28.0"
@ -931,6 +942,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mnemonist@npm:0.39.6":
version: 0.39.6
resolution: "mnemonist@npm:0.39.6"
dependencies:
obliterator: "npm:^2.0.1"
checksum: 10c0/a538945ea547976136ee6e16f224c0a50983143619941f6c4d2c82159e36eb6f8ee93d69d3a1267038fc5b16f88e2d43390023de10dfb145fa15c5e2befa1cdf
languageName: node
linkType: hard
"ms@npm:2.1.2": "ms@npm:2.1.2":
version: 2.1.2 version: 2.1.2
resolution: "ms@npm:2.1.2" resolution: "ms@npm:2.1.2"
@ -945,6 +965,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"obliterator@npm:^2.0.1":
version: 2.0.4
resolution: "obliterator@npm:2.0.4"
checksum: 10c0/ff2c10d4de7d62cd1d588b4d18dfc42f246c9e3a259f60d5716f7f88e5b3a3f79856b3207db96ec9a836a01d0958a21c15afa62a3f4e73a1e0b75f2c2f6bab40
languageName: node
linkType: hard
"on-exit-leak-free@npm:^2.1.0": "on-exit-leak-free@npm:^2.1.0":
version: 2.1.2 version: 2.1.2
resolution: "on-exit-leak-free@npm:2.1.2" resolution: "on-exit-leak-free@npm:2.1.2"

View file

@ -7,6 +7,7 @@
"type": "module", "type": "module",
"packageManager": "yarn@4.3.0", "packageManager": "yarn@4.3.0",
"dependencies": { "dependencies": {
"@fastify/cors": "^9.0.1",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"fastify": "^4.27.0", "fastify": "^4.27.0",
"jose": "^5.4.0" "jose": "^5.4.0"

View file

@ -3,9 +3,14 @@ import { ASSET_API_ENDPOINT, IDENTITY_API_LANDING_MESSAGE, LISTEN_PORT } from ".
import { contentFromSigned, verifySignature } from "./m2m.js"; import { contentFromSigned, verifySignature } from "./m2m.js";
import { startAuth } from "./auth.js"; import { startAuth } from "./auth.js";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import cors from "@fastify/cors";
let auth = await startAuth(); let auth = await startAuth();
app.register(cors, {
origin: true,
})
app.get("/", async () => { app.get("/", async () => {
return IDENTITY_API_LANDING_MESSAGE; return IDENTITY_API_LANDING_MESSAGE;
}); });

View file

@ -76,6 +76,16 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@fastify/cors@npm:^9.0.1":
version: 9.0.1
resolution: "@fastify/cors@npm:9.0.1"
dependencies:
fastify-plugin: "npm:^4.0.0"
mnemonist: "npm:0.39.6"
checksum: 10c0/4db9d3d02edbca741c8ed053819bf3b235ecd70e07c640ed91ba0fc1ee2dc8abedbbffeb79ae1a38ccbf59832e414cad90a554ee44227d0811d5a2d062940611
languageName: node
linkType: hard
"@fastify/error@npm:^3.3.0, @fastify/error@npm:^3.4.0": "@fastify/error@npm:^3.3.0, @fastify/error@npm:^3.4.0":
version: 3.4.1 version: 3.4.1
resolution: "@fastify/error@npm:3.4.1" resolution: "@fastify/error@npm:3.4.1"
@ -591,6 +601,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fastify-plugin@npm:^4.0.0":
version: 4.5.1
resolution: "fastify-plugin@npm:4.5.1"
checksum: 10c0/f58f79cd9d3c88fd7f79a3270276c6339fc57bbe72ef14d20b73779193c404e317ac18e8eae2c5071b3909ebee45d7eb6871da4e65464ac64ed0d9746b4e9b9f
languageName: node
linkType: hard
"fastify@npm:^4.27.0": "fastify@npm:^4.27.0":
version: 4.27.0 version: 4.27.0
resolution: "fastify@npm:4.27.0" resolution: "fastify@npm:4.27.0"
@ -713,6 +730,7 @@ __metadata:
resolution: "identity-api@workspace:." resolution: "identity-api@workspace:."
dependencies: dependencies:
"@eslint/js": "npm:^9.5.0" "@eslint/js": "npm:^9.5.0"
"@fastify/cors": "npm:^9.0.1"
dotenv: "npm:^16.4.5" dotenv: "npm:^16.4.5"
eslint: "npm:9.x" eslint: "npm:9.x"
fastify: "npm:^4.27.0" fastify: "npm:^4.27.0"
@ -900,6 +918,15 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"mnemonist@npm:0.39.6":
version: 0.39.6
resolution: "mnemonist@npm:0.39.6"
dependencies:
obliterator: "npm:^2.0.1"
checksum: 10c0/a538945ea547976136ee6e16f224c0a50983143619941f6c4d2c82159e36eb6f8ee93d69d3a1267038fc5b16f88e2d43390023de10dfb145fa15c5e2befa1cdf
languageName: node
linkType: hard
"ms@npm:2.1.2": "ms@npm:2.1.2":
version: 2.1.2 version: 2.1.2
resolution: "ms@npm:2.1.2" resolution: "ms@npm:2.1.2"
@ -914,6 +941,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"obliterator@npm:^2.0.1":
version: 2.0.4
resolution: "obliterator@npm:2.0.4"
checksum: 10c0/ff2c10d4de7d62cd1d588b4d18dfc42f246c9e3a259f60d5716f7f88e5b3a3f79856b3207db96ec9a836a01d0958a21c15afa62a3f4e73a1e0b75f2c2f6bab40
languageName: node
linkType: hard
"on-exit-leak-free@npm:^2.1.0": "on-exit-leak-free@npm:^2.1.0":
version: 2.1.2 version: 2.1.2
resolution: "on-exit-leak-free@npm:2.1.2" resolution: "on-exit-leak-free@npm:2.1.2"

View file

@ -41,6 +41,7 @@
"@fortawesome/free-solid-svg-icons": "^6.5.2", "@fortawesome/free-solid-svg-icons": "^6.5.2",
"@fortawesome/svelte-fontawesome": "^0.2.2", "@fortawesome/svelte-fontawesome": "^0.2.2",
"felte": "^1.2.14", "felte": "^1.2.14",
"fuse.js": "^7.0.0",
"mime": "^4.0.3" "mime": "^4.0.3"
} }
} }

View file

@ -0,0 +1,74 @@
<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";
export let required = false
export let displayText = true
export let slim = false
let feelingsDropdownShown = false
export let chosenFeelings: KnownFeeling[] = []
$: feelingsToChoose = FEELINGS.filter(v => !chosenFeelings.includes(v))
let dispatch = createEventDispatcher()
function addFeeling(feeling: KnownFeeling) {
chosenFeelings = [feeling, ...chosenFeelings];
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}
<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>

View file

@ -2,6 +2,7 @@ export const TITLED_ENTRIES = ["event", "environment", "memory"];
export const FEELINGS = ["relaxed", "afraid", "angry", "bad", "bored", "confused", "excited", "fine", "happy", "hurt", "in love", "mad", "nervous", "okay", "sad", "scared", "shy", "sleepy", "active", "surprised", "tired", "upset", "worried"]; export const FEELINGS = ["relaxed", "afraid", "angry", "bad", "bored", "confused", "excited", "fine", "happy", "hurt", "in love", "mad", "nervous", "okay", "sad", "scared", "shy", "sleepy", "active", "surprised", "tired", "upset", "worried"];
export type KnownFeeling = "relaxed" | "afraid" | "angry" | "bad" | "bored" | "confused" | "excited" | "fine" | "happy" | "hurt" | "in love" | "mad" | "nervous" | "okay" | "sad" | "scared" | "shy" | "sleepy" | "active" | "surprised" | "tired" | "upset" | "worried"; export type KnownFeeling = "relaxed" | "afraid" | "angry" | "bad" | "bored" | "confused" | "excited" | "fine" | "happy" | "hurt" | "in love" | "mad" | "nervous" | "okay" | "sad" | "scared" | "shy" | "sleepy" | "active" | "surprised" | "tired" | "upset" | "worried";
export type EntryKind = "song" | "album" | "event" | "memory" | "feeling" | "environment" | "date";
export type IdlessEntry = { export type IdlessEntry = {
base: SongEntry | AlbumEntry | EventEntry | MemoryEntry | FeelingEntry | EnvironmentEntry | DateEntry, base: SongEntry | AlbumEntry | EventEntry | MemoryEntry | FeelingEntry | EnvironmentEntry | DateEntry,

View file

@ -3,27 +3,73 @@
import { account, credentials } from "$lib/stores"; import { account, credentials } from "$lib/stores";
import { onMount } from "svelte"; import { onMount } from "svelte";
import Entries from "./Entries.svelte"; import Entries from "./Entries.svelte";
import Overview from "./Overview.svelte" import Overview from "./Overview.svelte";
import { FontAwesomeIcon } from "@fortawesome/svelte-fontawesome";
import { 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)))
let throtle = false;
function createPageHandler({ onLoadingStatusChanged, onEndReached }: { onLoadingStatusChanged: (status: boolean) => any, onEndReached: () => any }) {
let loadingPage = false;
let rechedEnd = false;
let currentOffset = 10; let currentOffset = 10;
let step = 5; let step = 5;
let entries = entryPage($credentials!, 0, currentOffset);
return {
initialEntries: entryPage($credentials!, 0, currentOffset),
nextPage: async () => {
if (loadingPage || reachedEnd) {
return undefined;
}
loadingPage = true;
onLoadingStatusChanged(loadingPage);
let page = await entryPage($credentials!, currentOffset, step);
currentOffset += step;
loadingPage = false;
onLoadingStatusChanged(loadingPage);
if (page.length === 0) {
reachedEnd = true;
onEndReached();
}
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 showFilterSelector = false
let chosenFilterFeelings = []
let filters = {
fromDate: null,
toDate: null,
kind: null,
feelings: null,
searchQuery: null,
}
onMount(() => { onMount(() => {
function handleScroll() { function handleScroll() {
if (!throtle && window.innerHeight + window.scrollY >= document.body.offsetHeight) { if (!filterStatus && window.innerHeight + window.scrollY >= document.body.offsetHeight) {
throtle = true
entries.then(async (page) => { entries.then(async (page) => {
console.log("new page requested") let secondPage = await nextPage()
let secondPage = await entryPage($credentials!, currentOffset, step); if (secondPage != null) {
page = [...page, ...secondPage]; page = [...page, ...secondPage];
currentOffset += step
throtle = false
entries = new Promise((resolve) => resolve(page)) entries = new Promise((resolve) => resolve(page))
}
}) })
} }
} }
@ -66,13 +112,41 @@
{/await} {/await}
</div> </div>
<div class="w-full flex items-baseline justify-between"> <h2 class="text-2xl mt-6">Entries</h2>
<h2 class="text-2xl mt-6">Memories</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-2 text-center hover:bg-violet-800 focus:ring-4 focus:ring-violet-300" href="/entry/new">+ Add an entry</a> <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> </div>
{#if showFilterSelector}
<FilterSelector on:updatedChosenFeelings={(e) => chosenFilterFeelings = e.detail} on:updatedFilter={(e) => filters = e.detail} chosenFeelings={chosenFilterFeelings} filters={filters}/>
{/if}
<div class="mt-3.5 flex flex-col gap-1"> <div class="mt-3.5 flex flex-col gap-1">
<Entries on:deleted={() => refreshEntries()} entries={entries}/> <Entries on:updatedFilterStatus={(e) => filterStatus = e.detail} on:deleted={() => refreshEntries()} entries={entries} filters={filters}/>
</div> </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}
{#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} {/if}
</div> </div>
</div> </div>

View file

@ -1,23 +1,97 @@
<script lang="ts"> <script lang="ts">
import { type Entry as EntryType, TITLED_ENTRIES } from "$lib/entry"; import type { Entry as EntryType, EntryKind, KnownFeeling } from "$lib/entry";
import ExternalLink from "./utils/ExternalLink.svelte"; import ExternalLink from "./utils/ExternalLink.svelte";
import FeelingPill from "./utils/FeelingPill.svelte"; import FeelingPill from "./utils/FeelingPill.svelte";
import Entry from "./utils/Entry.svelte"; import Entry from "./utils/Entry.svelte";
import EntryDescription from "./utils/EntryDescription.svelte"; import EntryDescription from "./utils/EntryDescription.svelte";
import AssetPreview from "./utils/AssetPreview.svelte"; import AssetPreview from "./utils/AssetPreview.svelte";
import { createEventDispatcher } from "svelte"; import { createEventDispatcher } from "svelte";
import Fuse from "fuse.js";
let dispatch = createEventDispatcher() let dispatch = createEventDispatcher()
export let entries: EntryType[] 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[] = [] 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
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: [
"title",
"description",
],
});
let results = fuse.search(filters.searchQuery!);
filteredEntries = results.map((v) => v.item);
}
if (filteredEntries.length !== entries.length) {
dispatch('updatedFilterStatus', true)
} else {
dispatch('updatedFilterStatus', false)
}
}
$: applyFilters(filters)
</script> </script>
{#each entries as entry (entry.id)} {#each filteredEntries as entry (entry.id)}
<Entry <Entry
on:extended={() => extended = [entry.id, ...extended]} on:extended={(e) => extended = [e.detail.id, ...extended]}
on:contracted={() => extended = extended.filter(v => v !== entry.id)} on:contracted={(e) => extended = extended.filter(v => v !== e.detail.id)}
on:deleted={(event) => { dispatch('deleted', event.detail) }} on:deleted={(e) => { dispatch('deleted', e.detail) }}
id={entry.id} id={entry.id}
kind={entry.base.kind} kind={entry.base.kind}

View file

@ -16,7 +16,7 @@
let prevExtended = isExtended let prevExtended = isExtended
$: if (prevExtended !== isExtended) { $: if (prevExtended !== isExtended) {
dispatch(isExtended ? 'extended' : 'contracted') dispatch(isExtended ? 'extended' : 'contracted', { id })
} }
async function processDeletion(id: string) { async function processDeletion(id: string) {
@ -44,7 +44,7 @@
</script> </script>
<div class={cardClass()} id={`entry__${id}`}> <div class={cardClass()} id={`entry__${id}`}>
<button on:click={() => isExtended = !isExtended}> <button on:click={() => { prevExtended = isExtended; isExtended = !isExtended }}>
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="flex items-center gap-2.5"> <div class="flex items-center gap-2.5">
<EntryKind kind={kind}/> <EntryKind kind={kind}/>

View file

@ -30,9 +30,10 @@
export let feeling: "relaxed" | "afraid" | "angry" | "bad" | "bored" | "confused" | "excited" | "fine" | "happy" | "hurt" | "in love" | "mad" | "nervous" | "okay" | "sad" | "scared" | "shy" | "sleepy" | "active" | "surprised" | "tired" | "upset" | "worried" | string export let feeling: "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 bgColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS["__DEFAULT__"])[0]
export let textColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS["__DEFAULT__"])[1] export let textColor: string = (DEFAULT_COLORS[feeling] || DEFAULT_COLORS["__DEFAULT__"])[1]
export let slim = false;
</script> </script>
<div class="inline-block py-0.5 px-1.5 rounded-full text-sm font-semibold my-0.5 w-22 text-center" style={`background-color: ${bgColor}; color: ${textColor}`}> <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"/> <slot name="pre"/>
<span>{feeling.charAt(0).toUpperCase() + feeling.slice(1)}</span> <span>{feeling.charAt(0).toUpperCase() + feeling.slice(1)}</span>

View file

@ -0,0 +1,105 @@
<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";
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 chosenFeelings: KnownFeeling[] = []
let touched = {
fromDate: false,
toDate: false,
}
function upstreamChanges() {
dispatch('updatedFilter', filters)
}
function handleKind(kind: string | EntryKind) {
if (kind.length === 0) {
filters.kind = null;
} else {
filters.kind = [kind as EntryKind];
}
upstreamChanges();
}
function handleDate(date: string | null, kind: "fromDate" | "toDate") {
touched[kind] = true;
if (date == null || date.length === 0) {
filters[kind] = null
} else {
filters[kind] = new Date(date)
}
upstreamChanges()
}
function handleSearchQuery(query: string) {
if (query.length === 0) {
filters.searchQuery = null;
} else {
filters.searchQuery = query.trim();
}
upstreamChanges()
}
function handleFeelings(feelings: KnownFeeling[]) {
chosenFeelings = feelings;
dispatch('updatedChosenFeelings', chosenFeelings)
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>

View file

@ -8,14 +8,10 @@
import FeelingPill from "../../dashboard/utils/FeelingPill.svelte"; import FeelingPill from "../../dashboard/utils/FeelingPill.svelte";
import { addEntry, uploadAsset } from "$lib/api"; import { addEntry, uploadAsset } from "$lib/api";
import { credentials, session_key } from "$lib/stores"; 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 feelingsDropdownShown = false
let chosenFeelings: KnownFeeling[] = []
$: feelingsToChoose = FEELINGS.filter(v => !chosenFeelings.includes(v))
let kind: EntryKind | null = "song" let kind: EntryKind | null = "song"
const { form, errors } = createForm({ const { form, errors } = createForm({
onSubmit: async (values) => { onSubmit: async (values) => {
@ -209,47 +205,8 @@
<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"> <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>
<div class="mb-5 flex flex-col"> <div class="mb-5">
<span class="block mb-2 text-sm font-medium text-gray-900">Feelings</span> <FeelingsChooser required={kind === "feeling"}/>
<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 p-2.5`} placeholder="https://www.music.tld/play/...">
{#if chosenFeelings.length > 0}
<div>
<span class="mr-1">Chosen:</span>
{#each chosenFeelings as feeling (feeling)}
<div class="inline">
<button type="button" on:click={() => chosenFeelings = chosenFeelings.filter(v => v !== feeling)}>
<FeelingPill feeling={feeling}>
<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 kind === "feeling"}
<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 p-1">
<button type="button" on:click={() => chosenFeelings = [feeling, ...chosenFeelings]}>
<FeelingPill feeling={feeling}>
<span class="pr-1" slot="pre"><FontAwesomeIcon icon={faPlus}/></span>
</FeelingPill>
</button>
</label>
{/each}
</div>
{#if $errors.feelings != null} {#if $errors.feelings != null}
<p class="text-sm text-red-600 mt-1.5"><span class="font-medium">{$errors.feelings[0]}</span></p> <p class="text-sm text-red-600 mt-1.5"><span class="font-medium">{$errors.feelings[0]}</span></p>
{/if} {/if}

View file

@ -1759,6 +1759,13 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"fuse.js@npm:^7.0.0":
version: 7.0.0
resolution: "fuse.js@npm:7.0.0"
checksum: 10c0/3574b7fc2e0ccb047e05dbe5f8f04e8f0754f62fa209669ef426ea1354a32ae7355620788af8f1d29f94e1fdecd513f1f3787f012848a31ec90bb4e0e6092504
languageName: node
linkType: hard
"glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2": "glob-parent@npm:^5.1.2, glob-parent@npm:~5.1.2":
version: 5.1.2 version: 5.1.2
resolution: "glob-parent@npm:5.1.2" resolution: "glob-parent@npm:5.1.2"
@ -1932,6 +1939,7 @@ __metadata:
eslint-config-prettier: "npm:^9.1.0" eslint-config-prettier: "npm:^9.1.0"
eslint-plugin-svelte: "npm:^2.36.0" eslint-plugin-svelte: "npm:^2.36.0"
felte: "npm:^1.2.14" felte: "npm:^1.2.14"
fuse.js: "npm:^7.0.0"
globals: "npm:^15.0.0" globals: "npm:^15.0.0"
mime: "npm:^4.0.3" mime: "npm:^4.0.3"
postcss: "npm:^8.4.38" postcss: "npm:^8.4.38"