implement filtering!
This commit is contained in:
parent
e4d65e303f
commit
3e6bed20a6
16 changed files with 439 additions and 72 deletions
|
@ -7,6 +7,7 @@ import { join } from "node:path";
|
|||
import mime from "mime";
|
||||
import { promisify } from "node:util";
|
||||
import { pipeline } from "node:stream";
|
||||
import cors from "@fastify/cors";
|
||||
|
||||
const M2M_ALGORITHM = "RSA-SHA512";
|
||||
const { private: M2M_PRIVATE_KEY, public: M2M_PUBLIC_KEY } = loadM2MKeys();
|
||||
|
@ -25,6 +26,9 @@ const fastify = new Fastify({
|
|||
});
|
||||
|
||||
fastify.register(multipart);
|
||||
fastify.register(cors, {
|
||||
origin: true,
|
||||
})
|
||||
|
||||
fastify.get("/", async () => {
|
||||
return signString(ASSET_API_LANDING_MESSAGE);
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
"type": "module",
|
||||
"packageManager": "yarn@4.3.0",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"@fastify/multipart": "^8.3.0",
|
||||
"fastify": "^4.28.0",
|
||||
"mime": "^4.0.3",
|
||||
|
|
|
@ -83,6 +83,16 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 1.3.0
|
||||
resolution: "@fastify/deepmerge@npm:1.3.0"
|
||||
|
@ -284,6 +294,7 @@ __metadata:
|
|||
resolution: "asset-api@workspace:."
|
||||
dependencies:
|
||||
"@eslint/js": "npm:^9.5.0"
|
||||
"@fastify/cors": "npm:^9.0.1"
|
||||
"@fastify/multipart": "npm:^8.3.0"
|
||||
eslint: "npm:9.x"
|
||||
fastify: "npm:^4.28.0"
|
||||
|
@ -931,6 +942,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.1.2
|
||||
resolution: "ms@npm:2.1.2"
|
||||
|
@ -945,6 +965,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.1.2
|
||||
resolution: "on-exit-leak-free@npm:2.1.2"
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
"type": "module",
|
||||
"packageManager": "yarn@4.3.0",
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^9.0.1",
|
||||
"dotenv": "^16.4.5",
|
||||
"fastify": "^4.27.0",
|
||||
"jose": "^5.4.0"
|
||||
|
|
|
@ -3,9 +3,14 @@ import { ASSET_API_ENDPOINT, IDENTITY_API_LANDING_MESSAGE, LISTEN_PORT } from ".
|
|||
import { contentFromSigned, verifySignature } from "./m2m.js";
|
||||
import { startAuth } from "./auth.js";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import cors from "@fastify/cors";
|
||||
|
||||
let auth = await startAuth();
|
||||
|
||||
app.register(cors, {
|
||||
origin: true,
|
||||
})
|
||||
|
||||
app.get("/", async () => {
|
||||
return IDENTITY_API_LANDING_MESSAGE;
|
||||
});
|
||||
|
|
|
@ -76,6 +76,16 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 3.4.1
|
||||
resolution: "@fastify/error@npm:3.4.1"
|
||||
|
@ -591,6 +601,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 4.27.0
|
||||
resolution: "fastify@npm:4.27.0"
|
||||
|
@ -713,6 +730,7 @@ __metadata:
|
|||
resolution: "identity-api@workspace:."
|
||||
dependencies:
|
||||
"@eslint/js": "npm:^9.5.0"
|
||||
"@fastify/cors": "npm:^9.0.1"
|
||||
dotenv: "npm:^16.4.5"
|
||||
eslint: "npm:9.x"
|
||||
fastify: "npm:^4.27.0"
|
||||
|
@ -900,6 +918,15 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.1.2
|
||||
resolution: "ms@npm:2.1.2"
|
||||
|
@ -914,6 +941,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 2.1.2
|
||||
resolution: "on-exit-leak-free@npm:2.1.2"
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
"@fortawesome/free-solid-svg-icons": "^6.5.2",
|
||||
"@fortawesome/svelte-fontawesome": "^0.2.2",
|
||||
"felte": "^1.2.14",
|
||||
"fuse.js": "^7.0.0",
|
||||
"mime": "^4.0.3"
|
||||
}
|
||||
}
|
||||
|
|
74
identity-web/src/lib/components/FeelingsChooser.svelte
Normal file
74
identity-web/src/lib/components/FeelingsChooser.svelte
Normal 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>
|
|
@ -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 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,
|
||||
|
|
|
@ -3,27 +3,73 @@
|
|||
import { account, credentials } from "$lib/stores";
|
||||
import { onMount } from "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)))
|
||||
let throtle = false;
|
||||
|
||||
function createPageHandler({ onLoadingStatusChanged, onEndReached }: { onLoadingStatusChanged: (status: boolean) => any, onEndReached: () => any }) {
|
||||
let loadingPage = false;
|
||||
let rechedEnd = false;
|
||||
let currentOffset = 10;
|
||||
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 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(() => {
|
||||
function handleScroll() {
|
||||
if (!throtle && window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
||||
throtle = true
|
||||
if (!filterStatus && window.innerHeight + window.scrollY >= document.body.offsetHeight) {
|
||||
entries.then(async (page) => {
|
||||
console.log("new page requested")
|
||||
let secondPage = await entryPage($credentials!, currentOffset, step);
|
||||
let secondPage = await nextPage()
|
||||
if (secondPage != null) {
|
||||
page = [...page, ...secondPage];
|
||||
|
||||
currentOffset += step
|
||||
throtle = false
|
||||
entries = new Promise((resolve) => resolve(page))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -66,13 +112,41 @@
|
|||
{/await}
|
||||
</div>
|
||||
|
||||
<div class="w-full flex items-baseline justify-between">
|
||||
<h2 class="text-2xl mt-6">Memories</h2>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
{#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">
|
||||
<Entries on:deleted={() => refreshEntries()} entries={entries}/>
|
||||
<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}
|
||||
|
||||
{#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>
|
||||
|
|
|
@ -1,23 +1,97 @@
|
|||
<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 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()
|
||||
|
||||
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
|
||||
|
||||
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>
|
||||
|
||||
{#each entries as entry (entry.id)}
|
||||
{#each filteredEntries as entry (entry.id)}
|
||||
<Entry
|
||||
on:extended={() => extended = [entry.id, ...extended]}
|
||||
on:contracted={() => extended = extended.filter(v => v !== entry.id)}
|
||||
on:deleted={(event) => { dispatch('deleted', event.detail) }}
|
||||
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}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
let prevExtended = isExtended
|
||||
|
||||
$: if (prevExtended !== isExtended) {
|
||||
dispatch(isExtended ? 'extended' : 'contracted')
|
||||
dispatch(isExtended ? 'extended' : 'contracted', { id })
|
||||
}
|
||||
|
||||
async function processDeletion(id: string) {
|
||||
|
@ -44,7 +44,7 @@
|
|||
</script>
|
||||
|
||||
<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 items-center gap-2.5">
|
||||
<EntryKind kind={kind}/>
|
||||
|
|
|
@ -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 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 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"/>
|
||||
|
||||
<span>{feeling.charAt(0).toUpperCase() + feeling.slice(1)}</span>
|
||||
|
|
105
identity-web/src/routes/dashboard/utils/FilterSelector.svelte
Normal file
105
identity-web/src/routes/dashboard/utils/FilterSelector.svelte
Normal 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>
|
|
@ -8,14 +8,10 @@
|
|||
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)))
|
||||
|
||||
let feelingsDropdownShown = false
|
||||
let chosenFeelings: KnownFeeling[] = []
|
||||
|
||||
$: feelingsToChoose = FEELINGS.filter(v => !chosenFeelings.includes(v))
|
||||
|
||||
let kind: EntryKind | null = "song"
|
||||
const { form, errors } = createForm({
|
||||
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">
|
||||
</div>
|
||||
|
||||
<div class="mb-5 flex flex-col">
|
||||
<span class="block mb-2 text-sm font-medium text-gray-900">Feelings</span>
|
||||
<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>
|
||||
<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}
|
||||
|
|
|
@ -1759,6 +1759,13 @@ __metadata:
|
|||
languageName: node
|
||||
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":
|
||||
version: 5.1.2
|
||||
resolution: "glob-parent@npm:5.1.2"
|
||||
|
@ -1932,6 +1939,7 @@ __metadata:
|
|||
eslint-config-prettier: "npm:^9.1.0"
|
||||
eslint-plugin-svelte: "npm:^2.36.0"
|
||||
felte: "npm:^1.2.14"
|
||||
fuse.js: "npm:^7.0.0"
|
||||
globals: "npm:^15.0.0"
|
||||
mime: "npm:^4.0.3"
|
||||
postcss: "npm:^8.4.38"
|
||||
|
|
Loading…
Reference in a new issue