feat(web): update ui

This commit is contained in:
arkohut 2024-07-07 00:09:35 +08:00
parent f8b5eb14e6
commit 3c66b9539d
3 changed files with 184 additions and 145 deletions

View File

@ -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",

View File

@ -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}
>
&lt; &lt;
</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}
>
&gt; &gt;
</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">&times;</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">&times;</span>
</button>
</div>
</div> </div>
</div> </div>

View File

@ -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>