Files
bewcloud/components/files/SearchFiles.tsx
Bruno Bernardino eabd888df2 Fix timezone issues with expenses.
I was able to reproduce the problem by setting my system to a timezone, and my `TZ` to a different one. Since it'll default to `UTC`, and to avoid having to pass it around from the system to the client (since we don't really care about the timezone), we simply force the timezone to UTC in the formatting as well, because, again, we don't store timezones or care about them for expenses.

Fixes #88
2025-08-22 12:55:10 +01:00

176 lines
5.8 KiB
TypeScript

import { useSignal } from '@preact/signals';
import { useEffect } from 'preact/hooks';
import { Directory, DirectoryFile } from '/lib/types.ts';
import { RequestBody, ResponseBody } from '/routes/api/files/search.tsx';
interface SearchFilesProps {}
export default function SearchFiles({}: SearchFilesProps) {
const isSearching = useSignal<boolean>(false);
const areResultsVisible = useSignal<boolean>(false);
const matchingDirectories = useSignal<Directory[]>([]);
const matchingFiles = useSignal<DirectoryFile[]>([]);
const searchTimeout = useSignal<ReturnType<typeof setTimeout>>(0);
const closeTimeout = useSignal<ReturnType<typeof setTimeout>>(0);
const dateFormatOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: '2-digit',
};
const dateFormat = new Intl.DateTimeFormat('en-GB', dateFormatOptions);
function searchFiles(searchTerm: string) {
if (searchTimeout.value) {
clearTimeout(searchTimeout.value);
}
if (searchTerm.trim().length < 2) {
return;
}
areResultsVisible.value = false;
searchTimeout.value = setTimeout(async () => {
isSearching.value = true;
try {
const requestBody: RequestBody = { searchTerm };
const response = await fetch(`/api/files/search`, {
method: 'POST',
body: JSON.stringify(requestBody),
});
if (!response.ok) {
throw new Error(`Failed to search files. ${response.statusText} ${await response.text()}`);
}
const result = await response.json() as ResponseBody;
if (!result.success) {
throw new Error('Failed to search files!');
}
matchingDirectories.value = [...result.directories];
matchingFiles.value = [...result.files];
if (matchingDirectories.value.length > 0 || matchingFiles.value.length > 0) {
areResultsVisible.value = true;
}
} catch (error) {
console.error(error);
}
isSearching.value = false;
}, 500);
}
function onFocus() {
if (matchingDirectories.value.length > 0 || matchingFiles.value.length > 0) {
areResultsVisible.value = true;
}
}
function onBlur() {
if (closeTimeout.value) {
clearTimeout(closeTimeout.value);
}
closeTimeout.value = setTimeout(() => {
areResultsVisible.value = false;
}, 300);
}
useEffect(() => {
return () => {
if (searchTimeout.value) {
clearTimeout(searchTimeout.value);
}
if (closeTimeout.value) {
clearTimeout(closeTimeout.value);
}
};
}, []);
return (
<>
<input
class='input-field w-72 mr-2'
type='search'
name='search'
placeholder='Search files...'
onInput={(event) => searchFiles(event.currentTarget.value)}
onFocus={() => onFocus()}
onBlur={() => onBlur()}
/>
{isSearching.value ? <img src='/images/loading.svg' class='white mr-2' width={18} height={18} /> : null}
{areResultsVisible.value
? (
<section class='relative inline-block text-left ml-2 text-sm'>
<section
class={`absolute right-0 z-10 mt-2 w-80 origin-top-right rounded-md bg-slate-600 shadow-lg ring-1 ring-black ring-opacity-15 focus:outline-none overflow-y-scroll max-h-[80%] min-h-56`}
role='menu'
aria-orientation='vertical'
aria-labelledby='view-button'
tabindex={-1}
>
<section class='py-1'>
<ol class='mt-2'>
{matchingDirectories.value.map((directory) => (
<li class='mb-1'>
<a
href={`/files?path=${encodeURIComponent(directory.parent_path)}${
encodeURIComponent(directory.directory_name)
}`}
class={`block px-2 py-2 hover:no-underline hover:opacity-60 bg-slate-700 cursor-pointer font-normal`}
target='_blank'
rel='noopener noreferrer'
>
<time
datetime={new Date(directory.updated_at).toISOString()}
class='mr-2 flex-none text-slate-100 block text-xs'
>
{dateFormat.format(new Date(directory.updated_at))}
</time>
<p class='flex-auto truncate font-medium text-white'>
{directory.directory_name}
</p>
</a>
</li>
))}
{matchingFiles.value.map((file) => (
<li class='mb-1'>
<a
href={`/files/open/${encodeURIComponent(file.file_name)}?path=${
encodeURIComponent(file.parent_path)
}`}
class={`block px-2 py-2 hover:no-underline hover:opacity-60 bg-slate-700 cursor-pointer font-normal`}
target='_blank'
rel='noopener noreferrer'
>
<time
datetime={new Date(file.updated_at).toISOString()}
class='mr-2 flex-none text-slate-100 block text-xs'
>
{dateFormat.format(new Date(file.updated_at))}
</time>
<p class='flex-auto truncate font-medium text-white'>
{file.file_name}
</p>
</a>
</li>
))}
</ol>
</section>
</section>
</section>
)
: null}
</>
);
}