mirror of
https://github.com/tcsenpai/pensieve.git
synced 2025-06-08 20:25:30 +00:00
feat(web): update ui
This commit is contained in:
parent
f8b5eb14e6
commit
3c66b9539d
@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev",
|
"dev": "vite dev",
|
||||||
"build": "vite build",
|
"build": "PUBLIC_API_ENDPOINT= vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
|
||||||
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
|
||||||
|
@ -9,9 +9,14 @@
|
|||||||
*/
|
*/
|
||||||
export let title;
|
export let title;
|
||||||
/**
|
/**
|
||||||
* @type {any}
|
* @type {Array<string>}
|
||||||
*/
|
*/
|
||||||
export let content;
|
export let tags = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {Array<{key: string, source: string, value: any}>}
|
||||||
|
*/
|
||||||
|
export let metadata_entries = [];
|
||||||
/**
|
/**
|
||||||
* @type {any}
|
* @type {any}
|
||||||
*/
|
*/
|
||||||
@ -27,42 +32,64 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full" id="my-modal">
|
<div class="fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full" id="my-modal">
|
||||||
<div class="relative top-10 mx-auto p-10 border w-11/12 max-w-14xl shadow-lg rounded-md bg-white group">
|
<div
|
||||||
|
class="relative top-10 mx-auto p-10 border w-11/12 max-w-14xl shadow-lg rounded-md bg-white group"
|
||||||
|
>
|
||||||
<!-- Button container -->
|
<!-- Button container -->
|
||||||
<div class="group">
|
<div class="group">
|
||||||
<button class="absolute left-5 top-1/2 transform -translate-y-1/2 text-white bg-gray-300 hover:bg-gray-400 font-bold rounded-full text-2xl w-12 h-12 opacity-0 group-hover:opacity-100" on:click={onPrevious}>
|
<button
|
||||||
|
class="absolute left-5 top-1/2 transform -translate-y-1/2 text-white bg-gray-300 hover:bg-gray-400 font-bold rounded-full text-2xl w-12 h-12 opacity-0 group-hover:opacity-100"
|
||||||
|
on:click={onPrevious}
|
||||||
|
>
|
||||||
<
|
<
|
||||||
</button>
|
</button>
|
||||||
<button class="absolute right-5 top-1/2 transform -translate-y-1/2 text-white bg-gray-300 hover:bg-gray-400 font-bold rounded-full text-2xl w-12 h-12 opacity-0 group-hover:opacity-100" on:click={onNext}>
|
<button
|
||||||
|
class="absolute right-5 top-1/2 transform -translate-y-1/2 text-white bg-gray-300 hover:bg-gray-400 font-bold rounded-full text-2xl w-12 h-12 opacity-0 group-hover:opacity-100"
|
||||||
|
on:click={onNext}
|
||||||
|
>
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
<!-- Your modal content goes here -->
|
<!-- Your modal content goes here -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<!-- Image container -->
|
<!-- Image container -->
|
||||||
<div class="flex-none w-full md:w-1/2 h-96 md:h-auto">
|
<div class="flex-none w-full md:w-1/2 h-96 md:h-auto">
|
||||||
<img class="h-full w-full object-cover" src={image} alt={title} />
|
<img class="h-full w-full object-cover" src={image} alt={title} />
|
||||||
</div>
|
</div>
|
||||||
<!-- Description container -->
|
<!-- Description container -->
|
||||||
<div class="mt-4 md:mt-0 md:ml-6 overflow-y-auto h-96 md:h-auto">
|
<div class="mt-4 md:mt-0 md:ml-6 overflow-y-auto h-96 md:h-auto">
|
||||||
<div class="uppercase tracking-wide text-sm text-indigo-600 font-bold">Image Title</div>
|
<div class="uppercase tracking-wide text-sm text-indigo-600 font-bold">Image Title</div>
|
||||||
<p class="block mt-1 text-lg leading-tight font-medium text-black hover:underline">
|
<p class="block mt-1 text-lg leading-tight font-medium text-black hover:underline">
|
||||||
{title}
|
{title}
|
||||||
</p>
|
</p>
|
||||||
<p class="mt-2 text-gray-600">
|
<div class="uppercase tracking-wide text-sm text-indigo-600 font-bold">TAGS</div>
|
||||||
{#each content as item}
|
<div class="mt-2 text-gray-600">
|
||||||
<span class="text-base text-gray-500 inline-block">{item}</span>
|
{#each tags as tag}
|
||||||
{/each}
|
<span class="text-base text-gray-500 inline-block">{tag}</span>
|
||||||
</p>
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="uppercase tracking-wide text-sm text-indigo-600 font-bold">METADATA</div>
|
||||||
<div class="absolute top-0 right-0 pt-4 pr-4">
|
<div class="mt-2 text-gray-600">
|
||||||
<button class="text-gray-400 hover:text-gray-600" on:click={onClose}>
|
{#each metadata_entries as entry}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<div class="mb-2">
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<span class="font-bold">{entry.key}:</span>
|
||||||
<span class="text-2xl">×</span>
|
{#if typeof entry.value === 'object'}
|
||||||
</button>
|
<pre class="bg-gray-100 p-2 rounded overflow-y-auto max-h-96">{JSON.stringify(entry.value, null, 2)}</pre>
|
||||||
</div>
|
{:else}
|
||||||
|
{entry.value}
|
||||||
|
{/if}
|
||||||
|
<span class="text-sm text-gray-500">({entry.source})</span>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="absolute top-0 right-0 pt-4 pr-4">
|
||||||
|
<button class="text-gray-400 hover:text-gray-600" on:click={onClose}>
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
|
<span class="text-2xl">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,152 +1,164 @@
|
|||||||
<script>
|
<script>
|
||||||
import Figure from "$lib/Figure.svelte";
|
import Figure from '$lib/Figure.svelte';
|
||||||
|
import { PUBLIC_API_ENDPOINT } from '$env/static/public';
|
||||||
|
|
||||||
|
let searchString = '';
|
||||||
|
/**
|
||||||
|
|
||||||
let searchString = '';
|
|
||||||
/**
|
|
||||||
* @type {any[]}
|
* @type {any[]}
|
||||||
*/
|
*/
|
||||||
let searchResults = [];
|
let searchResults = [];
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
let debounceTimer;
|
let debounceTimer;
|
||||||
let showModal= false;
|
let showModal = false;
|
||||||
let selectedImage = 0;
|
let selectedImage = 0;
|
||||||
|
|
||||||
const debounceDelay = 300;
|
const debounceDelay = 300;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} query
|
* @param {string} query
|
||||||
*/
|
*/
|
||||||
async function searchItems(query) {
|
async function searchItems(query) {
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:8080/search?q=${encodeURIComponent(query)}`);
|
const apiEndpoint = typeof PUBLIC_API_ENDPOINT !== 'undefined' ? PUBLIC_API_ENDPOINT : window.location.origin;
|
||||||
if (!response.ok) {
|
const response = await fetch(`${apiEndpoint}/search?q=${encodeURIComponent(query)}`);
|
||||||
throw new Error('Network response was not ok');
|
if (!response.ok) {
|
||||||
}
|
throw new Error('Network response was not ok');
|
||||||
searchResults = await response.json();
|
}
|
||||||
console.log(searchResults);
|
searchResults = await response.json();
|
||||||
} catch (error) {
|
console.log(searchResults);
|
||||||
} finally {
|
} catch (error) {
|
||||||
isLoading = false;
|
} finally {
|
||||||
}
|
isLoading = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} query
|
* @param {string} query
|
||||||
*/
|
*/
|
||||||
function debounceSearch(query) {
|
function debounceSearch(query) {
|
||||||
clearTimeout(debounceTimer);
|
clearTimeout(debounceTimer);
|
||||||
debounceTimer = setTimeout(() => {
|
debounceTimer = setTimeout(() => {
|
||||||
searchItems(query);
|
searchItems(query);
|
||||||
}, debounceDelay);
|
}, debounceDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} path
|
* @param {string} path
|
||||||
*/
|
*/
|
||||||
function filename(path) {
|
function filename(path) {
|
||||||
let splits = path.split('/');
|
let splits = path.split('/');
|
||||||
return splits[splits.length - 1];
|
return splits[splits.length - 1];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {number} index
|
* @param {number} index
|
||||||
*/
|
*/
|
||||||
function openModal(index) {
|
function openModal(index) {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
showModal = true;
|
showModal = true;
|
||||||
selectedImage = index;
|
selectedImage = index;
|
||||||
disableScroll();
|
disableScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
showModal = false;
|
showModal = false;
|
||||||
enableScroll();
|
enableScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ key: string; }} event
|
* @param {{ key: string; }} event
|
||||||
*/
|
*/
|
||||||
function handleKeydown(event) {
|
function handleKeydown(event) {
|
||||||
if (showModal) {
|
if (showModal) {
|
||||||
if (event.key === 'Escape') {
|
if (event.key === 'Escape') {
|
||||||
closeModal();
|
closeModal();
|
||||||
} else if (event.key === 'ArrowRight') {
|
} else if (event.key === 'ArrowRight') {
|
||||||
selectedImage = (selectedImage + 1) % searchResults.length;
|
selectedImage = (selectedImage + 1) % searchResults.length;
|
||||||
} else if (event.key === 'ArrowLeft') {
|
} else if (event.key === 'ArrowLeft') {
|
||||||
selectedImage = (selectedImage - 1 + searchResults.length) % searchResults.length;
|
selectedImage = (selectedImage - 1 + searchResults.length) % searchResults.length;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const disableScroll = () => {
|
const disableScroll = () => {
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
};
|
};
|
||||||
|
|
||||||
const enableScroll = () => {
|
const enableScroll = () => {
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
};
|
};
|
||||||
|
|
||||||
$: if (searchString.trim()) {
|
$: if (searchString.trim()) {
|
||||||
debounceSearch(searchString);
|
debounceSearch(searchString);
|
||||||
} else {
|
} else {
|
||||||
searchResults = [];
|
searchResults = [];
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window on:keydown={handleKeydown} />
|
<svelte:window on:keydown={handleKeydown} />
|
||||||
|
|
||||||
<div class="container mx-auto my-10">
|
<div class="container mx-auto my-10">
|
||||||
<input type="text" class="input input-bordered w-full my-4 p-2 text-lg border border-gray-500" bind:value={searchString} placeholder="Type to search..." />
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full my-4 p-2 text-lg border border-gray-500"
|
||||||
|
bind:value={searchString}
|
||||||
|
placeholder="Type to search..."
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="container mx-auto">
|
<div class="container mx-auto">
|
||||||
{#if isLoading}
|
{#if isLoading}
|
||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
{:else if searchString}
|
{:else if searchString}
|
||||||
<div class="grid grid-cols-4 gap-4">
|
<div class="grid grid-cols-4 gap-4">
|
||||||
{#each searchResults as item, index}
|
{#each searchResults as item, index}
|
||||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||||
<div class="bg-white rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300 ease-in-out" on:click={() => openModal(index)}>
|
<div
|
||||||
<figure class="px-5 pt-5">
|
class="bg-white rounded-lg overflow-hidden shadow-md hover:shadow-xl transition-shadow duration-300 ease-in-out"
|
||||||
<img class="w-full h-48 object-cover" src={`http://localhost:8080/files/${item.filepath}`} alt="" />
|
on:click={() => openModal(index)}
|
||||||
</figure>
|
>
|
||||||
<div class="p-4">
|
<figure class="px-5 pt-5">
|
||||||
<h2 class="text-lg font-bold mb-2">{filename(item.filepath)}</h2>
|
<img
|
||||||
<p class="text-gray-700 line-clamp-5">{''}</p>
|
class="w-full h-48 object-cover"
|
||||||
</div>
|
src={`http://localhost:8080/files/${item.filepath}`}
|
||||||
</div>
|
alt=""
|
||||||
{/each}
|
/>
|
||||||
</div>
|
</figure>
|
||||||
{:else}
|
<div class="p-4">
|
||||||
<p>Type something to start searching...</p>
|
<h2 class="text-lg font-bold mb-2">{filename(item.filepath)}</h2>
|
||||||
{/if}
|
<p class="text-gray-700 line-clamp-5">{''}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<p>Type something to start searching...</p>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
{#if searchResults.length && showModal}
|
{#if searchResults.length && showModal}
|
||||||
<Figure
|
<Figure
|
||||||
image={`http://localhost:8080/files/${searchResults[selectedImage].filepath}`}
|
image={`http://localhost:8080/files/${searchResults[selectedImage].filepath}`}
|
||||||
title={filename(searchResults[selectedImage].filepath)}
|
title={filename(searchResults[selectedImage].filepath)}
|
||||||
content={''}
|
tags={searchResults[selectedImage].tags}
|
||||||
onClose={closeModal}
|
metadata_entries={searchResults[selectedImage].metadata_entries}
|
||||||
onNext={() => openModal((selectedImage + 1) % searchResults.length)}
|
content={''}
|
||||||
onPrevious={() => openModal((selectedImage - 1 + searchResults.length) % searchResults.length)}
|
onClose={closeModal}
|
||||||
/>
|
onNext={() => openModal((selectedImage + 1) % searchResults.length)}
|
||||||
|
onPrevious={() => openModal((selectedImage - 1 + searchResults.length) % searchResults.length)}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<footer class="mx-auto mt-32 w-full container">
|
<footer class="mx-auto mt-32 w-full container">
|
||||||
<div class="border-t border-slate-900/5 py-10">
|
<div class="border-t border-slate-900/5 py-10">
|
||||||
<p class="mt-2 text-sm leading-6 text-slate-500">© 2023 Labs Inc. All rights reserved.</p>
|
<p class="mt-2 text-sm leading-6 text-slate-500">© 2023 Labs Inc. All rights reserved.</p>
|
||||||
<div class="mt-2 flex items-center space-x-4 text-sm font-semibold leading-6 text-slate-700">
|
<div class="mt-2 flex items-center space-x-4 text-sm font-semibold leading-6 text-slate-700">
|
||||||
<a href="/privacy-policy">Privacy policy</a>
|
<a href="/privacy-policy">Privacy policy</a>
|
||||||
<div class="h-4 w-px bg-slate-500/20"></div>
|
<div class="h-4 w-px bg-slate-500/20" />
|
||||||
<a href="/changelog">Changelog</a>
|
<a href="/changelog">Changelog</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user