Pastabble frontend

This commit is contained in:
Maurice
2024-02-01 15:52:03 +01:00
parent 293a0af9c5
commit a1b5c513ea
29 changed files with 2410 additions and 121 deletions

View File

@@ -0,0 +1,67 @@
<script lang="ts">
import hljs from 'highlight.js';
import LanguageSelector from './LanguageSelector.svelte';
export let code: string | undefined;
export let language: string | undefined;
let displayCode: string | undefined;
let showAlert = false;
let initial = true;
$: if(code) {
highlight();
initial = false;
}
$: if(language) {
if(initial || !code) {
highlight();
}
}
function highlight() {
if(language) {
const res = hljs.highlight(language!, code!);
if(!res.errorRaised) {
displayCode = res.value;
}
} else {
const res = hljs.highlightAuto(code!);
if(!res.errorRaised) {
displayCode = res.value;
language = res.language;
}
}
}
async function copy() {
await navigator.clipboard.writeText(code!);
showAlert = true;
setTimeout(() => showAlert = false, 3000);
}
async function share() {
await navigator.clipboard.writeText(window.location.href);
showAlert = true;
setTimeout(() => showAlert = false, 3000);
}
</script>
<div class="flex flex-col m-3 gap-5">
<div role="alert" class="alert alert-success transition-opacity ease-in-out duration-500 {showAlert ? 'opacity-100' : 'opacity-0'}">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<span>Copied to your clipboard!</span>
</div>
<div class="flex flex-col md:flex-row gap-5">
<div class="mockup-code flex-1">
<pre class="p-5"><code>{@html displayCode}</code></pre>
</div>
<div class="flex flex-col gap-3">
<LanguageSelector bind:language />
<button on:click={copy} class="btn btn-outline btn-primary">Copy</button>
<button on:click={share} class="btn btn-outline btn-primary">Share link</button>
</div>
</div>
</div>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import hljs from 'highlight.js';
const languages = hljs.listLanguages();
export let language: string | undefined = undefined;
</script>
<select bind:value={language} class="select select-primary select-bordered w-full max-w-xs">
<option value="{undefined}" disabled selected>Choose programming language</option>
{#each languages as lang}
<option value="{lang}">{lang}</option>
{/each}
</select>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import { onMount } from "svelte";
let darkTheme: boolean;
let mounted = false;
onMount(() => {
mounted = true;
darkTheme = localStorage['theme'] ? localStorage['theme'] === 'dark' : isDarkMode();
});
$: if(darkTheme) {
localStorage.setItem('theme', 'dark');
document.documentElement.setAttribute('data-theme', 'dark');
} else if(mounted) {
localStorage.setItem('theme', 'light');
document.documentElement.setAttribute('data-theme', 'light');
}
const isDarkMode = () =>
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
</script>
<label class="flex cursor-pointer gap-2 items-center">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/></svg>
<input bind:checked={darkTheme} type="checkbox" class="toggle theme-controller"/>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</label>

View File

@@ -0,0 +1,5 @@
export default interface NoteDetails {
content: string;
language?: string;
created: string;
}

View File

@@ -0,0 +1,176 @@
<script lang="ts">
import { push } from "svelte-spa-router";
import LanguageSelector from "../components/LanguageSelector.svelte";
let language: string | undefined;
let lookupNoteId: string = '';
let enteredUrl: string = '';
let createdUrl: string = '';
let newNoteId: string = '';
let newNoteCode: string = '';
let urlError = false;
let lookupNoteIdError = false;
let creatingNote = false;
let creatingUrl = false;
let copied = false;
let showAlert = false;
$: if(copied) {
setTimeout(() => {
copied = false;
}, 5000);
}
$: if(showAlert) {
window.scrollTo({
top: 0,
behavior: 'smooth'
})
}
async function createNote() {
creatingNote = true;
const res = await fetch(language ? `/${newNoteId}?lang=${language}` : `/${newNoteId}`, {
method: 'POST',
body: newNoteCode
});
if(res.ok) {
const id = await res.text();
push(`/${id}`);
}
}
async function lookupNote() {
if(lookupNoteId.length === 0) {
lookupNoteIdError = true;
}
push(`/${lookupNoteId}`);
}
async function copyUrl() {
await navigator.clipboard.writeText(createdUrl);
copied = true;
}
async function shortenUrl() {
if(!isValidUrl(enteredUrl)) {
urlError = true;
return;
}
creatingUrl = true;
const res = await fetch('/to', {
method: 'POST',
body: enteredUrl
});
if(res.status === 200) {
const id = await res.text();
createdUrl = `${location.origin}/to/${id}`;
enteredUrl = '';
showAlert = true;
}
creatingUrl = false;
}
const isValidUrl = (url: string) => {
var urlPattern = new RegExp('^(https?:\\/\\/)?'+ // validate protocol
'((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|'+ // validate domain name
'((\\d{1,3}\\.){3}\\d{1,3}))'+ // validate OR ip (v4) address
'(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*'+ // validate port and path
'(\\?[;&a-z\\d%_.~+=-]*)?'+ // validate query string
'(\\#[-a-z\\d_]*)?$','i'); // validate fragment locator
return !!urlPattern.test(url);
}
</script>
<div role="alert" class="alert transition-opacity ease-in-out duration-500 my-5 {showAlert ? 'opacity-100' : 'opacity-0'}">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
<div class="flex flex-col md:flex-row gap-3 items-center w-full">
<span>Link created: </span>
<a href={createdUrl} class="font-medium text-blue-600 dark:text-blue-500 hover:underline">{createdUrl}</a>
<button on:click={copyUrl} class="btn btn-info ml-auto">
{copied ? 'Copied' : 'Copy'}
<svg class:hidden={!copied} xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
</button>
</div>
</div>
<div class="flex flex-col md:flex-row gap-5 justify-around">
<div class="mt-10">
<div>
<h2 class="text-2xl font-bold mb-3">Lookup existing note</h2>
<div class="flex gap-2">
<input
on:keydown={(e) => {
lookupNoteIdError = false;
e.key === 'Enter' && lookupNote();
}}
bind:value={lookupNoteId}
type="text"
placeholder="Note ID"
class:input-error={lookupNoteIdError}
class:input-primary={!lookupNoteIdError}
class="input input-bordered w-full max-w-xs"
/>
<button on:click={lookupNote} class="btn btn-primary">Find</button>
</div>
</div>
<div class="mt-10">
<h2 class="text-2xl font-bold mb-3">URL shortener</h2>
<div class="flex gap-2">
<input
bind:value={enteredUrl}
on:keydown={(e) => {
lookupNoteIdError = false;
e.key === 'Enter' && shortenUrl();
}}
type="text"
placeholder="Enter your long URL"
class:input-error={urlError}
class="input input-bordered w-full max-w-xs"
/>
<button disabled={creatingUrl} on:click={shortenUrl} class="btn btn-outline btn-primary">
Shorten
<span class:hidden={!creatingUrl} class="loading loading-spinner"></span>
</button>
</div>
</div>
</div>
<div class="mt-10">
<h2 class="text-2xl font-bold mb-3">Or create a new one</h2>
<div class="flex flex-col md:flex-row gap-3">
<input
type="text"
placeholder="Note ID (empty for random)"
class="input input-bordered w-full max-w-xs"
bind:value={newNoteId}
/>
<LanguageSelector bind:language />
</div>
<textarea
class="textarea textarea-bordered mt-5 w-full h-32"
placeholder="Paste your code here..."
bind:value={newNoteCode}
></textarea>
<button disabled={creatingNote} on:click={createNote} class="btn btn-outline btn-primary w-full mt-3">
Save your paste
<span class:hidden={!creatingNote} class="loading loading-spinner"></span>
</button>
</div>
</div>

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import { push } from "svelte-spa-router";
import CodeBlock from "../components/CodeBlock.svelte";
import type NoteDetails from "../models/note_details";
export let params: any;
let paste: NoteDetails | undefined;
let code: string | undefined;
let language: string | undefined;
let noteDoesNotExist = false;
let noteId: string;
if(params.id) {
noteId = params.id;
lookup();
}
async function lookup() {
const res = await fetch(`/${noteId}?details=y`);
if(res.ok) {
paste = await res.json();
code = paste?.content;
language = paste?.language;
} else {
if(res.status === 404) {
noteDoesNotExist = true;
}
}
}
function home() {
push('/');
}
</script>
<div>
{#if noteDoesNotExist}
<div role="alert" class="alert alert-warning mt-10">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
<span>Note with id <b>#{noteId}</b> not found!</span>
</div>
<button on:click={home} class="btn btn-outline btn-primary mt-5">Back home</button>
{:else if code}
<CodeBlock {language} {code} />
{/if}
</div>