Skip to content

Commit

Permalink
Added save button which opens link creation form
Browse files Browse the repository at this point in the history
Will also open on ctrl+v and paste the url
Only shows url field at first, then autofills with title
Has a 1500ms debounce to keep it from opening prematurely if the user
is typing the address, will increase by 750ms each time it's triggered
Closes dialog form after link created
  • Loading branch information
mberry committed Oct 1, 2023
1 parent 2c69af4 commit 1cebff9
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 70 deletions.
7 changes: 6 additions & 1 deletion src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,12 @@
},
"tauri": {
"allowlist": {
"all": true
"all": true,
"clipboard": {
"all": true,
"writeText": true,
"readText": true
}
},
"bundle": {
"active": true,
Expand Down
34 changes: 31 additions & 3 deletions src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,20 @@
import { SvelteToast } from '@zerodevx/svelte-toast';
import { readCurrentLinks } from './components/utils';
import Loading from '~icons/line-md/loading-loop';
import { readText } from '@tauri-apps/api/clipboard';
let initialFetch: Promise<LinkInfo[]>;
let showForm = false;
let url = '';
async function onKeyDown(event: KeyboardEvent) {
// Open save dialog if ctrl+v is used but only if it's not already open
// It then auto-fills the url field with clipboard contents
if (event.ctrlKey && event.key == 'v' && !showForm) {
url = await readText() ?? "";
showForm = !showForm;
}
}
onMount(() => {
initialFetch = readCurrentLinks();
Expand All @@ -19,8 +31,16 @@
</script>

<div class="relative flex h-screen min-h-screen w-screen flex-col text-white">
<h1 class="fixed flex h-14 w-full items-center bg-neutral-800 px-8 text-lg font-medium">
ARK Shelf
<h1 class="fixed flex h-14 w-full items-center justify-between bg-neutral-800 px-8 text-lg font-medium">
ARK Shelf
<!-- Show Link Creation Form -->
<button on:click={() => (showForm = !showForm)} class="text-white px-4 py-2 rounded-md ml-4 border hover:bg-blue-400 border-blue-400">
{#if showForm}
Hide
{:else}
Save
{/if}
</button>
</h1>
<main class="absolute top-14 h-[calc(100vh-3.5rem)] w-screen">
<div class="flex h-full overflow-hidden overflow-y-scroll bg-neutral-950 px-8 py-4">
Expand All @@ -36,7 +56,13 @@
{/each}
{/await}
</div>
<Form />

<!-- Clicking Save button will open the link creation form
Clicking Hide or sucessfully submitting the link will close it.
If opened via ctrl+v then the meta fields will also open -->
{#if showForm}
<Form url={url} bind:show={showForm}/>
{/if}
</div>
</main>
</div>
Expand All @@ -54,3 +80,5 @@
--toastContainerLeft: auto;
}
</style>

<svelte:window on:keydown={onKeyDown} />
113 changes: 59 additions & 54 deletions src/components/Form.svelte
Original file line number Diff line number Diff line change
@@ -1,44 +1,44 @@
<script lang="ts">
import { toast } from '@zerodevx/svelte-toast';
import { linksInfos } from '../store';
import { createLink, debounce, getPreview } from './utils';
import { createLink, getPreview } from './utils';
import Calendar from '~icons/ic/baseline-calendar-month';
import Scores from '~icons/ic/baseline-format-list-bulleted';
let url = '';
let title = '';
let description = '';
const mode = linksInfos.mode;
$: disabled = !url;
// Used by App.svelte
export let url = '';
export let show = false;
const auto = async () => {
if (url && title && description) {
return;
} else if (url) {
let showMeta = false;
let inputWaitTime = 1500
let timer;
let titleElement: HTMLInputElement;
let descriptionElement: HTMLInputElement;
// Waits inputWaitTime after every new keypress into the URL form before trying for a preview and open the meta fields
// This might seem like a lot but most people type with staggered delays, 1500ms as a starting point seems a decent compromise
// If delay time is hit with an invalid url,then increase inputWaitTime by 750ms to accomodate slower typing and reduce notifications.
const debounce = async () => {
clearTimeout(timer);
timer = setTimeout( async () => {
showMeta = true;
const graph = await getPreview(url);
if (graph) {
title = graph.title ?? '';
description = graph.description ?? '';
titleElement.value = graph.title ?? '';
descriptionElement.value = graph.description ?? '';
} else {
inputWaitTime += 750
toast.push('Failed to fetch website data');
}
}
};
let error = false;
const debouncedCheck = debounce((url: string) => {
if ($linksInfos.some(l => l.url === url)) {
error = true;
} else {
error = false;
}
}, 200);
}, inputWaitTime);
}
</script>

<div class="w-56">
<div class="flex w-full justify-between">
<!-- Sort by Calendar Button -->
<button
class="rounded-md p-2"
class:bg-green-400={$mode === 'date'}
Expand All @@ -47,6 +47,8 @@
}}
><Calendar />
</button>

<!-- Sort by Score button -->
<button
class="rounded-md p-2"
class:bg-green-400={$mode === 'score'}
Expand All @@ -56,6 +58,8 @@
<Scores />
</button>
</div>

<!-- Link Creation Form -->
<form
class="sticky top-0 flex flex-col space-y-2"
on:submit|preventDefault={async e => {
Expand All @@ -69,11 +73,15 @@
url,
desc,
};
if ($linksInfos.every(l => l.url !== url)) {
if ($linksInfos.some(link => link.url != url)) {
toast.push("There is already a link with the same URL")
return
}
if ($linksInfos.every(link => link.url !== url)) {
const newLink = await createLink(data);
if (newLink) {
linksInfos.update(links => {
links = links.filter(l => l.url !== url);
links = links.filter(link => link.url !== url);
links.push(newLink);
return links;
});
Expand All @@ -82,56 +90,53 @@
} else {
toast.push('Error creating link');
}
show = false;
}
}}>

<!-- URL Field -->
<label for="url" aria-label="URL" />
{#if error}
<p class="break-words text-red-500">There is already a link with the same URL</p>
{/if}
<input
type="text"
id="url"
name="url"
required
placeholder="URL*"
class="rounded-md bg-neutral-950 px-2 py-3 outline-none ring-1 ring-neutral-500"
on:keyup={e => {
debouncedCheck(e.currentTarget.value);
}}
on:change={e => {
debouncedCheck(e.currentTarget.value);
}}
bind:value={url}
on:paste|preventDefault={e => {
const text = e.clipboardData?.getData('text');
if (text) {
url = text;
description = '';
title = '';
}
}} />
<label for="title" aria-label="Title" />
<input
on:input={debounce}
/>

<!--
Meta Fields
If the input url debounce timeout has been triggered, show the title and description fields
Title is autopopulated if possible
-->
{#if showMeta}
<label for="title" aria-label="Title" />
<input
type="text"
id="title"
name="title"
required
placeholder="Title*"
bind:value={title}
class="rounded-md bg-neutral-950 px-2 py-3 outline-none ring-1 ring-neutral-500" />
<label for="description" aria-label="Optional description" />
<input
bind:this={titleElement}
class="rounded-md bg-neutral-950 px-2 py-3 outline-none ring-1 ring-neutral-500"
/>
<label for="description" aria-label="Optional description" />
<input
type="text"
name="description"
bind:value={description}
bind:this={descriptionElement}
placeholder="Description (Optional)"
class="rounded-md bg-neutral-950 px-2 py-3 outline-none ring-1 ring-neutral-500"
id="description" />
id="description"
/>
{/if}

<!-- Create Button -->
<div class="flex justify-between">
<button type="submit" class="pl-2 text-blue-400" disabled={error}>CREATE</button>
<button class="pr-2 text-rose-700" {disabled} type="button" on:click={auto}>
AUTO FILLED
</button>
<button type="submit" class="pl-2 text-blue-400">CREATE</button>
</div>
</form>
</div>
12 changes: 0 additions & 12 deletions src/components/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,3 @@ export const createScore = async ({ value, url }: { value: number; url: string }
return;
}
};

export const debounce = (callback: unknown, wait = 500) => {
let timeoutId: number;
return (...args: unknown[]) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
if (typeof callback === 'function') {
callback(...args);
}
}, wait);
};
};

0 comments on commit 1cebff9

Please sign in to comment.