Make it public!

This commit is contained in:
Bruno Bernardino
2024-03-16 08:40:24 +00:00
commit a5cafdddca
114 changed files with 9569 additions and 0 deletions

199
islands/dashboard/Links.tsx Normal file
View File

@@ -0,0 +1,199 @@
import { useSignal } from '@preact/signals';
import { useEffect } from 'preact/hooks';
import { DashboardLink } from '/lib/types.ts';
import { validateUrl } from '/lib/utils.ts';
import { RequestBody, ResponseBody } from '/routes/api/dashboard/save-links.tsx';
interface LinksProps {
initialLinks: DashboardLink[];
}
export default function Links({ initialLinks }: LinksProps) {
const hasSavedTimeout = useSignal<ReturnType<typeof setTimeout>>(0);
const isSaving = useSignal<boolean>(false);
const hasSaved = useSignal<boolean>(false);
const links = useSignal<DashboardLink[]>(initialLinks);
async function saveLinks(newLinks: DashboardLink[]) {
if (isSaving.value) {
return;
}
hasSaved.value = false;
isSaving.value = true;
const oldLinks = [...links.value];
links.value = newLinks;
try {
const requestBody: RequestBody = { links: newLinks };
const response = await fetch(`/api/dashboard/save-links`, {
method: 'POST',
body: JSON.stringify(requestBody),
});
const result = await response.json() as ResponseBody;
if (!result.success) {
throw new Error('Failed to save notes!');
}
} catch (error) {
console.error(error);
links.value = [...oldLinks];
}
isSaving.value = false;
hasSaved.value = true;
if (hasSavedTimeout.value) {
clearTimeout(hasSavedTimeout.value);
}
hasSavedTimeout.value = setTimeout(() => {
hasSaved.value = false;
}, 3000);
}
useEffect(() => {
return () => {
if (hasSavedTimeout.value) {
clearTimeout(hasSavedTimeout.value);
}
};
});
function onClickAddLink() {
const name = (prompt(`What's the **name** for the new link?`) || '').trim();
const url = (prompt(`What's the **URL** for the new link?`) || '').trim();
if (!name || !url) {
alert('A name and URL are required for a new link!');
return;
}
if (!validateUrl(url)) {
alert('Invalid URL!');
return;
}
const newLinks = [...links.value, { name, url }];
saveLinks(newLinks);
}
function onClickDeleteLink(indexToDelete: number) {
if (confirm('Are you sure you want to delete this link?')) {
const newLinks = [...links.value];
newLinks.splice(indexToDelete, 1);
saveLinks(newLinks);
}
}
function onClickMoveLeftLink(indexToMoveLeft: number) {
if (indexToMoveLeft <= 0) {
return;
}
if (confirm('Are you sure you want to move this link left?')) {
const newLinks = [...links.value];
const linkToMove = newLinks.splice(indexToMoveLeft, 1);
newLinks.splice(indexToMoveLeft - 1, 0, linkToMove[0]);
saveLinks(newLinks);
}
}
return (
<>
<section class='flex flex-row items-center justify-end mb-4'>
<section class='flex items-center'>
<button
class='inline-block justify-center gap-x-1.5 rounded-md bg-[#51A4FB] px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-400 ml-2'
type='button'
title='Add new link'
onClick={() => onClickAddLink()}
>
<img
src='/images/add.svg'
alt='Add new link'
class={`white`}
width={20}
height={20}
/>
</button>
</section>
</section>
<section class='mx-auto max-w-7xl px-6 lg:px-8 my-8'>
<section class='group grid grid-cols-1 gap-x-8 gap-y-16 text-center lg:grid-cols-3'>
{links.value.map((link, index) => (
<div class='group mx-auto flex max-w-xs flex-col gap-y-4 rounded shadow-md bg-slate-700 relative hover:bg-slate-600'>
<article class='order-first text-3xl font-semibold tracking-tight sm:text-2xl'>
<a href={link.url} class='text-white py-4 px-8 block' target='_blank' rel='noreferrer noopener'>
{link.name}
</a>
</article>
<span
class='invisible group-hover:visible absolute top-0 right-0 -mr-3 -mt-3 cursor-pointer'
onClick={() => onClickDeleteLink(index)}
>
<img
src='/images/delete.svg'
class='red drop-shadow-md'
width={24}
height={24}
alt='Delete link'
title='Delete link'
/>
</span>
{index > 0
? (
<span
class='invisible group-hover:visible absolute top-0 left-0 -ml-3 -mt-3 cursor-pointer'
onClick={() => onClickMoveLeftLink(index)}
>
<img
src='/images/left-circle.svg'
class='gray'
width={24}
height={24}
alt='Move link left'
title='Move link left'
/>
</span>
)
: null}
</div>
))}
</section>
<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,98 @@
import { useSignal, useSignalEffect } from '@preact/signals';
import { useEffect } from 'preact/hooks';
import { RequestBody, ResponseBody } from '/routes/api/dashboard/save-notes.tsx';
interface NotesProps {
initialNotes: string;
}
export default function Notes({ initialNotes }: NotesProps) {
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 saveNotes(newNotes: string) {
if (saveTimeout.value) {
clearTimeout(saveTimeout.value);
}
saveTimeout.value = setTimeout(async () => {
hasSaved.value = false;
isSaving.value = true;
try {
const requestBody: RequestBody = { notes: newNotes };
const response = await fetch(`/api/dashboard/save-notes`, {
method: 'POST',
body: JSON.stringify(requestBody),
});
const result = await response.json() as ResponseBody;
if (!result.success) {
throw new Error('Failed to save notes!');
}
} 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'>
<textarea
class='my-2 input-field text-sm font-mono'
onInput={(event) => saveNotes(event.currentTarget.value)}
rows={10}
>
{initialNotes}
</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>
);
}