Add basic Notes UI

This commit is contained in:
Bruno Bernardino
2024-04-26 14:31:25 +01:00
parent 2920de90b6
commit 3f5422f8eb
14 changed files with 849 additions and 104 deletions

109
islands/notes/Note.tsx Normal file
View File

@@ -0,0 +1,109 @@
import { useSignal, useSignalEffect } from '@preact/signals';
import { useEffect } from 'preact/hooks';
import { RequestBody, ResponseBody } from '/routes/api/notes/save.tsx';
import FilesBreadcrumb from '/components/files/FilesBreadcrumb.tsx';
interface NoteProps {
fileName: string;
currentPath: string;
contents: string;
}
export default function Note({ fileName, currentPath, contents }: NoteProps) {
const saveTimeout = useSignal<ReturnType<typeof setTimeout>>(0);
const hasSavedTimeout = useSignal<ReturnType<typeof setTimeout>>(0);
const isSaving = useSignal<boolean>(false);
const hasSaved = useSignal<boolean>(false);
function saveNote(newNotes: string) {
if (saveTimeout.value) {
clearTimeout(saveTimeout.value);
}
saveTimeout.value = setTimeout(async () => {
hasSaved.value = false;
isSaving.value = true;
try {
const requestBody: RequestBody = { fileName, currentPath, contents: newNotes };
const response = await fetch(`/api/notes/save`, {
method: 'POST',
body: JSON.stringify(requestBody),
});
const result = await response.json() as ResponseBody;
if (!result.success) {
throw new Error('Failed to save note!');
}
} catch (error) {
console.error(error);
}
isSaving.value = false;
hasSaved.value = true;
}, 1000);
}
useSignalEffect(() => {
if (hasSaved.value && !hasSavedTimeout.value) {
hasSavedTimeout.value = setTimeout(() => {
hasSaved.value = false;
}, 3000);
}
});
useEffect(() => {
return () => {
if (saveTimeout.value) {
clearTimeout(saveTimeout.value);
}
if (hasSavedTimeout.value) {
clearTimeout(hasSavedTimeout.value);
}
};
}, []);
return (
<section class='flex flex-col'>
<section class='mx-auto max-w-7xl px-4 py-6 sm:px-6 lg:px-8 w-full flex flex-row items-center justify-start'>
<FilesBreadcrumb path={currentPath} isShowingNotes={true} />
<h3 class='text-base text-white font-semibold'>
<span class='mr-2 text-xs'>/</span>
{decodeURIComponent(fileName)}
</h3>
</section>
<textarea
class='my-2 input-field text-sm font-mono mx-auto max-w-7xl'
onInput={(event) => saveNote(event.currentTarget.value)}
rows={20}
>
{contents}
</textarea>
<span
class={`flex justify-end items-center text-sm mt-1 mx-2 ${
hasSaved.value ? 'text-green-600' : 'text-slate-100'
}`}
>
{isSaving.value
? (
<>
<img src='/images/loading.svg' class='white mr-2' width={18} height={18} />Saving...
</>
)
: null}
{hasSaved.value
? (
<>
<img src='/images/check.svg' class='green mr-2' width={18} height={18} />Saved!
</>
)
: null}
{!isSaving.value && !hasSaved.value ? <>&nbsp;</> : null}
</span>
</section>
);
}

View File

@@ -0,0 +1,21 @@
import { Directory, DirectoryFile } from '/lib/types.ts';
import MainNotes from '/components/notes/MainNotes.tsx';
interface NotesWrapperProps {
initialDirectories: Directory[];
initialFiles: DirectoryFile[];
initialPath: string;
}
// This wrapper is necessary because islands need to be the first frontend component, but they don't support functions as props, so the more complex logic needs to live in the component itself
export default function NotesWrapper(
{ initialDirectories, initialFiles, initialPath }: NotesWrapperProps,
) {
return (
<MainNotes
initialDirectories={initialDirectories}
initialFiles={initialFiles}
initialPath={initialPath}
/>
);
}